1use crate::arena::{ArenaAccess, ExprArena, ExprId, ExprNode, UnaryOp};
2
3#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
11pub enum ExprClass {
12 Linear,
13 Quadratic,
14 Nonlinear,
15}
16
17#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
21enum Degree {
22 Zero,
23 One,
24 Two,
25 Higher,
26}
27
28impl Degree {
29 fn add(self, other: Degree) -> Degree {
31 self.max(other)
32 }
33
34 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 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 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 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
182pub 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}