Skip to main content

pounce_algorithm/mu/oracle/
loqo.rs

1//! LOQO mu oracle — port of `IpLoqoMuOracle.{hpp,cpp}`. Phase 10.
2//!
3//! The LOQO rule chooses
4//!
5//! ```text
6//!   sigma = 0.1 * min(factor * (1 - xi) / xi, 2)^3
7//!   mu_new = clamp(sigma * avrg_compl, mu_min, mu_max)
8//! ```
9//!
10//! where `xi = curr_centrality_measure() = min_compl / avrg_compl` is
11//! a measure of how far the current iterate is from uniform
12//! complementarity, and `factor = 0.05` per upstream's hard-coded
13//! choice (`IpLoqoMuOracle.cpp:52`).
14
15use crate::mu::oracle::r#trait::MuOracle;
16use pounce_common::types::Number;
17
18pub struct LoqoMuOracle {
19    pub mu_min: Number,
20    pub mu_max: Number,
21    /// Latest cached `(avrg_compl, centrality_xi)` from
22    /// `IpoptCalculatedQuantities`. The full plumbing wires this in
23    /// after the CQ port lands; here we expose a setter so the
24    /// arithmetic can be unit-tested in isolation.
25    pub avrg_compl: Number,
26    pub centrality_xi: Number,
27}
28
29impl Default for LoqoMuOracle {
30    fn default() -> Self {
31        Self {
32            mu_min: 1e-11,
33            mu_max: 1e5,
34            avrg_compl: 1.0,
35            centrality_xi: 1.0,
36        }
37    }
38}
39
40impl LoqoMuOracle {
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Pure-arithmetic LOQO formula. Exposed standalone so it can be
46    /// validated against upstream's `IpLoqoMuOracle.cpp:43-65`
47    /// captures.
48    pub fn loqo_mu(avrg_compl: Number, centrality_xi: Number) -> Number {
49        let factor: Number = 0.05;
50        let xi = centrality_xi.max(Number::MIN_POSITIVE);
51        let bracket = (factor * (1.0 - xi) / xi).min(2.0);
52        let sigma = 0.1 * bracket.powi(3);
53        sigma * avrg_compl
54    }
55}
56
57impl MuOracle for LoqoMuOracle {
58    fn calculate_mu(&mut self) -> Option<Number> {
59        let raw = Self::loqo_mu(self.avrg_compl, self.centrality_xi);
60        Some(raw.clamp(self.mu_min, self.mu_max))
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn loqo_at_uniform_complementarity_is_zero() {
70        // xi = 1 (uniform) → bracket = 0 → sigma = 0 → mu = 0.
71        assert_eq!(LoqoMuOracle::loqo_mu(1.0, 1.0), 0.0);
72    }
73
74    #[test]
75    fn loqo_caps_bracket_at_two() {
76        // xi very small → bracket = min(0.05*(1-eps)/eps, 2) = 2.
77        // sigma = 0.1 * 8 = 0.8. mu = 0.8 * avrg_compl.
78        let m = LoqoMuOracle::loqo_mu(0.5, 1e-10);
79        assert!((m - 0.4).abs() < 1e-13);
80    }
81
82    #[test]
83    fn loqo_intermediate_xi() {
84        // xi = 0.5 → factor*(1-0.5)/0.5 = 0.05; bracket = 0.05.
85        // sigma = 0.1 * 1.25e-4 = 1.25e-5; mu = 1.25e-5 * 1 = 1.25e-5.
86        let m = LoqoMuOracle::loqo_mu(1.0, 0.5);
87        assert!((m - 1.25e-5).abs() < 1e-15);
88    }
89
90    #[test]
91    fn calculate_mu_clamps_to_band() {
92        let mut o = LoqoMuOracle {
93            mu_min: 1.0,
94            mu_max: 2.0,
95            avrg_compl: 1.0,
96            centrality_xi: 1.0, // raw = 0
97        };
98        assert_eq!(o.calculate_mu(), Some(1.0));
99    }
100}