pounce_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/// The base-problem multipliers recovered at an accepted minimum.
22#[derive(Clone, Debug, Default)]
23pub struct Duals {
24 /// Constraint duals, length `m`, in the user's `∇f + λ·∇g + z = 0`
25 /// convention.
26 pub lambda: Vec<Number>,
27 /// Bound multipliers, length `n` (empty when unavailable), `≥ 0` at an
28 /// active bound, in the frame of the problem the engine solved.
29 pub z_l: Vec<Number>,
30 pub z_u: Vec<Number>,
31}
32
33/// Accepted minima plus the dedup test.
34pub struct Archive {
35 dedup: Number,
36 /// Per-dimension scale `L` for the dedup metric.
37 l: Vec<Number>,
38 pub xs: Vec<Vec<Number>>,
39 pub fs: Vec<Number>,
40 /// Per-minimum base-problem constraint duals (length `m`), recovered by a
41 /// clean re-solve at the accepted point (issue #196, related). Parallel to
42 /// `xs` / `fs`.
43 pub ls: Vec<Vec<Number>>,
44 /// Per-minimum lower / upper bound multipliers (length `n`, or empty when
45 /// unavailable), recovered alongside `ls`. Parallel to `xs` / `fs`.
46 pub zls: Vec<Vec<Number>>,
47 pub zus: Vec<Vec<Number>>,
48}
49
50impl Archive {
51 pub fn new(dedup: Number, l: Vec<Number>) -> Self {
52 Self {
53 dedup,
54 l,
55 xs: Vec::new(),
56 fs: Vec::new(),
57 ls: Vec::new(),
58 zls: Vec::new(),
59 zus: Vec::new(),
60 }
61 }
62
63 pub fn len(&self) -> usize {
64 self.xs.len()
65 }
66
67 pub fn is_empty(&self) -> bool {
68 self.xs.is_empty()
69 }
70
71 /// Is `x` within `dedup` of any already-accepted minimum?
72 pub fn is_known(&self, x: &[Number]) -> bool {
73 self.xs
74 .iter()
75 .any(|m| scaled_distance(x, m, &self.l) <= self.dedup)
76 }
77
78 /// Is `x` within `radius` of any accepted minimum (MLSL clustering)?
79 pub fn near_any(&self, x: &[Number], radius: Number) -> bool {
80 self.xs
81 .iter()
82 .any(|m| scaled_distance(x, m, &self.l) <= radius)
83 }
84
85 pub fn add(&mut self, x: Vec<Number>, duals: Duals, f: Number) {
86 self.xs.push(x);
87 self.ls.push(duals.lambda);
88 self.zls.push(duals.z_l);
89 self.zus.push(duals.z_u);
90 self.fs.push(f);
91 }
92
93 /// Indices of the accepted minima ordered by ascending objective.
94 pub fn order_by_objective(&self) -> Vec<usize> {
95 let mut idx: Vec<usize> = (0..self.fs.len()).collect();
96 idx.sort_by(|&a, &b| {
97 self.fs[a]
98 .partial_cmp(&self.fs[b])
99 .unwrap_or(std::cmp::Ordering::Equal)
100 });
101 idx
102 }
103}