.. lsqfitgp/docs/derivatives.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
.. _derivs:
Taking derivatives
==================
The kernel of a derivative is the derivative of the kernel:
.. math::
\operatorname{Cov}[f'(x), f'(x')]
&= \frac \partial {\partial x} \frac \partial {\partial x'}
\operatorname{Cov}[f(x), f(x')] = \\
&= \frac \partial {\partial x} \frac \partial {\partial x'} k(x, x').
With :mod:`lsqfitgp` it's easy to use derivatives of functions in fits (the
automatic derivative calculations are implemented with `jax
`_). Let's dive into the code::
import lsqfitgp as lgp
import numpy as np
import gvar
gp = lgp.GP(lgp.ExpQuad())
x = np.linspace(-5, 5, 200)
gp = (gp
.addx(x, 'foo')
.addx(x, 'bar', deriv=1)
)
We said ``deriv=1`` to :meth:`~GP.addx`, easy as that. Let's plot a sample from
the prior::
from matplotlib import pyplot as plt
fig, ax = plt.subplots(num='lsqfitgp example')
y = gp.prior()
sample = gvar.sample(y)
ax.plot(x, sample['foo'], label='function')
ax.plot(x, sample['bar'], label='derivative', linestyle='--')
ax.axhline(0, linestyle=':', color='gray', zorder=-1)
ax.legend()
fig.savefig('derivatives1.png')
.. image:: derivatives1.png
This time we used :meth:`~GP.prior` without arguments, i.e., we did not specify
if we wanted ``'foo'`` or ``'bar'``. If you print ``sample``, you will see it
is a dictionary of arrays::
print(repr(sample))
Output:
.. code-block:: text
BufferDict({
'foo':
array([-0.54830765, -0.44060242, -0.33276906, -0.22575309, -0.12042057,
...
0.54683066, 0.51520693, 0.48205661, 0.44766003, 0.41235182]),
'bar':
array([ 2.13527672, 2.14804117, 2.14081828, 2.11552881, 2.07431084,
...
-0.61230835, -0.64528377, -0.67311803, -0.69489595, -0.70976863]),
})
:mod:`lsqfitgp` (and also `gvar`) can work with arrays or dictionaries of
arrays/scalars. This comes in handy to carry around all the numbers in one
object without forgetting the names of things.
Looking at the plot, the points where the derivative is zero seem to
correspond to minima/maxima of the function. Let's check this more accurately::
condition = np.diff(np.sign(sample['bar'])) != 0
xcenter = 1/2 * (x[1:] + x[:-1])
zeros = xcenter[condition]
for zero in zeros:
ax.axvline(zero, linestyle=':', color='gray', zorder=-1)
fig.savefig('derivatives2.png')
.. image:: derivatives2.png
It works! Through the correlations, the prior "knows" that one line is the
derivative of the other.
For a more realistic example, we add data. Imagine you have some datapoints, and
that you also know that at a certain point the function has a maximum. You don't
know its value, but you know it must have a maximum. In terms of derivatives, a
maximum is given by a zero first derivative and a negative second derivative. It
won't be complicated. I will stop using *foo* and *bar* and give meaningful
names to the points::
x = np.array([-5, -4, -3, 3, 4, 5])
yerr = 0.1
y = np.cos(x) # we got bored of sines already
y += yerr * np.random.randn(len(x))
y = gvar.gvar(y, yerr * np.ones(len(x)))
gp = (lgp
.GP(lgp.ExpQuad(scale=2))
.addx(x, 'data')
.addx(0, 'maximum-slope', deriv=1) # the cosine maximum is in 0
.addx(0, 'maximum-curvature', deriv=2)
)
xplot = np.linspace(-6, 6, 200)
gp = gp.addx(xplot, 'plot')
given = {
'data': y,
'maximum-slope': 0, # exactly zero
'maximum-curvature': gvar.gvar(-1, 0.3) # -1 ± 0.3
}
ypost = gp.predfromdata(given)
ax.cla()
ax.errorbar(x, gvar.mean(y), yerr=gvar.sdev(y), fmt='.k', capsize=2)
for sample in gvar.raniter(ypost, 4):
deriv2 = sample['maximum-curvature']
ax.plot(xplot, sample['plot'], label='deriv2 = {:.2f}'.format(deriv2))
ax.legend()
fig.savefig('derivatives3.png')
.. image:: derivatives3.png
Very good! However, I'm cheating. I modified the ``scale`` parameter until
I found that ``scale=2`` would give something similar to a cosine. Try to guess
what happens if you change the scale, and then try modifying it.