.. lsqfitgp/docs/in.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 .. _multiin: Multidimensional input ====================== :mod:`lsqfitgp` supports multidimensional input through `numpy structured arrays `_. Elements of structured arrays have named fields:: import numpy as np x = np.linspace(-3, 3, 30) y = np.linspace(-3, 3, 30) xy = np.empty((len(x), len(y)), dtype=[('x', float), ('y', float)]) xy['x'] = x[:, None] xy['y'] = y[None, :] Here we used a bit of Numpy indexing and broadcasting to fill a 30x30 array ``xy`` with a grid of x, y coordinates. We specified a data type ``[('x', float), ('y', float)]`` for the array ``xy``, this means that each element of ``xy`` is a pair of numbers. We specified a shape ``(len(x), len(y))``, where ``x`` and ``y`` are the two linspaces, and then filled separately the ``'x'`` and ``'y'`` fields of the ``xy`` elements with the linspaces. ``xy`` is a 2D array, while ``x`` and ``y`` are 1D, so using ``None`` we added a dummy dimension along which the copying is repeated. The colons ``:`` are empty slices and leave the dimension untouched. It is not important for ``xy`` to be a 2D array because we are using bidimensional points, it is just a convenience for building a rectangular grid. Do not confuse the number of dimensions of the array with the number of fields in its elements. We only used 30 points for the side of the grid. This is because 30x30 = 900 and at around 1000 datapoints :mod:`lsqfitgp` starts being slow. This is an inherent limitation of Gaussian processes, which can be overcome only with approximations or specialized algorithms. Now we put ``xy`` into a :class:`GP` as usual and extract a sample from the prior. :: import lsqfitgp as lgp import gvar gp = (lgp .GP(lgp.ExpQuad()) .addx(xy, 'foo') ) prior = gp.prior('foo') sample = gvar.sample(prior) We plot the sample in 3d:: from matplotlib import pyplot as plt fig, ax = plt.subplots(num='lsqfitgp example', subplot_kw=dict(projection='3d')) ax.plot_surface(xy['x'], xy['y'], sample, cmap='viridis') fig.savefig('in1.png') .. image:: in1.png We got a nice random surface. The :class:`ExpQuad` kernel just worked out of the box with multidimensional input. What kernel are we using really? :class:`ExpQuad` is a subclass of :class:`IsotropicKernel`. This means it only depends on the distance between points, i.e., it doesn't care what the input is as long as it can apply Pythagoras' theorem. So the kernel we used is: .. math:: k(x_1, y_1, x_2, y_2) = \exp \left( -\frac {(x_1 - x_2)^2 + (y_1 - y_2)^2} 2 \right). We can do more sophisticated things if we control how the kernel acts on each dimension. Let's use a random walk along :math:`x` and an exponential quadratic along :math:`y`:: gp = (lgp .GP(lgp.Wiener(dim='x', loc=-3) * lgp.ExpQuad(dim='y')) .addx(xy, 'foo') ) prior = gp.prior('foo') sample = gvar.sample(prior) ax.cla() ax.plot_surface(xy['x'], xy['y'], sample, cmap='viridis') fig.savefig('in2.png') .. image:: in2.png Uhm, quite unelegant. I don't like random walks any more. We used the ``dim`` keyword to specify the dimension, and ``loc=-3`` to translate the random walk (it raises an error if it gets negative input points). In many dimensions it can get tedious to specify all the field names, it would be simpler if multidimensional input was implemented with arrays. Luckily :mod:`numpy` supports array data types in fields. Let's redo the first example, this time without named fields:: xy = np.empty((len(x), len(y)), dtype=[('foo', float, 2)]) xy['foo'][..., 0] = x[:, None] xy['foo'][..., 1] = y[None, :] Ok, I lied a bit, there's still a field name around. It is needed because otherwise :mod:`lsqfitgp` would not know that the last axis with size 2 is a 2-dimensional point instead of a pair of points, so we encapsulate it in a field. :mod:`numpy` treats the ellipsis ``...`` as a string of colons ``:`` of the appropriate length. :: gp = (lgp .GP(lgp.ExpQuad()) .addx(xy, 'foo') ) prior = gp.prior('foo') sample = gvar.sample(prior) ax.cla() ax.plot_surface(xy['foo'][..., 0], xy['foo'][..., 1], sample, cmap='viridis') fig.savefig('in3.png') .. image:: in3.png However, with this method, it is not possible to apply a kernel only along a specific dimension. A kernel can be applied only on everything or on a single whole named field. By splitting your dimensions in named fields with different shapes this should still be flexible enough. To do arbitrary manipulations, write your own kernel.