Getting Started
As a simple example, one might need to model data that has a linear relationship
>>> x = [1.6, 3.2, 5.5, 7.8, 9.4]
>>> y = [7.8, 19.1, 17.6, 33.9, 45.4]
The first task to perform is to create a Model
and specify
the fit equation as a string (see the documentation of Model
for an overview of what arithmetic operations and functions are allowed in the equation)
>>> from msl.nlf import Model
>>> model = Model('a1+a2*x')
Provide an initial guess for the parameters (a1 and a2) and apply the fit
>>> result = model.fit(x, y, params=[1, 1])
>>> result.params
ResultParameters(
ResultParameter(name='a1', value=0.522439024..., uncert=5.132418149..., label=None),
ResultParameter(name='a2', value=4.406829268..., uncert=0.827701724..., label=None)
)
The Result
object that is returned from the fit contains
information about the fit result, such as the chi-square value and the covariance matrix,
but we simply showed a summary of the fit parameters above.
Input Parameters
If you want to have control over which parameters should be held constant during the
fitting process and which are allowed to vary or if you want to assign a label to a
parameter, you need to create an InputParameters
instance.
In this case, we will use one of the built-in models,
LinearModel
, to perform the linear fit and create
InputParameters
. We use the
InputParameters
instance to provide an initial
value for each parameter, define labels, and set whether the initial value of a
parameter is held constant during the fitting process
>>> from msl.nlf import LinearModel
>>> model = LinearModel()
>>> model.equation
'a1+a2*x'
>>> params = model.create_parameters()
>>> a1 = params.add(name='a1', value=0, constant=True, label='intercept')
>>> params['a2'] = 1, False, 'slope' # alternative way to add a parameter
>>> result = model.fit(x, y, params=params)
>>> result.params
ResultParameters(
ResultParameter(name='a1', value=0.0, uncert=0.0, label='intercept'),
ResultParameter(name='a2', value=4.4815604681..., uncert=0.3315980376..., label='slope')
)
We showed above that calling create_parameters()
is
one way to create an InputParameters
instance. It
can also be instantiated directly
>>> from msl.nlf import InputParameters
>>> params = InputParameters()
There are multiple ways to add a parameter to an
InputParameters
object. To add a parameter, you
could explicitly add an instance of an InputParameter
using the add()
method (or as one would
add items to a dict
)
>>> from msl.nlf import InputParameter
>>> a1 = params.add(InputParameter('a1', 1))
>>> a2 = params.add(InputParameter('a2', 2, constant=True))
>>> a3 = params.add(InputParameter('a3', 3, constant=True, label='label-3'))
>>> params['a4'] = InputParameter('a4', 4)
You could also specify multiple positional arguments (or assign several parameters using the mapping syntax)
>>> a5 = params.add('a5', 5)
>>> a6 = params.add('a6', 6, True)
>>> a7 = params.add('a7', 7, False, 'label-7')
>>> params['a8'] = 8
>>> params['a9'] = 9, True
>>> params['a10'] = 10, True, 'label-10'
or you could specify keyword arguments (or set it equal to a dict
)
>>> a11 = params.add(name='a11', value=11)
>>> a12 = params.add(name='a12', value=12, constant=True)
>>> a13 = params.add(name='a13', value=13, label='label-13')
>>> a14 = params.add(name='a14', value=14, constant=False, label='label-14')
>>> params['a15'] = {'value': 15}
>>> params['a16'] = {'value': 16, 'constant': True}
>>> params['a17'] = {'value': 17, 'label': 'label-17'}
>>> params['a18'] = {'value': 18, 'constant': False, 'label': 'label-18'}
There is an add_many()
method as well.
Here, we iterate through the collection of input parameters to see what it contains
>>> for param in params:
... print(param)
InputParameter(name='a1', value=1.0, constant=False, label=None)
InputParameter(name='a2', value=2.0, constant=True, label=None)
InputParameter(name='a3', value=3.0, constant=True, label='label-3')
InputParameter(name='a4', value=4.0, constant=False, label=None)
InputParameter(name='a5', value=5.0, constant=False, label=None)
InputParameter(name='a6', value=6.0, constant=True, label=None)
InputParameter(name='a7', value=7.0, constant=False, label='label-7')
InputParameter(name='a8', value=8.0, constant=False, label=None)
InputParameter(name='a9', value=9.0, constant=True, label=None)
InputParameter(name='a10', value=10.0, constant=True, label='label-10')
InputParameter(name='a11', value=11.0, constant=False, label=None)
InputParameter(name='a12', value=12.0, constant=True, label=None)
InputParameter(name='a13', value=13.0, constant=False, label='label-13')
InputParameter(name='a14', value=14.0, constant=False, label='label-14')
InputParameter(name='a15', value=15.0, constant=False, label=None)
InputParameter(name='a16', value=16.0, constant=True, label=None)
InputParameter(name='a17', value=17.0, constant=False, label='label-17')
InputParameter(name='a18', value=18.0, constant=False, label='label-18')
or just get all of the values
>>> params.values()
array([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,
14., 15., 16., 17., 18.])
You can get a specific parameter by its name or label (provided that the
label is not None
)
>>> params['a3']
InputParameter(name='a3', value=3.0, constant=True, label='label-3')
>>> params['label-14']
InputParameter(name='a14', value=14.0, constant=False, label='label-14')
and you can update a parameter by specifying its name or label to the
update()
method
>>> params.update('a1', value=5.3, label='intercept')
>>> params['a1']
InputParameter(name='a1', value=5.3, constant=False, label='intercept')
>>> params.update('label-7', value=1e3, constant=True, label='amplitude')
>>> params['a7']
InputParameter(name='a7', value=1000.0, constant=True, label='amplitude')
or you can update a parameter by directly modifying an attribute
>>> a1.label = 'something-new'
>>> a1.constant = False
>>> a1.value = -3.2
>>> params['a1']
InputParameter(name='a1', value=-3.2, constant=False, label='something-new')
>>> params['label-3'].label = 'fwhm'
>>> params['fwhm'].constant = True
>>> params['fwhm'].value = 0.03
>>> params['a3']
InputParameter(name='a3', value=0.03, constant=True, label='fwhm')
Debugging (Input)
If you call the fit()
method with debug=True the
fit function in the DLL is not called and an Input
object is returned that contains the information that would have been sent
to the fit function in the DLL
>>> model = LinearModel()
>>> info = model.fit(x, y, params=[1, 1], debug=True)
>>> info.weighted
False
>>> info.fit_method
<FitMethod.LM: 'Levenberg-Marquardt'>
>>> info.x
array([[1.6, 3.2, 5.5, 7.8, 9.4]])
You can display a summary of the input information
>>> info
Input(
absolute_residuals=True
correlated=False
correlations=
Correlations(
data=[]
is_correlated=[[False False]
[False False]]
)
delta=0.1
equation='a1+a2*x'
fit_method=<FitMethod.LM: 'Levenberg-Marquardt'>
max_iterations=999
params=
InputParameters(
InputParameter(name='a1', value=1.0, constant=False, label=None),
InputParameter(name='a2', value=1.0, constant=False, label=None)
)
residual_type=<ResidualType.DY_X: 'dy v x'>
second_derivs_B=True
second_derivs_H=True
tolerance=1e-20
ux=[[0. 0. 0. 0. 0.]]
uy=[0. 0. 0. 0. 0.]
uy_weights_only=False
weighted=False
x=[[1.6 3.2 5.5 7.8 9.4]]
y=[ 7.8 19.1 17.6 33.9 45.4]
)
Fit Result
When a fit is performed, the returned object is a
Result
instance
>>> model = LinearModel()
>>> result = model.fit(x, y, params=[1, 1])
>>> result.chisq
84.266087804...
>>> result.correlation
array([[ 1. , -0.88698141],
[-0.88698141, 1. ]])
>>> result.params.values()
array([0.52243902, 4.40682927])
>>> for param in result.params:
... print(param.name, param.value, param.uncert)
a1 0.5224390243941... 5.132418149940...
a2 4.4068292682920... 0.827701724508...
You can display a summary of the fit result
>>> result
Result(
calls=2
chisq=84.266087804878
correlation=[[ 1. -0.88698141]
[-0.88698141 1. ]]
covariance=[[ 0.93780488 -0.13414634]
[-0.13414634 0.02439024]]
dof=3.0
eof=5.299876973568286
iterations=22
params=
ResultParameters(
ResultParameter(name='a1', value=0.5224390243941934, uncert=5.132418149940028, label=None),
ResultParameter(name='a2', value=4.4068292682920465, uncert=0.8277017245089597, label=None)
)
)
Using the result object and the evaluate()
method,
the residuals can be calculated
>>> y - model.evaluate(x, result)
array([ 0.22663415, 4.47570732, -7.16 , -0.99570732, 3.45336585])
Save and Load .nlf Files
A Model
can be saved to a file and loaded from a file.
The file that is created with msl-nlf can also be opened in the Delphi
GUI application and a .nlf file that is created in the Delphi GUI application
can be loaded in msl-nlf. See the save()
method
and the load()
function for more details.
# Create a model
from msl.nlf import LinearModel
model = LinearModel()
model.fit([1, 2, 3], [0.07, 0.27, 0.33])
# Save the model to a file.
# The results of the fit are not written to the file, so if you are
# opening 'samples.nlf' in the Delphi GUI, click the Calculate button
# and the Results table and the Graphs will be updated.
model.save('samples.nlf')
# At a later date, load the file and perform the fit
from msl.nlf import load
loaded = load('samples.nlf')
results = loaded.fit(loaded.x, loaded.y, params=loaded.params)
A Model as a Context Manager
The fit function in the DLL reads the information it needs for the fitting process
from RAM but also from files on the hard disk. Configuration (and perhaps correlation)
files are written to a temporary directory for the DLL function to read from. This
temporary directory should automatically get deleted when you are done using the
Model
(when the objects reference count is 0 and gets
garbage collected).
Also, if loading a 32-bit DLL in 64-bit Python (see 32-bit vs 64-bit DLL) a client-server
application starts in the background when a Model
is
created. Similarly, the client-server application should automatically shut down
when you are done using the Model
.
A Model
can be used as a context manager (see The with statement)
which will delete the temporary directory (and shut down the client-server
application) once the with block is finished, for example,
from msl.nlf import Model
x = [1, 2, 3, 4, 5]
y = [1.1, 4.02, 9.2, 16.2, 25.5]
with Model('a1*x^2', dll='nlf32') as model: # temporary files created, client-server protocol starts
result = model.fit(x, y, params=[1])
# no longer in the 'with' block
# temporary files have been deleted
# the client-server protocol has shut down
# you must create a new Model if you want to use it again
It is your choice if you want to use a Model
as a
context manager. There is no difference in performance, but the cleanup
steps are more likely to occur when used as a context manager.