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.
- luas/GP.py +2097 -0
- luas/__init__.py +14 -0
- luas/convenience_fns.py +1035 -0
- luas/exoplanet/__init__.py +8 -0
- luas/exoplanet/transit_fns.py +405 -0
- luas/kernel_interface.py +59 -0
- luas/kernel_selector.py +296 -0
- luas/kernels/BlockKernel.py +621 -0
- luas/kernels/Conditional.py +174 -0
- luas/kernels/DoubleMultiLowRankKernel.py +535 -0
- luas/kernels/ExtendedLuasKernel.py +578 -0
- luas/kernels/GeneralKernel.py +354 -0
- luas/kernels/LuasKernel.py +904 -0
- luas/kernels/LuasKernelND.py +268 -0
- luas/kernels/LuasLasrachKernel.py +525 -0
- luas/kernels/LuasPlusMultiLowRankKernel.py +587 -0
- luas/kernels/MixingMatGeneral.py +320 -0
- luas/kernels/MixingMatQuasisep.py +336 -0
- luas/kernels/MultiLowRankKernel.py +539 -0
- luas/kernels/SeparableKernel.py +322 -0
- luas/kernels/WhiteNoiseKernel.py +230 -0
- luas/kernels/__init__.py +20 -0
- luas/kernels/base.py +434 -0
- luas/kernels/covtype.py +1956 -0
- luas/kernels/data/matern_SSMs.npz +0 -0
- luas/kernels/data/matern_SSMs_stable.npz +0 -0
- luas/kernels/data/sq_exp_modal_decomps.npz +0 -0
- luas/kernels/diagonal.py +55 -0
- luas/kernels/householder.py +431 -0
- luas/kernels/lowrank.py +240 -0
- luas/kernels/modal_decomp_calc.py +261 -0
- luas/kernels/quasisep.py +329 -0
- luas/kernels/tinygp_ext.py +939 -0
- luas/kronecker_fns.py +271 -0
- luas/luas_types.py +28 -0
- luas/numpyro_ext.py +75 -0
- luas/priors.py +141 -0
- luas/pymc_ext.py +191 -0
- luas/stable_gradients.py +412 -0
- luas_gp-0.1.0.dist-info/METADATA +64 -0
- luas_gp-0.1.0.dist-info/RECORD +44 -0
- luas_gp-0.1.0.dist-info/WHEEL +5 -0
- luas_gp-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|