Skip to content

Parameter bounds in non linear fitting do not work as expected #1218

Closed

Description

@streeto

I am using NonLinearLSQFitter and SLSQPFitter to fit some data to a parametric model I implemented. I set some boundaries in the parameters using the bounds={...} argument in the model __init__ method. It seems that the bouns are set correctly, I ran the code in debug mode and the NonLinearLSQFitter._set_bounds() method is reached, for example.

Using NonLinearLSQFitter the bounds are ignored, and using SLSQPFitter the fit returns the initial guess, while reporting success in the fit_info property.

Here's the code I used to trigger the bug:

from astropy.modeling import ParametricModel, Parameter, fitting
import numpy as np
from astropy.modeling.core import _convert_input, _convert_output
import matplotlib.pyplot as plt


class LineModel(ParametricModel):
    param_names = ['A', 'B']
    def __init__(self, A, B, **cons):
        try:
            param_dim = len(A)
        except TypeError:
            param_dim = 1
        self._A = Parameter(name='A', val=A, mclass=self, param_dim=param_dim)
        self._B = Parameter(name='B', val=B, mclass=self, param_dim=param_dim)
        ParametricModel.__init__(self, self.param_names, n_inputs=1, n_outputs=1, param_dim=param_dim, **cons)
        self.linear = False

    def eval(self, x, params):
        A, B = params
        return A * x + B

    def deriv(self, params, x, dummy=None):
        A, B = params
        deriv_dict = {}
        deriv_dict['A'] = x
        deriv_dict['B'] = np.ones_like(x)
        derivval = [deriv_dict[par] for par in self.param_names]
        return np.array(derivval).T

    def __call__(self, x):
        x, fmt = _convert_input(x, self.param_dim)
        result = self.eval(x, self.param_sets)
        return _convert_output(result, fmt)

# These values should be recovered by the fitting procedure.
A = -2.0
B = 0.5
x = np.linspace(-1.0, 1.0, 100)
y = A * x + B + np.random.normal(scale=0.1, size=100)

print 'Original A:', A
print 'Original B:', B

############################################################
# Case 1: NonLinearLSQFitter without bounds, OK.
############################################################
guess_A = 1.1
guess_B = 0.0

line_model = LineModel(guess_A, guess_B)
fitter = fitting.NonLinearLSQFitter(line_model)
fitter(x, y)

print 'NonLinearLSQFitter without bounds:'
print 'A =', line_model.A
print 'B =', line_model.B
plt.ioff()
plt.plot(x, y)
plt.plot(x, line_model(x))
plt.show()

############################################################
# Case 2: NonLinearLSQFitter with bounds.
# A should be out of bounds, but fits anyway.
############################################################
guess_A = 1.1
guess_B = 0.0
bounds={'A': (-1.5, 5.0), 'B': (-1.0, 1.0)}

line_model = LineModel(guess_A, guess_B, bounds=bounds)
fitter = fitting.NonLinearLSQFitter(line_model)
fitter(x, y)

print 'NonLinearLSQFitter with bounds:'
print 'A =', line_model.A
print 'B =', line_model.B
plt.ioff()
plt.plot(x, y)
plt.plot(x, line_model(x))
plt.show()

############################################################
# Case 3: SLSQPFitter without bounds, OK
############################################################
guess_A = 1.1
guess_B = 0.0

line_model = LineModel(guess_A, guess_B)
fitter = fitting.SLSQPFitter(line_model)
fitter(x, y)

print 'SLSQPFitter without bounds:'
print 'A =', line_model.A
print 'B =', line_model.B
plt.ioff()
plt.plot(x, y)
plt.plot(x, line_model(x))
plt.show()

############################################################
# Case 4: SLSQPFitter with bounds.
# Does not fit even if within bounds,
# and reports a successful fit.
############################################################
guess_A = 1.1
guess_B = 0.0
bounds={'A': (-5.0, 5.0), 'B': (-1.0, 1.0)}

line_model = LineModel(guess_A, guess_B, bounds=bounds)
fitter = fitting.SLSQPFitter(line_model)
fitter(x, y)

print 'SLSQPFitter with bounds:'
print 'A =', line_model.A
print 'B =', line_model.B
plt.ioff()
plt.plot(x, y)
plt.plot(x, line_model(x))
plt.show()

Activity

  1. nden commented on Jul 1, 2013

    @nden
    Contributor

    @streeto It's not an obvious fix but I the bug is in the way parameters.Parameters and core._ParameterProperty update model.parameters and individual parameters. I'll take a look today.

  2. streeto commented on Jul 1, 2013

    @streeto
    Author

    I just fixed it for NonLinearLSQFitter, there is a bug in _set_bounds():

    setattr(self.model, name, par if par > b[0] else b[0])
    setattr(self.model, name, par if par < b[1] else b[1])

    This is wrong because if we slip below b[0], the second setattr will set par again anyway. The fix is simple:

    par = max(par, b[0])
    par = min(par, b[1])
    setattr(self.model, name, par)
  3. nden commented on Jul 1, 2013

    @nden
    Contributor

    @streeto Excellent, thanks! Do you want to file a PR or do you want me to do it?
    I have a fix for the SLSQP fitter which wasn't passing the parameters to the model.

  4. streeto commented on Jul 2, 2013

    @streeto
    Author

    Sorry, my workspace is a mess, I am unable to prepare a pull request right now. Please proceed with the fix. :-)

  5. cdeil commented on Aug 8, 2013

    @cdeil
    Member

    @streeto Can you try to update your test example and see if everything works as expected with the current astropy.modeling?

    Currently your script gives an error (see below) because the API for creating user-defined models has changed a bit. See Gaussian1DModel as an example how to do it now.

    $ python a.py
    Original A: -2.0
    Original B: 0.5
    ERROR: ValueError: too many values to unpack [__main__]
    Traceback (most recent call last):
      File "a.py", line 53, in <module>
        fitter(x, y)
      File "/Users/deil/Library/Python/2.7/lib/python/site-packages/astropy-0.3.dev4991-py2.7-macosx-10.8-x86_64.egg/astropy/modeling/fitting.py", line 576, in __call__
        col_deriv=self.model.col_deriv, maxfev=maxiter, epsfcn=epsilon, full_output=True)
      File "/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/scipy/optimize/minpack.py", line 367, in leastsq
        _check_func('leastsq', 'Dfun', Dfun, x0, args, n, (n,m))
      File "/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/scipy/optimize/minpack.py", line 17, in _check_func
        res = atleast_1d(thefunc(*((x0[:numinputs],) + args)))
      File "/Users/deil/Library/Python/2.7/lib/python/site-packages/astropy-0.3.dev4991-py2.7-macosx-10.8-x86_64.egg/astropy/modeling/fitting.py", line 219, in _wrap_deriv
        return self.model.deriv(x, *pars)
      File "a.py", line 24, in deriv
        A, B = params
    ValueError: too many values to unpack
    
  6. streeto commented on Aug 8, 2013

    @streeto
    Author

    @nden, I updated the test, it works correctly. Below's the updated version. Thanks for the effort! Now to try it with my real world unstable models. :-)

    from astropy.modeling import Parametric1DModel, fitting
    import numpy as np
    import matplotlib.pyplot as plt
    
    
    class LineModel(Parametric1DModel):
        param_names = ['A', 'B']
    
        def __init__(self, A, B, **constraints):
            super(LineModel, self).__init__(locals())
    
        def eval(self, x, A, B):
            return A * x + B
    
        def deriv(self, x, A, B):
            df_dA = x
            df_dB = np.ones_like(x)
            return [df_dA, df_dB]
    
    
    
    A = -2.0
    B = 0.5
    x = np.linspace(-1.0, 1.0, 100)
    y = A * x + B + np.random.normal(scale=0.1, size=100)
    
    print 'Original A:', A
    print 'Original B:', B
    
    ############################################################
    # Case 1: NonLinearLSQFitter without bounds
    ############################################################
    guess_A = 1.1
    guess_B = 0.0
    
    line_model = LineModel(guess_A, guess_B)
    fitter = fitting.NonLinearLSQFitter(line_model)
    fitter(x, y)
    
    print 'NonLinearLSQFitter without bounds:'
    print 'A =', line_model.A
    print 'B =', line_model.B
    plt.ioff()
    plt.plot(x, y)
    plt.plot(x, line_model(x))
    plt.show()
    
    ############################################################
    # Case 2: NonLinearLSQFitter with bounds.
    ############################################################
    guess_A = 1.1
    guess_B = 0.0
    bounds={'A': [-1.5, 5.0], 'B': [-1.0, 1.0]}
    
    line_model = LineModel(guess_A, guess_B, bounds=bounds)
    fitter = fitting.NonLinearLSQFitter(line_model)
    fitter(x, y)
    
    print 'NonLinearLSQFitter with bounds:'
    print bounds
    print 'Fit results:'
    print 'A =', line_model.A
    print 'B =', line_model.B
    plt.ioff()
    plt.plot(x, y)
    plt.plot(x, line_model(x))
    plt.show()
    
    ############################################################
    # Case 3: SLSQPFitter without bounds
    ############################################################
    guess_A = 1.1
    guess_B = 0.0
    
    line_model = LineModel(guess_A, guess_B)
    fitter = fitting.SLSQPFitter(line_model)
    fitter(x, y, verblevel=1)
    
    print 'SLSQPFitter without bounds:'
    print 'A =', line_model.A
    print 'B =', line_model.B
    plt.ioff()
    plt.plot(x, y)
    plt.plot(x, line_model(x))
    plt.show()
    
    ############################################################
    # Case 4: SLSQPFitter with bounds.
    ############################################################
    guess_A = 1.1
    guess_B = 0.0
    bounds={'A': [-1.5, 5.0], 'B': [-1.0, 1.0]}
    
    line_model = LineModel(guess_A, guess_B, bounds=bounds)
    line_model.deriv = None
    fitter = fitting.SLSQPFitter(line_model)
    fitter(x, y, verblevel=1)
    
    print 'SLSQPFitter with bounds:'
    print bounds
    print 'Fit results:'
    print 'A =', line_model.A
    print 'B =', line_model.B
    plt.ioff()
    plt.plot(x, y)
    plt.plot(x, line_model(x))
    plt.show()
    print fitter.fit_info
  7. cdeil commented on Aug 8, 2013

    @cdeil
    Member

    @streeto Thanks! Please close this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions