Skip to main content

pounce_cli/minima/
archive.rs

1//! Archive of accepted minima with per-dimension-scaled dedup, mirroring
2//! `MinimaArchive` in `python/pounce/_minima.py`.
3//!
4//! Two points are "the same" when their Euclidean distance in the
5//! per-dimension scaled space `‖(a−b)/L‖` is within `dedup`, where `L` is
6//! the box width per variable (1.0 for unbounded dims). This makes `dedup`
7//! scale-free and keeps it consistent with the anisotropic repulsion widths.
8
9use pounce_common::types::Number;
10
11/// Scaled Euclidean distance `‖(a−b)/L‖`.
12pub fn scaled_distance(a: &[Number], b: &[Number], l: &[Number]) -> Number {
13    let mut acc = 0.0;
14    for i in 0..a.len() {
15        let d = (a[i] - b[i]) / l[i];
16        acc += d * d;
17    }
18    acc.sqrt()
19}
20
21/// Accepted minima plus the dedup test.
22pub struct Archive {
23    dedup: Number,
24    /// Per-dimension scale `L` for the dedup metric.
25    l: Vec<Number>,
26    pub xs: Vec<Vec<Number>>,
27    pub fs: Vec<Number>,
28    /// Per-minimum base-problem constraint duals (length `m`), recovered by a
29    /// clean re-solve at the accepted point (issue #196, related). Parallel to
30    /// `xs` / `fs`.
31    pub ls: Vec<Vec<Number>>,
32}
33
34impl Archive {
35    pub fn new(dedup: Number, l: Vec<Number>) -> Self {
36        Self {
37            dedup,
38            l,
39            xs: Vec::new(),
40            fs: Vec::new(),
41            ls: Vec::new(),
42        }
43    }
44
45    pub fn len(&self) -> usize {
46        self.xs.len()
47    }
48
49    pub fn is_empty(&self) -> bool {
50        self.xs.is_empty()
51    }
52
53    /// Is `x` within `dedup` of any already-accepted minimum?
54    pub fn is_known(&self, x: &[Number]) -> bool {
55        self.xs
56            .iter()
57            .any(|m| scaled_distance(x, m, &self.l) <= self.dedup)
58    }
59
60    /// Is `x` within `radius` of any accepted minimum (MLSL clustering)?
61    pub fn near_any(&self, x: &[Number], radius: Number) -> bool {
62        self.xs
63            .iter()
64            .any(|m| scaled_distance(x, m, &self.l) <= radius)
65    }
66
67    pub fn add(&mut self, x: Vec<Number>, lambda: Vec<Number>, f: Number) {
68        self.xs.push(x);
69        self.ls.push(lambda);
70        self.fs.push(f);
71    }
72
73    /// Indices of the accepted minima ordered by ascending objective.
74    pub fn order_by_objective(&self) -> Vec<usize> {
75        let mut idx: Vec<usize> = (0..self.fs.len()).collect();
76        idx.sort_by(|&a, &b| {
77            self.fs[a]
78                .partial_cmp(&self.fs[b])
79                .unwrap_or(std::cmp::Ordering::Equal)
80        });
81        idx
82    }
83}