Repository navigation
WIP: Improve modeling docs how to create user-defined models and fitters #1878
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
51c3513
085e966
b7130ff
df94be6
1ddd156
2569b45
ab90297
5793300
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| __doctest_requires__ = {('.'): ['matplotlib', 'scipy']} | ||
| import numpy as np | ||
| import matplotlib.pyplot as plt | ||
| from astropy.modeling.models import custom_model_1d | ||
| from astropy.modeling.fitting import NonLinearLSQFitter | ||
|
|
||
| # Define model | ||
| @custom_model_1d | ||
| def sum_of_gaussians(x, amplitude1=1., mean1=-1., sigma1=1., | ||
| amplitude2=1., mean2=1., sigma2=1.): | ||
| return (amplitude1 * np.exp(-0.5 * ((x - mean1) / sigma1) ** 2) + | ||
| amplitude2 * np.exp(-0.5 * ((x - mean2) / sigma2) ** 2)) | ||
|
|
||
| # Generate fake data | ||
| np.random.seed(0) | ||
| x = np.linspace(-5., 5., 200) | ||
| m_ref = sum_of_gaussians(amplitude1=2., mean1=-0.5, sigma1=0.4, | ||
| amplitude2=0.5, mean2=2., sigma2=1.0) | ||
| y = m_ref(x) + np.random.normal(0., 0.1, x.shape) | ||
|
|
||
| # Fit model to data | ||
| m_init = sum_of_gaussians() | ||
| fit = NonLinearLSQFitter() | ||
| m = fit(m_init, x, y) | ||
|
|
||
| # Plot the data and the best fit | ||
| plt.plot(x, y, 'o', color='k') | ||
| plt.plot(x, m(x), color='r', lw=2) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -177,7 +177,8 @@ Using `modeling` | |
| parameters | ||
| models | ||
| fitting | ||
| new | ||
| new_model | ||
| new_fitter | ||
| algorithms | ||
| design | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import numpy as np | ||
| from astropy.modeling import models, Parameter, format_input | ||
|
|
||
| class LineModel(models.PolynomialModel): | ||
| slope = Parameter('slope') | ||
| intercept = Parameter('intercept') | ||
| linear = True | ||
|
|
||
| def __init__(self, slope, intercept, param_dim=1, **constraints): | ||
| super(LineModel, self).__init__(slope=slope, intercept=intercept, | ||
| param_dim=param_dim, **constraints) | ||
| self.domain = [-1, 1] | ||
| self.window = [-1, 1] | ||
| self._order = 2 | ||
|
|
||
| @staticmethod | ||
| def eval(x, slope, intercept): | ||
| return slope * x + intercept | ||
|
|
||
| @staticmethod | ||
| def fit_deriv(x, slope, intercept): | ||
| d_slope = x | ||
| d_intercept = np.ones_like(x) | ||
| return [d_slope, d_intercept] | ||
|
|
||
| @format_input | ||
| def __call__(self, x): | ||
| return self.eval(x, *self.param_sets) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| __doctest_requires__ = {('Chi2Fitter.*'): ['scipy']} | ||
| from astropy.modeling.fitting import Fitter | ||
|
|
||
| class Chi2Fitter(Fitter): | ||
| """Chi-square (a.k.a. least squares) fitter. | ||
|
|
||
| Wraps the `scipy.optimize.minimize` function. | ||
| """ | ||
| supported_constraints = ['bounds', 'fixed'] | ||
|
|
||
| def errorfunc(self, fitparams, *args): | ||
| """Compute chi-square fit statistic.""" | ||
| model, x, y = args | ||
| self._fitter_to_model_params(model, fitparams) | ||
| return ((y - model(x)) ** 2).sum() | ||
|
|
||
| def __call__(self, model, x, y , maxiter=100): | ||
| """Minimize objective function.""" | ||
| from scipy.optimize import minimize | ||
| self._validate_constraints(model) | ||
| model_copy = model.copy() | ||
| x0, _ = model_copy._model_to_fit_params() | ||
| fitparams = minimize(self.errorfunc, x0=x0, | ||
| args=(model_copy, x, y), | ||
| iter=maxiter) | ||
| self._fitter_to_model_params(model_copy, fitparams) | ||
| return model_copy | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| *************************** | ||
| Defining a new Fitter class | ||
| *************************** | ||
|
|
||
| If you would like to use a fit statistic or minimizer different from the ones built into `~astropy.modeling`, | ||
| you have to create a subclass of `~astropy.modeling.fitting.Fitter`. | ||
|
|
||
| Here is an example how to define a least squares fitter for nonlinear models | ||
| by wrapping the `~scipy.optimize.minimize` minimisation function: | ||
|
|
||
| .. literalinclude:: new_fitter.py | ||
| :linenos: | ||
|
|
||
| The user calls ``Chi2Fitter.__call__`` with a given ``model`` and data ``x`` and ``y``, | ||
| which checks and reformats the inputs a bit and then calls `~scipy.optimize.minimize`, | ||
| which repeatedly calls ``Chi2Fitter.errorfunc`` in each fit iteration. | ||
|
|
||
| Let's go through it line by line: | ||
|
|
||
| * Line TODO: Create ``Chi2Fitter`` as a sub-class of `~astropy.modeling.fitting.Fitter`. | ||
| * Line TODO: Declare the supported contraints of this fitter as a list of strings. | ||
| TODO: link to complete list and explanation of supported constraints types. | ||
|
|
||
| The special method ``Chi2Fitter.errorfunc`` computes the chi-square fit statistic: | ||
|
|
||
| * The ``fitparams`` array contains the parameter values in the current fit iteration. | ||
| * The ``args`` parameter tuple can be used to pass other parameters needed to compute the | ||
| fit statistic, in this case ``args[0] = model``, ``args[1] = x`` and ``args[2] = y``. | ||
| * Line TODO: The `~astropy.modeling.fitting.Fitter._fitter_to_model_params` helper function is used | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I would consider this an implementation detail. "Users" aren't really supposed to use it at all; only people implementing new fitters. Of course users might sometimes have need to implement new fitters so this is one of those areas where the line between users and developers is blurred. Nevertheless I would still consider this an implementation detail that is subject to change. Going forward we need to think about an easier interface for creating "user-defined" fitters a la
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should add: I really like the idea of having a step by step explanation here, but I might hold off on that for now if the details are still in flux.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But users are supposed to run fits using the Did we decide to rename
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Found it. This will be fixed by #1817
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm confused--I thought we were talking about |
||
| to set the model parameters. | ||
| * Line TODO: The model is evaluated and the fit statistic | ||
| (in this case the sum of squared residuals) computed and returned. | ||
|
|
||
| The special method ``Chi2Fitter.__call__`` runs the minimisation: | ||
|
|
||
| * The main inputs are the ``model`` and the data ``x`` and ``y``. | ||
| Extra arguments like ``maxiter`` to control the fitting can be added. | ||
| * Line TODO: Import the `~scipy.optimize.fmin_slsqp` minimisation function. | ||
| Lines TODO to TODO: Checks if the model is fittable and has valid constraints. | ||
| * Line TODO: Make a copy of the model, because fitters are supposed to leave the state of the | ||
| input model unchanged and return a copy with optimised parameters. | ||
| * Line TODO: Call the `~astropy.modeling.fitting.Fitter._model_to_fit_params` helper function | ||
| to extract the input parameter values as a numpy array ``x0``. | ||
| * Line TODO: Call `~scipy.optimize.fmin_slsqp`, passing the fit statistic function ``self.errorfunc``, | ||
| the initial parameter values ``x0``, the ``model`` and data ``x`` and ``y`` as well a the ``iter`` parameter | ||
| that defines the maximum allowed number of ``self.errorfunc`` evaluations. | ||
| * Line TODO: Call the `~astropy.modeling.fitting.Fitter._fitter_to_model_params` helper function | ||
| to copy the ``fitparams`` returned by ``fmin_slsqp`` into the ``model_copy``. | ||
|
|
||
| Usage | ||
| ----- | ||
|
|
||
| You can use this user-defined `Chi2Fitter` just like the built-in astropy fitters:: | ||
|
|
||
| import numpy as np | ||
| from astropy.modeling import models | ||
|
|
||
| g = models.Gaussian1D(amplitude=1.2, mean=0.9, stddev=0.5) | ||
|
|
||
| # Generate fake data | ||
| np.random.seed(0) | ||
| x = np.linspace(-5., 5., 100) | ||
| y = g(x) + np.random.normal(0., 0.2, x.shape) | ||
|
|
||
| # Fit the data using a Gaussian | ||
| fitter = Chi2Fitter() | ||
| g = fitter(g, x, y, maxiter=50) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned above SLSQP handles internally bounds and they must be passed to the minimizer in the correct form: