The n-dimensional fitting tutorial ==================================== .. currentmodule:: PyAstronomy.funcFit The funcFit package supports n-dimensional fitting, which means that both the domain and the range may be multidimensional. An example for a multidimensional range would be the fitting of a circle or orbit, when time (1d) is mapped to image coordinates (2d). Fitting, for example, the structure of a point spread function (PSF) would be an example for a multidimensional domain, viz., the image coordinates, which are mapped onto a one-dimensional range (the flux or intensity). Fitting a circular orbit --------------------------- This example demonstrates how to fit a 2d model (location in plane) depending on a single variable (time), so that there is a mapping of the form .. math:: f: (R \times R^m) \rightarrow R^n , where the :math:`R^m` denotes the parameter vector. In particular, we assume x,y-position measurements at a number of times. Furthermore, all x and y measurements have an error. :: import numpy as np import matplotlib.pylab as plt from PyAstronomy import funcFit as fuf # Get the circular model and assign # parameter values c = fuf.Circle2d() c["r"] = 1.0 c["t0"] = 0.0 c["per"] = 3.0 # Evaluate the model at a number of # time stamps t = np.linspace(0.0, 10.0, 20) pos = c.evaluate(t) # Add some error to the "measurement" pos += np.reshape(np.random.normal(0.0, 0.2, pos.size), pos.shape) err = np.reshape(np.ones(pos.size), pos.shape) * 0.2 # Define free parameters and fit the model c.thaw(["r", "t0", "per"]) c.fit(t, pos, yerr=err) c.parameterSummary() # Evaluate the model at a larger number of # points for plotting tt = np.linspace(0.0, 10.0, 200) model = c.evaluate(tt) # Plot the result plt.errorbar(pos[::, 0], pos[::, 1], yerr=err[::, 1], xerr=err[::, 0], fmt='bp') plt.plot(model[::, 0], model[::, 1], 'r--') plt.show() .. note:: The only difference between the "normal" one-dimensional case and that exemplified here, is the dimension of the `y` and error arrays. .. _creatingCoordinateArrays: Creating "coordinate arrays" ----------------------------- If one has multiple coordinate axes, such as in the case of an image, it is often convenient to represent the coordinates using an appropriate array. `funcFit` provides the `coordinateGrid` function to help constructing an appropriate array. In particular, for n coordinate axes, the function will construct an array with n+1 dimensions, which gives the physical coordinates for every array index. .. note:: The user is free to use any format to give the coordinates. For example, the `meshgrid` in numpy could be applied to obtain a similar result. The :ref:`gauss2dExample` example demonstrates both use cases. :: from __future__ import print_function, division from PyAstronomy import funcFit as fuf import numpy as np # Constructing the two individual coordinate axes x = np.linspace(-2., 2., 50) y = np.linspace(-2., 2., 50) # Applying funcFit's "coordinateGrid" helper function # to built appropriate array-index -> coordinate mapping # needed for nD fitting. g = fuf.coordinateGrid(x, y) print("(x, y) coordinates at index (11, 28): ", g[11, 28]) .. _gauss2dExample: Fitting image data with a 2d-Gaussian model --------------------------------------------- The following two examples demonstrate how to fit 2d image data with a 2d Gaussian model using funcFit, i.e., we have the mapping .. math:: f: (R^2 \times R^m) \rightarrow R , where, again, :math:`R^m` denotes the parameter vector, :math:`R^2` is the image coordinate, and the result is the "level", "flux", or whatever the image shows. In this case, we need to take care of the parameter representation. In principle, we may choose the coordinate format ad libitum, however, the `evaluate` method of the fitting objects has to understand the format. Below, we show two conceivable approaches to specify the parameters. Let us say, we are given both an x- and a y-axis specifying the coordinates in the image. As demonstrated in :ref:`creatingCoordinateArrays`, the function :py:func:`coordinateGrid` can be used to construct an appropriate coordinate array mapping array index to physical coordinates. The class :py:class:`GaussFit2d` expects such a coordinate array, and the example below demonstrates how to use it. .. note:: Below the example, the `evaluate` method of `GaussFit2d` is shown to illustrate the use of the coordinate array. :: from PyAstronomy import funcFit as fuf import numpy as np import matplotlib.pylab as plt # Constructing the individual coordinate axes x = np.linspace(-2., 2., 50) y = np.linspace(-2., 2., 50) # Applying funcFit's "coordinateGrid" helper function # to built appropriate array-index -> coordinate mapping # needed for nD fitting. g = fuf.coordinateGrid(x, y) # Create the 2d-Gaussian model and assign # some model parameters. gf = fuf.GaussFit2d() gf["sigx"] = 0.75 gf["sigy"] = 0.4 gf["A"] = 1.0 gf["rho"] = 0.4 # Get the "data" by evaluating the model # and adding some noise. Note that the coordinate # mapping (array g) is passed to evaluate here. im = gf.evaluate(g) im += np.reshape(np.random.normal(0.0, 0.1, 2500), (50, 50)) err = np.ones((50, 50))*0.1 # Thaw parameters and fit gf.thaw(["A", "rho"]) gf.fit(g, im, yerr=err) # Show the resulting parameter values ... gf.parameterSummary() # ... and plot the result. plt.title("Image data") plt.imshow(np.transpose(im), origin="lower") plt.show() plt.title("Residuals") plt.imshow(np.transpose(im - gf.evaluate(g)), origin="lower") plt.show() The `evaluate` method of the `GaussFit2d` class has the following from: :: # The "evaluate" method of class GaussFit2d # Note how the coordinate grid (co argument) is used in the evaluation # of the model. Basically co[::,::,0] gives the x-coordinate for every # image pixel. Likewise, co[::,::,1] defines the y-coordinate. def evaluate(self, co): """ Evaluates the model for current parameter values. Parameters ---------- co : array Specifies the points at which to evaluate the model. """ if (self["sigx"] <= 0.0) or (self["sigy"] <= 0.0): raise(PE.PyAValError("Width(s) of Gaussian must be larger than zero.", \ solution="Change width ('sigx/y').")) if self["rho"] > 1.0: raise(PE.PyAValError("The correlation coefficient must be 0 <= rho <= 1.", \ solution="Change width ('sigx/y').")) result = 1.0/(2.*pi*self["sigx"]*self["sigy"]*sqrt(1.-self["rho"]**2)) * \ exp( ((co[::,::,0]-self["mux"])**2/self["sigx"]**2 + (co[::,::,1]-self["muy"])**2/self["sigy"]**2 - \ 2.*self["rho"]*(co[::,::,0]-self["mux"])*(co[::,::,1]-self["muy"])/(self["sigx"]*self["sigy"])) / \ (-2.*(1.-self["rho"]**2)) ) return result Alternatively, you may find it more hand the coordinate axes directly to the `evaluate` function. This is possible and demonstrated in the following example, which relies on the :py:class:`GaussFit2dTuple` class, whose `evaluate` method expects a tuple of coordinate axes. Note the difference in the `evaluate` method. In this case, we use numpy's `meshgrid` function to create coordinate mapping similar to the one used before. :: from PyAstronomy import funcFit as fuf import numpy as np import matplotlib.pylab as plt # Constructing the individual coordinate axes x = np.linspace(-2., 2., 50) y = np.linspace(-2., 2., 50) # Create the 2d-Gaussian model and assign # some model parameters. gf = fuf.GaussFit2dTuple() gf["sigx"] = 0.75 gf["sigy"] = 0.4 gf["A"] = 1.0 gf["rho"] = 0.4 # Get the "data" by evaluating the model # and adding some noise. Note that the coordinate # mapping (array g) is passed to evaluate here. im = gf.evaluate((x, y)) im += np.reshape(np.random.normal(0.0, 0.1, 2500), (50, 50)) err = np.ones((50, 50))*0.1 # Thaw parameters and fit gf.thaw(["A", "rho"]) gf.fit((x, y), im, yerr=err) # Show the resulting parameter values ... gf.parameterSummary() # ... and plot the result. plt.title("Image data") plt.imshow(np.transpose(im), origin="lower") plt.show() plt.title("Residuals") plt.imshow(np.transpose(im - gf.evaluate((x, y))), origin="lower") plt.show() The associated `evaluate` method looks like this: :: # The "evaluate" method of class GaussFit2dTuple # A coordinate grid is constructed using numpy's meshgrid # function. However, you may do whatever you prefer to # map the input to the model coordinates. def evaluate(self, co): """ Evaluates the model for current parameter values. Parameters ---------- co : array Specifies the points at which to evaluate the model. """ if (self["sigx"] <= 0.0) or (self["sigy"] <= 0.0): raise(PE.PyAValError("Width(s) of Gaussian must be larger than zero.", \ solution="Change width ('sigx/y').")) if self["rho"] > 1.0: raise(PE.PyAValError("The correlation coefficient must be 0 <= rho <= 1.", \ solution="Change width ('sigx/y').")) xx, yy = meshgrid(co[0], co[1]) result = 1.0/(2.*pi*self["sigx"]*self["sigy"]*sqrt(1.-self["rho"]**2)) * \ exp( ((xx-self["mux"])**2/self["sigx"]**2 + (yy-self["muy"])**2/self["sigy"]**2 - \ 2.*self["rho"]*(xx-self["mux"])*(yy-self["muy"])/(self["sigx"]*self["sigy"])) / \ (-2.*(1.-self["rho"]**2)) ) return result