.. lsqfitgp/docs/nonlinear.rst .. .. Copyright (c) 2020, 2022, 2023, Giacomo Petrillo .. .. This file is part of lsqfitgp. .. .. lsqfitgp is free software: you can redistribute it and/or modify .. it under the terms of the GNU General Public License as published by .. the Free Software Foundation, either version 3 of the License, or .. (at your option) any later version. .. .. lsqfitgp is distributed in the hope that it will be useful, .. but WITHOUT ANY WARRANTY; without even the implied warranty of .. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the .. GNU General Public License for more details. .. .. You should have received a copy of the GNU General Public License .. along with lsqfitgp. If not, see . .. currentmodule:: lsqfitgp .. _nonlinear: Nonlinear models ================ Using `GP` we can define a Gaussian process. Using `GP.addlintransf` we can represent finite linear transformations of the process, and we can take derivatives with `GP.addx`. This means that we can only do linear operations on the process before putting the data in. A common non-linear operation is putting a boundary on the possible data values. Gaussian distributions don't play nicely with boundaries---they are defined on :math:`(-\infty,\infty)`---so it is necessary to map the Gaussian process space to an interval with a nonlinear function. `lsqfitgp` is designed to work with the general purpose fitting module `lsqfit` (after which it takes the name) for this kind of situations. If you want to know more, it has a `good documentation `_. Let's see how to fit some data that is constrained in (-1, 1). To map the Gaussian process space to the data space, we'll use a hyperbolic tangent. It has the properties :math:`\tanh(\pm\infty) = \pm 1`, :math:`\tanh'(0) = 1`, :math:`\tanh(-x) = -\tanh(x)`. We first define a Gaussian process as usual and take a sample from it as fake data. :: import lsqfitgp as lgp import numpy as np import gvar gp = lgp.GP(lgp.ExpQuad()) x = np.arange(15) gp = gp.addx(x, 'data') data_gp = gvar.sample(gp.prior('data')) Then we map it to (-1, 1):: data = np.tanh(data_gp) Note that we first sampled the latent Gaussian process obtaining ``data_gp``, and then passed it through our nonlinear function to obtain the fake data. A possibly serious mistake would be to do the converse, first passing the prior through the nonlinear function, with ``np.tanh(gp.prior('data'))``, and then sampling from it. To see this intuitively, consider that in the latter case the fake data would not satisfy the requirement of being bounded within (-1, 1). Now we'll add errors to the data. If data does not have errors, there's not really a problem to start with: you can map the data to :math:`(-\infty, \infty)` with `~numpy.arctanh`, do the Gaussian process fit, take some samples, map the samples back with `~numpy.tanh`. You may do that even with errors, either at first order using `gvar.arctanh`, or by transforming the errors manually yourself. However, in that way you would be doing a fit with Gaussian errors on the transformed data. What we will do is a fit with Gaussian errors on the data itself. Another case we won't explore in which just transforming the data before the fit is not sufficient is when the mapping between the Gaussian process and the data depends on a fit parameter. :: err = 0.1 rng = np.random.default_rng([2023, 8, 23, 17, 18]) data += err * rng.standard_normal(len(data)) data = gvar.gvar(data, np.full_like(data, err)) Then as usual we add a finer grid of points where we will compute the prediction:: xplot = np.linspace(-10, 25, 200) gp = gp.addx(xplot, 'plot') Now we define the prior and model function following the requirements of :class:`lsqfit.nonlinear_fit` and run the fit:: import lsqfit prior = { 'gproc': gp.prior('data') } def fcn(params): return gvar.tanh(params['gproc']) fit = lsqfit.nonlinear_fit(data=data, fcn=fcn, prior=prior) print(fit.format(maxline=True)) Output: .. code-block:: text Least Square Fit: chi2/dof [dof] = 1.1 [15] Q = 0.36 logGBF = -11.047 Parameters: gproc 0 -0.15 (10) [ 0.0 (1.0) ] 1 0.71 (15) [ 0.0 (1.0) ] 2 -1.30 (34) [ 0.0 (1.0) ] * 3 -2.37 (56) [ 0.0 (1.0) ] ** 4 -1.20 (30) [ 0.0 (1.0) ] * 5 0.58 (13) [ 0.0 (1.0) ] 6 0.55 (13) [ 0.0 (1.0) ] 7 0.24 (10) [ 0.0 (1.0) ] 8 -0.52 (12) [ 0.0 (1.0) ] 9 -0.41 (11) [ 0.0 (1.0) ] 10 0.54 (13) [ 0.0 (1.0) ] 11 0.86 (18) [ 0.0 (1.0) ] 12 0.51 (12) [ 0.0 (1.0) ] 13 0.33 (11) [ 0.0 (1.0) ] 14 0.15 (10) [ 0.0 (1.0) ] Fit: key y[key] f(p)[key] -------------------------------------- 0 -0.17 (10) -0.149 (99) 1 0.67 (10) 0.610 (97) 2 -0.99 (10) -0.861 (88) * 3 -0.91 (10) -0.983 (19) 4 -0.92 (10) -0.832 (91) 5 0.57 (10) 0.523 (97) 6 0.47 (10) 0.500 (96) 7 0.26 (10) 0.235 (98) 8 -0.50 (10) -0.476 (96) 9 -0.39 (10) -0.388 (97) 10 0.50 (10) 0.494 (96) 11 0.71 (10) 0.696 (93) 12 0.46 (10) 0.467 (97) 13 0.33 (10) 0.322 (98) 14 0.15 (10) 0.152 (99) Settings: svdcut/n = 1e-12/0 tol = (1e-08,1e-10,1e-10*) (itns/time = 22/0.1) fitter = scipy_least_squares method = trf Let's plot everything. First we compute the posterior on the ``xplot`` points:: gpplot = gp.predfromfit({'data': fit.p['gproc']}, 'plot') This time we use `GP.predfromfit` instead of the usual `GP.predfromdata`. This method takes into account that the distribution represented by ``fit.p['gproc']`` is not the uncertainty of some datapoints, but is already the distribution of some points of our process. We want to "extend" ``fit.p['gproc']``, not condition on it. Then we inject the extended posterior into a copy of the fit result dictionary:: fitp = dict(fit.p) # dict() makes a copy of fit.p fitp['gproc'] = gpplot This copy-and-replace step is a bit redundant here, it is for when there are also other parameters beside the Gaussian process, and we do not want to modify the fit result dictionary for good bookkeeping practice. Then we plot both the data space and the Gaussian process space. :: from matplotlib import pyplot as plt fig, axs = plt.subplots(2, 1, sharex=True, num='lsqfitgp example', figsize=[6.4, 7]) for sample in gvar.raniter(fitp, 2): axs[0].plot(xplot, fcn(sample), color='red', alpha=0.5) axs[1].plot(xplot, sample['gproc'], color='red', alpha=0.5) ax = axs[0] ax.set_title('data space') for boundary in 1, -1: ax.axhline(boundary, color='gray', linestyle=':') ax.errorbar(x, gvar.mean(data), yerr=gvar.sdev(data), fmt='.k', capsize=4) ax = axs[1] ax.set_title('Gaussian process space') ax.plot(x, data_gp, '.k', label='true') ax.plot(x, np.arctanh(gvar.mean(data)), 'xk', label='with errors, mapped back') ax.legend() fig.tight_layout() fig.savefig('nonlinear1.png') .. image:: nonlinear1.png