.. _lmfit: https://lmfit.github.io/lmfit-py/ .. _asteval: https://lmfit.github.io/asteval .. _fitting-parameters_sec: =============== Parameters =============== The parameters used in the fitting model are all meant to be continuous variables -- floating point numbers. In general, the fitting procedure may assign any value to any parameter. In many cases, however, you may want to place some restrictions on the value a parameter can take. For example, if you're fitting data to a line, you may want to ensure that the slope of the line is positive. For more complex cases, you might want to write a a general model describing the data, but keep some of the parameters in the model fixed. In Larch, a **Parameter** is a fundamental data type designed to hold variables for fits that can be restricted in the values it can take. A Parameter has several attributes, the most important of which is a ``value`` -- the current value. A Parameter's value can be changed of course -- this is what will happen during a fit -- but other attributes can be set to help determine its value too. In most cases, a Parameter can be used as a floating point number, and its value attribute will be used. Thus:: larch> x = param(10) larch> print x param(10, vary=False) larch> print x+2 12 To create a Parameter, use the :func:`param` function, which takes a value as its first argument, and a few optional keyword arguments to control whether the value is to be varied in a fit or kept fixed, to set optional upper and lower bounds for the Parameter value, or to set an algebraic expression to use to evaluate its value as a **constrained Parameter**. .. function:: param(value, vary=False, min=None, max=None, expr=None) define a Parameter, setting some of it principle attributes :param value: floating point value. This value may be adjusted during a fit. :param vary: flag telling whether Parameter is to be varied during a fit (``True``, ``False``) [``False``] :param min: minimum value the Parameter can take. :param max: maximum value the Parameter can take. :param expr: algebraic expression for a constrained Parameter. See :ref:`param-constraints-label` for details. :returns: a new Parameter defined according to input. A Parameter may have the following attributes to either control its value or give additional information about its value: ============== ========================== ============= ============================= attribute meaning default set by which functions: ============== ========================== ============= ============================= value value :func:`param` vary value can change in fit ``False`` :func:`param` min lower bound ``None`` :func:`param` max upper bound ``None`` :func:`param` name optional name ``None`` :func:`param` expr algebraic constraint ``None`` :func:`param` stderr standard error :func:`minimize` correl correlations :func:`minimize` uvalue value with uncertainty :func:`minimize` ============== ========================== ============= ============================= .. function:: guess(value, min=None, max=None, expr=None) define a variable Parameter, setting some of it principle attributes. The arguments here are identical to :func:`param`, except that ``vary=True`` is set. An example of creating some parameters, and creating a group of parameters would be:: # create some Parameters c1 = param(0.75) # a constant (non-varying) parameter a1 = param(1.0, min=0, max=5, vary=True) # a bounded variable parameter a2 = guess(10., min=0) # a semi-bounded variable parameter # create a group of parameters, either from existing parameters # or ones created right here params = group(a1 = a1, a2 = a2, centroid = param(99, vary=False) ) # add more parameters to the group: params.c1 = c1 # add a constrained parameter: dependent on other parameters in the group params.e1 = param(expr='a1 - c1*sqrt(a2)') setting bounds ~~~~~~~~~~~~~~~ Upper and lower bounds can be set on a Parameters value using the *min* and *max* arguments to :func:`param` or by setting the *min* and *max* attribute of an existing Parameter. To remove a bound, set the corresponding attribute to ``None``. During a fit, a Parameter's value may approach or even equal one of the bounds, but will never violate the boundary. It should be kept in mind that a Parameter with a best-fit value at or very close to a boundary may not have an accurate estimate of its uncertainty. In some cases, it may even be that a best-fit value at a boundary will prevent a reasonable estimate of the uncertainty in any of the other Parameters in the fit. .. _param-constraints-label: using algebraic constraints ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It is often useful to be able to build a fitting model in which Parameters in the model are related to one another. As a simple example, it might be useful to fit a spectrum with a sum of two lineshapes that have different centroids, but the same width. As a second example, it might be useful to fit a spectrum to a sum of two model spectra where the relative weight of the model spectra must add to 1. For each of these cases, one could write a model function that implemented such constraints. Rather than trying to capture and encourage such special cases, Larch takes a more general approach, allowing Parameters to get their value from an algebraic expression. Thus, one might define an objective function for a sum of two Gaussian functions (discussed in more detail in :ref:`lineshape-functions-label`), as:: def fit_2gauss(params, data): model = params.amp1 * gaussian(data.x, params.cen1, params.wid1) + \ params.amp2 * gaussian(data.x, params.cen2, params.wid2) return (data.y - model) enddef This is general and does not impose any relations between the parameter values within the objective function. But one can place such relations in the definitions of the parameters and have them obeyed within the fit. That is, one could constrain the two widths of the Gaussians to be the same value with:: params.wid1 = guess(1, min=0) params.wid2 = param(expr='wid1') and the value of `params.wid2` will have the same value as `params.wid1` every time the objective is called, and will not be an independent variable in the fit. For the second example, one could constrain the two amplitude parameters to add to 1 and each be between 0 and 1 as:: params.amp1 = guess(0.5, min=0, max=1) params.amp2 = param(expr='1 - amp1') .. index:: _sys.fiteval One can use more complex expressions, and also access built-in values and common mathematical functions, like `pi`, `sin`, and `log`. Essentially any valid Python/Larch expression is allowed, including slicing of arrays and array methods. Some additional details are discussed below (:ref:`fitting-fiteval_sec`), .. versionchanged:: 0.9.34 `_sys.paramGroup` is no longer used, and `_sys.fiteval` is used instead. .. _param-param_group-label: :func:`param_group`: creating a Parameter Group for fitting constraints ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ While the examples of constraint expressions above will work during a fit, the constraint values will not be updated immediately in the main Larch interpreter. That is, doing:: larch> params = group(amp1=guess(0.6, min=0, max=1), amp2=param(expr='1 - amp1')) larch> print(params.amp1, params.amp2) (, ) That is, the value of constrained parameter `amp2` is not properly set yet. To be clear, a fit with this group of parameters will work, but it's sometimes useful to see the values for the constrained parameters. The function :func:`param_group` will create a "live, working" group of parameters:: larch> params = param_group(amp1=guess(0.6, min=0, max=1), amp2=param(expr='1 - amp1')) larch> print(params.amp1, params.amp2) (, ) In addition, you can change the value of `params.amp1`, with the value of `params.amp2` being automatically updated: larch> params.amp1.value = 0.2 larch> print(params.amp1, params.amp2) (, ) .. function:: param_group(**kws) create and return a *Parameter Group* that uses `_sys.fiteval` for constraint expression. :param kws: optional keyword/argument values for parameters :returns: a new Parameter Group with working constraint expressions. A Parameter Group can contain non-Parameter values as well as fitting Parameters. For backward compatibility, a simple group containing parameters will work with fitting, but a :func:`param_group` is recommended for many cases. .. _fitting-fiteval_sec: `fiteval` and details about algebraic constraints ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Beginning with version 0.9.34, Larch uses the `lmfit`_ python library to do all the fitting. This project is related to and very similar to Larch itself, but is maintained and developed separately. Lmfit supports algebraic constraints like those discussed in the previous section, using an isolated, embedded mini-interpreter (very similar to Larch itself, based on `asteval`_). Within Larch, this embedded expression interpreter for fitting constraints is held in the Larch system variable `_sys.fiteval`. The set of available functions and variables is in its symbol table, `_sys.fiteval.symtable`, which has more than 400 named functions and variables available, most of them from numpy. During a fit, all the components of the *paramgroup* given to :func:`minimize` will be put put into the `_sys.fiteval` symbol table. Any of these variables can be used in the constraint expressions. In addition, all the true parameters in the *paramgroup* will be converted into `lmfit.Parameters`. After the fit is complete, the updated parameter values will be put back into the The :func:`param_group` function discussed above keeps an internal link to `_sys.fiteval` and uses that for evaluating constraint expressions. That is, following the above example, one can see the current values for `params.amp1` and `params.amp2` within the `_sys.fiteval` symbol table:: larch> params = param_group(amp1=guess(0.6, min=0, max=1), amp2=param(expr='1 - amp1')) larch> print(params.amp1, params.amp2) (, ) larch> print(_sys.fiteval.symtable.amp2) 0.4 larch> params1.amp.value = 0.1 larch> print(params.amp1, params.amp2) (, ) larch> print(_sys.fiteval.symtable.amp2) 0.9 Because `_sys.fiteval` is used for all fits with :func:`minimize` (and for XAFS, with :func:`feffit`), you may find yourself wanting to clear or reset the fitting symbol table for a new fit. This should not be necessary, but it is available with the function :func:`reset_fiteval`: .. function:: reset_fiteval() clear and reset `_sys.fiteval` for a new fit. This function takes no arguments. .. _fitting-uncertainties_sec: working with uncertainties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. _uncertainties: https://packages.python.org/uncertainties/ After a fit, each Parameter that was actually varied in the fit should be assigned information about the uncertainty in the fitted value as well as its best fit value. On rare occasions (such as when a best-fit value is very close to a bound) the setting of uncertainties is not possible. The primary way the uncertainty for a Parameter is expressed is with the ``stderr`` attribute, which holds the estimated standard error for the Parameter's value. The correlation with all other Parameters is held in the ``correl`` attribute -- a dictionary with keys of variable names and values of correlation with that variable. In addition, the two-dimensional covariance matrix will be held in the ``covar`` attribute of the parameter group for each fit. Note that the uncertainties calculated for constrained parameters involving more than one variable will encapsulate not only the simple propogation of errors for the independent variables, but also their correlation. This can have a significant impact on the uncertainties for constrained parameters. Finally, each Parameter will have a ``uvalue`` attribute which is a special object from the `uncertainties`_ package that holds both the best-fit value and standard error. A key feature of these ``uvalue`` attributes is that they can be used in simple mathematical expressions (addition, subtraction, multiplication, division, exponentiation) and propogate the uncertainties to the result (ignoring correlations).