pmcore/routines/expansion/adaptative_grid.rs
1use crate::structs::theta::Theta;
2use anyhow::Result;
3use faer::Row;
4
5/// Implements the adaptive grid algorithm for support point expansion.
6///
7/// This function generates up to 2 new support points in each dimension for each existing support point.
8/// New support points are symmetrically placed around the original support point, at a distance of `eps` * (range_max - range_min).
9/// If the new support point is too close to an existing support point, or it is outside the given range, it is discarded.
10///
11/// # Arguments
12///
13/// * `theta` - A mutable reference to a 2D array representing the existing support points.
14/// * `eps` - A floating-point value representing the fraction of the range to use for generating new support points.
15/// * `ranges` - A slice of tuples representing the range of values for each dimension.
16/// * `min_dist` - A floating-point value representing the minimum distance between support points.
17///
18/// # Returns
19///
20/// A 2D array containing the updated support points after the adaptive grid expansion.
21///
22pub fn adaptative_grid(
23 theta: &mut Theta,
24 eps: f64,
25 ranges: &[(f64, f64)],
26 min_dist: f64,
27) -> Result<()> {
28 let mut candidates = Vec::new();
29
30 // Collect all points first to avoid borrowing conflicts
31 for spp in theta.matrix().row_iter() {
32 for (j, val) in spp.iter().enumerate() {
33 let l = eps * (ranges[j].1 - ranges[j].0); //abs?
34 if val + l < ranges[j].1 {
35 let mut plus = Row::zeros(spp.ncols());
36 plus[j] = l;
37 plus += spp;
38 candidates.push(plus.iter().copied().collect::<Vec<f64>>());
39 }
40 if val - l > ranges[j].0 {
41 let mut minus = Row::zeros(spp.ncols());
42 minus[j] = -l;
43 minus += spp;
44 candidates.push(minus.iter().copied().collect::<Vec<f64>>());
45 }
46 }
47 }
48
49 // Option 1: Check all points against the original theta, then add them
50 let keep = candidates
51 .iter()
52 .filter(|point| theta.check_point(point, min_dist))
53 .cloned()
54 .collect::<Vec<_>>();
55
56 for point in keep {
57 theta.add_point(point.as_slice())?;
58 }
59
60 Ok(())
61
62 // Option 2: Check and add points one by one
63 // Now add all the points after the immutable borrow is released
64 //for point in candidates {
65 // theta.suggest_point(point, min_dist, ranges);
66 //}
67}
68/*
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use crate::structs::theta::Theta;
73 use faer::mat;
74
75 #[test]
76 fn test_expected() {
77 let original = Theta::from(mat![[1.0, 10.0]]);
78
79 let ranges = [(0.0, 1.0), (0.0, 10.0)];
80 let eps = 0.1;
81 let min_dist = 0.05;
82
83 let mut theta = original.clone();
84 adaptative_grid(&mut theta, eps, &ranges, min_dist);
85
86 let expected = mat![[1.0, 10.0], [0.9, 10.0], [1.0, 9.0]];
87
88 // Check that both matrices have the same number of rows
89 assert_eq!(
90 theta.matrix().nrows(),
91 expected.nrows(),
92 "Number of points in theta doesn't match expected"
93 );
94
95 // Check that all points in expected are in theta
96 for i in 0..expected.nrows() {
97 let expected_point = expected.row(i);
98 let mut found = false;
99
100 for j in 0..theta.matrix().nrows() {
101 let theta_point = theta.matrix().row(j);
102
103 // Check if points match (within small epsilon for floating-point comparison)
104 if (expected_point[0] - theta_point[0]).abs() < 1e-10
105 && (expected_point[1] - theta_point[1]).abs() < 1e-10
106 {
107 found = true;
108 break;
109 }
110 }
111
112 assert!(
113 found,
114 "Expected point [{}, {}] not found in theta",
115 expected_point[0], expected_point[1]
116 );
117 }
118
119 // Check that all points in theta are in expected
120 for i in 0..theta.matrix().nrows() {
121 let theta_point = theta.matrix().row(i);
122 let mut found = false;
123
124 for j in 0..expected.nrows() {
125 let expected_point = expected.row(j);
126
127 // Check if points match (within small epsilon)
128 if (theta_point[0] - expected_point[0]).abs() < 1e-10
129 && (theta_point[1] - expected_point[1]).abs() < 1e-10
130 {
131 found = true;
132 break;
133 }
134 }
135
136 assert!(
137 found,
138 "Point [{}, {}] in theta was not expected",
139 theta_point[0], theta_point[1]
140 );
141 }
142 }
143
144 #[test]
145 fn test_basic_expansion() {
146 // Create initial theta with a single point [0.5, 0.5]
147 let mut theta = Theta::from(mat![[0.5, 0.5]]);
148
149 // Define ranges for two dimensions
150 let ranges = [(0.0, 1.0), (0.0, 1.0)];
151
152 // Set expansion parameters
153 let eps = 0.1;
154 let min_dist = 0.05;
155
156 // Apply adaptive grid
157 adaptative_grid(&mut theta, eps, &ranges, min_dist);
158
159 // Should generate 4 new points around the original:
160 // [0.6, 0.5], [0.4, 0.5], [0.5, 0.6], [0.5, 0.4]
161 // Total 5 points including the original
162 assert_eq!(theta.matrix().nrows(), 5);
163
164 // Verify the original point is preserved
165 let matrix = theta.matrix();
166 let mut has_original = false;
167
168 for i in 0..matrix.nrows() {
169 let row = matrix.row(i);
170 if (row[0] - 0.5).abs() < 1e-10 && (row[1] - 0.5).abs() < 1e-10 {
171 has_original = true;
172 break;
173 }
174 }
175 assert!(has_original, "Original point should be preserved");
176
177 // Verify expansion points were created
178 let expected_points = vec![(0.6, 0.5), (0.4, 0.5), (0.5, 0.6), (0.5, 0.4)];
179 for (x, y) in expected_points {
180 let mut found = false;
181 for i in 0..matrix.nrows() {
182 let row = matrix.row(i);
183 if (row[0] - x).abs() < 1e-10 && (row[1] - y).abs() < 1e-10 {
184 found = true;
185 break;
186 }
187 }
188 assert!(found, "Expected point ({}, {}) not found", x, y);
189 }
190 }
191
192 #[test]
193 fn test_boundary_conditions() {
194 // Create initial theta with points near boundaries
195 let mut theta = Theta::from(mat![
196 [0.05, 0.5], // Near lower boundary in x
197 [0.95, 0.5], // Near upper boundary in x
198 [0.5, 0.05], // Near lower boundary in y
199 [0.5, 0.95], // Near upper boundary in y
200 ]);
201
202 let ranges = [(0.0, 1.0), (0.0, 1.0)];
203 let eps = 0.1;
204 let min_dist = 0.05;
205
206 // Store original count
207 let original_count = theta.matrix().nrows();
208
209 adaptative_grid(&mut theta, eps, &ranges, min_dist);
210
211 // Each point should generate fewer than 4 new points due to boundaries
212 assert!(theta.matrix().nrows() > original_count);
213 assert!(theta.matrix().nrows() < original_count + 4 * 4);
214
215 // Verify no points are outside the range
216 let matrix = theta.matrix();
217 for i in 0..matrix.nrows() {
218 let row = matrix.row(i);
219 assert!(row[0] >= ranges[0].0 && row[0] <= ranges[0].1);
220 assert!(row[1] >= ranges[1].0 && row[1] <= ranges[1].1);
221 }
222 }
223
224 #[test]
225 fn test_min_distance_constraint() {
226 // Create initial theta with close points
227 let mut theta = Theta::from(mat![
228 [0.5, 0.5],
229 [0.55, 0.5], // Close to first point
230 ]);
231
232 let ranges = [(0.0, 1.0), (0.0, 10.0)];
233 let eps = 0.1;
234 let min_dist = 0.15; // Large enough to prevent some points from being added
235
236 adaptative_grid(&mut theta, eps, &ranges, min_dist);
237
238 // We should have fewer points than the maximum possible expansion
239 // due to the minimum distance constraint
240 assert!(theta.matrix().nrows() < 2 + 2 * 4);
241 }
242}
243 */