1use rustc_hash::{FxBuildHasher, FxHashMap};
2use smallvec::smallvec;
3
4use crate::arena::{ExprArena, ExprId, ExprNode, VarId};
5
6#[derive(Clone, Debug, Default)]
8pub struct LinearTerms {
9 pub coeffs: Vec<(VarId, f64)>,
10 pub constant: f64,
11}
12
13struct CoeffAccum {
16 coeffs: Vec<(VarId, f64)>,
17 slot: FxHashMap<VarId, usize>,
18}
19
20impl CoeffAccum {
21 fn with_capacity(n: usize) -> Self {
22 Self {
23 coeffs: Vec::with_capacity(n),
24 slot: FxHashMap::with_capacity_and_hasher(n, FxBuildHasher),
25 }
26 }
27
28 fn add(&mut self, v: VarId, c: f64) {
31 if let Some(&i) = self.slot.get(&v) {
32 self.coeffs[i].1 += c;
33 } else {
34 self.slot.insert(v, self.coeffs.len());
35 self.coeffs.push((v, c));
36 }
37 }
38
39 fn extend(&mut self, terms: impl IntoIterator<Item = (VarId, f64)>) {
40 for (v, c) in terms {
41 self.add(v, c);
42 }
43 }
44
45 fn into_coeffs(self) -> Vec<(VarId, f64)> {
46 self.coeffs
47 }
48}
49
50fn as_linear(arena: &ExprArena, id: ExprId, resolve_params: bool) -> Option<LinearTerms> {
56 match arena.get(id) {
57 ExprNode::Const(c) => Some(LinearTerms { coeffs: Vec::new(), constant: *c }),
58 ExprNode::Param(p) if resolve_params => {
59 Some(LinearTerms { coeffs: Vec::new(), constant: arena.param_value(*p) })
60 }
61 ExprNode::Var(v) => Some(LinearTerms { coeffs: vec![(*v, 1.0)], constant: 0.0 }),
62 ExprNode::Linear { coeffs, constant } => {
63 Some(LinearTerms { coeffs: coeffs.clone(), constant: *constant })
64 }
65 ExprNode::Neg(inner) => {
66 let inner = *inner;
67 as_linear(arena, inner, resolve_params).map(|mut t| {
68 t.coeffs.iter_mut().for_each(|(_, c)| *c = -*c);
69 t.constant = -t.constant;
70 t
71 })
72 }
73 ExprNode::Add(children) => {
74 let mut acc = CoeffAccum::with_capacity(children.len() * 4);
75 let mut constant = 0.0;
76 for &child in children {
77 let t = as_linear(arena, child, resolve_params)?;
78 acc.extend(t.coeffs);
79 constant += t.constant;
80 }
81 Some(LinearTerms { coeffs: acc.into_coeffs(), constant })
82 }
83 ExprNode::Mul(children) => {
84 let mut scalar = 1.0;
86 let mut linear: Option<LinearTerms> = None;
87 for &child in children {
88 match arena.get(child) {
89 ExprNode::Const(c) => scalar *= c,
90 ExprNode::Param(p) if resolve_params => scalar *= arena.param_value(*p),
91 _ if linear.is_none() => {
92 linear = Some(as_linear(arena, child, resolve_params)?);
93 }
94 _ => return None,
95 }
96 }
97 Some(match linear {
98 None => LinearTerms { coeffs: Vec::new(), constant: scalar },
99 Some(mut t) => {
100 t.coeffs.iter_mut().for_each(|(_, c)| *c *= scalar);
101 t.constant *= scalar;
102 t
103 }
104 })
105 }
106 _ => None,
107 }
108}
109
110fn push_linear(arena: &mut ExprArena, mut t: LinearTerms) -> ExprId {
112 t.coeffs.retain(|(_, c)| *c != 0.0);
113 arena.push(ExprNode::Linear { coeffs: t.coeffs, constant: t.constant })
114}
115
116pub(crate) fn add_into(arena: &mut ExprArena, lhs: ExprId, rhs: ExprId) -> ExprId {
119 if let (Some(lt), Some(rt)) = (as_linear(arena, lhs, false), as_linear(arena, rhs, false)) {
120 let mut acc = CoeffAccum::with_capacity(lt.coeffs.len() + rt.coeffs.len());
121 acc.extend(lt.coeffs);
122 acc.extend(rt.coeffs);
123 return push_linear(
124 arena,
125 LinearTerms { coeffs: acc.into_coeffs(), constant: lt.constant + rt.constant },
126 );
127 }
128 arena.push(ExprNode::Add(smallvec![lhs, rhs]))
129}
130
131pub(crate) fn add_n(arena: &mut ExprArena, ids: &[ExprId]) -> ExprId {
138 match ids {
139 [] => panic!("add_n on an empty term list"),
140 [one] => *one,
141 _ => arena.push(ExprNode::Add(ids.iter().copied().collect())),
142 }
143}
144
145pub(crate) fn sub_into(arena: &mut ExprArena, lhs: ExprId, rhs: ExprId) -> ExprId {
147 let neg = neg_into(arena, rhs);
148 add_into(arena, lhs, neg)
149}
150
151pub(crate) fn mul_into(arena: &mut ExprArena, lhs: ExprId, rhs: ExprId) -> ExprId {
154 if let ExprNode::Const(c) = *arena.get(lhs) {
155 if let Some(mut t) = as_linear(arena, rhs, false) {
156 t.coeffs.iter_mut().for_each(|(_, co)| *co *= c);
157 t.constant *= c;
158 return push_linear(arena, t);
159 }
160 }
161 if let ExprNode::Const(c) = *arena.get(rhs) {
162 if let Some(mut t) = as_linear(arena, lhs, false) {
163 t.coeffs.iter_mut().for_each(|(_, co)| *co *= c);
164 t.constant *= c;
165 return push_linear(arena, t);
166 }
167 }
168 arena.push(ExprNode::Mul(smallvec![lhs, rhs]))
169}
170
171pub(crate) fn div_into(arena: &mut ExprArena, num: ExprId, den: ExprId) -> ExprId {
175 if let ExprNode::Const(c) = *arena.get(den) {
176 if c != 0.0 {
177 if let Some(mut t) = as_linear(arena, num, false) {
178 let inv = 1.0 / c;
179 t.coeffs.iter_mut().for_each(|(_, co)| *co *= inv);
180 t.constant *= inv;
181 return push_linear(arena, t);
182 }
183 let inv = arena.push(ExprNode::Const(1.0 / c));
184 return mul_into(arena, num, inv);
185 }
186 }
187 arena.push(ExprNode::Div(num, den))
188}
189
190pub(crate) fn neg_into(arena: &mut ExprArena, rhs: ExprId) -> ExprId {
192 if let Some(mut t) = as_linear(arena, rhs, false) {
193 t.coeffs.iter_mut().for_each(|(_, c)| *c = -*c);
194 t.constant = -t.constant;
195 return push_linear(arena, t);
196 }
197 arena.push(ExprNode::Neg(rhs))
198}
199
200pub fn extract_linear(arena: &ExprArena, id: ExprId) -> Option<LinearTerms> {
208 as_linear(arena, id, true)
209}
210
211#[derive(Copy, Clone, Debug, PartialEq, Eq)]
215pub struct SignedExpr {
216 pub id: ExprId,
217 pub neg: bool,
218}
219
220pub fn split_linear(arena: &ExprArena, id: ExprId) -> (LinearTerms, Vec<SignedExpr>) {
232 if let Some(lt) = as_linear(arena, id, true) {
233 return (lt, Vec::new());
234 }
235 let mut lin = CoeffAccum::with_capacity(0);
236 let mut constant = 0.0;
237 let mut residual: Vec<SignedExpr> = Vec::new();
238 let mut sign_stack: smallvec::SmallVec<[(ExprId, f64); 8]> = smallvec![(id, 1.0)];
239 while let Some((cur, sign)) = sign_stack.pop() {
240 match arena.get(cur) {
241 ExprNode::Add(children) => {
242 for c in children.iter().copied() {
243 sign_stack.push((c, sign));
244 }
245 }
246 ExprNode::Neg(inner) => sign_stack.push((*inner, -sign)),
247 _ => {
248 if let Some(mut t) = as_linear(arena, cur, true) {
249 if (sign - 1.0).abs() > 0.0 {
250 t.coeffs.iter_mut().for_each(|(_, c)| *c *= sign);
251 t.constant *= sign;
252 }
253 lin.extend(t.coeffs);
254 constant += t.constant;
255 } else {
256 residual.push(SignedExpr { id: cur, neg: sign < 0.0 });
257 }
258 }
259 }
260 }
261 let mut coeffs = lin.into_coeffs();
262 coeffs.retain(|(_, c)| *c != 0.0);
263 (LinearTerms { coeffs, constant }, residual)
264}
265
266pub fn describe_nonlinear_term(
270 arena: &ExprArena,
271 id: ExprId,
272 resolve: &impl Fn(VarId) -> String,
273) -> Option<String> {
274 use crate::render::{PREC_ADD, PREC_UNARY, render_node};
275 let (_, residual) = split_linear(arena, id);
276 residual.first().map(|s| {
277 if s.neg {
278 format!("-{}", render_node(arena, s.id, resolve, PREC_UNARY))
279 } else {
280 render_node(arena, s.id, resolve, PREC_ADD)
281 }
282 })
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use crate::arena::{ExprArena, ExprNode, VarId};
289
290 #[test]
291 fn param_times_var_stays_symbolic_until_extracted() {
292 let mut arena = ExprArena::new();
295 let pid = arena.new_param(3.0);
296 let price = arena.param(pid);
297 let xnode = arena.push(ExprNode::Var(VarId(0)));
298 let prod = mul_into(&mut arena, price, xnode);
299 assert!(matches!(arena.get(prod), ExprNode::Mul(_)));
300
301 let terms = extract_linear(&arena, prod).expect("linear");
302 assert_eq!(terms.coeffs, vec![(VarId(0), 3.0)]);
303 assert!(terms.constant.abs() < f64::EPSILON);
304 }
305
306 #[test]
307 fn rebinding_param_updates_extracted_coeff() {
308 let mut arena = ExprArena::new();
309 let pid = arena.new_param(3.0);
310 let price = arena.param(pid);
311 let xnode = arena.push(ExprNode::Var(VarId(0)));
312 let prod = mul_into(&mut arena, price, xnode);
313
314 arena.set_param_value(pid, 10.0);
315 let terms = extract_linear(&arena, prod).expect("linear");
316 assert_eq!(terms.coeffs, vec![(VarId(0), 10.0)]);
317 }
318
319 #[test]
320 fn param_plus_var_resolves_constant() {
321 let mut arena = ExprArena::new();
322 let pid = arena.new_param(5.0);
323 let price = arena.param(pid);
324 let xnode = arena.push(ExprNode::Var(VarId(0)));
325 let sum = add_into(&mut arena, price, xnode);
326 let terms = extract_linear(&arena, sum).expect("linear");
327 assert_eq!(terms.coeffs, vec![(VarId(0), 1.0)]);
328 assert!((terms.constant - 5.0).abs() < f64::EPSILON);
329 }
330
331 #[test]
332 fn add_extraction_is_first_seen_ordered_and_merges() {
333 let mut arena = ExprArena::new();
336 let z = arena.push(ExprNode::Var(VarId(2)));
337 let x = arena.push(ExprNode::Var(VarId(0)));
338 let y = arena.push(ExprNode::Var(VarId(1)));
339 let sum = arena.push(ExprNode::Add(smallvec::smallvec![z, x, y, x]));
340
341 let terms = extract_linear(&arena, sum).expect("linear");
342 assert_eq!(terms.coeffs, vec![(VarId(2), 1.0), (VarId(0), 2.0), (VarId(1), 1.0)]);
343 assert!(terms.constant.abs() < f64::EPSILON);
344 assert_eq!(extract_linear(&arena, sum).unwrap().coeffs, terms.coeffs);
345 }
346
347 #[test]
348 fn wide_sum_merges_repeated_vars_in_order() {
349 let mut arena = ExprArena::new();
350 let n = 50u32;
351 let mut ids = Vec::new();
352 for _ in 0..3 {
353 for v in 0..n {
354 ids.push(arena.push(ExprNode::Var(VarId(v))));
355 }
356 }
357 let sum = arena.push(ExprNode::Add(ids.into_iter().collect()));
358 let terms = extract_linear(&arena, sum).expect("linear");
359 let expected: Vec<(VarId, f64)> = (0..n).map(|v| (VarId(v), 3.0)).collect();
360 assert_eq!(terms.coeffs, expected);
361 }
362
363 fn names(v: VarId) -> String {
364 match v.0 {
365 0 => "x".to_string(),
366 1 => "y".to_string(),
367 n => format!("v{n}"),
368 }
369 }
370
371 #[test]
372 fn describe_renders_the_first_nonlinear_summand() {
373 let mut arena = ExprArena::new();
374 let x = arena.push(ExprNode::Var(VarId(0)));
375 let y = arena.push(ExprNode::Var(VarId(1)));
376
377 let prod = arena.push(ExprNode::Mul(smallvec::smallvec![x, y]));
378 assert_eq!(describe_nonlinear_term(&arena, prod, &names).as_deref(), Some("x * y"));
379
380 let two = arena.constant(2.0);
381 let pow = arena.push(ExprNode::Pow(x, two));
382 assert_eq!(describe_nonlinear_term(&arena, pow, &names).as_deref(), Some("x^2"));
383
384 let s = arena.push(ExprNode::Sin(x));
385 assert_eq!(describe_nonlinear_term(&arena, s, &names).as_deref(), Some("sin(x)"));
386
387 let div = arena.push(ExprNode::Div(x, y));
388 assert_eq!(describe_nonlinear_term(&arena, div, &names).as_deref(), Some("x / y"));
389 }
390
391 #[test]
392 fn describe_isolates_the_nonlinear_part_of_a_mixed_expression() {
393 let mut arena = ExprArena::new();
394 let x = arena.push(ExprNode::Var(VarId(0)));
395 let y = arena.push(ExprNode::Var(VarId(1)));
396 let z = arena.push(ExprNode::Var(VarId(2)));
397 let two = arena.constant(2.0);
398 let two_z = arena.push(ExprNode::Mul(smallvec::smallvec![two, z]));
399 let prod = arena.push(ExprNode::Mul(smallvec::smallvec![x, y]));
400 let sum = arena.push(ExprNode::Add(smallvec::smallvec![two_z, prod]));
401 assert_eq!(describe_nonlinear_term(&arena, sum, &names).as_deref(), Some("x * y"));
402 }
403
404 #[test]
405 fn describe_returns_none_for_affine() {
406 let mut arena = ExprArena::new();
407 let x = arena.push(ExprNode::Var(VarId(0)));
408 let three = arena.constant(3.0);
409 let sum = arena.push(ExprNode::Add(smallvec::smallvec![x, three]));
410 assert_eq!(describe_nonlinear_term(&arena, sum, &names), None);
411 }
412
413 #[test]
414 fn describe_falls_back_to_index_for_unknown_var() {
415 let mut arena = ExprArena::new();
416 let a = arena.push(ExprNode::Var(VarId(7)));
417 let b = arena.push(ExprNode::Var(VarId(8)));
418 let prod = arena.push(ExprNode::Mul(smallvec::smallvec![a, b]));
419 assert_eq!(describe_nonlinear_term(&arena, prod, &names).as_deref(), Some("v7 * v8"));
420 }
421}