Skip to main content

oximo_expr/
classify.rs

1use crate::arena::{ArenaAccess, ExprArena, ExprId, ExprNode, UnaryOp};
2
3/// Highest-degree polynomial class an expression belongs to, ignoring constant
4/// folding. Used by backends to pick between linear, quadratic, and general
5/// nonlinear translation paths.
6///
7/// Variants are ordered by increasing degree, so `max` of two classes yields the
8/// dominating one (e.g. a model with a quadratic objective and a nonlinear
9/// constraint is `Nonlinear`).
10#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
11pub enum ExprClass {
12    Linear,
13    Quadratic,
14    Nonlinear,
15}
16
17/// Polynomial-degree bucket. `Higher` is a saturating sentinel for "anything
18/// above quadratic". Both polynomial degree > 2 and transcendentals collapse
19/// into it, since neither fits a QP solver's quadratic API.
20#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
21enum Degree {
22    Zero,
23    One,
24    Two,
25    Higher,
26}
27
28impl Degree {
29    /// `+` on a sum: take the maximum, saturating at `Higher`.
30    fn add(self, other: Degree) -> Degree {
31        self.max(other)
32    }
33
34    /// `*` on a product: add ordinal degrees, saturating at `Higher`.
35    fn mul(self, other: Degree) -> Degree {
36        match (self, other) {
37            (Degree::Higher, _) | (_, Degree::Higher) => Degree::Higher,
38            (Degree::Zero, x) | (x, Degree::Zero) => x,
39            (Degree::One, Degree::One) => Degree::Two,
40            _ => Degree::Higher,
41        }
42    }
43
44    /// `^n` on a power: multiply by `n`, saturating at `Higher`.
45    fn pow(self, n: u32) -> Degree {
46        match (self, n) {
47            (_, 0) | (Degree::Zero, _) => Degree::Zero,
48            (d, 1) => d,
49            (Degree::One, 2) => Degree::Two,
50            _ => Degree::Higher,
51        }
52    }
53}
54
55#[cfg(test)]
56fn recursive_degree(arena: &(impl ArenaAccess + ?Sized), id: ExprId) -> Degree {
57    match arena.get(id) {
58        ExprNode::Const(_) | ExprNode::Param(_) => Degree::Zero,
59        ExprNode::Var(_) | ExprNode::Linear { .. } => Degree::One,
60        ExprNode::Unary(UnaryOp::Neg, inner) => recursive_degree(arena, *inner),
61        ExprNode::Add(children) => {
62            let mut d = Degree::Zero;
63            for c in children {
64                d = d.add(recursive_degree(arena, *c));
65                if d == Degree::Higher {
66                    return d;
67                }
68            }
69            d
70        }
71        ExprNode::Mul(children) => {
72            let mut d = Degree::Zero;
73            for c in children {
74                d = d.mul(recursive_degree(arena, *c));
75                if d == Degree::Higher {
76                    return d;
77                }
78            }
79            d
80        }
81        ExprNode::Pow(base, exp) => {
82            let ExprNode::Const(e) = arena.get(*exp) else { return Degree::Higher };
83            if (*e - e.round()).abs() >= f64::EPSILON || *e < 0.0 {
84                return Degree::Higher;
85            }
86            // Bucket the exponent into the only values `Degree::pow` treats
87            // distinctly.
88            let n = match e.round() {
89                v if v < 0.5 => 0,
90                v if v < 1.5 => 1,
91                v if v < 2.5 => 2,
92                _ => 3,
93            };
94            recursive_degree(arena, *base).pow(n)
95        }
96        // Transcendentals are always > quadratic. Division is too: `div_into`
97        // folds the only degree-preserving case (constant denominator) before a
98        // `Div` node is created, so any other `Div` has a non-constant
99        // denominator.
100        ExprNode::Div(_, _)
101        | ExprNode::Atan2(_, _)
102        | ExprNode::Min(_)
103        | ExprNode::Max(_)
104        | ExprNode::Unary(_, _) => Degree::Higher,
105    }
106}
107
108struct DegreeFolder<'a, A: ?Sized>(&'a A);
109
110enum DegreeOp {
111    Add,
112    Mul,
113    Pow(u32),
114}
115struct DegreeState<'a> {
116    rest: &'a [ExprId],
117    value: Degree,
118    op: DegreeOp,
119}
120
121impl<'a, A: ArenaAccess + ?Sized> crate::fold::Folder for DegreeFolder<'a, A> {
122    type Value = Degree;
123    type State = DegreeState<'a>;
124
125    fn start(&self, id: ExprId) -> std::ops::ControlFlow<Option<Degree>, Self::State> {
126        use std::ops::ControlFlow::{Break, Continue};
127        let (rest, op) = match self.0.get(id) {
128            ExprNode::Const(_) | ExprNode::Param(_) => return Break(Some(Degree::Zero)),
129            ExprNode::Var(_) | ExprNode::Linear { .. } => return Break(Some(Degree::One)),
130            ExprNode::Add(c) => (c.as_slice(), DegreeOp::Add),
131            ExprNode::Mul(c) => (c.as_slice(), DegreeOp::Mul),
132            ExprNode::Unary(UnaryOp::Neg, c) => (std::slice::from_ref(c), DegreeOp::Add),
133            ExprNode::Pow(base, exp) => {
134                let ExprNode::Const(e) = self.0.get(*exp) else {
135                    return Break(Some(Degree::Higher));
136                };
137                if (*e - e.round()).abs() >= f64::EPSILON || *e < 0.0 {
138                    return Break(Some(Degree::Higher));
139                }
140                let n = match e.round() {
141                    v if v < 0.5 => 0,
142                    v if v < 1.5 => 1,
143                    v if v < 2.5 => 2,
144                    _ => 3,
145                };
146                (std::slice::from_ref(base), DegreeOp::Pow(n))
147            }
148            _ => return Break(Some(Degree::Higher)),
149        };
150        Continue(DegreeState { rest, value: Degree::Zero, op })
151    }
152
153    fn next(&self, state: &mut Self::State) -> std::ops::ControlFlow<Option<Degree>, ExprId> {
154        use std::ops::ControlFlow::{Break, Continue};
155        while state.value != Degree::Higher {
156            let Some((&child, tail)) = state.rest.split_first() else { break };
157            state.rest = tail;
158            match self.start(child) {
159                Break(Some(value)) => self.accept(state, value),
160                _ => return Continue(child),
161            }
162        }
163        Break(Some(state.value))
164    }
165
166    fn accept(&self, state: &mut Self::State, value: Degree) {
167        state.value = match state.op {
168            DegreeOp::Add => state.value.add(value),
169            DegreeOp::Mul => state.value.mul(value),
170            DegreeOp::Pow(n) => value.pow(n),
171        };
172    }
173}
174
175fn degree(arena: &(impl ArenaAccess + ?Sized), mut id: ExprId) -> Degree {
176    while let ExprNode::Unary(UnaryOp::Neg, child) = arena.get(id) {
177        id = *child;
178    }
179    crate::fold::fold(arena, id, DegreeFolder(arena)).expect("degree classification is total")
180}
181
182/// Classify an expression as Linear, Quadratic (polynomial degree <= 2 with at
183/// least one degree-2 term), or Nonlinear (transcendentals, non-integer powers,
184/// or polynomial degree > 2).
185pub fn classify(arena: &ExprArena, id: ExprId) -> ExprClass {
186    classify_access(arena, id)
187}
188
189pub(crate) fn classify_access(arena: &(impl ArenaAccess + ?Sized), id: ExprId) -> ExprClass {
190    match degree(arena, id) {
191        Degree::Zero | Degree::One => ExprClass::Linear,
192        Degree::Two => ExprClass::Quadratic,
193        Degree::Higher => ExprClass::Nonlinear,
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn iterative_degree_matches_recursive_classification() {
203        let (arena, ids) = crate::fold::test_arena();
204        for id in ids {
205            assert_eq!(degree(&arena, id), recursive_degree(&arena, id), "node {id:?}");
206        }
207    }
208    use crate::arena::{ExprArena, ExprNode, VarId};
209    use smallvec::smallvec;
210
211    fn var(arena: &mut ExprArena, i: u32) -> ExprId {
212        arena.push(ExprNode::Var(VarId(i)))
213    }
214
215    #[test]
216    fn linear_var_sum() {
217        let mut a = ExprArena::new();
218        let x = var(&mut a, 0);
219        let y = var(&mut a, 1);
220        let sum = a.push(ExprNode::Add(smallvec![x, y]));
221        assert_eq!(classify(&a, sum), ExprClass::Linear);
222    }
223
224    #[test]
225    fn quadratic_mul_two_vars() {
226        let mut a = ExprArena::new();
227        let x = var(&mut a, 0);
228        let y = var(&mut a, 1);
229        let xy = a.push(ExprNode::Mul(smallvec![x, y]));
230        assert_eq!(classify(&a, xy), ExprClass::Quadratic);
231    }
232
233    #[test]
234    fn quadratic_pow_two() {
235        let mut a = ExprArena::new();
236        let x = var(&mut a, 0);
237        let two = a.push(ExprNode::Const(2.0));
238        let sq = a.push(ExprNode::Pow(x, two));
239        assert_eq!(classify(&a, sq), ExprClass::Quadratic);
240    }
241
242    #[test]
243    fn nonlinear_pow_three() {
244        let mut a = ExprArena::new();
245        let x = var(&mut a, 0);
246        let three = a.push(ExprNode::Const(3.0));
247        let cube = a.push(ExprNode::Pow(x, three));
248        assert_eq!(classify(&a, cube), ExprClass::Nonlinear);
249    }
250
251    #[test]
252    fn nonlinear_div() {
253        let mut a = ExprArena::new();
254        let x = var(&mut a, 0);
255        let y = var(&mut a, 1);
256        let q = a.push(ExprNode::Div(x, y));
257        assert_eq!(classify(&a, q), ExprClass::Nonlinear);
258    }
259
260    #[test]
261    fn nonlinear_sin() {
262        let mut a = ExprArena::new();
263        let x = var(&mut a, 0);
264        let s = a.push(ExprNode::Unary(UnaryOp::Sin, x));
265        assert_eq!(classify(&a, s), ExprClass::Nonlinear);
266    }
267
268    #[test]
269    fn nonlinear_abs() {
270        let mut a = ExprArena::new();
271        let x = var(&mut a, 0);
272        let s = a.push(ExprNode::Unary(UnaryOp::Abs, x));
273        assert_eq!(classify(&a, s), ExprClass::Nonlinear);
274    }
275
276    #[test]
277    fn nonlinear_triple_mul() {
278        let mut arena = ExprArena::new();
279        let x = var(&mut arena, 0);
280        let y = var(&mut arena, 1);
281        let z = var(&mut arena, 2);
282        let prod = arena.push(ExprNode::Mul(smallvec![x, y, z]));
283        assert_eq!(classify(&arena, prod), ExprClass::Nonlinear);
284    }
285
286    #[test]
287    fn linear_promoted_by_const_mul() {
288        let mut a = ExprArena::new();
289        let x = var(&mut a, 0);
290        let c = a.push(ExprNode::Const(3.0));
291        let m = a.push(ExprNode::Mul(smallvec![c, x]));
292        assert_eq!(classify(&a, m), ExprClass::Linear);
293    }
294
295    #[test]
296    fn param_alone_is_linear() {
297        let mut a = ExprArena::new();
298        let p = a.new_param(4.0);
299        let pn = a.param(p);
300        assert_eq!(classify(&a, pn), ExprClass::Linear);
301    }
302
303    #[test]
304    fn param_times_var_is_linear() {
305        let mut a = ExprArena::new();
306        let p = a.new_param(4.0);
307        let pn = a.param(p);
308        let x = var(&mut a, 0);
309        let m = a.push(ExprNode::Mul(smallvec![pn, x]));
310        assert_eq!(classify(&a, m), ExprClass::Linear);
311    }
312
313    #[test]
314    fn param_times_var_squared_is_quadratic() {
315        let mut a = ExprArena::new();
316        let p = a.new_param(4.0);
317        let pn = a.param(p);
318        let x = var(&mut a, 0);
319        let two = a.push(ExprNode::Const(2.0));
320        let sq = a.push(ExprNode::Pow(x, two));
321        let m = a.push(ExprNode::Mul(smallvec![pn, sq]));
322        assert_eq!(classify(&a, m), ExprClass::Quadratic);
323    }
324}