luas-gp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. luas/GP.py +2097 -0
  2. luas/__init__.py +14 -0
  3. luas/convenience_fns.py +1035 -0
  4. luas/exoplanet/__init__.py +8 -0
  5. luas/exoplanet/transit_fns.py +405 -0
  6. luas/kernel_interface.py +59 -0
  7. luas/kernel_selector.py +296 -0
  8. luas/kernels/BlockKernel.py +621 -0
  9. luas/kernels/Conditional.py +174 -0
  10. luas/kernels/DoubleMultiLowRankKernel.py +535 -0
  11. luas/kernels/ExtendedLuasKernel.py +578 -0
  12. luas/kernels/GeneralKernel.py +354 -0
  13. luas/kernels/LuasKernel.py +904 -0
  14. luas/kernels/LuasKernelND.py +268 -0
  15. luas/kernels/LuasLasrachKernel.py +525 -0
  16. luas/kernels/LuasPlusMultiLowRankKernel.py +587 -0
  17. luas/kernels/MixingMatGeneral.py +320 -0
  18. luas/kernels/MixingMatQuasisep.py +336 -0
  19. luas/kernels/MultiLowRankKernel.py +539 -0
  20. luas/kernels/SeparableKernel.py +322 -0
  21. luas/kernels/WhiteNoiseKernel.py +230 -0
  22. luas/kernels/__init__.py +20 -0
  23. luas/kernels/base.py +434 -0
  24. luas/kernels/covtype.py +1956 -0
  25. luas/kernels/data/matern_SSMs.npz +0 -0
  26. luas/kernels/data/matern_SSMs_stable.npz +0 -0
  27. luas/kernels/data/sq_exp_modal_decomps.npz +0 -0
  28. luas/kernels/diagonal.py +55 -0
  29. luas/kernels/householder.py +431 -0
  30. luas/kernels/lowrank.py +240 -0
  31. luas/kernels/modal_decomp_calc.py +261 -0
  32. luas/kernels/quasisep.py +329 -0
  33. luas/kernels/tinygp_ext.py +939 -0
  34. luas/kronecker_fns.py +271 -0
  35. luas/luas_types.py +28 -0
  36. luas/numpyro_ext.py +75 -0
  37. luas/priors.py +141 -0
  38. luas/pymc_ext.py +191 -0
  39. luas/stable_gradients.py +412 -0
  40. luas_gp-0.1.0.dist-info/METADATA +64 -0
  41. luas_gp-0.1.0.dist-info/RECORD +44 -0
  42. luas_gp-0.1.0.dist-info/WHEEL +5 -0
  43. luas_gp-0.1.0.dist-info/licenses/LICENSE +21 -0
  44. luas_gp-0.1.0.dist-info/top_level.txt +1 -0
luas/GP.py ADDED
@@ -0,0 +1,2097 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from copy import deepcopy
4
+ import jax
5
+ import jax.numpy as jnp
6
+ from typing import Any, Optional, Callable, Union, Dict, Tuple
7
+ from tqdm import tqdm
8
+ from warnings import warn
9
+
10
+ from luas.kernel_selector import kernel_selector
11
+ from luas.kernels.covtype import CovType
12
+ from luas.kernels.Conditional import Conditional
13
+ from luas.luas_types import Scalar, PyTree, JAXArray, Kernel
14
+ from luas.kronecker_fns import calc_data_shape
15
+ from luas.convenience_fns import (
16
+ array_to_pytree_2D,
17
+ pytree_to_array_2D,
18
+ order_list,
19
+ large_hessian_calc,
20
+ transf_from_unbounded_params,
21
+ transf_to_unbounded_params,
22
+ varying_params_wrapper,
23
+ get_corr_mat,
24
+ check_params_in_bounds,
25
+ bin_data_2D,
26
+ interp_params,
27
+ stack_params,
28
+ stack_obs_and_pred_points,
29
+ diagnose_bad_laplace,
30
+ )
31
+
32
+ __all__ = ["GP"]
33
+
34
+
35
+ class GP(object):
36
+ r"""Gaussian process interface for 1D data sets, and for 2D (or higher) data sets on a grid.
37
+
38
+ The covariance matrix of a 2D (or higher) data set is built from kernels along each dimension,
39
+ e.g. one along wavelength and one along time. When the object is created, ``luas`` looks at the
40
+ structure of these kernels and chooses an optimised method (one of the
41
+ :doc:`Kronecker kernel classes <kronecker_kernels>`) for calculating the log likelihood and
42
+ its gradients. ``verbose = True`` prints which one was chosen, and ``use_kernel`` can be
43
+ used to choose a different one.
44
+
45
+ Example:
46
+ A 2D GP with a squared exponential kernel along each dimension plus white noise::
47
+
48
+ def kf(p, X):
49
+ Kl = luas.kernels.SquaredExp(p["l_l"], sigma = p["h"])
50
+ Kt = luas.kernels.SquaredExp(p["l_t"])
51
+ Sl = luas.kernels.Noise(p["sigma_l"])
52
+ St = luas.kernels.Noise(1.)
53
+ return (Sl, St), (Kl, Kt)
54
+
55
+ gp = luas.GP(p, (x_l, x_t), kf = kf, mf = transit_2D)
56
+ logL = gp.logL(p, Y)
57
+
58
+ Args:
59
+ p (PyTree): Parameters used to evaluate the kernel function once when the object is created,
60
+ to check its structure and choose which GP optimisation to use. Must contain every parameter
61
+ the kernel function needs.
62
+ X (JAXArray | tuple): The input locations (regression variables). For 1D data this is a JAXArray
63
+ of shape ``(N,)``, or ``(d, N)`` for ``d`` different input variables. For 2D or higher this is a
64
+ tuple with one such JAXArray per dimension, in the same order as the dimensions of the
65
+ observed data array.
66
+ kf (Callable | CovType, optional): The kernel function, called as ``kf(p, X, *kf_args, **kf_kwargs)``.
67
+ For 1D data it should return a single kernel from :doc:`luas.kernels <kernels>` (or a sum or
68
+ product of kernels). For 2D or higher it should return either a tuple with one kernel per
69
+ dimension, representing the Kronecker product of those kernels, or a tuple of such tuples
70
+ which are summed. For example ``(Sl, St), (Kl, Kt)`` represents
71
+ :math:`\mathbf{S}_l \otimes \mathbf{S}_t + \mathbf{K}_l \otimes \mathbf{K}_t`, where the
72
+ first term is usually the noise term. It can also return a Kronecker kernel object directly,
73
+ or a fixed covariance object can be given instead of a function. If not given, the
74
+ covariance matrix is the identity matrix (unweighted least squares).
75
+ mf (Callable, optional): The deterministic mean function, called as ``mf(p, X, *mf_args, **mf_kwargs)``.
76
+ Must return a JAXArray with the same shape as the observed data. Defaults to zero.
77
+ logPrior (Callable, optional): The log prior, called as
78
+ ``logPrior(p, X, *logPrior_args, **logPrior_kwargs)`` and returning a scalar. Defaults to zero.
79
+ jit (bool, optional): Whether to ``jax.jit`` compile :meth:`logL`, :meth:`logP`,
80
+ :meth:`logL_stored`, :meth:`logP_stored`, :meth:`logL_hessianable` and
81
+ :meth:`logP_hessianable`. Set to ``False`` to use a mean function which isn't written in
82
+ ``JAX``, or to make debugging easier. Defaults to ``True``.
83
+ use_kernel (optional): A Kronecker kernel class to use instead of the one ``luas`` chooses
84
+ automatically, e.g. ``luas.GeneralKernel`` builds the full covariance matrix and uses a
85
+ Cholesky decomposition with no optimisations. When more than one optimisation is valid
86
+ (e.g. ``luas.LuasKernel`` and ``luas.LuasLasrachKernel``) it can be worth testing which is
87
+ fastest. Any keyword arguments the class needs can be given as extra keyword arguments.
88
+ kernel_select_kwargs (dict, optional): Keyword arguments passed to the automatic choice of GP
89
+ optimisation for 2D data, e.g. ``max_gen_cholesky_blocks``.
90
+ verbose (bool, optional): Whether to print which GP optimisation is being used. Defaults to ``True``.
91
+ kf_args (tuple, optional): Extra positional arguments passed to the kernel function.
92
+ kf_kwargs (dict, optional): Extra keyword arguments passed to the kernel function.
93
+ mf_args (tuple, optional): Extra positional arguments passed to the mean function.
94
+ mf_kwargs (dict, optional): Extra keyword arguments passed to the mean function.
95
+ logPrior_args (tuple, optional): Extra positional arguments passed to the log prior function.
96
+ logPrior_kwargs (dict, optional): Extra keyword arguments passed to the log prior function.
97
+ **kernel_kwargs: Any other keyword arguments are passed to the Kronecker kernel class when it is
98
+ built, e.g. ``fast_dim`` for ``luas.LuasLasrachKernel``. These override any values chosen
99
+ automatically.
100
+
101
+ Attributes:
102
+ x (JAXArray | tuple): The input locations ``X``.
103
+ dim (int): The number of dimensions of the data set.
104
+ data_shape (tuple): The shape of the data set, e.g. ``(N_l, N_t)``.
105
+ opt_name (str): The name of the GP optimisation (Kronecker kernel class) being used.
106
+
107
+ """
108
+
109
+ def __init__(
110
+ self,
111
+ p: PyTree,
112
+ X: Tuple,
113
+ kf: Optional[Callable] = None,
114
+ mf: Optional[Callable] = None,
115
+ logPrior: Optional[Callable] = None,
116
+ jit: Optional[bool] = True,
117
+ use_kernel: Kernel = None,
118
+ kernel_select_kwargs: Optional[PyTree] = {},
119
+ verbose = True,
120
+ kf_args = (), kf_kwargs = {},
121
+ mf_args = (), mf_kwargs = {},
122
+ logPrior_args = (), logPrior_kwargs = {},
123
+ **kernel_kwargs,
124
+ ):
125
+ self.x = X
126
+
127
+ if isinstance(X, tuple):
128
+ self.dim = len(X)
129
+ else:
130
+ self.dim = 1
131
+
132
+ for d in range(self.dim):
133
+ if (self.dim == 1 and X.ndim != 1) or (self.dim > 1 and X[d].ndim != 1):
134
+ raise Exception(f"Currently input locations for dimension {d} must be a 1D array, got shape {X[d].shape}."\
135
+ " In future this will be extended to allow multiple input variables as shape (d, N).")
136
+
137
+ self.data_shape = calc_data_shape(self.x)
138
+
139
+ # Specify kernel function and include any args or kwargs needed
140
+ if isinstance(kf, CovType) or kf is None:
141
+ self.kf = kf
142
+ elif isinstance(kf_args, tuple) and isinstance(kf_kwargs, dict):
143
+ self.kf = lambda p, X, **kwargs: kf(p, X, *kf_args, **kf_kwargs, **kwargs)
144
+ else:
145
+ raise Exception("""kf_args must be a Tuple and kf_kwargs must be a dict.
146
+ For a single argument use kf_args = (arg1,)""")
147
+
148
+ # Specify mean function and include any args or kwargs needed
149
+ if mf is None:
150
+ # Mean function returns zeros by default
151
+ # Returns a zero array which varies in shape depending on the inputs
152
+ self.mf = lambda p, X: jnp.zeros(calc_data_shape(X))
153
+ else:
154
+ # Ensure mean function is of the form mf(p, X) and for the
155
+ # observed inputs X should return an array of the same shape
156
+ # as the observed data Y.
157
+ if isinstance(mf_args, tuple):
158
+ self.mf = lambda p, X: mf(p, X, *mf_args, **mf_kwargs)
159
+ else:
160
+ raise Exception("mf_args must be a Tuple, for one argument use mf_args = (arg1,)")
161
+
162
+ # Specify logprior function and include any args or kwargs needed
163
+ if logPrior is None:
164
+ # Log Prior function returns zero by default
165
+ self.logPrior = lambda p, X: 0.
166
+ else:
167
+ # Custom logPrior function which must take only a single argument p
168
+ if isinstance(logPrior_args, tuple) and isinstance(logPrior_kwargs, dict):
169
+ self.logPrior = lambda p, X: logPrior(p, X, *logPrior_args, **logPrior_kwargs)
170
+ else:
171
+ raise Exception("""logPrior_args must be a Tuple and logPrior_kwargs must be a dict.
172
+ For a single argument use logPrior_args = (arg1,)""")
173
+
174
+ # Identify GP optimisation to use
175
+ self.build_kf = kernel_selector(p, self.x, kf = self.kf, use_kernel = use_kernel,
176
+ verbose = verbose, kernel_select_kwargs = kernel_select_kwargs,
177
+ **kernel_kwargs)
178
+ kf_eval = self.build_kf(p, self.x)
179
+
180
+ self.opt_name = kf_eval.opt_name
181
+
182
+ # aliases because Americans exist
183
+ self.optimize = self.optimise
184
+ self.orthogonalize = self.orthogonalise
185
+
186
+ if verbose:
187
+ print(f"Using: {self.opt_name}")
188
+
189
+ if "orthogonalise" in kernel_kwargs.keys() and kernel_kwargs["orthogonalise"] == True:
190
+ if kf_eval.opt_name == "MultiLowRankKernel":
191
+ print("Will orthogonalise components, note this will ignore any non-orthogonal component of the whitened alpha vectors!")
192
+ else:
193
+ raise Exception("Orthogonalise components only implemented for MultiLowRankKernel with N_alpha > 1")
194
+
195
+ if jit:
196
+ # Have to option to avoid JIT compiling which can sometimes be useful
197
+ # e.g. it can make debugging easier, or if using a mean function which cannot be JIT compiled
198
+ # Note that NumPyro will perform JIT compilation anyway but PyMC will not
199
+ self.logL = jax.jit(self.logL)
200
+ self.logP = jax.jit(self.logP)
201
+ self.logL_stored = jax.jit(self.logL_stored)
202
+ self.logP_stored = jax.jit(self.logP_stored)
203
+ self.logL_hessianable = jax.jit(self.logL_hessianable)
204
+ self.logP_hessianable = jax.jit(self.logP_hessianable)
205
+
206
+
207
+ def sample(self, p, add_mf = True, samples = 1, return_variance = False):
208
+ r"""Draws random samples from the GP. Useful for visualising a GP model and generating
209
+ synthetic data.
210
+
211
+ Samples are drawn using the matrix square root of the covariance matrix
212
+ :math:`\mathbf{K} = \mathbf{L}\mathbf{L}^T` as :math:`\vec{y} = \vec{\mu} + \mathbf{L}\vec{z}`,
213
+ where :math:`\vec{z}` is drawn from a standard normal distribution using ``numpy.random``
214
+ (so ``np.random.seed`` can be used for reproducible draws).
215
+
216
+ Args:
217
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
218
+ and any parameters needed by the mean function.
219
+ add_mf (bool, optional): Whether to add the mean function to the samples. Defaults to ``True``.
220
+ samples (int, optional): The number of samples to draw. Defaults to 1.
221
+ return_variance (bool, optional): If ``True`` (and ``samples > 1``), return a Monte Carlo estimate
222
+ of the variance at each data point from the samples instead of the samples themselves.
223
+ Used by :meth:`get_diagonal`. Defaults to ``False``.
224
+
225
+ Returns:
226
+ JAXArray: For ``samples = 1``, a single sample with shape ``GP.data_shape``. For ``samples > 1``,
227
+ an array of shape ``(samples, *GP.data_shape)``, or the estimated variance with shape
228
+ ``GP.data_shape`` if ``return_variance = True``.
229
+
230
+ """
231
+
232
+ kf_eval = self.build_kf(p, self.x)
233
+ kf_eval, stored_values = kf_eval.decompose(self.x, stored_values = {})
234
+
235
+ # The Conditional CovType has a non-square matrix square root
236
+ # which expects a random normal vector of a different shape to the output data
237
+ z_shape = getattr(kf_eval, "sqrt_input_shape", self.data_shape)
238
+
239
+ if samples == 1:
240
+ z = np.random.normal(size = z_shape)
241
+ R = kf_eval.matrix_sqrt(z)
242
+ else:
243
+ z = np.random.normal(size = (samples, *z_shape))
244
+ matrix_sqrt_jit = jax.jit(kf_eval.matrix_sqrt)
245
+
246
+ if return_variance:
247
+ variance = jnp.zeros(self.data_shape)
248
+ for i in tqdm(range(samples)):
249
+ # for loop because vmap can be super memory intensive
250
+ variance += matrix_sqrt_jit(z[i, ...])**2
251
+ return variance/(samples - 1)
252
+ else:
253
+ R = jnp.zeros((samples, *self.data_shape))
254
+ for i in tqdm(range(samples)):
255
+ # for loop because vmap can be super memory intensive
256
+ R = R.at[i, ...].set(matrix_sqrt_jit(z[i, ...]))
257
+
258
+ if add_mf:
259
+ R += self.mf(p, self.x)
260
+
261
+ return R
262
+
263
+ def get_diagonal(self, p, samples = None, **kwargs):
264
+ r"""Returns the diagonal of the covariance matrix, i.e. the variance at each data point.
265
+
266
+ Uses the exact diagonal if the kernel being used can calculate it. Otherwise, if ``samples``
267
+ is given, the diagonal is estimated from that many random samples (see :meth:`sample`), with an
268
+ error which decreases as ``1/sqrt(samples)``.
269
+
270
+ Args:
271
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
272
+ and any parameters needed by the mean function.
273
+ samples (int, optional): The number of samples to use if the diagonal has to be estimated.
274
+ If not given and the exact diagonal isn't available, an Exception is raised.
275
+ **kwargs: Passed to the kernel's ``diagonal`` method.
276
+
277
+ Returns:
278
+ JAXArray: The variance at each data point, with shape ``GP.data_shape``.
279
+
280
+ """
281
+
282
+ kf_eval = self.build_kf(p, self.x)
283
+
284
+ if hasattr(kf_eval, "diagonal"):
285
+ return kf_eval.diagonal(self.x, **kwargs)
286
+ else:
287
+ if samples is not None:
288
+ print("Estimating diagonal of GP covariance matrix stochastically, this may take a while for large models...")
289
+ diagonal_est = self.sample(p, samples = samples, return_variance = True)
290
+ return diagonal_est
291
+ else:
292
+ raise Exception("Evaluating the diagonal of the covariance matrix not implemented!\
293
+ You can evaluate it stochastically, e.g. by setting samples = 1000")
294
+
295
+ def logL(
296
+ self,
297
+ p: PyTree,
298
+ Y: JAXArray,
299
+ ) -> Scalar:
300
+ r"""Computes the log likelihood.
301
+
302
+ .. math::
303
+
304
+ \log \mathcal{L} = -\frac{1}{2} \vec{r}^T \mathbf{K}^{-1} \vec{r}
305
+ - \frac{1}{2} \log |\mathbf{K}| - \frac{N}{2} \log 2\pi
306
+
307
+ where :math:`\vec{r} = \vec{y} - \vec{\mu}` are the residuals of the data after subtracting the
308
+ mean function, :math:`\mathbf{K}` is the covariance matrix and :math:`N` is the total number of data points.
309
+
310
+ This method is JIT compiled unless the GP was created with ``jit = False``. Use
311
+ :meth:`logL_hessianable` instead if second derivatives (e.g. ``jax.hessian``) are needed.
312
+
313
+ Args:
314
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
315
+ and any parameters needed by the mean function.
316
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``),
317
+ e.g. ``(N_l, N_t)`` for 2D data.
318
+
319
+ Returns:
320
+ Scalar: The log likelihood.
321
+
322
+ """
323
+
324
+ # Calculate the residuals after subtraction of the deterministic mean function
325
+ R = Y - self.mf(p, self.x)
326
+ kf_eval = self.build_kf(p, self.x)
327
+
328
+ kf_eval, stored_values = kf_eval.decompose(self.x, stored_values = {})
329
+
330
+ logL_val = kf_eval.logL(R, stored_values = stored_values)
331
+
332
+ return logL_val
333
+
334
+
335
+ def pymc_model(
336
+ self,
337
+ p: PyTree,
338
+ Y: JAXArray,
339
+ param_bounds: PyTree = {},
340
+ vars: Optional[list] = None,
341
+ fixed_vars: Optional[list] = None,
342
+ bound_tol = 1e-9,
343
+ ):
344
+ r"""Builds a ``PyMC`` model of the GP posterior, for optimisation or sampling with ``PyMC``.
345
+
346
+ Each parameter being varied is given a uniform prior between its bounds in ``param_bounds``
347
+ (``pm.Uniform``), or an improper flat prior if it has no bounds (``pm.Flat``). The log posterior
348
+ from :meth:`logP_stored`, which includes the GP's own log prior function, is added to the model as a
349
+ ``pm.Potential``. Prints which parameters are being varied and which are fixed.
350
+
351
+ Requires ``PyMC`` to be installed.
352
+
353
+ Example:
354
+ ``param_bounds`` should be of the form ``param_bounds[par] = [lower_bound, upper_bound]``,
355
+ where the bounds are scalars or arrays with the same shape as ``p[par]``.
356
+
357
+ Args:
358
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
359
+ and any parameters needed by the mean function. Also passed to the log prior function. Used as the values of any fixed parameters and as the starting values of the
360
+ parameters being varied.
361
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``),
362
+ e.g. ``(N_l, N_t)`` for 2D data.
363
+ param_bounds (PyTree, optional): Lower and upper bounds for any of the parameters being varied.
364
+ vars (:obj:`list` of :obj:`str`, optional): The names of the parameters to vary. All other
365
+ parameters are kept fixed. Cannot be given together with ``fixed_vars``.
366
+ fixed_vars (:obj:`list` of :obj:`str`, optional): The names of the parameters to keep fixed, as an
367
+ alternative to ``vars``. If neither is given, all parameters are varied.
368
+ bound_tol (Scalar, optional): Starting values which lie exactly on a bound are moved inside it by
369
+ this amount (see :func:`luas.convenience_fns.check_params_in_bounds`). Defaults to ``1e-9``.
370
+
371
+ Returns:
372
+ (pm.Model, dict, PyTree): A tuple of three elements: the ``PyMC`` model; a dictionary of the
373
+ ``PyMC`` variables for each parameter being varied, together with the values of the fixed
374
+ parameters; and the starting values of the parameters being varied (as NumPy arrays), which can
375
+ be used as the starting point for ``pm.find_MAP`` or ``pm.sample``.
376
+
377
+ """
378
+ # Lazy import to avoid PyMC being a dependency unless needed
379
+ from luas.pymc_ext import build_pymc_model
380
+
381
+ # This function returns a PyTree p_vary which has only the (key, value) pairs of
382
+ # parameters being varied
383
+ # make_p is also returned which is a function with returns the parameters in p_vary
384
+ # with all fixed parameters added back in
385
+ p_vary, make_p = varying_params_wrapper(p, vars = vars, fixed_vars = fixed_vars)
386
+ p_fixed = {k:v for (k, v) in p.items() if k not in p_vary.keys()}
387
+
388
+ ordered_param_list = order_list(list(p_vary.keys()))
389
+
390
+ p_vary = check_params_in_bounds(p_vary, param_bounds, bound_tol = bound_tol)
391
+
392
+ print(f"Varying parameters: {ordered_param_list}\nFixed parameters: {list(p_fixed.keys())}")
393
+
394
+ # build a PyMC model for optimisation and inference
395
+ model, var_dict = build_pymc_model(self, p_vary, Y, ordered_param_list, p_fixed = p_fixed, param_bounds = param_bounds)
396
+
397
+ return model, var_dict, p_vary
398
+
399
+
400
+ def numpyro_model(
401
+ self,
402
+ p: PyTree,
403
+ Y: JAXArray,
404
+ param_bounds: PyTree = {},
405
+ vars: Optional[list] = None,
406
+ fixed_vars: Optional[list] = None,
407
+ bound_tol = 1e-9,
408
+ ):
409
+ r"""Builds a ``NumPyro`` model of the GP posterior, for optimisation or sampling with ``NumPyro``.
410
+
411
+ Each parameter being varied is given a uniform prior between its bounds in ``param_bounds``
412
+ (``pm.Uniform``), or an improper flat prior if it has no bounds (``pm.Flat``). The log posterior
413
+ from :meth:`logP_stored`, which includes the GP's own log prior function, is added to the model as a
414
+ ``pm.Potential``. Prints which parameters are being varied and which are fixed.
415
+
416
+ Requires ``PyMC`` to be installed.
417
+
418
+ Example:
419
+ ``param_bounds`` should be of the form ``param_bounds[par] = [lower_bound, upper_bound]``,
420
+ where the bounds are scalars or arrays with the same shape as ``p[par]``.
421
+
422
+ Args:
423
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
424
+ and any parameters needed by the mean function. Also passed to the log prior function. Used as the values of any fixed parameters and as the starting values of the
425
+ parameters being varied.
426
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``),
427
+ e.g. ``(N_l, N_t)`` for 2D data.
428
+ param_bounds (PyTree, optional): Lower and upper bounds for any of the parameters being varied.
429
+ vars (:obj:`list` of :obj:`str`, optional): The names of the parameters to vary. All other
430
+ parameters are kept fixed. Cannot be given together with ``fixed_vars``.
431
+ fixed_vars (:obj:`list` of :obj:`str`, optional): The names of the parameters to keep fixed, as an
432
+ alternative to ``vars``. If neither is given, all parameters are varied.
433
+ bound_tol (Scalar, optional): Starting values which lie exactly on a bound are moved inside it by
434
+ this amount (see :func:`luas.convenience_fns.check_params_in_bounds`). Defaults to ``1e-9``.
435
+
436
+ Returns:
437
+ (pm.Model, dict, PyTree): A tuple of three elements: the ``PyMC`` model; a dictionary of the
438
+ ``PyMC`` variables for each parameter being varied, together with the values of the fixed
439
+ parameters; and the starting values of the parameters being varied (as NumPy arrays), which can
440
+ be used as the starting point for ``pm.find_MAP`` or ``pm.sample``.
441
+
442
+ """
443
+ # Lazy import to avoid PyMC being a dependency unless needed
444
+ from luas.numpyro_ext import build_numpyro_model
445
+
446
+ # This function returns a PyTree p_vary which has only the (key, value) pairs of
447
+ # parameters being varied
448
+ # make_p is also returned which is a function with returns the parameters in p_vary
449
+ # with all fixed parameters added back in
450
+ p_vary, make_p = varying_params_wrapper(p, vars = vars, fixed_vars = fixed_vars)
451
+ p_fixed = {k:v for (k, v) in p.items() if k not in p_vary.keys()}
452
+
453
+ ordered_param_list = order_list(list(p_vary.keys()))
454
+
455
+ p_vary = check_params_in_bounds(p_vary, param_bounds, bound_tol = bound_tol)
456
+
457
+ # build a PyMC model for optimisation and inference
458
+ model = build_numpyro_model(self, p_vary, Y, ordered_param_list, p_fixed = p_fixed, param_bounds = param_bounds)
459
+
460
+ return model
461
+
462
+
463
+
464
+
465
+ def optimise(self, p_init, Y, param_bounds = {}, vars = None, fixed_vars = None, maxeval = 20000, include_transformed = False, **kwargs):
466
+ r"""Finds the parameters which maximise the posterior (the maximum a posteriori fit) using
467
+ ``pm.find_MAP``. You can also use the alias ``optimize`` because Americans.
468
+
469
+ Builds a ``PyMC`` model with :meth:`pymc_model` and optimises it starting from ``p_init``. Requires
470
+ ``PyMC`` to be installed.
471
+
472
+ Args:
473
+ p_init (PyTree): Starting values of all the parameters. Parameters which aren't being varied are
474
+ kept fixed at these values.
475
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``),
476
+ e.g. ``(N_l, N_t)`` for 2D data.
477
+ param_bounds (PyTree, optional): Lower and upper bounds for any of the parameters being varied,
478
+ in the form ``param_bounds[par] = [lower_bound, upper_bound]``.
479
+ vars (:obj:`list` of :obj:`str`, optional): The names of the parameters to vary. All other
480
+ parameters are kept fixed. Cannot be given together with ``fixed_vars``.
481
+ fixed_vars (:obj:`list` of :obj:`str`, optional): The names of the parameters to keep fixed, as an
482
+ alternative to ``vars``. If neither is given, all parameters are varied.
483
+ maxeval (int, optional): The maximum number of optimisation steps. Defaults to 5000.
484
+ include_transformed (bool, optional): Whether to also return the values of the bounded parameters
485
+ in the unbounded space ``PyMC`` optimises in. Defaults to ``False``.
486
+ **kwargs: Passed to ``pm.find_MAP``, e.g. ``method``.
487
+
488
+ Returns:
489
+ PyTree: A copy of ``p_init`` with the parameters being varied replaced by their optimised values.
490
+
491
+ """
492
+ # Lazy import to avoid PyMC being a dependency unless needed
493
+ from luas.pymc_ext import find_MAP
494
+
495
+ pymc_model, var_dict, p_pymc = self.pymc_model(p_init, Y, param_bounds = param_bounds, vars = vars, fixed_vars = fixed_vars)
496
+
497
+ # Use PyMC's maximum posteriori optimisation function
498
+ map_estimate = find_MAP(
499
+ model = pymc_model, # PyMC model to optimise
500
+ include_transformed = include_transformed, # If this is true it will also output the PyMC transformed values of bounded parameters
501
+ start = p_pymc, # Starting point of optimisations
502
+ maxeval = maxeval, # Maximum steps to run (normally will converge before this)
503
+ **kwargs,
504
+ )
505
+
506
+ # Create a new dictionary of optimised values which includes our fixed parameters
507
+ p_opt = deepcopy(p_init)
508
+ p_opt.update(map_estimate)
509
+
510
+ return p_opt
511
+
512
+
513
+ def logL_stored(
514
+ self,
515
+ p: PyTree,
516
+ Y: JAXArray,
517
+ stored_values: PyTree,
518
+ ) -> Tuple[Scalar, PyTree]:
519
+ r"""Computes the log likelihood and also returns values stored from the decomposition of
520
+ the covariance matrix.
521
+
522
+ Takes the same inputs and returns the same outputs as the function ``luas`` uses for inference with
523
+ ``PyMC``, which passes the stored values from each call on to the next. The input ``stored_values``
524
+ are passed on to the decomposition and reused by kernels which support this (``luas.LuasKernel``
525
+ created with ``use_stored_values = True``), which saves time when some hyperparameters are fixed or
526
+ are sampled separately (e.g. with blocked Gibbs sampling).
527
+
528
+ Note:
529
+ This method does not give accurate second derivatives (e.g. using ``jax.hessian``). Use
530
+ :meth:`logL_hessianable_stored` if they are needed.
531
+
532
+ Args:
533
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
534
+ and any parameters needed by the mean function.
535
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``),
536
+ e.g. ``(N_l, N_t)`` for 2D data.
537
+ stored_values (PyTree): Values stored from a previous decomposition of the covariance
538
+ matrix, such as the output of a previous call to this method. Their contents depend on
539
+ the kernel class being used.
540
+
541
+ Returns:
542
+ (Scalar, PyTree): A tuple of the log likelihood and the values stored from the decomposition of
543
+ the covariance matrix.
544
+
545
+ """
546
+
547
+ R = Y - self.mf(p, self.x)
548
+
549
+ kf_eval = self.build_kf(p, self.x)
550
+ kf_eval, stored_values = kf_eval.decompose(self.x, stored_values = stored_values)
551
+ logL_val = kf_eval.logL(R, stored_values = stored_values)
552
+
553
+ return logL_val, stored_values
554
+
555
+
556
+ def logP(
557
+ self,
558
+ p: PyTree,
559
+ Y: JAXArray,
560
+ ) -> Scalar:
561
+ r"""Computes the log posterior (up to a constant), i.e. the log prior plus the log likelihood.
562
+
563
+ .. math::
564
+
565
+ \log \mathcal{P} = \log \pi(\vec{p}) + \log \mathcal{L}
566
+
567
+ where :math:`\log \pi(\vec{p})` is the log prior function the GP was created with, and the log
568
+ likelihood is calculated as in :meth:`logL`.
569
+
570
+ This method is JIT compiled unless the GP was created with ``jit = False``. Use
571
+ :meth:`logP_hessianable` instead if second derivatives (e.g. ``jax.hessian``) are needed.
572
+
573
+ Args:
574
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
575
+ and any parameters needed by the mean function. Also passed to the log prior function.
576
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``),
577
+ e.g. ``(N_l, N_t)`` for 2D data.
578
+
579
+ Returns:
580
+ Scalar: The log posterior.
581
+
582
+ """
583
+
584
+ R = Y - self.mf(p, self.x)
585
+
586
+ kf_eval = self.build_kf(p, self.x)
587
+ kf_eval, stored_values = kf_eval.decompose(self.x, stored_values = {})
588
+ logL_val = kf_eval.logL(R, stored_values = stored_values)
589
+
590
+ logPrior = self.logPrior(p, self.x)
591
+ logP = logPrior + logL_val
592
+
593
+ return logP
594
+
595
+
596
+ def logP_stored(
597
+ self,
598
+ p: PyTree,
599
+ Y: JAXArray,
600
+ stored_values: PyTree,
601
+ ) -> Tuple[Scalar, PyTree]:
602
+ r"""Computes the log posterior and also returns values stored from the decomposition of the
603
+ covariance matrix.
604
+
605
+ This is the function ``luas`` uses by default for inference with ``PyMC`` (see :meth:`pymc_model`),
606
+ which passes the stored values from each call on to the next. The input ``stored_values`` are passed
607
+ on to the decomposition and reused by kernels which support this (``luas.LuasKernel`` created with
608
+ ``use_stored_values = True``), which saves time when some hyperparameters are fixed or are sampled
609
+ separately (e.g. with blocked Gibbs sampling).
610
+
611
+ Note:
612
+ This method does not give accurate second derivatives (e.g. using ``jax.hessian``). Use
613
+ :meth:`logP_hessianable_stored` if they are needed.
614
+
615
+ Args:
616
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
617
+ and any parameters needed by the mean function. Also passed to the log prior function.
618
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``),
619
+ e.g. ``(N_l, N_t)`` for 2D data.
620
+ stored_values (PyTree): Values stored from a previous decomposition of the covariance
621
+ matrix, such as the output of a previous call to this method. Their contents depend on
622
+ the kernel class being used.
623
+
624
+ Returns:
625
+ (Scalar, PyTree): A tuple of the log posterior and the values stored from the decomposition of
626
+ the covariance matrix.
627
+
628
+ """
629
+
630
+
631
+ R = Y - self.mf(p, self.x)
632
+
633
+ kf_eval = self.build_kf(p, self.x)
634
+ kf_eval, stored_values = kf_eval.decompose(self.x, stored_values = stored_values)
635
+ logL_val = kf_eval.logL(R, stored_values = stored_values)
636
+
637
+ logPrior = self.logPrior(p, self.x)
638
+ logP = logPrior + logL_val
639
+
640
+ return logP, stored_values
641
+
642
+
643
+ def crossval(self, p, Y, params_to_stack = None, train_ind = None, **kwargs):
644
+ r"""Cross-validation for 2D data: fits the GP to a subset of the rows and/or columns of the
645
+ data and predicts the rest.
646
+
647
+ The GP is conditioned on the training rows and columns selected by ``train_ind``, and the remaining
648
+ rows and columns are predicted using :meth:`predict`. Comparing the predictions with the held-out
649
+ data tests how well the GP model predicts data it wasn't fit to.
650
+
651
+ Example:
652
+ To train on every other row and all columns, with a transit depth ``p["rp"]`` that has one value
653
+ per row::
654
+
655
+ train_l = np.arange(N_l) % 2 == 0
656
+ gp_mean, M, gp_cond = gp.crossval(p, Y, params_to_stack = [["rp"], []],
657
+ train_ind = (train_l, None))
658
+
659
+ Args:
660
+ p (PyTree): Parameters for the full data set, including any hyperparameters needed by the kernel
661
+ function and any parameters needed by the mean function.
662
+ Y (JAXArray): Observed data for the full data set, with shape ``(N_l, N_t)``.
663
+ params_to_stack (list): A list ``[params_l, params_t]`` of two lists of parameter names. The
664
+ parameters in ``params_l`` have one value per row (e.g. a wavelength-dependent transit depth)
665
+ and are split between the training and predicted rows, and likewise ``params_t`` for columns.
666
+ Use an empty list for a dimension which isn't split.
667
+ train_ind (tuple): A tuple ``(train_l, train_t)`` of boolean masks of shape ``(N_l,)`` and
668
+ ``(N_t,)`` which are ``True`` for the rows and columns to train on. Either can be ``None`` to
669
+ train on all rows or all columns, but not both.
670
+ **kwargs: Passed to :meth:`predict`, e.g. ``return_predicted_loc``.
671
+
672
+ Returns:
673
+ (JAXArray, JAXArray, GP): The outputs of :meth:`predict`: the GP predictive mean, the mean function
674
+ at the predicted locations and the conditional GP object.
675
+
676
+ """
677
+
678
+ x_l, x_t = self.x
679
+ train_l, train_t = train_ind
680
+ stack_p_dim1, stack_p_dim2 = params_to_stack
681
+
682
+ p_obs = deepcopy(p)
683
+ p_pred = deepcopy(p)
684
+
685
+ if train_l is None and train_t is None:
686
+ raise Exception("Both train_l and train_t cannot be None!")
687
+
688
+ if train_l is not None:
689
+ x_l_pred = x_l[..., ~train_l]
690
+ x_l = x_l[..., train_l]
691
+ Y = Y[train_l, :]
692
+
693
+ for par in stack_p_dim1:
694
+ p_pred[par] = p_obs[par][~train_l]
695
+ p_obs[par] = p_obs[par][train_l]
696
+ else:
697
+ x_l_pred = None
698
+
699
+ if train_t is not None:
700
+ x_t_pred = x_t[..., ~train_t]
701
+ x_t = x_t[..., train_t]
702
+ Y = Y[:, train_t]
703
+
704
+ for par in stack_p_dim2:
705
+ p_pred[par] = p_obs[par][~train_t]
706
+ p_obs[par] = p_obs[par][train_t]
707
+ else:
708
+ x_t_pred = None
709
+
710
+ X_obs = (x_l, x_t)
711
+ X_pred = (x_l_pred, x_t_pred)
712
+
713
+ gp_mean_pred, M_pred, gp_cond_obj = self.predict(
714
+ p_obs, Y,
715
+ p_pred = p_pred,
716
+ X_obs = X_obs,
717
+ X_pred = X_pred,
718
+ params_to_stack= params_to_stack,
719
+ **kwargs,
720
+ )
721
+ return gp_mean_pred, M_pred, gp_cond_obj
722
+
723
+
724
+
725
+ def predict(self, p, Y,
726
+ p_pred = None,
727
+ X_obs = None,
728
+ X_pred = None,
729
+ params_to_stack = None,
730
+ params_to_interpolate = None,
731
+ input_vecs_for_interp = None,
732
+ sort = None,
733
+ fast_dim = None, return_predicted_loc = True,
734
+ return_pred_err = False, samples = 1000):
735
+
736
+ r"""Makes GP predictions: computes the GP predictive mean, and optionally the predictive
737
+ uncertainty, at a set of prediction locations given the observed data.
738
+
739
+ The GP predictive mean and covariance at the prediction locations are
740
+
741
+ .. math::
742
+
743
+ \mathbb{E}[\vec{y}_*] = \vec{\mu}_* + \mathbf{K}_*^T \mathbf{K}^{-1} (\vec{y} - \vec{\mu})
744
+
745
+ .. math::
746
+
747
+ \mathrm{Var}[\vec{y}_*] = \mathbf{K}_{**} - \mathbf{K}_*^T \mathbf{K}^{-1} \mathbf{K}_*
748
+
749
+ where :math:`\mathbf{K}` is the covariance matrix of the observed data, :math:`\mathbf{K}_*` is the
750
+ covariance between the observed and prediction locations, :math:`\mathbf{K}_{**}` is the covariance
751
+ matrix of the prediction locations, and :math:`\vec{\mu}` and :math:`\vec{\mu}_*` are the mean
752
+ function at the observed and prediction locations.
753
+
754
+ To do this, the observed and prediction locations along each dimension are stacked together (and
755
+ sorted, see ``sort``), and the kernel is evaluated over the full stacked set of locations (see
756
+ :func:`luas.convenience_fns.stack_obs_and_pred_points`). The hyperparameters are assumed to be the
757
+ same at the observed and prediction locations, except for any parameters given in
758
+ ``params_to_stack`` or ``params_to_interpolate``.
759
+
760
+ Note:
761
+ The predictive uncertainty is estimated from random samples of the conditional GP, which can be
762
+ slow for large data sets. The returned conditional GP object can also be used to draw samples
763
+ from the predictive distribution directly, e.g. ``gp_cond.sample(p_pred)``, but its .
764
+
765
+ Args:
766
+ p (PyTree): Parameters at the observed locations, including any hyperparameters needed by the
767
+ kernel function and any parameters needed by the mean function.
768
+ Y (JAXArray): Observed data at the locations ``X_obs``.
769
+ p_pred (PyTree, optional): Parameters at the prediction locations. Only used for parameters
770
+ which take different values at the prediction locations (see ``params_to_stack``).
771
+ All other values are from p. Note that scalar values cannot be affected by p_pred,
772
+ so having a different value for observed and predicted locations requires making it
773
+ a vector as long as the dimension it's applied in.
774
+ X_obs (JAXArray | tuple, optional): Observed input locations, in the same format as ``X`` when
775
+ creating the GP. Defaults to the input locations the GP was created with.
776
+ X_pred (JAXArray | tuple, optional): Prediction locations. For 1D data a JAXArray. For 2D data a
777
+ tuple ``(x_l_pred, x_t_pred)``, where either entry can be ``None`` to make predictions only at
778
+ the observed locations along that dimension. If not given, predictions are made at the
779
+ observed locations.
780
+ params_to_stack (list, optional): Names of parameters which have one value per input location
781
+ (e.g. a wavelength-dependent transit depth). Their values from ``p`` and ``p_pred`` are joined
782
+ to cover the full set of stacked locations (see
783
+ :func:`luas.convenience_fns.stack_params`). For 1D data a list of names, for 2D data a list
784
+ with one list of names per dimension.
785
+ params_to_interpolate (list, optional): Names of parameters which have one value per input
786
+ location, whose values at the prediction locations are linearly interpolated from their
787
+ values in ``p`` (see :func:`luas.convenience_fns.interp_params`). For 1D data a list of
788
+ names, for 2D data a list with one list of names (or ``None``) per dimension.
789
+ input_vecs_for_interp (int | list, optional): If the input locations have more than one row
790
+ (shape ``(d, N)``), the row to interpolate over. For 2D data a list with one entry per
791
+ dimension, which must be given if ``params_to_interpolate`` is.
792
+ sort (bool | list, optional): Whether to sort the stacked input locations along each dimension
793
+ (one bool per dimension for 2D data). Most quasiseparable kernels need sorted inputs.
794
+ Defaults to ``True`` for every dimension.
795
+ fast_dim (int, optional): For 2D data when ``X_pred`` isn't given, the dimension (0 or 1) along
796
+ which a copy of the observed locations is stacked to make predictions at the observed
797
+ locations. Defaults to 0.
798
+ return_predicted_loc (bool, optional): For 2D data, if ``True`` only return predictions at the
799
+ prediction locations, otherwise return predictions at the full stacked set of observed and
800
+ prediction locations. Defaults to ``True``.
801
+ return_pred_err (bool, optional): Whether to also estimate the predictive standard deviation at
802
+ each prediction location. Defaults to ``False``.
803
+ samples (int, optional): The number of random samples used to estimate the predictive standard
804
+ deviation if ``return_pred_err = True``. The error in the estimate decreases as
805
+ ``1/sqrt(samples)``. Defaults to 1000.
806
+
807
+ Returns:
808
+ tuple: If ``return_pred_err = False``, a tuple ``(gp_mean, M_pred, gp_cond)``, otherwise
809
+ ``(gp_mean, pred_err, M_pred, gp_cond)``, where:
810
+
811
+ - **gp_mean** (JAXArray) - The GP predictive mean, including the mean function.
812
+ - **pred_err** (JAXArray) - The estimated predictive standard deviation.
813
+ - **M_pred** (JAXArray) - The mean function at the prediction locations.
814
+ - **gp_cond** (GP) - A GP object describing the conditional (predictive) distribution at the
815
+ prediction locations, which takes ``p_pred`` as its parameters. Warning: still experimental
816
+ and may break on some other methods!
817
+
818
+ """
819
+ if self.dim != 1 and self.dim != 2:
820
+ raise Exception(f"GP prediction only implemented for 1D and 2D GPs, not {self.dim} dimensions")
821
+
822
+ p_obs = deepcopy(p)
823
+
824
+ if X_obs is None:
825
+ # If observed locations not specified, assume training on the full dataset
826
+ X_obs = deepcopy(self.x)
827
+
828
+ if p_pred is None:
829
+ # If predicted params not specified, assume they match the observed parameters
830
+ # But may stack/interpolate to new predicted locations
831
+ p_pred = deepcopy(p_obs)
832
+
833
+ if sort is None:
834
+ # Important to sort for most quasisep kernels
835
+ sort = [True]*self.dim
836
+
837
+ if X_pred is None:
838
+ # If no prediction locations given then taken to be same as observed locations
839
+ pred_loc_equals_obs = True
840
+
841
+ if self.dim == 1:
842
+ X_pred = X_obs.copy()
843
+ elif fast_dim is None or fast_dim == 0:
844
+ X_pred = [X_obs[0], None]
845
+ elif fast_dim == 1:
846
+ X_pred = [None, X_obs[1]]
847
+ else:
848
+ raise Exception("fast_dim keyword argument must be None, 0 or 1")
849
+ else:
850
+ X_pred = list(X_pred)
851
+ pred_loc_equals_obs = False
852
+
853
+
854
+ # Stacks or interpolates parameters from observed locations to predicted locations for each dimension
855
+ # Works out location of predicted and observed locations within array
856
+ # Will sort the full stacked regression variables if requested, typically necessary for quasisep dimensions
857
+ if self.dim == 1:
858
+ X_full, X_pred, obs_points, pred_points, sort_fn = stack_obs_and_pred_points(X_obs, X_pred, sort = sort)
859
+ N_full = X_full.shape[-1]
860
+ sort_fns = [sort_fn]
861
+
862
+ if params_to_stack is not None:
863
+ params_to_stack = [params_to_stack]
864
+ observed_ind = jnp.arange(N_full)[obs_points]
865
+ predicted_ind = jnp.arange(N_full)[pred_points]
866
+ p_pred = interp_params(p_obs, p_pred, X_obs, X_pred,
867
+ interp_param = params_to_interpolate,
868
+ interp_row = input_vecs_for_interp)
869
+
870
+ elif self.dim == 2:
871
+ x_l_full, X_pred[0], obs_points_l, pred_points_l, sort_fn_l = stack_obs_and_pred_points(X_obs[0], X_pred[0], sort = sort[0])
872
+ x_t_full, X_pred[1], obs_points_t, pred_points_t, sort_fn_t = stack_obs_and_pred_points(X_obs[1], X_pred[1], sort = sort[1])
873
+ sort_fns = [sort_fn_l, sort_fn_t]
874
+ X_full = (x_l_full, x_t_full)
875
+ N_full = calc_data_shape(X_full)
876
+
877
+ observed_ind = jnp.ix_(jnp.arange(N_full[0])[obs_points_l],
878
+ jnp.arange(N_full[1])[obs_points_t])
879
+
880
+ if return_predicted_loc:
881
+ # Will only output locations at grid inputs of (X_pred)
882
+ predicted_ind = jnp.ix_(jnp.arange(N_full[0])[pred_points_l],
883
+ jnp.arange(N_full[1])[pred_points_t])
884
+ else:
885
+ # Returns the full output locations at X_full
886
+ predicted_ind = jnp.ix_(jnp.arange(N_full[0]), jnp.arange(N_full[1]))
887
+
888
+ if params_to_interpolate is not None:
889
+ for d in range(self.dim):
890
+ p_pred = interp_params(p_obs, p_pred, X_obs[d], X_pred[d],
891
+ interp_param = params_to_interpolate[d],
892
+ interp_row = input_vecs_for_interp[d])
893
+
894
+
895
+ # GP mean calculation
896
+ # Calculate mean function at observed locations
897
+ M_obs = self.mf(p_obs, X_obs)
898
+ R = Y - M_obs
899
+
900
+ # Calculate K^-1 R
901
+ kf_eval = self.build_kf(p_obs, X_obs)
902
+ kf_eval, stored_values = kf_eval.decompose(X_obs)
903
+ K_inv_R = kf_eval.inverse(R)
904
+ logdet_K_train = stored_values["logdet"]
905
+
906
+ # Keep the decomposed training kernel, the conditional covariance needs K^-1
907
+ kf_eval_train = kf_eval
908
+
909
+ # Calculate K_*^T K_inv_R by building the full ((K, K_*), (K_*^T, K_**))
910
+ K_inv_R_to_mult = jnp.zeros(N_full)
911
+ K_inv_R_to_mult = K_inv_R_to_mult.at[observed_ind].set(K_inv_R)
912
+
913
+ # mf of conditional GP
914
+ def gp_mean_cond_fn(p_pred, X_full):
915
+ p_full = stack_params(p_obs, p_pred, stack_names = params_to_stack, sort_fns = sort_fns)
916
+
917
+ kf_eval_full = self.build_kf(p_full, X_full)
918
+ Ks_Kinv_R_term = kf_eval_full.matmul(X_full, X_full, K_inv_R_to_mult, wn = False, full = True)
919
+
920
+ if pred_loc_equals_obs:
921
+ M_pred = M_obs.copy()
922
+ elif return_predicted_loc:
923
+ M_pred = self.mf(p_full, X_full)[predicted_ind]
924
+ else:
925
+ M_pred = self.mf(p_full, X_full)
926
+
927
+ gp_mean_pred = M_pred + Ks_Kinv_R_term[predicted_ind]
928
+ return gp_mean_pred, M_pred
929
+
930
+ def kf_cond(p_pred, X_full):
931
+ p_full = stack_params(p_obs, p_pred, stack_names = params_to_stack, sort_fns = sort_fns)
932
+
933
+ kf_eval_full = self.build_kf(p_full, X_full)
934
+ return Conditional(kf_eval_full, kf_train = kf_eval_train, X_full = X_full,
935
+ observed_ind = observed_ind, predicted_ind = predicted_ind,
936
+ logdet_K_train = logdet_K_train)
937
+
938
+ mf_cond = lambda p_pred, X_full: gp_mean_cond_fn(p_pred, X_full)[0]
939
+ gp_cond = GP(p_pred, X_full, mf = mf_cond, kf = kf_cond)
940
+ gp_cond.data_shape = calc_data_shape(X_pred)
941
+
942
+ gp_mean_pred, M_pred = gp_mean_cond_fn(p_pred, X_full)
943
+
944
+ if return_pred_err:
945
+ # Currently only estimates diagonal of predicted covariance stochastically
946
+ # Default is samples = 1000 which may be expensive for large models
947
+ # Relative error in estimate is 1/sqrt(samples): 1000 samples -> ~3.2% error in estimate
948
+ pred_err = jnp.sqrt(gp_cond.get_diagonal(p_pred, samples = samples))
949
+ return gp_mean_pred, pred_err, M_pred, gp_cond
950
+ else:
951
+ return gp_mean_pred, M_pred, gp_cond
952
+
953
+
954
+ def gp_mean_calc(
955
+ self,
956
+ p,
957
+ Y,
958
+ return_mf = False,
959
+ ):
960
+ r"""Computes the GP predictive mean at the observed locations.
961
+
962
+ .. math::
963
+
964
+ \mathbb{E}[\vec{y}] = \vec{\mu} + \mathbf{K}_{\mathrm{nw}} \mathbf{K}^{-1} (\vec{y} - \vec{\mu})
965
+
966
+ where :math:`\mathbf{K}_{\mathrm{nw}}` is the covariance matrix without white noise. Faster than
967
+ :meth:`predict` when only the fit at the observed locations is needed, and used by :meth:`plot`.
968
+
969
+ Args:
970
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
971
+ and any parameters needed by the mean function.
972
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``),
973
+ e.g. ``(N_l, N_t)`` for 2D data.
974
+ return_mf (bool, optional): Whether to also return the mean function. Defaults to ``False``.
975
+
976
+ Returns:
977
+ JAXArray or (JAXArray, JAXArray): The GP predictive mean (including the mean function), with
978
+ shape ``GP.data_shape``. If ``return_mf = True``, a tuple of the GP predictive mean and the mean
979
+ function.
980
+
981
+ """
982
+ # Perform GP regression at the observed data locations
983
+ M = self.mf(p, self.x)
984
+
985
+ res = Y - M
986
+ K_inv_R = self.apply_inverse(p, res)
987
+ gp_mean = M + self.matmul(p, K_inv_R, wn = False, full = True)
988
+
989
+ if return_mf:
990
+ return gp_mean, M
991
+ else:
992
+ return gp_mean
993
+
994
+
995
+ def laplace_approx(
996
+ self,
997
+ p: PyTree,
998
+ Y: PyTree,
999
+ regularise_std: Optional[PyTree] = None,
1000
+ vars: Optional[list] = None,
1001
+ fixed_vars: Optional[list] = None,
1002
+ return_array: Optional[bool] = False,
1003
+ large: Optional[bool] = False,
1004
+ large_block_size: Optional[int] = 20,
1005
+ large_jit: Optional[bool] = True,
1006
+ logP_fn: Optional[Callable] = None,
1007
+ hessian_mat: Optional[JAXArray] = None,
1008
+ n_top: Optional[int] = 3,
1009
+ ) -> Tuple[Union[PyTree, JAXArray], list]:
1010
+ r"""Computes the Laplace approximation of the posterior at the location of ``p``, which should
1011
+ be the best-fit (maximum a posteriori) parameters.
1012
+
1013
+ The Laplace approximation is a Gaussian approximation of the posterior centred on the best fit. Its
1014
+ covariance matrix is the negative inverse of the Hessian of the log posterior at the best fit, which is
1015
+ equivalent to a second-order Taylor series approximation of the log posterior. It gives a quick estimate
1016
+ of the posterior without running MCMC, and is also useful for initialising MCMC with a good tuning
1017
+ matrix when many correlated parameters are being sampled.
1018
+
1019
+ The negative Hessian (the precision matrix) must be positive definite for the covariance matrix to
1020
+ exist. It may not be if ``p`` is not at the best fit, if a parameter has no effect on the log
1021
+ posterior, or if some parameters are degenerate with each other. In that case a ``ValueError`` is
1022
+ raised which lists the parameters responsible (see
1023
+ :func:`luas.convenience_fns.diagnose_bad_laplace`). Poorly constrained or degenerate parameters can be
1024
+ handled by placing Gaussian priors on them with ``regularise_std``.
1025
+
1026
+ Note:
1027
+ This calculation can be memory intensive for large data sets with many free parameters. Setting
1028
+ ``large = True`` with a small ``large_block_size`` reduces the memory cost by calculating the
1029
+ Hessian in blocks of rows.
1030
+
1031
+ Args:
1032
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
1033
+ and any parameters needed by the mean function. Also passed to the log prior function.
1034
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``),
1035
+ e.g. ``(N_l, N_t)`` for 2D data.
1036
+ regularise_std (PyTree, optional): Standard deviations of Gaussian priors centred on ``p`` to
1037
+ place on parameters which are poorly constrained by the data, given as
1038
+ ``regularise_std[par] = std``. Each ``std`` is in the same units as ``p[par]``, must be
1039
+ positive, and can be a scalar or an array that broadcasts to the shape of ``p[par]``. For each
1040
+ of these parameters :math:`1/\sigma^2` is added to the diagonal of the precision matrix.
1041
+ Smaller values force ill-constrained parameters to appear better constrained, and a prior on
1042
+ one parameter of a degenerate pair is usually enough. This is a soft limit rather than a hard
1043
+ maximum: it also slightly reduces the variance of well-constrained parameters, and the variance
1044
+ is only guaranteed to be at most :math:`\sigma^2` if the precision matrix without the prior is
1045
+ positive semi-definite. The result is the Laplace approximation of the posterior multiplied by
1046
+ these priors, which is suitable as a tuning matrix for MCMC but should not be quoted as
1047
+ parameter uncertainties. Names not in ``p`` raise an exception, and names of parameters being
1048
+ kept fixed are ignored. Defaults to ``None``, which adds no priors.
1049
+ vars (:obj:`list` of :obj:`str`, optional): The names of the parameters to calculate the Laplace
1050
+ approximation for. All other parameters are kept fixed. Cannot be given together with
1051
+ ``fixed_vars``.
1052
+ fixed_vars (:obj:`list` of :obj:`str`, optional): The names of the parameters to keep fixed, as an
1053
+ alternative to ``vars``. If neither is given, all parameters are included.
1054
+ return_array (bool, optional): Whether to return the covariance matrix as a JAXArray, or as a nested
1055
+ PyTree where the covariance between parameters ``p1`` and ``p2`` is ``cov_mat[p1][p2]``.
1056
+ Defaults to ``False``.
1057
+ large (bool, optional): Whether to calculate the Hessian in blocks of rows to reduce the memory cost
1058
+ (see :func:`luas.convenience_fns.large_hessian_calc`). This can take significantly longer but
1059
+ makes no approximation. Defaults to ``False``.
1060
+ large_block_size (int, optional): The number of rows of the Hessian calculated at once when
1061
+ ``large = True``. Larger values may be faster but cost more memory. Defaults to 20.
1062
+ large_jit (bool, optional): Whether to JIT compile the Hessian calculation when ``large = True``.
1063
+ Defaults to ``True``.
1064
+ logP_fn (Callable, optional): The function to take the Hessian of, called as ``logP_fn(p, Y)``.
1065
+ Defaults to :meth:`logP_hessianable`.
1066
+ hessian_mat (JAXArray, optional): A Hessian matrix to use instead of calculating it, as a 2D JAXArray
1067
+ ordered as ``jax.flatten_util.ravel_pytree`` orders the parameters being varied. ``Y`` is then
1068
+ not used.
1069
+ n_top (int, optional): If the precision matrix is not positive definite, the number of parameters
1070
+ listed in the error message for each problematic direction. Values below 2 can hide which
1071
+ parameters are degenerate with each other. Defaults to 3.
1072
+
1073
+ Returns:
1074
+ (JAXArray, :obj:`list` of :obj:`str`) or (PyTree, :obj:`list` of :obj:`str`): A tuple of the
1075
+ covariance matrix of the Laplace approximation (as a JAXArray if ``return_array = True``, otherwise
1076
+ as a nested PyTree) and the list of parameter names in the order they appear in the JAXArray. The
1077
+ list matches how ``jax.flatten_util.ravel_pytree`` orders the parameters and is returned in both
1078
+ cases for consistency.
1079
+
1080
+ Raises:
1081
+ ValueError: If the precision matrix (including any ``regularise_std`` priors) is not positive
1082
+ definite. The message lists the parameters responsible.
1083
+ Exception: If the Hessian contains NaN or inf values, or if ``regularise_std`` names a parameter
1084
+ not in ``p`` or gives a value that is not positive.
1085
+
1086
+ """
1087
+
1088
+ if logP_fn is None:
1089
+ logP_fn = self.logP_hessianable
1090
+
1091
+ # This function returns a PyTree p_vary which has only the (key, value) pairs of
1092
+ # parameters being varied
1093
+ # make_p is also returned which is a function with returns the parameters in p_vary
1094
+ # with all fixed parameters added back in
1095
+ p_vary, make_p = varying_params_wrapper(p, vars = vars, fixed_vars = fixed_vars)
1096
+
1097
+ if hessian_mat is None:
1098
+ # Generate a wrapper function which takes only the parameters being varied and
1099
+ # calculates the log posterior (this avoids calculating derivatives of fixed parameters)
1100
+ logP_hessianable_wrapper = lambda p_vary: logP_fn(make_p(p_vary), Y)
1101
+
1102
+ if large:
1103
+ # For large data sets and many free parameters, breaking up the hessian calculation
1104
+ # into blocks of rows of size large_block_size has a much lower memory cost
1105
+ hessian_mat = large_hessian_calc(logP_hessianable_wrapper, p_vary, block_size = large_block_size,
1106
+ return_array = True, jit = large_jit)
1107
+ else:
1108
+ # If memory cost is not an issue then directly calculating the full hessian is faster
1109
+ hessian_mat = jax.hessian(logP_hessianable_wrapper)(p_vary)
1110
+
1111
+ # For inverting to a covariance matrix we need to convert the nested PyTree returned by
1112
+ # jax.hessian into a matrix which we can do with this helper function from luas.convenience_fns
1113
+ hessian_mat = pytree_to_array_2D(p_vary, hessian_mat)
1114
+
1115
+ if not bool(jnp.all(jnp.isfinite(hessian_mat))):
1116
+ raise Exception("The Hessian contains NaN or inf values, so the Laplace approximation is not valid.")
1117
+
1118
+ # Precision matrix is just the negative hessian
1119
+ precision_mat = -hessian_mat
1120
+
1121
+ if regularise_std is not None:
1122
+ for k in regularise_std.keys():
1123
+ if k not in p.keys():
1124
+ raise Exception(f"regularise_std prior on parameter {k} not found in model parameters.")
1125
+ elif jnp.any(regularise_std[k] <= 0.):
1126
+ raise Exception(f"Must give a positive value for normal priors on parameters, gave {regularise_std[k]} for {k}.")
1127
+
1128
+ regularise_dict = {}
1129
+ for k in p_vary.keys():
1130
+ if k in regularise_std.keys():
1131
+ regularise_dict[k] = jnp.broadcast_to(1/regularise_std[k]**2, jnp.shape(p_vary[k]))
1132
+ else:
1133
+ regularise_dict[k] = jnp.broadcast_to(0., jnp.shape(p_vary[k]))
1134
+ regularise_arr, _ = jax.flatten_util.ravel_pytree(regularise_dict)
1135
+ precision_mat += jnp.diag(regularise_arr)
1136
+
1137
+ # Performs the actual Laplace approximation by inverting the negative hessian/precision matrix
1138
+ L = jnp.linalg.cholesky(precision_mat)
1139
+
1140
+ if bool(jnp.all(jnp.isfinite(jnp.diag(L)))):
1141
+ L_inv = jax.scipy.linalg.solve_triangular(L, jnp.eye(L.shape[0]), lower=True)
1142
+ cov_mat = L_inv.T @ L_inv
1143
+ else:
1144
+ raise ValueError(
1145
+ "Could not invert the precision matrix because it is not positive definite:\n"
1146
+ + diagnose_bad_laplace(precision_mat, p_vary, n_top = n_top) +
1147
+ "\nTry re-optimising with a smaller tol and/or higher maxeval, or set regularise_std on the parameters listed."
1148
+ )
1149
+
1150
+ # Generate the list which gives the order of the parameters in the covariance matrix
1151
+ ordered_param_list = order_list(list(p_vary.keys()))
1152
+
1153
+ if return_array:
1154
+ return cov_mat, ordered_param_list
1155
+ else:
1156
+ # If returning a nested PyTree use array_to_pytree_2D to convert
1157
+ return array_to_pytree_2D(p_vary, cov_mat), ordered_param_list
1158
+
1159
+
1160
+ def laplace_approx_with_bounds(
1161
+ self,
1162
+ p: PyTree,
1163
+ Y: JAXArray,
1164
+ param_bounds: PyTree,
1165
+ vars: Optional[list] = None,
1166
+ fixed_vars: Optional[list] = None,
1167
+ large: Optional[bool] = False,
1168
+ large_block_size: Optional[int] = 20,
1169
+ return_array: Optional[bool] = False,
1170
+ large_jit: Optional[bool] = True,
1171
+ regularise_std: Optional[PyTree] = None,
1172
+ **kwargs,
1173
+ ) -> Tuple[Union[PyTree, JAXArray], list]:
1174
+ r"""Computes the Laplace approximation at the location of ``p`` in the unbounded space that
1175
+ ``PyMC`` and ``NumPyro`` sample bounded parameters in.
1176
+
1177
+ ``PyMC`` and ``NumPyro`` sample a parameter bounded between :math:`a` and :math:`b` as an unbounded
1178
+ variable :math:`x`, which is mapped to the bounded parameter using a sigmoid transform (see
1179
+ :func:`luas.convenience_fns.transf_from_unbounded`). This method calculates the Laplace approximation
1180
+ with respect to these unbounded variables, including the log Jacobian of the transform, so the result
1181
+ can be used as a tuning matrix for sampling with these libraries.
1182
+
1183
+ See :meth:`laplace_approx` for more details about the Laplace approximation.
1184
+
1185
+ Example:
1186
+ ``param_bounds`` should be of the form ``param_bounds[par] = [lower_bound, upper_bound]``, where
1187
+ ``lower_bound`` and ``upper_bound`` have the same shape as ``p[par]``.
1188
+
1189
+ Args:
1190
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
1191
+ and any parameters needed by the mean function. Also passed to the log prior function.
1192
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``),
1193
+ e.g. ``(N_l, N_t)`` for 2D data.
1194
+ param_bounds (PyTree): Lower and upper bounds for any of the parameters in ``p``.
1195
+ vars (:obj:`list` of :obj:`str`, optional): The names of the parameters to calculate the Laplace
1196
+ approximation for. All other parameters are kept fixed. Cannot be given together with
1197
+ ``fixed_vars``.
1198
+ fixed_vars (:obj:`list` of :obj:`str`, optional): The names of the parameters to keep fixed, as an
1199
+ alternative to ``vars``. If neither is given, all parameters are included.
1200
+ large (bool, optional): Whether to calculate the Hessian in blocks of rows to reduce the memory cost.
1201
+ Defaults to ``False``.
1202
+ large_block_size (int, optional): The number of rows of the Hessian calculated at once when
1203
+ ``large = True``. Defaults to 20.
1204
+ return_array (bool, optional): Whether to return the covariance matrix as a JAXArray, or as a nested
1205
+ PyTree. Defaults to ``False``.
1206
+ large_jit (bool, optional): Whether to JIT compile the Hessian calculation when ``large = True``.
1207
+ Defaults to ``True``.
1208
+ regularise_std (PyTree, optional): Standard deviations of Gaussian priors to place on poorly
1209
+ constrained parameters, passed to :meth:`laplace_approx` (see there for details). For parameters
1210
+ in ``param_bounds`` the standard deviation currently applies to the transformed (unbounded)
1211
+ variable rather than to the parameter itself, while other parameters use their own units. This
1212
+ will likely change in a future version. Every name must be a parameter being varied, as naming
1213
+ a fixed parameter raises an exception. Defaults to ``None``, which adds no priors.
1214
+ **kwargs: Passed to :meth:`laplace_approx`, e.g. ``n_top``.
1215
+
1216
+ Returns:
1217
+ (JAXArray, :obj:`list` of :obj:`str`) or (PyTree, :obj:`list` of :obj:`str`): A tuple of the
1218
+ covariance matrix of the Laplace approximation with respect to the unbounded variables (as a JAXArray
1219
+ if ``return_array = True``, otherwise as a nested PyTree) and the list of parameter names in the order
1220
+ they appear in the JAXArray.
1221
+
1222
+ """
1223
+
1224
+ # This function returns a PyTree p_vary which has only the (key, value) pairs of
1225
+ # parameters being varied
1226
+ # make_p is also returned which is a function with returns the parameters in p_vary
1227
+ # with all fixed parameters added back in
1228
+ p_vary, make_p = varying_params_wrapper(p, vars = vars, fixed_vars = fixed_vars)
1229
+
1230
+ # Check that parameters to be varied are actually within the specified bounds
1231
+ p_vary = check_params_in_bounds(p_vary, param_bounds)
1232
+
1233
+ # Transform the parameters being varied to the transformed values which are sampled by
1234
+ # PyMC and NumPyro
1235
+ p_transf = transf_to_unbounded_params(p_vary, param_bounds)
1236
+
1237
+ # Create a function which returns the transformed values back to the full set of parameters
1238
+ # untransformed including fixed parameters
1239
+ def transf_back_to_p(p_transf):
1240
+ p_vary = transf_from_unbounded_params(p_transf, param_bounds)
1241
+ return make_p(p_vary)
1242
+
1243
+ # Write a wrapper function which takes the transformed parameters and calculates the log Posterior
1244
+ pymc_logP_hessianable = lambda p_transf: self.logP_hessianable(transf_back_to_p(p_transf), Y)
1245
+
1246
+ if large:
1247
+ # For large data sets and many free parameters, breaking up the hessian calculation
1248
+ # into blocks of rows of size large_block_size has a much lower memory cost
1249
+ hessian_mat = large_hessian_calc(pymc_logP_hessianable, p_transf, block_size = large_block_size,
1250
+ return_array = False, jit = large_jit)
1251
+ else:
1252
+ # If memory cost is not an issue then directly calculating the full hessian is faster
1253
+ hessian_mat = jax.hessian(pymc_logP_hessianable)(p_transf)
1254
+
1255
+ # Loop over each parameter being varied
1256
+ for par in p_transf.keys():
1257
+ # Select just the bounded parameters
1258
+ if par in param_bounds.keys():
1259
+
1260
+ # Reshape to ensure hessian wrt 2D arrays can be dealt with by flattening
1261
+ n = np.size(p_transf[par])
1262
+ hessian_mat[par][par] = jnp.reshape(hessian_mat[par][par], (n, n))
1263
+
1264
+ # Add to the diagonal of the hessian an additional term
1265
+ # which is equal to the hessian of the jacobian of the transformation
1266
+ # performed by PyMC and NumPyro
1267
+ # This term is added to ensure the transformation these inference libraries perform
1268
+ # does not impact the choice of priors made
1269
+ exp_minus_p = jnp.exp(-p_transf[par])
1270
+ hessian_mat[par][par] += jnp.diag(jnp.ravel(-2*exp_minus_p/(1+exp_minus_p)**2))
1271
+
1272
+
1273
+ # Convert hessian from a nested PyTree to a 2D JAXArray for GP.laplace_approx to be able
1274
+ # to invert to calculate the covariance matrix
1275
+ hessian_mat = pytree_to_array_2D(p_transf, hessian_mat)
1276
+ cov_mat, ordered_param_list = self.laplace_approx(p_transf, Y, hessian_mat = hessian_mat,
1277
+ return_array = return_array, regularise_std = regularise_std,
1278
+ **kwargs)
1279
+
1280
+ return cov_mat, ordered_param_list
1281
+
1282
+
1283
+ def plot_cov_mat(
1284
+ self,
1285
+ p: PyTree,
1286
+ corr: Optional[bool] = False,
1287
+ wn: Optional[bool] = True,
1288
+ x_plot: Optional[JAXArray] = None,
1289
+ full: Optional[bool] = False,
1290
+ axes_labels = None,
1291
+ return_fig = False,
1292
+ ) -> plt.Figure:
1293
+ r"""Plots the covariance matrices along each dimension built from the parameters.
1294
+
1295
+ For 1D data this plots the full covariance matrix. For 2D (or higher) data each column of the figure
1296
+ is one dimension, with the first row showing the :math:`\mathbf{\Sigma}` (noise) matrix along that
1297
+ dimension and each following row showing the matrix from one of the other Kronecker terms. This needs
1298
+ a Kronecker kernel class which stores these matrices (e.g. ``luas.LuasKernel``).
1299
+
1300
+ Args:
1301
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function. Any extra
1302
+ parameters (e.g. for the mean function) are ignored.
1303
+ corr (bool, optional): If ``True``, plot correlation matrices instead of covariance matrices.
1304
+ Defaults to ``False``.
1305
+ wn (bool, optional): Whether to include white noise in the covariance matrices. Defaults to ``True``.
1306
+ x_plot (tuple, optional): The values along each axis used by ``plt.pcolormesh``, with one JAXArray
1307
+ per dimension. Defaults to the input locations the GP was created with (the first row, if there
1308
+ are several input variables along a dimension).
1309
+ full (bool, optional): Plotting the full covariance matrix of a 2D data set is not implemented, so
1310
+ this must be ``False``.
1311
+ axes_labels (:obj:`list` of :obj:`str`, optional): Axis labels for each dimension. Defaults to
1312
+ ``$x_1$``, ``$x_2$``, ...
1313
+ return_fig (bool, optional): Whether to return the figure. Defaults to ``False``.
1314
+
1315
+ Returns:
1316
+ plt.Figure: The figure, if ``return_fig = True``.
1317
+
1318
+ """
1319
+
1320
+ # If no x and y axes for the plots specified, defaults to x_l, x_t
1321
+ # If x_l or x_t contain multiple rows then pick the first row
1322
+ if x_plot is None:
1323
+ x_plot = ()
1324
+ if self.dim == 1:
1325
+ x_plot = (self.x,)
1326
+ else:
1327
+ for d in range(self.dim):
1328
+ if self.x[d].ndim > 1:
1329
+ x_plot += (self.x[d][0],)
1330
+ else:
1331
+ x_plot += (self.x[d],)
1332
+
1333
+ if axes_labels is None:
1334
+ axes_labels = [rf"$x_{d+1}$" for d in range(self.dim)]
1335
+
1336
+ kf_eval = self.build_kf(p, self.x)
1337
+ N_kron_terms = 1 + len(kf_eval.K_list)
1338
+
1339
+ if self.dim == 1:
1340
+ K_list = [[kf_eval.evaluate(self.x, self.x, wn = wn, full = True)]]
1341
+ else:
1342
+ K_list = []
1343
+ for d in range(self.dim):
1344
+ K_list.append([kf_eval.Sigma[d].evaluate(self.x[d], self.x[d], wn = wn, full = True)])
1345
+
1346
+ # Build each component matrix
1347
+ for i in range(N_kron_terms-1):
1348
+ K_list[-1].append(kf_eval.K_list[i][d].evaluate(self.x[d], self.x[d], wn = wn, full = True))
1349
+
1350
+
1351
+ fig, ax = plt.subplots(N_kron_terms, self.dim, figsize = (5*self.dim, 5*N_kron_terms), squeeze=False)
1352
+
1353
+ if full:
1354
+ raise Exception("Plotting full covariance matrix not implemented")
1355
+ else:
1356
+ # Individually plot each of the 4 component covariance matrices
1357
+
1358
+ for d in range(self.dim):
1359
+ for i in range(N_kron_terms):
1360
+ if corr:
1361
+ K_list[d][i] = get_corr_mat(K_list[d][i])
1362
+
1363
+ if i == 0:
1364
+ ax[0][d].set_title(rf"$\Sigma_d$$_{d+1}$")
1365
+ elif i == 1 and N_kron_terms == 2:
1366
+ ax[1][d].set_title(rf"K$_d$$_{d+1}$")
1367
+ else:
1368
+ ax[i][d].set_title(rf"K$_d$$_{d+1}$[{i}]")
1369
+
1370
+ ax[i][d].set_ylabel(axes_labels[d])
1371
+ ax[i][d].set_xlabel(axes_labels[d])
1372
+
1373
+ img1 = ax[i][d].pcolormesh(x_plot[d], x_plot[d], K_list[d][i])
1374
+ ax[i][d].invert_yaxis()
1375
+ plt.colorbar(mappable = img1, ax = ax[i][d])
1376
+
1377
+ plt.tight_layout()
1378
+
1379
+ if return_fig:
1380
+ return fig
1381
+
1382
+
1383
+
1384
+ def plot(
1385
+ self,
1386
+ p: PyTree,
1387
+ Y: JAXArray,
1388
+ x_plot = None,
1389
+ return_fig = None,
1390
+ return_fit = False,
1391
+ axes_labels = None,
1392
+ sharey = None,
1393
+ **fig_kwargs,
1394
+ ) -> plt.Figure:
1395
+ r"""Visualises the fit to the data.
1396
+
1397
+ Plots four panels: the observed data, the mean function, the GP predictive mean without the mean
1398
+ function, and the residuals after subtracting the GP predictive mean (including the mean function).
1399
+ For 1D data the first panel also overlays the mean function and the GP predictive mean on the data.
1400
+ For 2D data each panel is a ``plt.pcolormesh`` image.
1401
+
1402
+ For a good fit, the residuals should look like white noise with no remaining correlations, and the
1403
+ GP predictive mean without the mean function should only be fitting correlated noise, not structure
1404
+ which belongs in the mean function.
1405
+
1406
+ Args:
1407
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function and any
1408
+ parameters needed by the mean function.
1409
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``).
1410
+ x_plot (tuple, optional): The values along each axis of the plot, with one JAXArray per dimension.
1411
+ Defaults to the input locations the GP was created with (the first row, if there are several
1412
+ input variables along a dimension).
1413
+ return_fig (bool, optional): Whether to return the figure. Defaults to ``False``, unless 3D plotting
1414
+ which defaults to ``True``.
1415
+ return_fit (bool, optional): Whether to return the GP predictive mean and mean function (only if
1416
+ ``return_fig = False``). Defaults to ``False``.
1417
+ axes_labels (:obj:`list` of :obj:`str`, optional): Axis labels for each dimension.
1418
+ Defaults to ``["$x_1$", "$x_2$"]``.
1419
+ sharey (bool, optional): Whether the panels share a y-axis. Defaults to ``False`` for 1D data and
1420
+ ``True`` for 2D data.
1421
+ **fig_kwargs: Passed to ``plt.figure``.
1422
+
1423
+ Returns:
1424
+ plt.Figure or (JAXArray, JAXArray): The figure if ``return_fig = True``, or a tuple of the GP
1425
+ predictive mean and the mean function if ``return_fit = True``. Otherwise nothing is returned.
1426
+
1427
+ """
1428
+
1429
+ # If no x and y axes for the plots specified, defaults to x_l, x_t
1430
+ # If x_l or x_t contain multiple rows then pick the first row
1431
+ if x_plot is None:
1432
+ x_plot = ()
1433
+ if self.dim == 1:
1434
+ x_plot = (self.x,)
1435
+ else:
1436
+ for x in self.x:
1437
+ if x.ndim == 1:
1438
+ x_plot += (x.copy(),)
1439
+ else:
1440
+ x_plot += (x[0, :],)
1441
+
1442
+ if return_fig is None:
1443
+ if self.dim < 3:
1444
+ return_fig = False
1445
+ else:
1446
+ return_fig = True
1447
+
1448
+ if axes_labels is None:
1449
+ axes_labels = []
1450
+ for i in range(self.dim):
1451
+ axes_labels.append(rf"$x_{i}$")
1452
+
1453
+ if sharey is None:
1454
+ # default to sharey = False for 1D fits, but True for 2D fits
1455
+ if self.dim == 1:
1456
+ sharey = False
1457
+ else:
1458
+ sharey = True
1459
+
1460
+ # Perform GP regression at the observed data locations
1461
+ gp_mean, M = self.gp_mean_calc(p, Y, return_mf = True)
1462
+
1463
+ titles = ["Data", "Mean function", "GP mean (excl. mean function)", "Residual noise"]
1464
+ data = [Y, M, gp_mean - M, Y - gp_mean]
1465
+
1466
+ if self.dim == 1:
1467
+ fig = plt.figure(figsize = (20, 5), **fig_kwargs)
1468
+ ax = fig.subplots(1, 4, sharey = sharey)
1469
+ ax[0].plot(x_plot[0], Y, label = "Data")
1470
+ ax[0].plot(x_plot[0], M, label = "Mean function")
1471
+ ax[0].plot(x_plot[0], gp_mean, "k", label = "GP mean")
1472
+ ax[0].legend()
1473
+
1474
+ for i in range(1, 4):
1475
+ ax[i].plot(x_plot[0], data[i])
1476
+ ax[i].set_xlabel(axes_labels[0])
1477
+ ax[i].set_title(titles[i])
1478
+ plt.tight_layout()
1479
+
1480
+ elif self.dim == 2:
1481
+
1482
+ fig = plt.figure(figsize = (20, 5), **fig_kwargs)
1483
+ ax = fig.subplots(1, 4, sharey = sharey)
1484
+
1485
+ ims = [ax[i].pcolormesh(x_plot[1], x_plot[0], data[i], shading="nearest")
1486
+ for i in range(4)]
1487
+
1488
+ for i, (im, title) in enumerate(zip(ims, titles)):
1489
+ cbar = fig.colorbar(im, ax=ax[i], location="top", pad=0.02,
1490
+ fraction=0.03, aspect=40)
1491
+ cbar.ax.set_title(title, pad=4, fontsize = 14)
1492
+
1493
+ # Label axes
1494
+ ax[0].set_ylabel(axes_labels[0], fontsize = 14)
1495
+ for i in range(4):
1496
+ ax[i].set_xlabel(axes_labels[1], fontsize = 14)
1497
+
1498
+ # pcolormesh defaults to having the y-axis decrease with height which is weird so invert it
1499
+ plt.gca().invert_yaxis()
1500
+ plt.tight_layout()
1501
+
1502
+ elif self.dim == 3:
1503
+ # Interactive player
1504
+ from luas.convenience_fns import make_video
1505
+ fig = make_video(*data, X=self.x, titles=titles,
1506
+ axes_labels = axes_labels, **fig_kwargs)
1507
+
1508
+ else:
1509
+ raise Exception(f"Plotting of {self.dim}D data not yet implemented")
1510
+
1511
+ if return_fig:
1512
+ return fig
1513
+ elif return_fit:
1514
+ return gp_mean, M
1515
+
1516
+
1517
+ def plot_lightcurves(self, p, Y, sep = None, time_dim = 1, xlabel = r"Time", ylabel = "Values + offset", bin = None,
1518
+ figsize = (12, 8), sharey = True, titles = None, **fig_kwargs):
1519
+ r"""Plots each row of a 2D data set as a light curve, with the fit and the residuals.
1520
+
1521
+ Plots three panels with the light curves offset vertically from each other: the data with the mean
1522
+ function (dashed) and the GP predictive mean (solid); the data and the GP predictive mean after
1523
+ subtracting the mean function; and the residuals after subtracting the GP predictive mean.
1524
+
1525
+ Args:
1526
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function and any
1527
+ parameters needed by the mean function.
1528
+ Y (JAXArray): Observed data, with shape ``(N_l, N_t)``.
1529
+ sep (Scalar, optional): The vertical offset between light curves. Defaults to four times the
1530
+ largest standard deviation of any light curve.
1531
+ time_dim (int, optional): Which dimension of the data is time (0 or 1). Defaults to 1.
1532
+ xlabel (str, optional): Label of the x-axis. Defaults to ``"Time"``.
1533
+ ylabel (str, optional): Label of the y-axis. Defaults to ``"Values + offset"``.
1534
+ bin (tuple, optional): Bin sizes ``(l_bin, t_bin)`` to bin the data along each dimension before
1535
+ plotting (see :func:`luas.convenience_fns.bin_data_2D`).
1536
+ figsize (tuple, optional): Figure size. Defaults to ``(12, 8)``.
1537
+ sharey (bool, optional): Whether the panels share a y-axis. Defaults to ``True``.
1538
+ **fig_kwargs: Passed to ``plt.subplots``.
1539
+
1540
+ Returns:
1541
+ plt.Figure: The figure.
1542
+
1543
+ """
1544
+
1545
+ if self.dim != 2:
1546
+ raise Exception("plot_lightcurves only implemented for 2D data")
1547
+
1548
+ gp_mean, M = self.gp_mean_calc(p, Y, return_mf=True)
1549
+
1550
+ if bin is not None:
1551
+ _, _, Y = bin_data_2D(self.x[0], self.x[1], Y, bin[0], bin[1])
1552
+ _, _, M = bin_data_2D(self.x[0], self.x[1], M, bin[0], bin[1])
1553
+ x_bin0, x_bin1, gp_mean = bin_data_2D(self.x[0], self.x[1], gp_mean, bin[0], bin[1])
1554
+ x_t = [x_bin0, x_bin1][time_dim]
1555
+ else:
1556
+ x_t = self.x[time_dim]
1557
+
1558
+ if titles is None:
1559
+ titles = ["Raw light curves", "Data - Mean Function", "Data - GP"]
1560
+
1561
+ if time_dim == 0:
1562
+ gp_mean = gp_mean.T
1563
+ M = M.T
1564
+ Y = Y.T
1565
+
1566
+ N_l = Y.shape[0]
1567
+
1568
+ if sep is None:
1569
+ diff_between_curves = Y[:-1, :] - Y[1:, :]
1570
+ sep = 2 * diff_between_curves.max()
1571
+
1572
+ fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize = figsize, sharey = sharey, **fig_kwargs)
1573
+ for i in range(N_l):
1574
+ ax1.plot(x_t, Y[i, :] + np.arange(0, -sep*N_l, -sep)[i], 'o', color = "tab:blue", ms = 2)
1575
+ ax1.plot(x_t, M[i, :] + np.arange(0, -sep*N_l, -sep)[i], '--', color = "tab:orange")
1576
+ ax1.plot(x_t, gp_mean[i, :] + np.arange(0, -sep*N_l, -sep)[i], 'k-', ms = 3)
1577
+
1578
+ mean_M = M[i, :].mean()
1579
+ ax2.plot(x_t, Y[i, :] - M[i, :] + np.arange(mean_M, mean_M-sep*N_l, -sep)[i], 'o', color = "tab:blue", ms = 2)
1580
+ ax2.plot(x_t, np.ones(M.shape[1]) * np.arange(mean_M, mean_M-sep*N_l, -sep)[i], '--', color = "tab:orange")
1581
+ ax2.plot(x_t, gp_mean[i, :] - M[i, :] + np.arange(mean_M, mean_M-sep*N_l, -sep)[i], 'k-')
1582
+
1583
+ ax3.plot(x_t, Y[i, :] - gp_mean[i, :] + np.arange(mean_M, mean_M-sep*N_l, -sep)[i], 'o', color = "tab:blue", ms = 2)
1584
+ ax3.plot(x_t, np.arange(1, 1-sep*N_l, -sep)[i]*mean_M*np.ones_like(x_t), 'k-', ms = 3)
1585
+
1586
+ ax1.set_title(titles[0])
1587
+ ax2.set_title(titles[1])
1588
+ ax3.set_title(titles[2])
1589
+ ax1.set_xlabel(xlabel)
1590
+ ax2.set_xlabel(xlabel)
1591
+ ax3.set_xlabel(xlabel)
1592
+ ax1.set_ylabel(ylabel)
1593
+ plt.tight_layout()
1594
+ return fig
1595
+
1596
+
1597
+ def autocorrelate(
1598
+ self,
1599
+ p: PyTree,
1600
+ Y: JAXArray,
1601
+ max_sep_l: Optional[int] = None,
1602
+ max_sep_t: Optional[int] = None,
1603
+ include_gp_mean: Optional[bool] = True,
1604
+ mat: Optional[JAXArray] = None,
1605
+ plot: Optional[bool] = True,
1606
+ plot_kwargs: Optional[dict] = None,
1607
+ zero_centre: Optional[bool] = False,
1608
+ cov: Optional[bool] = False,
1609
+ ) -> Union[JAXArray, Tuple[JAXArray, plt.Figure]]:
1610
+ r"""Calculates (and plots) a quick, approximate 2D autocorrelation of the residuals using
1611
+ ``jax.scipy.signal.correlate2d``, to check if there is any correlation left in the residuals
1612
+ after subtracting the GP predictive mean.
1613
+
1614
+ If ``include_gp_mean = False``, the autocorrelation of the data minus the mean function (without the
1615
+ GP predictive mean) is calculated instead, which can help choose which kernels to fit with.
1616
+ Alternatively any matrix ``mat`` can be given, in which case ``p`` and ``Y`` are not used.
1617
+
1618
+ Note:
1619
+ This assumes the data is evenly spaced along both dimensions. It is also not an exact
1620
+ autocorrelation: the residuals are assumed to have zero mean, and all values are divided by the
1621
+ overall variance of the residuals rather than by the standard deviations of the specific residuals
1622
+ being multiplied together. This means some values can lie outside the interval [-1, 1], but it is
1623
+ fast and should be reasonably accurate except for widely separated parts of the data. For this
1624
+ reason, only separations up to half the size of the data are plotted by default.
1625
+
1626
+ Args:
1627
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function and any
1628
+ parameters needed by the mean function.
1629
+ Y (JAXArray): Observed data, with shape ``(N_l, N_t)``.
1630
+ max_sep_l (int, optional): The largest separation in rows to plot. Defaults to half the number of rows.
1631
+ max_sep_t (int, optional): The largest separation in columns to plot. Defaults to half the number of
1632
+ columns.
1633
+ include_gp_mean (bool, optional): Whether to subtract the GP predictive mean as well as the mean
1634
+ function from the data. Defaults to ``True``.
1635
+ mat (JAXArray, optional): A matrix to calculate the autocorrelation of instead of the residuals.
1636
+ plot (bool, optional): Whether to plot the autocorrelation. Defaults to ``True``.
1637
+ plot_kwargs (dict, optional): Passed to ``plt.imshow``, replacing the default plot settings.
1638
+ zero_centre (bool, optional): Whether to set the value at zero separation (which is always 1) to 0,
1639
+ which makes weak correlations at other separations easier to see. Defaults to ``False``.
1640
+ cov (bool, optional): If ``True``, return the autocovariance instead of the autocorrelation.
1641
+ Defaults to ``False``.
1642
+
1643
+ Returns:
1644
+ JAXArray or (JAXArray, AxesImage): The autocorrelation matrix. If ``plot = True``, a tuple of the
1645
+ autocorrelation matrix and the image returned by ``plt.imshow``.
1646
+
1647
+ """
1648
+
1649
+ # Sets the maximum separation to visualise the autocorrelation for
1650
+ # Once separations are too large the autocorrelation becomes very noisy
1651
+ # as it is based off of very few values
1652
+ if mat is not None:
1653
+ # If a general matrix mat is given, defaults to autocorrelation of half its length
1654
+ # in each dimension
1655
+ if max_sep_l is None:
1656
+ max_sep_l = mat.shape[0]//2
1657
+ if max_sep_t is None:
1658
+ max_sep_t = mat.shape[1]//2
1659
+
1660
+ # Set residuals matrix equal to mat
1661
+ res = mat
1662
+
1663
+ else:
1664
+ # For a matrix of residuals will also default to half the length of the data in
1665
+ # each dimension
1666
+ if max_sep_l is None:
1667
+ max_sep_l = self.data_shape[0]//2
1668
+ if max_sep_t is None:
1669
+ max_sep_t = self.data_shape[1]//2
1670
+
1671
+
1672
+ if include_gp_mean:
1673
+ # Perform GP regression
1674
+ gp_mean = self.gp_mean_calc(p, Y)
1675
+
1676
+ # Includes GP mean fit to data when subtracting from observed data
1677
+ res = Y - gp_mean
1678
+ else:
1679
+ M = self.mf(p, self.x)
1680
+ # Only calculates the observed data minus the deterministic mean function
1681
+ res = Y - M
1682
+
1683
+ # Performs the autocorrelation of the res matrix
1684
+ auto_corr = jax.scipy.signal.correlate2d(res, res)
1685
+
1686
+ # jax.scipy.signal.correlate2d will pad with zeros as necessary for the autocorrelation
1687
+ # which will artificially reduce the strength of correlation at large separations.
1688
+ # These lines autocorrelate a matrix of ones to divide auto_corr by so that a values
1689
+ # with more zeros used for padding will be divided through by lower numbers
1690
+ # This should normalise things correctly and ensures if the residuals are constant
1691
+ # then they will produce a correlation of ones everywhere.
1692
+ ones = jnp.ones_like(res)
1693
+ auto_corr_ones = jax.scipy.signal.correlate2d(ones, ones)
1694
+ auto_corr /= auto_corr_ones
1695
+
1696
+ # Find the centre of the auto_corr 2D array
1697
+ n_l, n_t = auto_corr.shape
1698
+ auto_corr_centre = ((n_l-1)//2, (n_t-1)//2)
1699
+
1700
+ if not cov:
1701
+ # Unless calculating the autocovariance we divide by the variance at zero separation
1702
+ auto_corr /= auto_corr[auto_corr_centre[0], auto_corr_centre[1]]
1703
+
1704
+ if zero_centre:
1705
+ # Can be helpful to zero the centre (which will always show correlation = 1)
1706
+ # to help visualise weaker correlation at non-zero separations
1707
+ auto_corr = auto_corr.at[auto_corr_centre[0], auto_corr_centre[1]].set(0.)
1708
+
1709
+ if plot:
1710
+ if mat is None and plot_kwargs is None:
1711
+ # Calculate the x and y axes values assuming equal separation of data points
1712
+ # It is assumed if giving values to plot_kwargs that this will be handled by the user
1713
+
1714
+ # First check if x_l and x_t contain multiple rows in which case pick the top row
1715
+ if self.x.ndim > 1:
1716
+ x_l_plot = self.x[0][0, :]
1717
+ else:
1718
+ x_l_plot = self.x[0]
1719
+
1720
+ if self.x.ndim > 1:
1721
+ x_t_plot = self.x[1][0, :]
1722
+ else:
1723
+ x_t_plot = self.x[1]
1724
+
1725
+ # Calculates the average separation between points along each dimension
1726
+ l_step = x_l_plot.ptp()/(x_l_plot.size-1)
1727
+ t_step = x_t_plot.ptp()/(x_t_plot.size-1)
1728
+
1729
+ # Calculate the maximum separations in each dimension in values given in x_l and x_t
1730
+ extent = [t_step*-(max_sep_t+0.5), t_step*(max_sep_t+0.5),
1731
+ l_step*(max_sep_l+0.5), l_step*-(max_sep_l+0.5)]
1732
+ else:
1733
+ extent = None
1734
+
1735
+ # Select the correct range of separations to plot
1736
+ l_plot_range = [auto_corr_centre[0]-max_sep_l, auto_corr_centre[0]+max_sep_l+1]
1737
+ t_plot_range = [auto_corr_centre[1]-max_sep_t, auto_corr_centre[1]+max_sep_t+1]
1738
+
1739
+ if plot_kwargs:
1740
+ fig = plt.imshow(auto_corr[l_plot_range[0]:l_plot_range[1], t_plot_range[0]:t_plot_range[1]],
1741
+ **plot_kwargs)
1742
+ else:
1743
+ fig = plt.imshow(auto_corr[l_plot_range[0]:l_plot_range[1], t_plot_range[0]:t_plot_range[1]],
1744
+ aspect = 'auto', interpolation = "none", extent = extent)
1745
+
1746
+ plt.xlabel(r"$\Delta$t")
1747
+ plt.ylabel(r"$\Delta$l")
1748
+ plt.colorbar()
1749
+
1750
+ return auto_corr, fig
1751
+
1752
+ else:
1753
+ # If not plotting then just returns the autocorrelation matrix
1754
+ return auto_corr
1755
+
1756
+
1757
+
1758
+ def transform_fn(
1759
+ self,
1760
+ p: PyTree,
1761
+ Y: JAXArray,
1762
+ inverse = True,
1763
+ transpose = 0,
1764
+ ) -> Scalar:
1765
+ r"""Applies the transformation the kernel uses along all dimensions except its fast dimension to
1766
+ the residuals of the data.
1767
+
1768
+ Some Kronecker kernel classes (``luas.LuasKernelND``, ``luas.LuasLasrachKernel``,
1769
+ ``luas.ExtendedLuasKernel``, ``luas.MultiLowRankKernel`` and ``luas.DoubleMultiLowRankKernel``) factorise
1770
+ the matrix square root of the covariance matrix as :math:`\mathbf{L} = \mathbf{T}\tilde{\mathbf{L}}`,
1771
+ where :math:`\mathbf{T}` acts along every dimension except the fast dimension and
1772
+ :math:`\tilde{\mathbf{L}}` acts along the fast dimension. This method subtracts the mean function from
1773
+ ``Y`` and applies :math:`\mathbf{T}^{-1}` (or :math:`\mathbf{T}` if ``inverse = False``) to the
1774
+ residuals. Useful for looking at the data in the basis the kernel uses.
1775
+
1776
+ Args:
1777
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
1778
+ and any parameters needed by the mean function.
1779
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``),
1780
+ e.g. ``(N_l, N_t)`` for 2D data.
1781
+ inverse (bool, optional): If ``True``, apply :math:`\mathbf{T}^{-1}`, otherwise apply
1782
+ :math:`\mathbf{T}`. Defaults to ``True``.
1783
+ transpose (int, optional): If ``1``, apply the transpose (:math:`\mathbf{T}^{-T}` or
1784
+ :math:`\mathbf{T}^T`) instead. Defaults to ``0``.
1785
+
1786
+ Returns:
1787
+ JAXArray: The transformed residuals, with shape ``GP.data_shape``.
1788
+
1789
+ """
1790
+
1791
+ # Calculate the residuals after subtraction of the deterministic mean function
1792
+ R = Y - self.mf(p, self.x)
1793
+ kf_eval = self.build_kf(p, self.x)
1794
+
1795
+ kf_eval, stored_values = kf_eval.decompose(self.x, stored_values = {})
1796
+
1797
+ if inverse:
1798
+ logL_val = kf_eval.inv_transform_fn(R, transpose = transpose)
1799
+ else:
1800
+ logL_val = kf_eval.transform_fn(R, transpose = transpose)
1801
+
1802
+ return logL_val
1803
+
1804
+
1805
+
1806
+ def apply_inverse(
1807
+ self,
1808
+ p: PyTree,
1809
+ R: JAXArray,
1810
+ ) -> Scalar:
1811
+ r"""Multiplies an array by the inverse of the covariance matrix,
1812
+ :math:`\mathbf{K}^{-1} \vec{r}`.
1813
+
1814
+ Args:
1815
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
1816
+ and any parameters needed by the mean function.
1817
+ R (JAXArray): The array to multiply, with shape ``GP.data_shape``.
1818
+
1819
+ Returns:
1820
+ JAXArray: :math:`\mathbf{K}^{-1} \vec{r}`, with shape ``GP.data_shape``.
1821
+
1822
+ """
1823
+
1824
+ kf_eval = self.build_kf(p, self.x)
1825
+
1826
+ kf_eval, stored_values = kf_eval.decompose(self.x, stored_values = {})
1827
+ K_inv_R = kf_eval.inverse(R)
1828
+
1829
+ return K_inv_R
1830
+
1831
+
1832
+ def matmul(
1833
+ self,
1834
+ p: PyTree,
1835
+ R: JAXArray,
1836
+ full = True,
1837
+ **kwargs,
1838
+ ) -> Scalar:
1839
+ r"""Multiplies an array by the covariance matrix, :math:`\mathbf{K} \vec{r}`.
1840
+
1841
+ Args:
1842
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
1843
+ and any parameters needed by the mean function.
1844
+ R (JAXArray): The array to multiply, with shape ``GP.data_shape``.
1845
+ full (bool, optional): Whether to use the full covariance matrix. Defaults to ``True``.
1846
+ **kwargs: Passed to the kernel's ``matmul`` method, e.g. ``wn = False`` to leave out white noise.
1847
+
1848
+ Returns:
1849
+ JAXArray: :math:`\mathbf{K} \vec{r}`, with shape ``GP.data_shape``.
1850
+
1851
+ """
1852
+
1853
+ kf_eval = self.build_kf(p, self.x)
1854
+
1855
+ if len(self.x) == 1:
1856
+ K_R = kf_eval.matmul(self.x[0], self.x[0], R, full = full, **kwargs)
1857
+ else:
1858
+ K_R = kf_eval.matmul(self.x, self.x, R, full = full, **kwargs)
1859
+
1860
+ return K_R
1861
+
1862
+
1863
+ def orthogonalise(self, p, A):
1864
+ r"""Removes the non-orthogonal part of a set of low-rank component vectors.
1865
+
1866
+ Only available for ``luas.MultiLowRankKernel``. The columns of ``A`` are whitened by the noise
1867
+ covariance matrix along the dimension other than the fast dimension, orthogonalised using a
1868
+ Householder QR decomposition, and then transformed back.
1869
+
1870
+ Args:
1871
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
1872
+ and any parameters needed by the mean function.
1873
+ A (JAXArray): The component vectors, with one vector per column.
1874
+
1875
+ Returns:
1876
+ JAXArray: The orthogonalised component vectors, with the same shape as ``A``.
1877
+
1878
+ """
1879
+ if self.opt_name == "MultiLowRankKernel":
1880
+ kf_eval = self.build_kf(p, self.x)
1881
+ return kf_eval.make_orthogonal(A, self.x)
1882
+ else:
1883
+ raise Exception("Orthogonalising components only compatible with MultiLowRankKernel")
1884
+
1885
+
1886
+ def apply_matrix_sqrt(
1887
+ self,
1888
+ p: PyTree,
1889
+ R: JAXArray,
1890
+ transpose = 0,
1891
+ ) -> Scalar:
1892
+ r"""Multiplies an array by the matrix square root of the covariance matrix,
1893
+ :math:`\mathbf{L} \vec{r}`, where :math:`\mathbf{K} = \mathbf{L} \mathbf{L}^T`.
1894
+
1895
+ If :math:`\vec{z}` is drawn from a standard normal distribution then :math:`\mathbf{L} \vec{z}` is a
1896
+ random sample from the GP (without the mean function).
1897
+
1898
+ Args:
1899
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
1900
+ and any parameters needed by the mean function.
1901
+ R (JAXArray): The array to multiply, with shape ``GP.data_shape``.
1902
+ transpose (int, optional): If ``1``, multiply by :math:`\mathbf{L}^T` instead. Defaults to ``0``.
1903
+
1904
+ Returns:
1905
+ JAXArray: :math:`\mathbf{L} \vec{r}` (or :math:`\mathbf{L}^T \vec{r}`), with shape ``GP.data_shape``.
1906
+
1907
+ """
1908
+
1909
+ kf_eval = self.build_kf(p, self.x)
1910
+
1911
+ kf_eval, stored_values = kf_eval.decompose(self.x, stored_values = {})
1912
+ L_R = kf_eval.matrix_sqrt(R, transpose = transpose)
1913
+
1914
+ return L_R
1915
+
1916
+
1917
+ def apply_matrix_inv_sqrt(
1918
+ self,
1919
+ p: PyTree,
1920
+ R: JAXArray,
1921
+ transpose = 0,
1922
+ ) -> Scalar:
1923
+ r"""Multiplies an array by the inverse of the matrix square root of the covariance matrix,
1924
+ :math:`\mathbf{L}^{-1} \vec{r}`, where :math:`\mathbf{K} = \mathbf{L} \mathbf{L}^T`.
1925
+
1926
+ Applied to the residuals of the data, this whitens them: if the GP model is correct the result should
1927
+ look like independent draws from a standard normal distribution.
1928
+
1929
+ Args:
1930
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
1931
+ and any parameters needed by the mean function.
1932
+ R (JAXArray): The array to multiply, with shape ``GP.data_shape``.
1933
+ transpose (int, optional): If ``1``, multiply by :math:`\mathbf{L}^{-T}` instead. Defaults to ``0``.
1934
+
1935
+ Returns:
1936
+ JAXArray: :math:`\mathbf{L}^{-1} \vec{r}` (or :math:`\mathbf{L}^{-T} \vec{r}`), with shape
1937
+ ``GP.data_shape``.
1938
+
1939
+ """
1940
+
1941
+ kf_eval = self.build_kf(p, self.x)
1942
+
1943
+ kf_eval, stored_values = kf_eval.decompose(self.x, stored_values = {})
1944
+ L_inv_R = kf_eval.matrix_inv_sqrt(R, transpose = transpose)
1945
+
1946
+ return L_inv_R
1947
+
1948
+
1949
+
1950
+ def logL_hessianable(
1951
+ self,
1952
+ p: PyTree,
1953
+ Y: JAXArray,
1954
+ ) -> Scalar:
1955
+ r"""Computes the log likelihood in a way which gives accurate second derivatives.
1956
+
1957
+ Returns the same value as :meth:`logL`, but uses a calculation which is more numerically stable for
1958
+ second derivatives such as ``jax.hessian``, at the cost of being slower for gradients. This method
1959
+ only returns the log likelihood, so ``jax.hessian`` must still be applied to it.
1960
+
1961
+ This method is JIT compiled unless the GP was created with ``jit = False``.
1962
+
1963
+ Args:
1964
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
1965
+ and any parameters needed by the mean function.
1966
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``),
1967
+ e.g. ``(N_l, N_t)`` for 2D data.
1968
+
1969
+ Returns:
1970
+ Scalar: The log likelihood.
1971
+
1972
+ """
1973
+
1974
+ # Subtract mean function from observed data
1975
+ R = Y - self.mf(p, self.x)
1976
+
1977
+ # Calculate log likelihood and stored values from decomposition
1978
+ kf_eval = self.build_kf(p, self.x)
1979
+ kf_eval, stored_values = kf_eval.decompose(self.x, stored_values = {})
1980
+ logL_val = kf_eval.logL_hessianable(R, stored_values = stored_values)
1981
+
1982
+ return logL_val
1983
+
1984
+
1985
+ def logL_hessianable_stored(
1986
+ self,
1987
+ p: PyTree,
1988
+ Y: JAXArray,
1989
+ stored_values: PyTree,
1990
+ ) -> Tuple[Scalar, PyTree]:
1991
+ r"""Computes the log likelihood in a way which gives accurate second derivatives, and also
1992
+ returns values stored from the decomposition of the covariance matrix.
1993
+
1994
+ The ``hessianable`` version of :meth:`logL_stored`. The input ``stored_values`` are passed on to
1995
+ the decomposition and reused by kernels which support this (``luas.LuasKernel`` created with
1996
+ ``use_stored_values = True``).
1997
+
1998
+ Args:
1999
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
2000
+ and any parameters needed by the mean function.
2001
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``),
2002
+ e.g. ``(N_l, N_t)`` for 2D data.
2003
+ stored_values (PyTree): Values stored from a previous decomposition of the covariance
2004
+ matrix, such as the output of a previous call to this method. Their contents depend on
2005
+ the kernel class being used.
2006
+
2007
+ Returns:
2008
+ (Scalar, PyTree): A tuple of the log likelihood and the values stored from the decomposition of
2009
+ the covariance matrix.
2010
+
2011
+ """
2012
+
2013
+ # Subtract mean function from observed data
2014
+ R = Y - self.mf(p, self.x)
2015
+
2016
+ kf_eval = self.build_kf(p, self.x)
2017
+ kf_eval, stored_values = kf_eval.decompose(self.x, stored_values = stored_values)
2018
+ logL_val = kf_eval.logL_hessianable(R, stored_values = stored_values)
2019
+
2020
+ return logL_val, stored_values
2021
+
2022
+
2023
+ def logP_hessianable(
2024
+ self,
2025
+ p: PyTree,
2026
+ Y: JAXArray,
2027
+ ) -> Scalar:
2028
+ r"""Computes the log posterior in a way which gives accurate second derivatives.
2029
+
2030
+ Returns the same value as :meth:`logP`, but uses a calculation which is more numerically stable for
2031
+ second derivatives such as ``jax.hessian``, at the cost of being slower for gradients. Used by
2032
+ :meth:`laplace_approx`. This method only returns the log posterior, so ``jax.hessian`` must still be
2033
+ applied to it.
2034
+
2035
+ This method is JIT compiled unless the GP was created with ``jit = False``.
2036
+
2037
+ Args:
2038
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
2039
+ and any parameters needed by the mean function. Also passed to the log prior function.
2040
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``),
2041
+ e.g. ``(N_l, N_t)`` for 2D data.
2042
+
2043
+ Returns:
2044
+ Scalar: The log posterior.
2045
+
2046
+ """
2047
+
2048
+ logPrior = self.logPrior(p, self.x)
2049
+ R = Y - self.mf(p, self.x)
2050
+
2051
+ kf_eval = self.build_kf(p, self.x)
2052
+ kf_eval, stored_values = kf_eval.decompose(self.x, stored_values = {})
2053
+ logL_val = kf_eval.logL_hessianable(R, stored_values = stored_values)
2054
+ logP = logPrior + logL_val
2055
+
2056
+ return logP
2057
+
2058
+
2059
+ def logP_hessianable_stored(
2060
+ self,
2061
+ p: PyTree,
2062
+ Y: JAXArray,
2063
+ stored_values: PyTree,
2064
+ ) -> Tuple[Scalar, PyTree]:
2065
+ r"""Computes the log posterior in a way which gives accurate second derivatives, and also
2066
+ returns values stored from the decomposition of the covariance matrix.
2067
+
2068
+ The ``hessianable`` version of :meth:`logP_stored`. The input ``stored_values`` are passed on to
2069
+ the decomposition and reused by kernels which support this (``luas.LuasKernel`` created with
2070
+ ``use_stored_values = True``).
2071
+
2072
+ Args:
2073
+ p (PyTree): Parameters, including any hyperparameters needed by the kernel function
2074
+ and any parameters needed by the mean function. Also passed to the log prior function.
2075
+ Y (JAXArray): Observed data, with the same shape as the data set (``GP.data_shape``),
2076
+ e.g. ``(N_l, N_t)`` for 2D data.
2077
+ stored_values (PyTree): Values stored from a previous decomposition of the covariance
2078
+ matrix, such as the output of a previous call to this method. Their contents depend on
2079
+ the kernel class being used.
2080
+
2081
+ Returns:
2082
+ (Scalar, PyTree): A tuple of the log posterior and the values stored from the decomposition of
2083
+ the covariance matrix.
2084
+
2085
+ """
2086
+
2087
+
2088
+ R = Y - self.mf(p, self.x)
2089
+
2090
+ kf_eval = self.build_kf(p, self.x)
2091
+ kf_eval, stored_values = kf_eval.decompose(self.x, stored_values = stored_values)
2092
+ logL_val = kf_eval.logL_hessianable(R, stored_values = stored_values)
2093
+
2094
+ logPrior = self.logPrior(p, self.x)
2095
+ logP = logPrior + logL_val
2096
+
2097
+ return logP, stored_values