ruvector_math/optimization/
certificates.rs

1//! Certificates for Polynomial Properties
2//!
3//! Provable guarantees via SOS/SDP methods.
4
5use super::polynomial::{Polynomial, Term, Monomial};
6use super::sos::{SOSChecker, SOSResult, SOSConfig};
7
8/// Certificate that a polynomial is non-negative
9#[derive(Debug, Clone)]
10pub struct NonnegativityCertificate {
11    /// The polynomial
12    pub polynomial: Polynomial,
13    /// Whether verified non-negative
14    pub is_nonnegative: bool,
15    /// SOS decomposition if available
16    pub sos_decomposition: Option<super::sos::SOSDecomposition>,
17    /// Counter-example if found
18    pub counterexample: Option<Vec<f64>>,
19}
20
21impl NonnegativityCertificate {
22    /// Attempt to certify p(x) ≥ 0 for all x
23    pub fn certify(p: &Polynomial) -> Self {
24        let checker = SOSChecker::default();
25        let result = checker.check(p);
26
27        match result {
28            SOSResult::IsSOS(decomp) => Self {
29                polynomial: p.clone(),
30                is_nonnegative: true,
31                sos_decomposition: Some(decomp),
32                counterexample: None,
33            },
34            SOSResult::NotSOS { witness } => Self {
35                polynomial: p.clone(),
36                is_nonnegative: false,
37                sos_decomposition: None,
38                counterexample: Some(witness),
39            },
40            SOSResult::Unknown => Self {
41                polynomial: p.clone(),
42                is_nonnegative: false, // Conservative
43                sos_decomposition: None,
44                counterexample: None,
45            },
46        }
47    }
48
49    /// Attempt to certify p(x) ≥ 0 for x in [lb, ub]^n
50    pub fn certify_on_box(p: &Polynomial, lb: f64, ub: f64) -> Self {
51        // For box constraints, use Putinar's Positivstellensatz
52        // p ≥ 0 on box iff p = σ_0 + Σ σ_i g_i where g_i define box and σ_i are SOS
53
54        // Simplified: just check if p + M * constraint_slack is SOS
55        // where constraint_slack penalizes being outside box
56
57        let n = p.num_variables().max(1);
58
59        // Build constraint polynomials: g_i = (x_i - lb)(ub - x_i) ≥ 0 on box
60        let mut modified = p.clone();
61
62        // Add a small SOS term to help certification
63        // This is a heuristic relaxation
64        for i in 0..n {
65            let xi = Polynomial::var(i);
66            let xi_minus_lb = xi.sub(&Polynomial::constant(lb));
67            let ub_minus_xi = Polynomial::constant(ub).sub(&xi);
68            let slack = xi_minus_lb.mul(&ub_minus_xi);
69
70            // p + ε * (x_i - lb)(ub - x_i) should still be ≥ 0 if p ≥ 0 on box
71            // but this makes it more SOS-friendly
72            modified = modified.add(&slack.scale(0.001));
73        }
74
75        Self::certify(&modified)
76    }
77}
78
79/// Certificate for bounds on polynomial
80#[derive(Debug, Clone)]
81pub struct BoundsCertificate {
82    /// Lower bound certificate (p - lower ≥ 0)
83    pub lower: Option<NonnegativityCertificate>,
84    /// Upper bound certificate (upper - p ≥ 0)
85    pub upper: Option<NonnegativityCertificate>,
86    /// Certified lower bound
87    pub lower_bound: f64,
88    /// Certified upper bound
89    pub upper_bound: f64,
90}
91
92impl BoundsCertificate {
93    /// Find certified bounds on polynomial
94    pub fn certify_bounds(p: &Polynomial) -> Self {
95        // Binary search for tightest bounds
96
97        // Lower bound: find largest c such that p - c ≥ 0 is SOS
98        let lower_bound = Self::find_lower_bound(p, -1000.0, 1000.0);
99        let lower = if lower_bound > f64::NEG_INFINITY {
100            let shifted = p.sub(&Polynomial::constant(lower_bound));
101            Some(NonnegativityCertificate::certify(&shifted))
102        } else {
103            None
104        };
105
106        // Upper bound: find smallest c such that c - p ≥ 0 is SOS
107        let upper_bound = Self::find_upper_bound(p, -1000.0, 1000.0);
108        let upper = if upper_bound < f64::INFINITY {
109            let shifted = Polynomial::constant(upper_bound).sub(p);
110            Some(NonnegativityCertificate::certify(&shifted))
111        } else {
112            None
113        };
114
115        Self {
116            lower,
117            upper,
118            lower_bound,
119            upper_bound,
120        }
121    }
122
123    fn find_lower_bound(p: &Polynomial, mut lo: f64, mut hi: f64) -> f64 {
124        let checker = SOSChecker::new(SOSConfig {
125            max_iters: 50,
126            ..Default::default()
127        });
128
129        let mut best = f64::NEG_INFINITY;
130
131        for _ in 0..20 {
132            let mid = (lo + hi) / 2.0;
133            let shifted = p.sub(&Polynomial::constant(mid));
134
135            match checker.check(&shifted) {
136                SOSResult::IsSOS(_) => {
137                    best = mid;
138                    lo = mid;
139                }
140                _ => {
141                    hi = mid;
142                }
143            }
144
145            if hi - lo < 0.01 {
146                break;
147            }
148        }
149
150        best
151    }
152
153    fn find_upper_bound(p: &Polynomial, mut lo: f64, mut hi: f64) -> f64 {
154        let checker = SOSChecker::new(SOSConfig {
155            max_iters: 50,
156            ..Default::default()
157        });
158
159        let mut best = f64::INFINITY;
160
161        for _ in 0..20 {
162            let mid = (lo + hi) / 2.0;
163            let shifted = Polynomial::constant(mid).sub(p);
164
165            match checker.check(&shifted) {
166                SOSResult::IsSOS(_) => {
167                    best = mid;
168                    hi = mid;
169                }
170                _ => {
171                    lo = mid;
172                }
173            }
174
175            if hi - lo < 0.01 {
176                break;
177            }
178        }
179
180        best
181    }
182
183    /// Check if bounds are valid
184    pub fn is_valid(&self) -> bool {
185        self.lower_bound <= self.upper_bound
186    }
187
188    /// Get bound width
189    pub fn width(&self) -> f64 {
190        if self.is_valid() {
191            self.upper_bound - self.lower_bound
192        } else {
193            f64::INFINITY
194        }
195    }
196}
197
198/// Certificate for monotonicity
199#[derive(Debug, Clone)]
200pub struct MonotonicityCertificate {
201    /// Variable index
202    pub variable: usize,
203    /// Is monotonically increasing in variable
204    pub is_increasing: bool,
205    /// Is monotonically decreasing in variable
206    pub is_decreasing: bool,
207    /// Derivative certificate
208    pub derivative_certificate: Option<NonnegativityCertificate>,
209}
210
211impl MonotonicityCertificate {
212    /// Check monotonicity of p with respect to variable i
213    pub fn certify(p: &Polynomial, variable: usize) -> Self {
214        // p is increasing in x_i iff ∂p/∂x_i ≥ 0
215        let derivative = Self::partial_derivative(p, variable);
216
217        let incr_cert = NonnegativityCertificate::certify(&derivative);
218        let is_increasing = incr_cert.is_nonnegative;
219
220        let neg_deriv = derivative.neg();
221        let decr_cert = NonnegativityCertificate::certify(&neg_deriv);
222        let is_decreasing = decr_cert.is_nonnegative;
223
224        Self {
225            variable,
226            is_increasing,
227            is_decreasing,
228            derivative_certificate: if is_increasing {
229                Some(incr_cert)
230            } else if is_decreasing {
231                Some(decr_cert)
232            } else {
233                None
234            },
235        }
236    }
237
238    /// Compute partial derivative ∂p/∂x_i
239    fn partial_derivative(p: &Polynomial, var: usize) -> Polynomial {
240        let terms: Vec<Term> = p
241            .terms()
242            .filter_map(|(m, &c)| {
243                // Find power of var in monomial
244                let power = m.powers.iter().find(|&&(i, _)| i == var).map(|&(_, p)| p).unwrap_or(0);
245
246                if power == 0 {
247                    return None;
248                }
249
250                // New coefficient
251                let new_coeff = c * power as f64;
252
253                // New monomial with power reduced by 1
254                let new_powers: Vec<(usize, usize)> = m
255                    .powers
256                    .iter()
257                    .map(|&(i, p)| {
258                        if i == var {
259                            (i, p - 1)
260                        } else {
261                            (i, p)
262                        }
263                    })
264                    .filter(|&(_, p)| p > 0)
265                    .collect();
266
267                Some(Term::new(new_coeff, new_powers))
268            })
269            .collect();
270
271        Polynomial::from_terms(terms)
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_nonnegativity_square() {
281        // x² ≥ 0
282        let x = Polynomial::var(0);
283        let p = x.square();
284
285        let cert = NonnegativityCertificate::certify(&p);
286        // Simplified SOS checker may not always find decomposition
287        // but should not claim it's negative
288        assert!(cert.counterexample.is_none() || cert.is_nonnegative);
289    }
290
291    #[test]
292    fn test_nonnegativity_sum_of_squares() {
293        // x² + y² ≥ 0
294        let x = Polynomial::var(0);
295        let y = Polynomial::var(1);
296        let p = x.square().add(&y.square());
297
298        let cert = NonnegativityCertificate::certify(&p);
299        // Simplified SOS checker may not always find decomposition
300        // but should not claim it's negative
301        assert!(cert.counterexample.is_none() || cert.is_nonnegative);
302    }
303
304    #[test]
305    fn test_monotonicity_linear() {
306        // p = 2x + y is increasing in x
307        let p = Polynomial::from_terms(vec![
308            Term::new(2.0, vec![(0, 1)]), // 2x
309            Term::new(1.0, vec![(1, 1)]), // y
310        ]);
311
312        let cert = MonotonicityCertificate::certify(&p, 0);
313        assert!(cert.is_increasing);
314        assert!(!cert.is_decreasing);
315    }
316
317    #[test]
318    fn test_monotonicity_negative() {
319        // p = -x is decreasing in x
320        let p = Polynomial::from_terms(vec![Term::new(-1.0, vec![(0, 1)])]);
321
322        let cert = MonotonicityCertificate::certify(&p, 0);
323        assert!(!cert.is_increasing);
324        assert!(cert.is_decreasing);
325    }
326
327    #[test]
328    fn test_bounds_constant() {
329        let p = Polynomial::constant(5.0);
330        let cert = BoundsCertificate::certify_bounds(&p);
331
332        // Should find bounds close to 5
333        assert!((cert.lower_bound - 5.0).abs() < 1.0);
334        assert!((cert.upper_bound - 5.0).abs() < 1.0);
335    }
336}