Skip to content

Commit c4d2b12

Browse files
authored
Merge pull request #17 from niharika2999/niharika-krithika-mohammad-regionConstraints
Adding region constraints to pysensors PR
2 parents 571b40d + 082a963 commit c4d2b12

File tree

11 files changed

+2447
-5
lines changed

11 files changed

+2447
-5
lines changed

examples/spatially_constrained_qr.ipynb

Lines changed: 1846 additions & 0 deletions
Large diffs are not rendered by default.

pysensors/basis/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from ._identity import Identity
22
from ._random_projection import RandomProjection
33
from ._svd import SVD
4+
from ._custom import Custom
45

5-
6-
__all__ = ["Identity", "SVD", "RandomProjection"]
6+
__all__ = ["Identity", "SVD", "RandomProjection","Custom"]

pysensors/basis/_custom.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""
2+
custom mode basis class.
3+
"""
4+
from ._base import InvertibleBasis
5+
from ._base import MatrixMixin
6+
7+
class Custom(InvertibleBasis, MatrixMixin):
8+
"""
9+
Use a custom transformation to map input features to
10+
custom modes.
11+
12+
Assumes the data has already been centered (to have mean 0).
13+
14+
Parameters
15+
----------
16+
n_basis_modes : int, optional (default 10)
17+
Number of basis modes to retain. Cannot be larger than
18+
the number of features ``n_features``, or the number of examples
19+
``n_examples``.
20+
U: The custom basis matrix
21+
22+
Attributes
23+
----------
24+
basis_matrix_ : numpy ndarray, shape (n_features, n_basis_modes)
25+
The top n_basis_modes left singular vectors of the training data.
26+
27+
"""
28+
29+
def __init__(self, U, n_basis_modes=10, **kwargs):
30+
'''
31+
kwargs : Not defined but added to remain consistent with prior basis functions.
32+
'''
33+
if isinstance(n_basis_modes, int) and n_basis_modes > 0:
34+
super(Custom, self).__init__()#n_components=n_basis_modes, **kwargs
35+
self._n_basis_modes = n_basis_modes
36+
self.custom_basis_ = U
37+
else:
38+
raise ValueError("n_basis_modes must be a positive integer.")
39+
40+
def fit(self, X):
41+
"""
42+
Parameters
43+
----------
44+
X : array-like, shape (n_samples, n_features)
45+
The training data.
46+
47+
Returns
48+
-------
49+
self : instance
50+
"""
51+
# self.basis_matrix_ = self.custom_basis_[:,: self.n_basis_modes] @ self.custom_basis_[:,: self.n_basis_modes].T @ X[: self.n_basis_modes, :].T.copy()
52+
# self.basis_matrix_ = self.custom_basis_ @ self.custom_basis_.T @ X[: self.n_basis_modes, :].T.copy()
53+
self.basis_matrix_ = self.custom_basis_[:,:self.n_basis_modes]
54+
# self.basis_matrix_ = (X @ self.custom_basis_[:,:self.n_basis_modes] @ self.custom_basis_[:,:self.n_basis_modes].T)[:self.n_basis_modes,:].T
55+
56+
# self.basis_matrix_ = ((X @ self.custom_basis_).T)[:,:self.n_basis_modes]
57+
# self.basis_matrix_ = ((X @ self.custom_basis_ @ self.custom_basis_.T).T)[:,:self.n_basis_modes]
58+
return self
59+
60+
def matrix_inverse(self, n_basis_modes=None):
61+
"""
62+
Get the inverse matrix mapping from measurement space to
63+
coordinates with respect to the basis.
64+
65+
Note that this is not the inverse of the matrix returned by
66+
``self.matrix_representation``. It is the (pseudo) inverse of
67+
the matrix whose columns are the basis modes.
68+
69+
Parameters
70+
----------
71+
n_basis_modes : positive int, optional (default None)
72+
Number of basis modes to be used to compute inverse.
73+
74+
Returns
75+
-------
76+
B : numpy ndarray, shape (n_basis_modes, n_features)
77+
The inverse matrix.
78+
"""
79+
n_basis_modes = self._validate_input(n_basis_modes)
80+
81+
return self.basis_matrix_[:, :n_basis_modes].T
82+
83+
@property
84+
def n_basis_modes(self):
85+
"""Number of basis modes."""
86+
return self._n_basis_modes
87+
88+
@n_basis_modes.setter
89+
def n_basis_modes(self, n_basis_modes):
90+
self._n_basis_modes = n_basis_modes
91+
self.n_components = n_basis_modes

pysensors/classification/_sspoc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def fit(
259259

260260
if self.threshold is None:
261261
# Chosen as in Brunton et al. (2016)
262-
threshold = np.sqrt(np.sum(s**2)) / (
262+
threshold = np.sqrt(np.sum(s ** 2)) / (
263263
2 * self.basis_matrix_inverse_.shape[0] * n_classes
264264
)
265265
else:

pysensors/optimizers/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from ._ccqr import CCQR
22
from ._qr import QR
3-
3+
from ._gqr import GQR
44

55
__all__ = [
66
"CCQR",
77
"QR",
8+
"GQR"
89
]

pysensors/optimizers/_gqr.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import numpy as np
2+
import pysensors
3+
4+
from pysensors.optimizers._qr import QR
5+
6+
import matplotlib.pyplot as plt
7+
from sklearn import datasets
8+
from sklearn import metrics
9+
from mpl_toolkits.axes_grid1 import make_axes_locatable
10+
11+
import pysensors as ps
12+
from matplotlib.patches import Circle
13+
from pysensors.utils._norm_calc import returnInstance as normCalcReturnInstance
14+
15+
class GQR(QR):
16+
"""
17+
General QR optimizer for sensor selection.
18+
Ranks sensors in descending order of "importance" based on
19+
reconstruction accuracy. This is an extension that requires a more intrusive
20+
access to the QR optimizer to facilitate a more adaptive optimization. This is a generalized version of cost constraints
21+
in the sense that users can allow `n_const_sensors` in the constrained area.
22+
if n = 0 this converges to the CCQR results. and if no constrained region it should converge to the results from QR optimizer.
23+
24+
See the following reference for more information
25+
Manohar, Krithika, et al.
26+
"Data-driven sparse sensor placement for reconstruction:
27+
Demonstrating the benefits of exploiting known patterns."
28+
IEEE Control Systems Magazine 38.3 (2018): 63-86.
29+
30+
Niharika Karnik, Mohammad G. Abdo, Carlos E. Estrada Perez, Jun Soo Yoo, Joshua J. Cogliati, Richard S. Skifton,
31+
Pattrick Calderoni, Steven L. Brunton, and Krithika Manohar.
32+
Optimal Sensor Placement with Adaptive Constraints for Nuclear Digital Twins. 2023. arXiv: 2306 . 13637 [math.OC].
33+
34+
@ authors: Niharika Karnik (@nkarnik2999), Mohammad Abdo (@Jimmy-INL), and Krithika Manohar (@kmanohar)
35+
"""
36+
def __init__(self):
37+
"""
38+
Attributes
39+
----------
40+
pivots_ : np.ndarray, shape [n_features]
41+
Ranked list of sensor locations.
42+
idx_constrained : np.ndarray, shape [No. of constrained locations]
43+
Column Indices of the sensors in the constrained locations.
44+
n_sensors : integer,
45+
Total number of sensors
46+
n_const_sensors : integer,
47+
Total number of sensors required by the user in the constrained region.
48+
all_sensors : np.ndarray, shape [n_features]
49+
Optimally placed list of sensors obtained from QR pivoting algorithm.
50+
constraint_option : string,
51+
max_n_const_sensors : The number of sensors in the constrained region should be less than or equal to n_const_sensors.
52+
exact_n_const_sensors : The number of sensors in the constrained region should be exactly equal to n_const_sensors.
53+
"""
54+
self.pivots_ = None
55+
self.idx_constrained = []
56+
self.n_sensors = None
57+
self.n_const_sensors = 0
58+
self.all_sensors = []
59+
self.constraint_option = ''
60+
self.nx = None
61+
self.ny = None
62+
self.r = 1
63+
64+
def fit(self,basis_matrix,**optimizer_kws):
65+
"""
66+
Parameters
67+
----------
68+
basis_matrix: np.ndarray, shape [n_features, n_samples]
69+
Matrix whose columns are the basis vectors in which to
70+
represent the measurement data.
71+
optimizer_kws: dictionary, optional
72+
Keyword arguments to be passed to the qr method.
73+
74+
Returns
75+
-------
76+
self: a fitted :class:`pysensors.optimizers.GQR` instance
77+
"""
78+
[setattr(self,name,optimizer_kws.get(name,getattr(self,name))) for name in optimizer_kws.keys()]
79+
self._norm_calc_Instance = normCalcReturnInstance(self, self.constraint_option)
80+
n_features, n_samples = basis_matrix.shape # We transpose basis_matrix below
81+
max_const_sensors = len(self.idx_constrained) # Maximum number of sensors allowed in the constrained region
82+
83+
# Initialize helper variables
84+
R = basis_matrix.conj().T.copy()
85+
p = np.arange(n_features)
86+
k = min(n_samples, n_features)
87+
88+
89+
for j in range(k):
90+
r = R[j:, j:]
91+
92+
# Norm of each column
93+
dlens = np.sqrt(np.sum(np.abs(r) ** 2, axis=0))
94+
dlens_updated = self._norm_calc_Instance(self.idx_constrained, dlens, p, j, self.n_const_sensors, dlens_old=dlens, all_sensors=self.all_sensors, n_sensors=self.n_sensors, nx=self.nx, ny=self.ny, r=self.r)
95+
i_piv = np.argmax(dlens_updated)
96+
dlen = dlens_updated[i_piv]
97+
98+
if dlen > 0:
99+
u = r[:, i_piv] / dlen
100+
u[0] += np.sign(u[0]) + (u[0] == 0)
101+
u /= np.sqrt(abs(u[0]))
102+
else:
103+
u = r[:, i_piv]
104+
u[0] = np.sqrt(2)
105+
106+
# Track column pivots
107+
i_piv += j # true permutation index is i_piv shifted by the iteration counter j
108+
p[[j, i_piv]] = p[[i_piv, j]]
109+
110+
# Switch columns
111+
R[:, [j, i_piv]] = R[:, [i_piv, j]]
112+
113+
# Apply reflector
114+
R[j:, j:] -= np.outer(u, np.dot(u, R[j:, j:]))
115+
R[j + 1 :, j] = 0
116+
self.pivots_ = p
117+
return self

pysensors/utils/__init__.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
11
from ._base import validate_input
22
from ._optimizers import constrained_binary_solve
33
from ._optimizers import constrained_multiclass_solve
4-
4+
from ._constraints import get_constraind_sensors_indices
5+
from ._constraints import get_constrained_sensors_indices_linear
6+
from ._norm_calc import exact_n
7+
from ._norm_calc import max_n
8+
from ._norm_calc import predetermined
9+
from ._validation import determinant
10+
from ._validation import relative_reconstruction_error
511

612
__all__ = [
713
"constrained_binary_solve",
814
"constrained_multiclass_solve",
915
"validate_input",
16+
"get_constraind_sensors_indices",
17+
"get_constrained_sensors_indices_linear",
18+
"box_constraints",
19+
"functional_constraints",
20+
"exact_n",
21+
"max_n",
22+
"predetermined",
23+
"determinant",
24+
"relative_reconstruction_error"
1025
]

pysensors/utils/_constraints.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
2+
"""
3+
Various utility functions for mapping constrained sensors locations with the column indices for class GQR.
4+
"""
5+
6+
import numpy as np
7+
8+
9+
def get_constraind_sensors_indices(x_min, x_max, y_min, y_max, nx, ny, all_sensors):
10+
"""
11+
Function for mapping constrained sensor locations on the grid with the column indices of the basis_matrix.
12+
13+
Parameters
14+
----------
15+
x_min: int, lower bound for the x-axis constraint
16+
x_max : int, upper bound for the x-axis constraint
17+
y_min : int, lower bound for the y-axis constraint
18+
y_max : int, upper bound for the y-axis constraint
19+
nx : int, image pixel (x dimensions of the grid)
20+
ny : int, image pixel (y dimensions of the grid)
21+
all_sensors : np.ndarray, shape [n_features], ranked list of sensor locations.
22+
23+
Returns
24+
-------
25+
idx_constrained : np.darray, shape [No. of constrained locations], array which contains the constrained
26+
locations of the grid in terms of column indices of basis_matrix.
27+
"""
28+
n_features = len(all_sensors)
29+
image_size = int(np.sqrt(n_features))
30+
a = np.unravel_index(all_sensors, (nx,ny))
31+
constrained_sensorsx = []
32+
constrained_sensorsy = []
33+
for i in range(n_features):
34+
if (a[0][i] >= x_min and a[0][i] <= x_max) and (a[1][i] >= y_min and a[1][i] <= y_max):
35+
constrained_sensorsx.append(a[0][i])
36+
constrained_sensorsy.append(a[1][i])
37+
38+
constrained_sensorsx = np.array(constrained_sensorsx)
39+
constrained_sensorsy = np.array(constrained_sensorsy)
40+
constrained_sensors_array = np.stack((constrained_sensorsy, constrained_sensorsx), axis=1)
41+
constrained_sensors_tuple = np.transpose(constrained_sensors_array)
42+
if len(constrained_sensorsx) == 0: ##Check to handle condition when number of sensors in the constrained region = 0
43+
idx_constrained = []
44+
else:
45+
idx_constrained = np.ravel_multi_index(constrained_sensors_tuple, (nx,ny))
46+
return idx_constrained
47+
48+
def get_constrained_sensors_indices_linear(x_min, x_max, y_min, y_max,df):
49+
"""
50+
Function for obtaining constrained column indices from already existing linear sensor locations on the grid.
51+
52+
Parameters
53+
----------
54+
x_min: int, lower bound for the x-axis constraint
55+
x_max : int, upper bound for the x-axis constraint
56+
y_min : int, lower bound for the y-axis constraint
57+
y_max : int, upper bound for the y-axis constraint
58+
df : pandas.DataFrame, a dataframe containing the features and samples
59+
60+
Returns
61+
-------
62+
idx_constrained : np.darray, shape [No. of constrained locations], array which contains the constrained
63+
locations of the grid in terms of column indices of basis_matrix.
64+
"""
65+
x = df['X (m)'].to_numpy()
66+
n_features = x.shape[0]
67+
y = df['Y (m)'].to_numpy()
68+
idx_constrained = []
69+
for i in range(n_features):
70+
if (x[i] >= x_min and x[i] <= x_max) and (y[i] >= y_min and y[i] <= y_max):
71+
idx_constrained.append(i)
72+
return idx_constrained

0 commit comments

Comments
 (0)