Skip to main content

solver/lookup/
adaptive.rs

1use serde::{Deserialize, Serialize};
2use std::fs::File;
3use std::io::{BufReader, BufWriter};
4use std::path::Path;
5
6/// Result of recursive bisection: a set of segments with refined points.
7#[derive(Clone, Debug)]
8pub struct AdaptiveSegments {
9    /// Ordered list of grid points (strictly increasing)
10    pub points: Vec<f64>,
11}
12
13impl AdaptiveSegments {
14    /// Create from a set of points (will be sorted and deduplicated)
15    pub fn from_points(mut points: Vec<f64>) -> Self {
16        points.sort_by(|a, b| a.partial_cmp(b).unwrap());
17        points.dedup_by(|a, b| (*a - *b).abs() < 1e-14);
18        Self { points }
19    }
20
21    /// Length of the grid
22    pub fn len(&self) -> usize {
23        self.points.len()
24    }
25
26    /// Check if empty
27    pub fn is_empty(&self) -> bool {
28        self.points.is_empty()
29    }
30}
31
32/// Adaptive grid generator using recursive bisection.
33///
34/// This generator recursively subdivides intervals in 1D, refining only where
35/// the underlying function exhibits high variance (error exceeds tolerance).
36///
37/// **Algorithm:**
38/// 1. Start with an interval [a, b]
39/// 2. Evaluate the function at a, midpoint m, and b
40/// 3. Compute the linear interpolation at m from endpoints
41/// 4. If |f(m) - interpolated(m)| > tolerance, bisect into [a, m] and [m, b] and recurse
42/// 5. Otherwise, mark the interval as sufficiently smooth
43///
44/// This guarantees adaptive refinement: dense grids form only near sharp features,
45/// while smooth regions remain coarse.
46pub struct AdaptiveGrid1D {
47    start: f64,
48    end: f64,
49    tolerance: f64,
50    max_depth: usize,
51}
52
53impl AdaptiveGrid1D {
54    /// Create a new adaptive 1D grid generator.
55    ///
56    /// # Arguments
57    /// * `start` - Left boundary
58    /// * `end` - Right boundary
59    /// * `tolerance` - Error threshold for bisection
60    /// * `max_depth` - Maximum recursion depth (prevents pathological refinement)
61    pub fn new(start: f64, end: f64, tolerance: f64, max_depth: usize) -> Self {
62        assert!(start < end, "start must be less than end");
63        assert!(tolerance > 0.0, "tolerance must be positive");
64        Self {
65            start,
66            end,
67            tolerance,
68            max_depth,
69        }
70    }
71
72    /// Generate an adaptive grid by evaluating `f` at points determined by tolerance.
73    ///
74    /// Returns a strictly increasing list of grid points.
75    pub fn generate<F>(&self, f: F) -> AdaptiveSegments
76    where
77        F: Fn(f64) -> f64,
78    {
79        let mut points = Vec::new();
80        self.bisect(self.start, self.end, &f, 0, &mut points);
81        points.push(self.end); // Always include right boundary
82        AdaptiveSegments::from_points(points)
83    }
84
85    /// Recursive bisection helper.
86    fn bisect<F>(&self, a: f64, b: f64, f: &F, depth: usize, points: &mut Vec<f64>)
87    where
88        F: Fn(f64) -> f64,
89    {
90        // Add left boundary only once
91        if points.is_empty() || (points.last().unwrap() - a).abs() > 1e-14 {
92            points.push(a);
93        }
94
95        let m = (a + b) / 2.0;
96        let fa = f(a);
97        let fm = f(m);
98        let fb = f(b);
99
100        // Linear interpolation at midpoint
101        let fm_interp = (fa + fb) / 2.0;
102        let error = (fm - fm_interp).abs();
103
104        if error > self.tolerance && depth < self.max_depth {
105            // Error too large: refine by bisecting
106            self.bisect(a, m, f, depth + 1, points);
107            self.bisect(m, b, f, depth + 1, points);
108        } else {
109            // Error acceptable: mark this interval as done, add midpoint if needed
110            if points.is_empty() || (points.last().unwrap() - m).abs() > 1e-14 {
111                points.push(m);
112            }
113        }
114    }
115}
116
117/// A two-tier indirection table for deterministic O(1) lookup.
118///
119/// **Architecture:**
120/// - **Level 0 (Coarse Array):** A flat, uniform array spanning the entire domain at low resolution.
121/// - **Level 1 (High-Resolution Array):** Packed blocks of fine-grained data for refined regions.
122///
123/// Each entry in Level 0 contains either:
124/// - A **Direct Value:** The actual lookup result (if no refinement needed)
125/// - A **Pointer:** An offset into Level 1 pointing to the start of a high-res block
126///
127/// A bit-flag (sign bit) distinguishes the two cases.
128#[derive(Clone, Serialize, Deserialize, Debug)]
129pub struct IndirectionTable {
130    /// Coarse uniform grid (Level 0). Entries are either direct values or pointers to Level 1.
131    /// Stored as (value_or_offset, is_pointer) pairs for clarity in serialization.
132    coarse: Vec<(f64, bool)>,
133    /// High-resolution packed blocks (Level 1). Contains refined values for adaptive regions.
134    fine: Vec<f64>,
135    /// Boundaries of the coarse grid
136    min: f64,
137    max: f64,
138    /// Coarse grid resolution
139    coarse_resolution: usize,
140}
141
142impl IndirectionTable {
143    /// Create an indirection table from coarse and fine-grained data.
144    ///
145    /// # Arguments
146    /// * `coarse` - Level 0 entries: (value_or_offset, is_pointer) tuples
147    /// * `fine` - Level 1: packed high-resolution blocks
148    /// * `min` - Domain minimum
149    /// * `max` - Domain maximum
150    /// * `coarse_resolution` - Number of cells in coarse grid
151    pub fn new(
152        coarse: Vec<(f64, bool)>,
153        fine: Vec<f64>,
154        min: f64,
155        max: f64,
156        coarse_resolution: usize,
157    ) -> Self {
158        assert_eq!(
159            coarse.len(),
160            coarse_resolution,
161            "coarse length must match coarse_resolution"
162        );
163        assert!(min < max, "min must be less than max");
164        Self {
165            coarse,
166            fine,
167            min,
168            max,
169            coarse_resolution,
170        }
171    }
172
173    /// Branchless $O(1)$ lookup at a single point.
174    ///
175    /// Returns either the direct value (single fetch) or looks up in Level 1 (dual fetch).
176    pub fn lookup(&self, x: f64) -> f64 {
177        if x <= self.min {
178            return self.coarse[0].0;
179        }
180        if x >= self.max {
181            return self.coarse[self.coarse_resolution - 1].0;
182        }
183
184        // Direct index calculation for coarse grid (O(1))
185        let normalized = (x - self.min) / (self.max - self.min);
186        let coarse_idx =
187            ((normalized * self.coarse_resolution as f64) as usize).min(self.coarse_resolution - 1);
188
189        let (payload, is_pointer) = self.coarse[coarse_idx];
190
191        if !is_pointer {
192            // Direct value: single memory fetch complete
193            payload
194        } else {
195            // Pointer into fine-grained array: second fetch required
196            let offset = payload as usize;
197            if offset < self.fine.len() {
198                self.fine[offset]
199            } else {
200                payload // Fallback to payload if offset invalid
201            }
202        }
203    }
204
205    /// Save the indirection table to disk.
206    pub fn save(&self, path: &Path) -> bincode::Result<()> {
207        let file = File::create(path)?;
208        let writer = BufWriter::new(file);
209        bincode::serialize_into(writer, self)
210    }
211
212    /// Load an indirection table from disk.
213    pub fn load(path: &Path) -> bincode::Result<Self> {
214        let file = File::open(path)?;
215        let reader = BufReader::new(file);
216        bincode::deserialize_from(reader)
217    }
218
219    /// Get statistics about the table structure.
220    pub fn stats(&self) -> IndirectionStats {
221        let num_direct = self.coarse.iter().filter(|(_, is_ptr)| !is_ptr).count();
222        let num_pointers = self.coarse.iter().filter(|(_, is_ptr)| *is_ptr).count();
223        let coarse_bytes = std::mem::size_of::<f64>() * 2 * self.coarse.len(); // (f64, bool) per entry
224        let fine_bytes = std::mem::size_of::<f64>() * self.fine.len();
225
226        IndirectionStats {
227            num_direct_values: num_direct,
228            num_pointers,
229            fine_array_size: self.fine.len(),
230            coarse_bytes,
231            fine_bytes,
232            total_bytes: coarse_bytes + fine_bytes,
233        }
234    }
235}
236
237/// Statistics about an indirection table.
238#[derive(Debug, Clone)]
239pub struct IndirectionStats {
240    pub num_direct_values: usize,
241    pub num_pointers: usize,
242    pub fine_array_size: usize,
243    pub coarse_bytes: usize,
244    pub fine_bytes: usize,
245    pub total_bytes: usize,
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_adaptive_grid_1d_linear() {
254        let grid_gen = AdaptiveGrid1D::new(0.0, 1.0, 0.1, 10);
255        let grid = grid_gen.generate(|x| x); // Linear function: no refinement needed
256
257        // Should have at least start and end
258        assert!(grid.len() >= 2);
259        assert!((grid.points[0] - 0.0).abs() < 1e-10);
260        assert!((grid.points[grid.len() - 1] - 1.0).abs() < 1e-10);
261
262        // Linear function should require minimal refinement
263        assert!(grid.len() <= 10, "Linear function should not over-refine");
264    }
265
266    #[test]
267    fn test_adaptive_grid_1d_quadratic() {
268        // f(x) = x^2: smooth curve, moderate refinement
269        let grid_gen = AdaptiveGrid1D::new(0.0, 1.0, 0.01, 10);
270        let grid = grid_gen.generate(|x| x * x);
271
272        assert!(grid.len() >= 2);
273        assert!((grid.points[0] - 0.0).abs() < 1e-10);
274        assert!((grid.points[grid.len() - 1] - 1.0).abs() < 1e-10);
275    }
276
277    #[test]
278    fn test_adaptive_grid_1d_sharp_feature() {
279        // f(x) = 1 / (1 + (x - 0.5)^2): bell curve with sharp peak
280        // This is more amenable to bisection-based refinement
281        let grid_gen = AdaptiveGrid1D::new(0.0, 1.0, 0.01, 12);
282        let grid = grid_gen.generate(|x| {
283            let dx = x - 0.5;
284            1.0 / (1.0 + 100.0 * dx * dx)
285        });
286
287        // Should refine around the peak at x=0.5
288        assert!(
289            grid.len() >= 5,
290            "Sharp features should trigger some refinement"
291        );
292    }
293
294    #[test]
295    fn test_adaptive_grid_monotonicity() {
296        let grid_gen = AdaptiveGrid1D::new(0.0, 1.0, 0.01, 10);
297        let grid = grid_gen.generate(|x| x.exp());
298
299        // Points must be strictly increasing
300        for i in 0..grid.len() - 1 {
301            assert!(
302                grid.points[i] < grid.points[i + 1],
303                "Grid points must be strictly increasing"
304            );
305        }
306    }
307
308    #[test]
309    fn test_indirection_table_direct_values() {
310        // Create a simple indirection table with only direct values (no refinement)
311        let coarse = vec![(1.0, false), (2.0, false), (3.0, false), (4.0, false)];
312        let fine = vec![];
313        let table = IndirectionTable::new(coarse, fine, 0.0, 4.0, 4);
314
315        // Lookups in each coarse region
316        assert_eq!(table.lookup(0.0), 1.0); // Left boundary
317        assert_eq!(table.lookup(0.5), 1.0); // First coarse cell
318        assert_eq!(table.lookup(1.0), 2.0); // Second coarse cell
319        assert_eq!(table.lookup(2.5), 3.0); // Third coarse cell
320        assert_eq!(table.lookup(4.0), 4.0); // Right boundary
321    }
322
323    #[test]
324    fn test_indirection_table_with_pointers() {
325        // Create an indirection table mixing direct values and pointers
326        let coarse = vec![
327            (1.0, false), // Direct value
328            (0.0, true),  // Pointer to fine[0]
329            (3.0, false), // Direct value
330            (1.0, true),  // Pointer to fine[1]
331        ];
332        let fine = vec![1.5, 2.0, 3.5]; // Fine-grained values
333        let table = IndirectionTable::new(coarse, fine, 0.0, 4.0, 4);
334
335        // Lookups that hit direct values
336        assert_eq!(table.lookup(0.1), 1.0);
337        assert_eq!(table.lookup(2.1), 3.0);
338
339        // Lookups that hit pointers (transition through fine array)
340        assert_eq!(table.lookup(1.1), 1.5); // Coarse[1] -> fine[0]
341        assert_eq!(table.lookup(3.1), 2.0); // Coarse[3] -> fine[1]
342    }
343
344    #[test]
345    fn test_indirection_stats() {
346        let coarse = vec![(1.0, false), (0.0, true), (3.0, false), (1.0, true)];
347        let fine = vec![1.5, 2.0, 3.5];
348        let table = IndirectionTable::new(coarse, fine, 0.0, 4.0, 4);
349
350        let stats = table.stats();
351        assert_eq!(stats.num_direct_values, 2);
352        assert_eq!(stats.num_pointers, 2);
353        assert_eq!(stats.fine_array_size, 3);
354        assert!(stats.total_bytes > 0);
355    }
356
357    #[test]
358    fn test_adaptive_grid_max_depth_limit() {
359        // Verify max_depth prevents infinite refinement
360        let grid_gen = AdaptiveGrid1D::new(0.0, 1.0, 0.0001, 5); // Very tight tolerance, shallow depth
361        let grid = grid_gen.generate(|x| x.sin());
362
363        // Should respect max_depth and not explode in size
364        assert!(
365            grid.len() < 1000,
366            "max_depth should prevent pathological refinement"
367        );
368    }
369
370    #[test]
371    fn test_adaptive_grid_constant_function() {
372        // Constant function should require minimal refinement (zero error everywhere)
373        let grid_gen = AdaptiveGrid1D::new(0.0, 1.0, 0.1, 10);
374        let grid = grid_gen.generate(|_x| 5.0);
375
376        // Should be minimal: algorithm adds boundaries and at least one midpoint
377        assert!(grid.len() <= 3, "Constant function should refine minimally");
378    }
379
380    #[test]
381    fn test_indirection_table_boundary_clamp() {
382        let coarse = vec![(1.0, false), (2.0, false), (3.0, false)];
383        let fine = vec![];
384        let table = IndirectionTable::new(coarse, fine, 1.0, 3.0, 3);
385
386        // Out-of-bounds on left
387        assert_eq!(table.lookup(-10.0), 1.0);
388        // Out-of-bounds on right
389        assert_eq!(table.lookup(100.0), 3.0);
390    }
391}