Skip to content

Commit 43a3a32

Browse files
committed
DOC: Add documentation, remove irrelevant noise assignments, cleanup
1 parent e79b1c2 commit 43a3a32

File tree

2 files changed

+191
-30
lines changed

2 files changed

+191
-30
lines changed

pysensors/optimizers/_tpgr.py

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,42 +7,80 @@
77

88
class TPGR(BaseEstimator):
99
"""
10-
2-Point Greedy Algorithm for Sensor Selection.
10+
Two-Point Greedy Algorithm for Sensor Selection.
1111
1212
See the following reference for more information
1313
1414
Klishin, Andrei A., et. al.
1515
Data-Induced Interactions of Sparse Sensors. 2023.
1616
arXiv:2307.11838 [cond-mat.stat-mech]
1717
18+
Parameters
19+
----------
20+
n_sensors : int
21+
The number of sensors to select.
22+
23+
prior: str or np.ndarray shape (n_basis_modes,), optional (default='decreasing')
24+
Prior Covariance Vector, typically a scaled identity vector or a vector
25+
containing normalized singular values. If 'decreasing', normalized singular
26+
values are used.
27+
28+
noise: float (default None)
29+
Magnitude of the gaussian uncorrelated sensor measurement noise.
30+
31+
Attributes
32+
----------
33+
sensors_ : list of int
34+
Indices of the selected sensors (rows from the basis matrix).
35+
1836
"""
1937

20-
def __init__(self, n_sensors=None, noise=None, prior="decreasing"):
38+
def __init__(self, n_sensors, prior="decreasing", noise=None):
2139
self.n_sensors = n_sensors
2240
self.noise = noise
2341
self.sensors_ = None
2442
self.prior = prior
2543

26-
def fit(self, basis_matrix, singular_values=None):
44+
def fit(self, basis_matrix, singular_values):
45+
"""
46+
Parameters
47+
----------
48+
basis_matrix: np.ndarray, shape (n_features, n_basis_modes)
49+
Matrix whose columns are the basis vectors in which to
50+
represent the measurement data.
51+
52+
singular_values : np.ndarray, shape (n_basis_modes,)
53+
Normalized singular values to be used if `prior="decreasing"`.
54+
55+
Returns
56+
-------
57+
self: a fitted :class:`pysensors.optimizers.TPGR` instance
58+
"""
2759
if isinstance(self.prior, str) and self.prior == "decreasing":
2860
computed_prior = singular_values
2961
elif isinstance(self.prior, np.ndarray):
3062
if self.prior.ndim != 1:
31-
raise ValueError("prior must be a 1D array")
63+
raise ValueError("prior must be a 1D array.")
3264
if self.prior.shape[0] != basis_matrix.shape[1]:
3365
raise ValueError(
3466
f"prior must be of shape {(basis_matrix.shape[1],)},"
35-
f" but got {self.prior.shape[0]}"
67+
f" but got {self.prior.shape[0]}."
3668
)
3769
computed_prior = self.prior
70+
else:
71+
raise ValueError(
72+
"Invalid prior: must be 'decreasing' or a 1D "
73+
"ndarray of appropriate length."
74+
)
3875
if self.noise is None:
3976
warnings.warn(
40-
"noise is None. noise will be set to the " "average of the prior"
77+
"noise is None. noise will be set to the average of the computed prior."
4178
)
4279
self.noise = computed_prior.mean()
4380
G = basis_matrix @ np.diag(computed_prior)
44-
self.G = G
4581
n = G.shape[0]
82+
if self.n_sensors > G.shape[0]:
83+
raise ValueError("n_sensors cannot exceed the number of available sensors.")
4684
mask = np.ones(n, dtype=bool)
4785
one_pt_energies = self._one_pt_energy(G)
4886
i = np.argmin(one_pt_energies)
@@ -66,13 +104,35 @@ def fit(self, basis_matrix, singular_values=None):
66104

67105
def _one_pt_energy(self, G):
68106
"""
69-
Compute the 1-pt energy
107+
Compute the one-pt energy of the sensors
108+
109+
Parameters
110+
----------
111+
G : np.ndarray, shape (n_features, n_basis_modes)
112+
Basis matrix weighted by the prior.
113+
114+
Returns
115+
-------
116+
np.ndarray, shape (n_features,)
70117
"""
71118
return -np.log(1 + np.einsum("ij,ij->i", G, G) / self.noise**2)
72119

73120
def _two_pt_energy(self, G_selected, G_remaining):
74121
"""
75-
Compute the 2-pt energy
122+
Compute the two-pt energy interations of the selected
123+
sensors with the remaining sensors
124+
125+
Parameters
126+
----------
127+
G_selected : np.ndarray, shape (k, n_basis_modes)
128+
Matrix of currently selected k sensors.
129+
130+
G_remaining : np.ndarray, shape (n_features - k, n_basis_modes)
131+
Matrix of currently remaining sensors.
132+
133+
Returns
134+
-------
135+
np.ndarray, shape (n_features - k,)
76136
"""
77137
J = 0.5 * np.sum(
78138
((G_remaining @ G_selected.T) ** 2)

pysensors/reconstruction/_sspor.py

Lines changed: 122 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ class SSPOR(BaseEstimator):
5757
ranked_sensors_: np.ndarray
5858
Sensor locations ranked in descending order of importance.
5959
60+
singular_values: np.ndarray
61+
Normalized singular values of the fitted data
62+
6063
Examples
6164
--------
6265
>>> import numpy as np
@@ -150,9 +153,8 @@ def fit(self, x, quiet=False, prefit_basis=False, seed=None, **optimizer_kws):
150153
# Check that n_sensors doesn't exceed dimension of basis vectors and
151154
# that it doesn't exceed the number of samples when using the CCQR optimizer.
152155
self._validate_n_sensors()
153-
# Calculate the singular values
156+
# Calculate the normalized singular values
154157
X_proj = x @ self.basis_matrix_
155-
# Normalized singular values
156158
self.singular_values = np.linalg.norm(X_proj, axis=0) / np.sqrt(x.shape[0])
157159
# Find sparse sensor locations
158160
if isinstance(self.optimizer, TPGR):
@@ -173,7 +175,7 @@ def fit(self, x, quiet=False, prefit_basis=False, seed=None, **optimizer_kws):
173175

174176
return self
175177

176-
def predict(self, x, method=None, noise=None, prior="decreasing", **solve_kws):
178+
def predict(self, x, method=None, prior="decreasing", noise=None, **solve_kws):
177179
"""
178180
Predict values at all positions given measurements at sensor locations.
179181
@@ -184,6 +186,21 @@ def predict(self, x, method=None, noise=None, prior="decreasing", **solve_kws):
184186
The measurements should be taken at the sensor locations specified by
185187
``self.get_selected_sensors()``.
186188
189+
method : {'unregularized', None}, optional
190+
If None (default), performs regularized reconstruction using the prior
191+
and noise. If 'unregularized', uses direct or least-squares inversion
192+
depending on matrix shape.
193+
194+
prior: str or np.ndarray, shape (n_basis_modes,), optional
195+
(default='decreasing')
196+
Prior Covariance Vector, typically a scaled identity vector or a vector
197+
containing normalized singular values. If 'decreasing', normalized singular
198+
values are used.
199+
200+
noise: float (default None)
201+
Magnitude of the gaussian uncorrelated sensor measurement noise.
202+
If None, noise will default to the average of the computed prior.
203+
187204
solve_kws: dict, optional
188205
keyword arguments to be passed to the linear solver used to invert
189206
the basis matrix.
@@ -202,9 +219,11 @@ def predict(self, x, method=None, noise=None, prior="decreasing", **solve_kws):
202219
# Although if the user changes the number of sensors between calls
203220
# the factorization will be wasted.
204221

205-
if self.n_sensors > self.basis_matrix_.shape[0]:
222+
if self.n_sensors > self.basis_matrix_.shape[0] and method == "unregularized":
206223
warnings.warn(
207-
"n_sensors exceeds dimension of basis modes. Performance may be poor"
224+
"n_sensors exceeds dimension of basis modes. Performance may be poor "
225+
"for unregularized reconstruction. Consider using regularized "
226+
"reconstruction"
208227
)
209228

210229
if method is None:
@@ -219,10 +238,15 @@ def predict(self, x, method=None, noise=None, prior="decreasing", **solve_kws):
219238
f" but got {prior.shape}"
220239
)
221240
computed_prior = prior
241+
else:
242+
raise ValueError(
243+
"Invalid prior: must be 'decreasing' or a 1D "
244+
"ndarray of appropriate length."
245+
)
222246
if noise is None:
223247
warnings.warn(
224248
"noise is None. noise will be set to the "
225-
"average of the normalized prior"
249+
"average of the computed prior"
226250
)
227251
noise = computed_prior.mean()
228252
return self._regularized_reconstruction(x, computed_prior, noise)
@@ -259,10 +283,6 @@ def _regularized_reconstruction(self, x, prior, noise):
259283
noise: float (default None)
260284
Magnitude of the gaussian uncorrelated sensor measurement noise
261285
"""
262-
if noise is None:
263-
noise = 1
264-
if not isinstance(prior, np.ndarray):
265-
raise ValueError("prior must be a numpy array")
266286
prior_cov = 1 / (prior**2)
267287
low_rank_selection_matrix = self.basis_matrix_[self.selected_sensors, :]
268288
composite_matrix = np.diag(prior_cov) + (
@@ -588,12 +608,15 @@ def std(self, prior, noise=None):
588608
589609
Parameters
590610
----------
591-
prior: np.ndarray (n_basis_modes,)
611+
prior: str or np.ndarray, shape (n_basis_modes,), optional
612+
(default='decreasing')
592613
Prior Covariance Vector, typically a scaled identity vector or a vector
593-
containing normalized sigular values.
614+
containing normalized singular values. If 'decreasing', normalized singular
615+
values are used.
594616
595617
noise: float (default None)
596618
Magnitude of the gaussian uncorrelated sensor measurement noise.
619+
If None, noise will default to the average of the computed prior.
597620
598621
Returns
599622
-------
@@ -602,10 +625,6 @@ def std(self, prior, noise=None):
602625
603626
"""
604627
check_is_fitted(self, "basis_matrix_")
605-
if noise is None:
606-
noise = 1
607-
if noise <= 0:
608-
raise ValueError("Noise must be positive")
609628
if isinstance(prior, str) and prior == "decreasing":
610629
computed_prior = self.singular_values
611630
elif isinstance(prior, np.ndarray):
@@ -617,6 +636,16 @@ def std(self, prior, noise=None):
617636
f" but got {prior.shape}"
618637
)
619638
computed_prior = prior
639+
else:
640+
raise ValueError(
641+
"Invalid prior: must be 'decreasing' or a 1D "
642+
"ndarray of appropriate length."
643+
)
644+
if noise is None:
645+
warnings.warn(
646+
"noise is None. noise will be set to the average of the computed prior"
647+
)
648+
noise = computed_prior.mean()
620649
sq_inv_prior = 1.0 / (computed_prior**2)
621650
low_rank_selection_matrix = self.basis_matrix_[self.selected_sensors, :]
622651
composite_matrix = np.diag(sq_inv_prior) + (
@@ -632,7 +661,35 @@ def std(self, prior, noise=None):
632661
return sigma
633662

634663
def one_pt_energy_landscape(self, prior="decreasing", noise=None):
635-
check_is_fitted(self, "optimizer")
664+
"""
665+
Compute the one-point energy landscape of the sensors
666+
667+
See the following reference for more information
668+
669+
Klishin, Andrei A., et. al.
670+
Data-Induced Interactions of Sparse Sensors. 2023.
671+
arXiv:2307.11838 [cond-mat.stat-mech]
672+
673+
Parameters
674+
----------
675+
prior: str or np.ndarray, shape (n_basis_modes,), optional
676+
(default='decreasing')
677+
Prior Covariance Vector, typically a scaled identity vector or a vector
678+
containing normalized singular values. If 'decreasing', normalized singular
679+
values are used.
680+
681+
noise: float (default None)
682+
Magnitude of the gaussian uncorrelated sensor measurement noise.
683+
If None, noise will default to the average of the computed prior.
684+
685+
Returns
686+
-------
687+
np.ndarray, shape (n_features,)
688+
"""
689+
if isinstance(self.optimizer, TPGR):
690+
check_is_fitted(self, "optimizer")
691+
else:
692+
"Energy landscapes can only be computed if TPGR optimizer is used."
636693
if isinstance(prior, str) and prior == "decreasing":
637694
computed_prior = self.singular_values
638695
elif isinstance(prior, np.ndarray):
@@ -644,16 +701,56 @@ def one_pt_energy_landscape(self, prior="decreasing", noise=None):
644701
f" but got {prior.shape}"
645702
)
646703
computed_prior = prior
704+
else:
705+
raise ValueError(
706+
"Invalid prior: must be 'decreasing' or a 1D "
707+
"ndarray of appropriate length."
708+
)
647709
if noise is None:
648710
warnings.warn(
649-
"noise is None. noise will be set to the "
650-
"average of the normalized prior"
711+
"noise is None. noise will be set to the average of the computed prior"
651712
)
652713
noise = computed_prior.mean()
653714
G = self.basis_matrix_ @ np.diag(computed_prior)
654715
return -np.log(1 + np.einsum("ij,ij->i", G, G) / noise**2)
655716

656717
def two_pt_energy_landscape(self, selected_sensors, prior="decreasing", noise=None):
718+
"""
719+
Compute the two-point energy landscape of the sensors. If selected_sensors is a
720+
singular sensor, the landscape will be the two point energy interations of that
721+
sensor with the remaining sensors. If selected_sensors is a list of sensors,
722+
the landscape will be the sum of two point energy interactions of the selected
723+
sensors with the remaining sensors.
724+
725+
See the following reference for more information
726+
727+
Klishin, Andrei A., et. al.
728+
Data-Induced Interactions of Sparse Sensors. 2023.
729+
arXiv:2307.11838 [cond-mat.stat-mech]
730+
731+
Parameters
732+
----------
733+
prior: str or np.ndarray, shape (n_basis_modes,), optional
734+
(default='decreasing')
735+
Prior Covariance Vector, typically a scaled identity vector or a vector
736+
containing normalized singular values. If 'decreasing', normalized singular
737+
values are used.
738+
739+
noise: float (default None)
740+
Magnitude of the gaussian uncorrelated sensor measurement noise.
741+
If None, noise will default to the average of the computed prior.
742+
743+
selected_sensors: list
744+
Indices of selected sensors for two point energy computation.
745+
746+
Returns
747+
-------
748+
np.ndarray, shape (n_features,)
749+
"""
750+
if isinstance(self.optimizer, TPGR):
751+
check_is_fitted(self, "optimizer")
752+
else:
753+
"Energy landscapes can only be computed if TPGR optimizer is used."
657754
check_is_fitted(self, "optimizer")
658755
if isinstance(prior, str) and prior == "decreasing":
659756
computed_prior = self.singular_values
@@ -666,10 +763,14 @@ def two_pt_energy_landscape(self, selected_sensors, prior="decreasing", noise=No
666763
f" but got {prior.shape}"
667764
)
668765
computed_prior = prior
766+
else:
767+
raise ValueError(
768+
"Invalid prior: must be 'decreasing' or a 1D "
769+
"ndarray of appropriate length."
770+
)
669771
if noise is None:
670772
warnings.warn(
671-
"noise is None. noise will be set to the "
672-
"average of the normalized prior"
773+
"noise is None. noise will be set to the average of the computed prior"
673774
)
674775
noise = computed_prior.mean()
675776
G = self.basis_matrix_ @ np.diag(computed_prior)

0 commit comments

Comments
 (0)