Skip to content

Commit 0fe64be

Browse files
committed
gp: use Cholesky factor rather than explicit inverse
1 parent 705a338 commit 0fe64be

File tree

1 file changed

+22
-14
lines changed

1 file changed

+22
-14
lines changed

UncertainSCI/gp/__init__.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from . import kernel
88

99
import numpy as np
10+
from scipy import linalg
1011

1112
NUGGET = 1e-6
1213

@@ -17,7 +18,7 @@ class ScalarGaussianProcess():
1718
x_obs: np.ndarray | None = None
1819
y_obs: np.ndarray | None = None
1920
sigma: np.ndarray | None = None
20-
inv: np.ndarray | None = None
21+
cho_factor: tuple[np.ndarray, bool] | None = None
2122

2223
def __init__(self,
2324
mu: wrapper.ScalarFunction,
@@ -46,24 +47,26 @@ def condition(self, x: np.ndarray,
4647
self.x_obs = x
4748
self.y_obs = y
4849
self.sigma = sigma
49-
self.inv = np.linalg.inv(self.k(x) + sigma * np.eye(len(x)))
50+
self.cho_factor = linalg.cho_factor(self.k(x) + sigma * np.eye(len(x)))
51+
52+
def tune(self):
53+
pass
5054

5155
def mu_posterior(self, x: np.ndarray):
52-
if (self.x_obs is None) or (self.y_obs is None) or (self.inv is None):
56+
if (self.x_obs is None) or (self.y_obs is None) or (self.cho_factor is None):
5357
raise ValueError('GP not conditioned: call ScalarGaussianProcess.'
5458
'condition with observations before computing '
5559
'posterior!')
56-
return self.mu(x) + self.k(x, self.x_obs) @ self.inv @ \
57-
(self.y_obs - self.mu(self.x_obs))
60+
return self.mu(x) + self.k(x, self.x_obs) @ \
61+
linalg.cho_solve(self.cho_factor, self.y_obs - self.mu(self.x_obs))
5862

5963
def k_posterior(self, x1: np.ndarray, x2: np.ndarray | None = None):
60-
if (self.x_obs is None) or (self.y_obs is None) or (self.inv is None):
64+
if (self.x_obs is None) or (self.y_obs is None) or (self.cho_factor is None):
6165
raise ValueError('GP not conditioned: call ScalarGaussianProcess.'
6266
'condition with observations before computing '
6367
'posterior!')
6468
return (self.k(x1) if x2 is None else self.k(x1, x2)) - \
65-
self.k(x1, self.x_obs) @ self.inv @ \
66-
(self.k(self.x_obs, x1) if x2 is None else self.k(self.x_obs, x2))
69+
self.k(x1, self.x_obs) @ linalg.cho_solve(self.cho_factor, self.k(self.x_obs, x1) if x2 is None else self.k(self.x_obs, x2))
6770

6871
def sample_prior(self, x: np.ndarray, n: int = 1) -> np.ndarray:
6972
ell = np.linalg.cholesky(self.k(x) + NUGGET * np.eye(len(x)))
@@ -84,7 +87,7 @@ class VectorGaussianProcess():
8487
x_obs: np.ndarray | None = None
8588
y_obs: np.ndarray | None = None
8689
sigma: np.ndarray | None = None
87-
inv: np.ndarray | None = None
90+
cho_factor: tuple[np.ndarray, bool] | None = None
8891

8992
def __init__(self,
9093
mu: wrapper.VectorFunction,
@@ -121,23 +124,26 @@ def condition(self, x: np.ndarray,
121124
self.x_obs = x
122125
self.y_obs = y
123126
self.sigma = sigma
124-
self.inv = np.linalg.inv(self.k(x) + sigma.flatten() * np.eye(len(x) * self.cdim))
127+
self.cho_factor = linalg.cho_factor(self.k(x) + sigma.flatten() * np.eye(len(x) * self.cdim))
128+
129+
def tune(self):
130+
pass
125131

126132
def mu_posterior(self, x: np.ndarray):
127-
if (self.x_obs is None) or (self.y_obs is None) or (self.inv is None):
133+
if (self.x_obs is None) or (self.y_obs is None) or (self.cho_factor is None):
128134
raise ValueError('GP not conditioned: call VectorGaussianProcess.'
129135
'condition with observations before computing '
130136
'posterior!')
131137
return self.mu(x) + \
132-
(self.k(x, self.x_obs) @ self.inv @ (self.y_obs - self.mu(self.x_obs)).flatten()).reshape((len(x), self.cdim))
138+
(self.k(x, self.x_obs) @ linalg.cho_solve(self.cho_factor, (self.y_obs - self.mu(self.x_obs)).flatten())).reshape((len(x), self.cdim))
133139

134140
def k_posterior(self, x1: np.ndarray, x2: np.ndarray | None = None):
135-
if (self.x_obs is None) or (self.y_obs is None) or (self.inv is None):
141+
if (self.x_obs is None) or (self.y_obs is None) or (self.cho_factor is None):
136142
raise ValueError('GP not conditioned: call VectorGaussianProcess.'
137143
'condition with observations before computing '
138144
'posterior!')
139145
return (self.k(x1) if x2 is None else self.k(x1, x2)) - \
140-
self.k(x1, self.x_obs) @ self.inv @ (self.k(self.x_obs, x1) if x2 is None else self.k(self.x_obs, x2))
146+
self.k(x1, self.x_obs) @ linalg.cho_solve(self.cho_factor, self.k(self.x_obs, x1) if x2 is None else self.k(self.x_obs, x2))
141147

142148
def sample_prior(self, x: np.ndarray, n: int = 1) -> np.ndarray:
143149
if n > 1:
@@ -147,6 +153,7 @@ def sample_prior(self, x: np.ndarray, n: int = 1) -> np.ndarray:
147153
sn = (len(x) * self.cdim)
148154
sr = (len(x), self.cdim)
149155

156+
# TODO: make this faster (use precomputed cho_factor and Schur complement)
150157
ell = np.linalg.cholesky(self.k(x) + NUGGET * np.eye(self.cdim * len(x)))
151158
y = (self.mu(x)[..., None] if n > 1 else self.mu(x)) + \
152159
(ell @ np.random.normal(0, 1, sn)).reshape(sr)
@@ -160,6 +167,7 @@ def sample_posterior(self, x: np.ndarray, n: int = 1) -> tuple[np.ndarray, np.nd
160167
sn = (len(x) * self.cdim)
161168
sr = (len(x), self.cdim)
162169

170+
# TODO: make this faster (use precomputed cho_factor and Schur complement)
163171
ell = np.linalg.cholesky(self.k_posterior(x) + NUGGET * np.eye(self.cdim * len(x)))
164172
y = (self.mu_posterior(x)[..., None] if n > 1 else self.mu_posterior(x)) + \
165173
(ell @ np.random.normal(0, 1, (sn))).reshape(sr)

0 commit comments

Comments
 (0)