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 */