Setting bounds on model can cause fit to diverge
See original GitHub issueDescription
When using astropy models to perform astro/photometry, fitting a model to a star image with bounds causes the fit to diverge to the edges of the parameter space. A fit without bounds works fine and converges to a value within the bounds. With the a more complex PSF model, with stronger side-maxima than a plain Airy disc, this happens basically every time. Both with the LevenbergMarquardt and Simplex fitters.
My suspicion without having fully understood the fitting framework is that these lines in astropy/modeling/fitting.py
effectively “brickwall” the parameters, throwing off the gradient calculation / simplex direction and cause the fit to “stick” to the edge of the parameter bounds.
# Check bounds constraints
if model.bounds[name] != (None, None):
_min, _max = model.bounds[name]
if _min is not None:
values = np.fmax(values, _min)
if _max is not None:
values = np.fmin(values, _max)
I found a possible explanation/solution to the issue described here: https://lmfit.github.io/lmfit-py/bounds.html
More recent versions of scipy.optimize.least_squares
seem to support bounds on parameters for trf
directly.
Expected behavior
Fit with bounds should not perform worse than a unbounded fit that finds the minimum within the bounds.
Steps to Reproduce
from astropy.modeling.functional_models import AiryDisk2D
from astropy.modeling.fitting import LevMarLSQFitter
import numpy as np
np.random.seed(999999)
fitter = LevMarLSQFitter()
y, x = np.mgrid[-5:5:50j, -5:5:50j]
# Airy Disk mostly works, bigger problems with models that have more sidelobes/wings
input_model = AiryDisk2D(radius=2)
# build noisy image from input model
img = np.random.poisson(1e5*input_model(x, y)) + np.random.normal(0, 20)
fit_model = input_model.copy()
# bad guess for amplitude, decent guesses for position causes fitter to want to explore
# out of bounds, gets stuck there if bounds enabled
fit_model.x_0 = 0.1
fit_model.y_0 = -0.08
fit_model.amplitude = 1e3
fitted = fitter(fit_model, x, y, img)
print(f'without bounds: {fitted.x_0=} {fitted.y_0=} {fitted.amplitude=}')
fit_model.bounds['x_0'] = (-0.2, 0.2)
fit_model.bounds['y_0'] = (-0.2, 0.2)
fitted = fitter(fit_model, x, y, img)
print(f'with bounds: {fitted.x_0=} {fitted.y_0=} {fitted.amplitude=}')
prints
without bounds: fitted.x_0=Parameter('x_0', value=0.00019776080448936212) fitted.y_0=Parameter('y_0', value=-9.740265185190977e-05) fitted.amplitude=Parameter('amplitude', value=99967.10092998871)
with bounds: fitted.x_0=Parameter('x_0', value=-0.2, bounds=(-0.2, 0.2)) fitted.y_0=Parameter('y_0', value=0.2, bounds=(-0.2, 0.2)) fitted.amplitude=Parameter('amplitude', value=92364.16417303146)
System Details
Linux-5.12.15-arch1-1-x86_64-with-glibc2.33
Python 3.9.6 (default, Jun 30 2021, 10:22:16)
[GCC 11.1.0]
Numpy 1.20.3
astropy 4.2.1
Scipy 1.7.0
Issue Analytics
- State:
- Created 2 years ago
- Comments:8 (8 by maintainers)
Top GitHub Comments
@WilliamJamieson Sorry for not responding earlier, I got a bit sidetracked on my fitting problem. I finally got around to verify that the new fitter seems to work fine and does not cause any issues with stuck bounds anymore.
Repro script is here in case you’re interested, output is a pandas dataframe for analysis: https://gist.github.com/krachyon/e94c46f2abd65351f77f9d00084e87b1
Thanks a lot for the fix!
I just opened a draft PR #12051 based on my prototype.