1use oximo_expr::{ExprArena, ExprId, LinearTerms, VarId, extract_linear, extract_quadratic};
2use smol_str::SmolStr;
3
4use crate::constraint::{Constraint, Sense};
5use crate::var::Variable;
6
7#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
8pub struct SocConstraintId(pub u32);
9
10impl SocConstraintId {
11 #[inline]
12 pub fn index(self) -> usize {
13 self.0 as usize
14 }
15}
16
17#[derive(Clone, Debug)]
25pub struct SocConstraint {
26 pub name: SmolStr,
27 pub terms: Vec<ExprId>,
28 pub bound: ExprId,
29 pub active: bool,
30}
31
32#[derive(Clone, Debug)]
38pub struct SocForm {
39 pub terms: Vec<LinearTerms>,
40 pub bound: LinearTerms,
41}
42
43pub fn detect_soc(arena: &ExprArena, vars: &[Variable], c: &Constraint) -> Option<SocForm> {
59 let (sense, rhs) = c.as_single()?;
60 if sense != Sense::Le {
61 return None;
62 }
63 let q = extract_quadratic(arena, c.lhs)?;
64 if !q.linear.is_empty() || q.constant - rhs != 0.0 {
65 return None;
66 }
67
68 let mut positives: Vec<(VarId, f64)> = Vec::new();
69 let mut negative: Option<(VarId, f64)> = None;
70 for &(row, col, h) in &q.hessian {
71 if row != col {
72 return None;
73 }
74 let coef = h / 2.0;
75 if coef > 0.0 {
76 positives.push((row, coef));
77 } else if coef < 0.0 {
78 if negative.is_some() {
79 return None;
80 }
81 negative = Some((row, -coef));
82 }
83 }
84 let (t, n) = negative?;
85 if positives.is_empty() || vars[t.index()].lb < 0.0 {
86 return None;
87 }
88
89 let terms = positives
90 .into_iter()
91 .map(|(x, p)| LinearTerms { coeffs: vec![(x, (p / n).sqrt())], constant: 0.0 })
92 .collect();
93 let bound = LinearTerms { coeffs: vec![(t, 1.0)], constant: 0.0 };
94 Some(SocForm { terms, bound })
95}
96
97pub fn explicit_soc_form(arena: &ExprArena, s: &SocConstraint) -> Option<SocForm> {
101 let terms = s.terms.iter().map(|&e| extract_linear(arena, e)).collect::<Option<Vec<_>>>()?;
102 let bound = extract_linear(arena, s.bound)?;
103 Some(SocForm { terms, bound })
104}