Skip to content

Commit a849cdb

Browse files
authored
Merge pull request #28 from niharika2999/Distance_constrained_and_uq
This PR adds Distance Constraints to GQR and adds Distance Constraint example notebooks.
2 parents 48fd635 + b82af27 commit a849cdb

13 files changed

+7681
-6194
lines changed

examples/OPTI-TWIST_constrained_sensing.ipynb

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

examples/Olivetti_constrained_sensing.ipynb

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

examples/functional_constraints_class.ipynb

Lines changed: 0 additions & 2401 deletions
This file was deleted.

examples/index.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ PySensors Examples
1313
sea_surface_temperature
1414
reconstruction_comparison
1515
two_point_greedy
16-
vandermonde
17-
spatially_constrained_qr
18-
functional_constraints_class
19-
simulation_constrained_sensing
16+
polynomial_curve_fitting
17+
spatial_constrained_qr
18+
Olivetti_constrained_sensing
19+
OPTI-TWIST_constrained_sensing
File renamed without changes.

examples/simulation_constrained_sensing.ipynb

Lines changed: 0 additions & 1822 deletions
This file was deleted.

examples/spatial_constrained_qr.ipynb

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

examples/spatially_constrained_qr.ipynb

Lines changed: 0 additions & 1845 deletions
This file was deleted.

pysensors/optimizers/_gqr.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ def __init__(self):
5757
self.n_const_sensors = 0
5858
self.all_sensors = []
5959
self.constraint_option = ""
60+
self.info = None
61+
self.X_axis = None
62+
self.Y_axis = None
6063
self.nx = None
6164
self.ny = None
6265
self.r = 1
@@ -95,14 +98,17 @@ def fit(self, basis_matrix, **optimizer_kws):
9598

9699
dlens = np.sqrt(np.sum(np.abs(r) ** 2, axis=0))
97100
dlens_updated = self._norm_calc_Instance(
98-
self.idx_constrained,
99101
dlens,
100102
p,
101103
j,
102-
self.n_const_sensors,
103104
dlens_old=dlens,
105+
idx_constrained=self.idx_constrained,
106+
n_const_sensors=self.n_const_sensors,
104107
all_sensors=self.all_sensors,
105108
n_sensors=self.n_sensors,
109+
info=self.info,
110+
X_axis=self.X_axis,
111+
Y_axis=self.Y_axis,
106112
nx=self.nx,
107113
ny=self.ny,
108114
r=self.r,

pysensors/utils/_constraints.py

Lines changed: 149 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,76 @@ def get_constrained_sensors_indices_dataframe(x_min, x_max, y_min, y_max, df, **
117117
return idx_constrained
118118

119119

120+
def get_constrained_sensors_indices_distance(j, piv, r, nx, ny, all_sensors):
121+
"""
122+
Efficiently finds sensors within radius r of a given sensor.
123+
124+
Parameters
125+
----------
126+
j : int
127+
Current iteration (0-indexed)
128+
piv : np.ndarray
129+
Array of sensor indices in order of placement
130+
r : float
131+
Radius constraint (minimum distance between sensors)
132+
nx, ny : int
133+
Grid dimensions
134+
all_sensors : np.ndarray
135+
Ranked list of sensor locations.
136+
137+
Returns
138+
-------
139+
idx_constrained : np.ndarray
140+
Array of sensor indices within radius r
141+
"""
142+
sensor_idx = max(0, j - 1)
143+
current_sensor = piv[sensor_idx]
144+
current_coords = np.unravel_index([current_sensor], (nx, ny))
145+
x_cord, y_cord = current_coords[0][0], current_coords[1][0]
146+
sensor_coords = np.unravel_index(all_sensors, (nx, ny))
147+
distances_sq = (sensor_coords[0] - x_cord) ** 2 + (sensor_coords[1] - y_cord) ** 2
148+
return all_sensors[distances_sq < r**2]
149+
150+
151+
def get_constrained_sensors_indices_distance_df(
152+
j, piv, r, df, all_sensors, X_axis, Y_axis
153+
):
154+
"""
155+
Efficiently finds sensors within radius r of a given sensor for DataFrame input.
156+
157+
Parameters
158+
----------
159+
j : int
160+
Current iteration (0-indexed)
161+
piv : np.ndarray
162+
Array of sensor indices in order of placement
163+
r : float
164+
Radius constraint (minimum distance between sensors)
165+
df : pd.DataFrame
166+
DataFrame containing sensor coordinates
167+
all_sensors : np.ndarray
168+
Ranked list of sensor locations
169+
X_axis : str
170+
Column name for X coordinates in the DataFrame
171+
Y_axis : str
172+
Column name for Y coordinates in the DataFrame
173+
174+
Returns
175+
-------
176+
idx_constrained : np.ndarray
177+
Array of sensor indices within radius r
178+
"""
179+
sensor_idx = max(0, j - 1)
180+
current_sensor = piv[sensor_idx]
181+
current_x = df.loc[current_sensor, X_axis]
182+
current_y = df.loc[current_sensor, Y_axis]
183+
sensors_df = df.loc[all_sensors]
184+
distances_sq = (sensors_df[X_axis] - current_x) ** 2 + (
185+
sensors_df[Y_axis] - current_y
186+
) ** 2
187+
return all_sensors[distances_sq.values < r**2]
188+
189+
120190
def load_functional_constraints(functionHandler):
121191
"""
122192
Parameters:
@@ -555,32 +625,37 @@ def plot_selected_sensors(
555625
"""
556626
n_samples, n_features = self.data.shape
557627
n_sensors = len(sensors)
558-
constrained = sensors[np.where(~np.isin(all_sensors[:n_sensors], sensors))[0]]
559-
unconstrained = sensors[np.where(np.isin(all_sensors[:n_sensors], sensors))[0]]
628+
constrained = sensors[~np.isin(sensors, all_sensors[:n_sensors])]
629+
unconstrained = sensors[np.isin(sensors, all_sensors[:n_sensors])]
630+
560631
if isinstance(self.data, np.ndarray):
561632
xconst = np.mod(constrained, np.sqrt(n_features))
562633
yconst = np.floor(constrained / np.sqrt(n_features))
563634
xunconst = np.mod(unconstrained, np.sqrt(n_features))
564635
yunconst = np.floor(unconstrained / np.sqrt(n_features))
636+
565637
self.ax.plot(xconst, yconst, "*", color=color_constrained)
566638
self.ax.plot(xunconst, yunconst, "*", color=color_unconstrained)
639+
567640
elif isinstance(self.data, pd.DataFrame):
568-
constCoords = get_coordinates_from_indices(
641+
xconst, yconst = get_coordinates_from_indices(
569642
constrained,
570643
self.data,
571644
Y_axis=self.Y_axis,
572645
X_axis=self.X_axis,
573646
Field=self.Field,
574647
)
575-
unconstCoords = get_coordinates_from_indices(
648+
649+
xunconst, yunconst = get_coordinates_from_indices(
576650
unconstrained,
577651
self.data,
578652
Y_axis=self.Y_axis,
579653
X_axis=self.X_axis,
580654
Field=self.Field,
581655
)
582-
self.ax.plot(constCoords, "*", color=color_constrained)
583-
self.ax.plot(unconstCoords, "*", color=color_unconstrained)
656+
657+
self.ax.plot(xconst, yconst, "*", color=color_constrained)
658+
self.ax.plot(xunconst, yunconst, "*", color=color_unconstrained)
584659

585660
def sensors_dataframe(self, sensors):
586661
"""
@@ -620,6 +695,7 @@ def annotate_sensors(
620695
"""
621696
Function to annotate the sensor location on the grid while also plotting the
622697
sensor location
698+
623699
Attributes
624700
----------
625701
sensors : np.darray,
@@ -639,29 +715,40 @@ def annotate_sensors(
639715
"""
640716
n_samples, n_features = self.data.shape
641717
n_sensors = len(sensors)
642-
constrained = sensors[np.where(~np.isin(all_sensors[:n_sensors], sensors))[0]]
643-
unconstrained = sensors[np.where(np.isin(all_sensors[:n_sensors], sensors))[0]]
718+
719+
# Fixed logic for finding constrained and unconstrained sensors
720+
constrained = sensors[~np.isin(sensors, all_sensors[:n_sensors])]
721+
unconstrained = sensors[np.isin(sensors, all_sensors[:n_sensors])]
722+
644723
if isinstance(self.data, np.ndarray):
645724
xTop = np.mod(sensors, np.sqrt(n_features))
646725
yTop = np.floor(sensors / np.sqrt(n_features))
726+
647727
xconst = np.mod(constrained, np.sqrt(n_features))
648728
yconst = np.floor(constrained / np.sqrt(n_features))
729+
649730
xunconst = np.mod(unconstrained, np.sqrt(n_features))
650731
yunconst = np.floor(unconstrained / np.sqrt(n_features))
732+
651733
data = np.vstack([sensors, xTop, yTop]).T # noqa:F841
734+
652735
self.ax.plot(xconst, yconst, "*", color=color_constrained, alpha=0.5)
653736
self.ax.plot(xunconst, yunconst, "*", color=color_unconstrained, alpha=0.5)
654-
for ind, i in enumerate(range(len(xTop))):
655-
self.ax.annotate(
656-
f"{str(ind)}",
657-
(xTop[i], yTop[i]),
658-
xycoords="data",
659-
xytext=(-20, 20),
660-
textcoords="offset points",
661-
color="r",
662-
fontsize=12,
663-
arrowprops=dict(arrowstyle="->", color="black"),
664-
)
737+
738+
# Improved annotation logic with index checking
739+
for ind in range(len(sensors)):
740+
if ind < len(xTop) and ind < len(yTop): # Make sure index is in bounds
741+
self.ax.annotate(
742+
f"{ind}",
743+
(xTop[ind], yTop[ind]),
744+
xycoords="data",
745+
xytext=(-20, 20),
746+
textcoords="offset points",
747+
color="r",
748+
fontsize=12,
749+
arrowprops=dict(arrowstyle="->", color="black"),
750+
)
751+
665752
elif isinstance(self.data, pd.DataFrame):
666753
xTop, yTop = get_coordinates_from_indices(
667754
sensors,
@@ -670,33 +757,41 @@ def annotate_sensors(
670757
X_axis=self.X_axis,
671758
Field=self.Field,
672759
)
760+
673761
xconst, yconst = get_coordinates_from_indices(
674762
constrained,
675763
self.data,
676764
Y_axis=self.Y_axis,
677765
X_axis=self.X_axis,
678766
Field=self.Field,
679767
)
768+
680769
xunconst, yunconst = get_coordinates_from_indices(
681770
unconstrained,
682771
self.data,
683772
Y_axis=self.Y_axis,
684773
X_axis=self.X_axis,
685774
Field=self.Field,
686775
)
776+
687777
self.ax.plot(xconst, yconst, "*", color=color_constrained, alpha=0.5)
688778
self.ax.plot(xunconst, yunconst, "*", color=color_unconstrained, alpha=0.5)
689-
for _, i in enumerate(range(len(sensors))):
690-
self.ax.annotate(
691-
f"{str(i)}",
692-
(xTop[i], yTop[i]),
693-
xycoords="data",
694-
xytext=(-20, 20),
695-
textcoords="offset points",
696-
color="r",
697-
fontsize=12,
698-
arrowprops=dict(arrowstyle="->", color="black"),
699-
)
779+
780+
# Improved annotation logic - check array lengths and indices
781+
for i in range(len(sensors)):
782+
if i < len(xTop) and i < len(yTop): # Make sure index is in bounds
783+
# Check that the coordinates are valid (not NaN)
784+
if np.isfinite(xTop[i]) and np.isfinite(yTop[i]):
785+
self.ax.annotate(
786+
f"{i}",
787+
(xTop[i], yTop[i]),
788+
xycoords="data",
789+
xytext=(-20, 20),
790+
textcoords="offset points",
791+
color="r",
792+
fontsize=12,
793+
arrowprops=dict(arrowstyle="->", color="black"),
794+
)
700795

701796

702797
class Circle(BaseConstraint):
@@ -1235,33 +1330,45 @@ def constraint_function(self, coords):
12351330
"""
12361331
Function to compute whether a certain point on the grid lies inside/outside
12371332
the defined constrained region
1333+
12381334
Attributes
12391335
----------
1240-
x : float,
1241-
x coordinate of point on the grid being evaluated to check whether it lies
1242-
inside or outside the constrained region
1243-
y : float,
1244-
y coordinate of point on the grid being evaluated to check whether it lies
1336+
coords : list or tuple
1337+
[x, y] coordinates of point on the grid being evaluated to check whether
1338+
it lies
12451339
inside or outside the constrained region
1340+
1341+
Returns
1342+
-------
1343+
bool
1344+
True if point satisfies the constraint (inside for "in", outside for "out"),
1345+
False otherwise
12461346
"""
1347+
if len(coords) != 2:
1348+
raise ValueError("coords must contain exactly 2 elements [x, y]")
1349+
12471350
x, y = coords[:]
1248-
# define point in polygon
12491351
polygon = self.xy_coords
12501352
n = len(polygon)
1353+
1354+
if n < 3:
1355+
raise ValueError("Polygon must have at least 3 vertices")
12511356
inFlag = False
12521357

12531358
for i in range(n):
12541359
x1, y1 = polygon[i]
12551360
x2, y2 = polygon[(i + 1) % n]
1256-
1257-
if (y1 < y and y2 >= y) or (y2 < y and y1 >= y):
1258-
if x1 + (y - y1) / (y2 - y1) * (x2 - x1) < x:
1259-
inFlag = not inFlag
1260-
1361+
if (y1 > y) != (y2 > y):
1362+
if y1 != y2:
1363+
x_intersect = x1 + (y - y1) * (x2 - x1) / (y2 - y1)
1364+
if x < x_intersect:
1365+
inFlag = not inFlag
12611366
if self.loc.lower() == "in":
12621367
return not inFlag
12631368
elif self.loc.lower() == "out":
12641369
return inFlag
1370+
else:
1371+
raise ValueError(f"Invalid constraint type: {self.loc}.Must be'in' or'out'")
12651372

12661373

12671374
class UserDefinedConstraints(BaseConstraint):
@@ -1385,7 +1492,7 @@ def draw(self, ax, **kwargs):
13851492
self.all_sensors, self.data
13861493
)
13871494
for k in range(len(xValue)):
1388-
G[k, i] = eval(
1495+
G[k, i] = not eval(
13891496
self.equations[i], {"x": xValue[k], "y": yValue[k]}
13901497
)
13911498
idx_const, rank = (

0 commit comments

Comments
 (0)