Skip to content

Commit e79b1c2

Browse files
committed
ENH: Make regularized least squares as defualt reconstruction method, compute prior and noise if not given
1 parent b145d81 commit e79b1c2

File tree

3 files changed

+135
-47
lines changed

3 files changed

+135
-47
lines changed

pysensors/optimizers/_tpgr.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import warnings
2+
13
import numpy as np
24
from sklearn.base import BaseEstimator
35
from sklearn.utils.validation import check_is_fitted
@@ -15,19 +17,30 @@ class TPGR(BaseEstimator):
1517
1618
"""
1719

18-
def __init__(self, prior, n_sensors=None, noise=1):
19-
self.prior = prior
20+
def __init__(self, n_sensors=None, noise=None, prior="decreasing"):
2021
self.n_sensors = n_sensors
2122
self.noise = noise
2223
self.sensors_ = None
23-
self.G = None
24+
self.prior = prior
2425

25-
def fit(self, basis_matrix):
26-
if self.n_sensors is None:
27-
self.n_sensors = basis_matrix.shape[
28-
1
29-
] # Set number of sensors to number of basis modes if unspecified
30-
G = basis_matrix @ np.diag(self.prior)
26+
def fit(self, basis_matrix, singular_values=None):
27+
if isinstance(self.prior, str) and self.prior == "decreasing":
28+
computed_prior = singular_values
29+
elif isinstance(self.prior, np.ndarray):
30+
if self.prior.ndim != 1:
31+
raise ValueError("prior must be a 1D array")
32+
if self.prior.shape[0] != basis_matrix.shape[1]:
33+
raise ValueError(
34+
f"prior must be of shape {(basis_matrix.shape[1],)},"
35+
f" but got {self.prior.shape[0]}"
36+
)
37+
computed_prior = self.prior
38+
if self.noise is None:
39+
warnings.warn(
40+
"noise is None. noise will be set to the " "average of the prior"
41+
)
42+
self.noise = computed_prior.mean()
43+
G = basis_matrix @ np.diag(computed_prior)
3144
self.G = G
3245
n = G.shape[0]
3346
mask = np.ones(n, dtype=bool)

pysensors/reconstruction/_sspor.py

Lines changed: 94 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from sklearn.utils.validation import check_is_fitted
77

88
from ..basis import Identity
9-
from ..optimizers import CCQR, QR
9+
from ..optimizers import CCQR, QR, TPGR
1010
from ..utils import validate_input
1111

1212
INT_DTYPES = (int, np.int64, np.int32, np.int16, np.int8)
@@ -150,11 +150,19 @@ def fit(self, x, quiet=False, prefit_basis=False, seed=None, **optimizer_kws):
150150
# Check that n_sensors doesn't exceed dimension of basis vectors and
151151
# that it doesn't exceed the number of samples when using the CCQR optimizer.
152152
self._validate_n_sensors()
153-
153+
# Calculate the singular values
154+
X_proj = x @ self.basis_matrix_
155+
# Normalized singular values
156+
self.singular_values = np.linalg.norm(X_proj, axis=0) / np.sqrt(x.shape[0])
154157
# Find sparse sensor locations
155-
self.ranked_sensors_ = self.optimizer.fit(
156-
self.basis_matrix_, **optimizer_kws
157-
).get_sensors()
158+
if isinstance(self.optimizer, TPGR):
159+
self.ranked_sensors_ = self.optimizer.fit(
160+
self.basis_matrix_, self.singular_values, **optimizer_kws
161+
).get_sensors()
162+
else:
163+
self.ranked_sensors_ = self.optimizer.fit(
164+
self.basis_matrix_, **optimizer_kws
165+
).get_sensors()
158166

159167
# Randomly shuffle sensors after self.basis.n_basis_modes
160168
rng = np.random.default_rng(seed)
@@ -165,7 +173,7 @@ def fit(self, x, quiet=False, prefit_basis=False, seed=None, **optimizer_kws):
165173

166174
return self
167175

168-
def predict(self, x, method=None, noise=None, prior=None, **solve_kws):
176+
def predict(self, x, method=None, noise=None, prior="decreasing", **solve_kws):
169177
"""
170178
Predict values at all positions given measurements at sensor locations.
171179
@@ -200,6 +208,25 @@ def predict(self, x, method=None, noise=None, prior=None, **solve_kws):
200208
)
201209

202210
if method is None:
211+
if isinstance(prior, str) and prior == "decreasing":
212+
computed_prior = self.singular_values
213+
elif isinstance(prior, np.ndarray):
214+
if prior.ndim != 1:
215+
raise ValueError("prior must be a 1D array")
216+
if prior.shape[0] != self.basis_matrix_.shape[1]:
217+
raise ValueError(
218+
f"prior must be of shape {(self.basis_matrix_.shape[1],)},"
219+
f" but got {prior.shape}"
220+
)
221+
computed_prior = prior
222+
if noise is None:
223+
warnings.warn(
224+
"noise is None. noise will be set to the "
225+
"average of the normalized prior"
226+
)
227+
noise = computed_prior.mean()
228+
return self._regularized_reconstruction(x, computed_prior, noise)
229+
elif method == "unregularized":
203230
# Square matrix
204231
if self.n_sensors == self.basis_matrix_.shape[1]:
205232
return self._square_predict(
@@ -210,12 +237,12 @@ def predict(self, x, method=None, noise=None, prior=None, **solve_kws):
210237
return self._rectangular_predict(
211238
x, self.ranked_sensors_[: self.n_sensors], **solve_kws
212239
)
213-
if method == "mle":
214-
return self._max_likelihood_reconstruction(x, prior, noise)
240+
else:
241+
raise NotImplementedError("Method not implemented")
215242

216-
def _max_likelihood_reconstruction(self, x, prior, noise):
243+
def _regularized_reconstruction(self, x, prior, noise):
217244
"""
218-
Reconstruct the state using maximum likelihood reconstruction
245+
Reconstruct the state using regularized reconstruction
219246
220247
See the following reference for more information
221248
@@ -234,15 +261,11 @@ def _max_likelihood_reconstruction(self, x, prior, noise):
234261
"""
235262
if noise is None:
236263
noise = 1
237-
prior_cov = np.diag(prior**2)
238-
sensor_selection_matrix = np.zeros(
239-
(len(self.selected_sensors), self.basis_matrix_.shape[0])
240-
)
241-
sensor_selection_matrix[
242-
np.arange(len(self.selected_sensors)), self.selected_sensors
243-
] = 1
244-
low_rank_selection_matrix = sensor_selection_matrix @ self.basis_matrix_
245-
composite_matrix = np.linalg.inv(prior_cov) + (
264+
if not isinstance(prior, np.ndarray):
265+
raise ValueError("prior must be a numpy array")
266+
prior_cov = 1 / (prior**2)
267+
low_rank_selection_matrix = self.basis_matrix_[self.selected_sensors, :]
268+
composite_matrix = np.diag(prior_cov) + (
246269
low_rank_selection_matrix.T @ low_rank_selection_matrix
247270
) / (noise**2)
248271
rhs = low_rank_selection_matrix.T @ x
@@ -583,15 +606,20 @@ def std(self, prior, noise=None):
583606
noise = 1
584607
if noise <= 0:
585608
raise ValueError("Noise must be positive")
586-
prior_sq = prior**2
587-
sensor_selection_matrix = np.zeros(
588-
(len(self.selected_sensors), self.basis_matrix_.shape[0])
589-
)
590-
sensor_selection_matrix[
591-
np.arange(len(self.selected_sensors)), self.selected_sensors
592-
] = 1
593-
low_rank_selection_matrix = sensor_selection_matrix @ self.basis_matrix_
594-
composite_matrix = np.diag(prior_sq) + (
609+
if isinstance(prior, str) and prior == "decreasing":
610+
computed_prior = self.singular_values
611+
elif isinstance(prior, np.ndarray):
612+
if prior.ndim != 1:
613+
raise ValueError("prior must be a 1D array")
614+
if prior.shape[0] != self.basis_matrix_.shape[1]:
615+
raise ValueError(
616+
f"prior must be of shape {(self.basis_matrix_.shape[1],)},"
617+
f" but got {prior.shape}"
618+
)
619+
computed_prior = prior
620+
sq_inv_prior = 1.0 / (computed_prior**2)
621+
low_rank_selection_matrix = self.basis_matrix_[self.selected_sensors, :]
622+
composite_matrix = np.diag(sq_inv_prior) + (
595623
low_rank_selection_matrix.T @ low_rank_selection_matrix
596624
) / (noise**2)
597625
diag_cov_matrix = (
@@ -603,14 +631,48 @@ def std(self, prior, noise=None):
603631
sigma = noise * np.sqrt(np.sum(diag_cov_matrix**2, axis=1))
604632
return sigma
605633

606-
def one_pt_energy_landscape(self, prior, noise):
634+
def one_pt_energy_landscape(self, prior="decreasing", noise=None):
607635
check_is_fitted(self, "optimizer")
608-
G = self.basis_matrix_ @ np.diag(prior)
636+
if isinstance(prior, str) and prior == "decreasing":
637+
computed_prior = self.singular_values
638+
elif isinstance(prior, np.ndarray):
639+
if prior.ndim != 1:
640+
raise ValueError("prior must be a 1D array")
641+
if prior.shape[0] != self.basis_matrix_.shape[1]:
642+
raise ValueError(
643+
f"prior must be of shape {(self.basis_matrix_.shape[1],)},"
644+
f" but got {prior.shape}"
645+
)
646+
computed_prior = prior
647+
if noise is None:
648+
warnings.warn(
649+
"noise is None. noise will be set to the "
650+
"average of the normalized prior"
651+
)
652+
noise = computed_prior.mean()
653+
G = self.basis_matrix_ @ np.diag(computed_prior)
609654
return -np.log(1 + np.einsum("ij,ij->i", G, G) / noise**2)
610655

611-
def two_pt_energy_landscape(self, prior, noise, selected_sensors):
656+
def two_pt_energy_landscape(self, selected_sensors, prior="decreasing", noise=None):
612657
check_is_fitted(self, "optimizer")
613-
G = self.basis_matrix_ @ np.diag(prior)
658+
if isinstance(prior, str) and prior == "decreasing":
659+
computed_prior = self.singular_values
660+
elif isinstance(prior, np.ndarray):
661+
if prior.ndim != 1:
662+
raise ValueError("prior must be a 1D array")
663+
if prior.shape[0] != self.basis_matrix_.shape[1]:
664+
raise ValueError(
665+
f"prior must be of shape {(self.basis_matrix_.shape[1],)},"
666+
f" but got {prior.shape}"
667+
)
668+
computed_prior = prior
669+
if noise is None:
670+
warnings.warn(
671+
"noise is None. noise will be set to the "
672+
"average of the normalized prior"
673+
)
674+
noise = computed_prior.mean()
675+
G = self.basis_matrix_ @ np.diag(computed_prior)
614676
mask = np.ones(G.shape[0], dtype=bool)
615677
mask[selected_sensors] = False
616678
G_selected = G[selected_sensors, :]

tests/reconstruction/test_sspor.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,24 @@ def test_predict_accuracy(data_vandermonde_testing):
147147
model.fit(data, seed=1)
148148
model.set_number_of_sensors(8)
149149
sensors = model.get_selected_sensors()
150-
assert sqrt(mean((x_test - model.predict(x_test[sensors])) ** 2)) <= 1.0e-3
150+
assert (
151+
sqrt(
152+
mean((x_test - model.predict(x_test[sensors], method="unregularized")) ** 2)
153+
)
154+
<= 1.0e-3
155+
)
151156

152157
# Should also work for row vectors
153158
x_test = x_test.reshape(1, -1)
154-
assert sqrt(mean((x_test - model.predict(x_test[:, sensors])) ** 2)) <= 1.0e-3
159+
assert (
160+
sqrt(
161+
mean(
162+
(x_test - model.predict(x_test[:, sensors], method="unregularized"))
163+
** 2
164+
)
165+
)
166+
<= 1.0e-3
167+
)
155168

156169

157170
def test_reconstruction_error(data_vandermonde_testing):
@@ -224,7 +237,7 @@ def test_update_n_basis_modes(data_random):
224237
assert model.basis_matrix_.shape[1] == data.shape[0]
225238

226239
n_basis_modes = 5
227-
model.update_n_basis_modes(n_basis_modes)
240+
model.update_n_basis_modes(n_basis_modes, x=data)
228241
assert model.basis.n_basis_modes == data.shape[0]
229242
assert model.basis_matrix_.shape[1] == n_basis_modes
230243

@@ -350,7 +363,7 @@ def test_predict_warns_when_n_sensors_exceeds_basis_dimension():
350363
UserWarning, match="n_sensors exceeds dimension of basis modes"
351364
):
352365
predictor.predict.__globals__["validate_input"] = lambda x, y: X.T
353-
predictor.predict(X)
366+
predictor.predict(X, method="unregularized")
354367
finally:
355368
if "validate_input" in predictor.predict.__globals__:
356369
predictor.predict.__globals__["validate_input"] = original_validate_input
@@ -376,7 +389,7 @@ def test_predict_no_warning_when_n_sensors_not_exceeds_basis_dimension():
376389
X = np.random.rand(2, 3)
377390
with warnings.catch_warnings(record=True) as recorded_warnings:
378391
predictor.predict.__globals__["validate_input"] = lambda x, y: X.T
379-
predictor.predict(X)
392+
predictor.predict(X, method="unregularized")
380393
relevant_warnings = [
381394
w
382395
for w in recorded_warnings
@@ -733,7 +746,7 @@ def test_maximal_likelihood_reconstruction():
733746
selected = model.get_selected_sensors()
734747
x_sensors = X[:, selected]
735748

736-
y_pred = model.predict(x_sensors, method="mle", prior=prior, noise=0.1)
749+
y_pred = model.predict(x_sensors, method=None, prior=prior, noise=0.1)
737750

738751
assert isinstance(y_pred, np.ndarray)
739752
assert y_pred.shape == (n_samples, n_features)

0 commit comments

Comments
 (0)