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 children: smallvec::SmallVec<[ExprId; 4]> = children.iter().copied().collect();
75 let mut acc = CoeffAccum::with_capacity(children.len() * 4);
76 let mut constant = 0.0;
77 for child in children {
78 let t = as_linear(arena, child, resolve_params)?;
79 acc.extend(t.coeffs);
80 constant += t.constant;
81 }
82 Some(LinearTerms { coeffs: acc.into_coeffs(), constant })
83 }
84 ExprNode::Mul(children) => {
85 let children: smallvec::SmallVec<[ExprId; 4]> = children.iter().copied().collect();
87 let mut scalar = 1.0;
88 let mut linear: Option<LinearTerms> = None;
89 for child in children {
90 match arena.get(child) {
91 ExprNode::Const(c) => scalar *= c,
92 ExprNode::Param(p) if resolve_params => scalar *= arena.param_value(*p),
93 _ if linear.is_none() => {
94 linear = Some(as_linear(arena, child, resolve_params)?);
95 }
96 _ => return None,
97 }
98 }
99 Some(match linear {
100 None => LinearTerms { coeffs: Vec::new(), constant: scalar },
101 Some(mut t) => {
102 t.coeffs.iter_mut().for_each(|(_, c)| *c *= scalar);
103 t.constant *= scalar;
104 t
105 }
106 })
107 }
108 _ => None,
109 }
110}
111
112fn push_linear(arena: &mut ExprArena, mut t: LinearTerms) -> ExprId {
114 t.coeffs.retain(|(_, c)| *c != 0.0);
115 arena.push(ExprNode::Linear { coeffs: t.coeffs, constant: t.constant })
116}
117
118pub(crate) fn add_into(arena: &mut ExprArena, lhs: ExprId, rhs: ExprId) -> ExprId {
121 if let (Some(lt), Some(rt)) = (as_linear(arena, lhs, false), as_linear(arena, rhs, false)) {
122 let mut acc = CoeffAccum::with_capacity(lt.coeffs.len() + rt.coeffs.len());
123 acc.extend(lt.coeffs);
124 acc.extend(rt.coeffs);
125 return push_linear(
126 arena,
127 LinearTerms { coeffs: acc.into_coeffs(), constant: lt.constant + rt.constant },
128 );
129 }
130 arena.push(ExprNode::Add(smallvec![lhs, rhs]))
131}
132
133pub(crate) fn add_n(arena: &mut ExprArena, ids: &[ExprId]) -> ExprId {
140 match ids {
141 [] => panic!("add_n on an empty term list"),
142 [one] => *one,
143 _ => arena.push(ExprNode::Add(ids.iter().copied().collect())),
144 }
145}
146
147pub(crate) fn sub_into(arena: &mut ExprArena, lhs: ExprId, rhs: ExprId) -> ExprId {
149 let neg = neg_into(arena, rhs);
150 add_into(arena, lhs, neg)
151}
152
153pub(crate) fn mul_into(arena: &mut ExprArena, lhs: ExprId, rhs: ExprId) -> ExprId {
156 if let ExprNode::Const(c) = *arena.get(lhs) {
157 if let Some(mut t) = as_linear(arena, rhs, false) {
158 t.coeffs.iter_mut().for_each(|(_, co)| *co *= c);
159 t.constant *= c;
160 return push_linear(arena, t);
161 }
162 }
163 if let ExprNode::Const(c) = *arena.get(rhs) {
164 if let Some(mut t) = as_linear(arena, lhs, false) {
165 t.coeffs.iter_mut().for_each(|(_, co)| *co *= c);
166 t.constant *= c;
167 return push_linear(arena, t);
168 }
169 }
170 arena.push(ExprNode::Mul(smallvec![lhs, rhs]))
171}
172
173pub(crate) fn div_into(arena: &mut ExprArena, num: ExprId, den: ExprId) -> ExprId {
177 if let ExprNode::Const(c) = *arena.get(den) {
178 if c != 0.0 {
179 if let Some(mut t) = as_linear(arena, num, false) {
180 let inv = 1.0 / c;
181 t.coeffs.iter_mut().for_each(|(_, co)| *co *= inv);
182 t.constant *= inv;
183 return push_linear(arena, t);
184 }
185 let inv = arena.push(ExprNode::Const(1.0 / c));
186 return mul_into(arena, num, inv);
187 }
188 }
189 arena.push(ExprNode::Div(num, den))
190}
191
192pub(crate) fn neg_into(arena: &mut ExprArena, rhs: ExprId) -> ExprId {
194 if let Some(mut t) = as_linear(arena, rhs, false) {
195 t.coeffs.iter_mut().for_each(|(_, c)| *c = -*c);
196 t.constant = -t.constant;
197 return push_linear(arena, t);
198 }
199 arena.push(ExprNode::Neg(rhs))
200}
201
202pub fn extract_linear(arena: &ExprArena, id: ExprId) -> Option<LinearTerms> {
210 as_linear(arena, id, true)
211}
212
213#[derive(Copy, Clone, Debug, PartialEq, Eq)]
217pub struct SignedExpr {
218 pub id: ExprId,
219 pub neg: bool,
220}
221
222pub fn split_linear(arena: &ExprArena, id: ExprId) -> (LinearTerms, Vec<SignedExpr>) {
234 if let Some(lt) = as_linear(arena, id, true) {
235 return (lt, Vec::new());
236 }
237 let mut lin = CoeffAccum::with_capacity(0);
238 let mut constant = 0.0;
239 let mut residual: Vec<SignedExpr> = Vec::new();
240 let mut sign_stack: smallvec::SmallVec<[(ExprId, f64); 8]> = smallvec![(id, 1.0)];
241 while let Some((cur, sign)) = sign_stack.pop() {
242 match arena.get(cur) {
243 ExprNode::Add(children) => {
244 for c in children.iter().copied() {
245 sign_stack.push((c, sign));
246 }
247 }
248 ExprNode::Neg(inner) => sign_stack.push((*inner, -sign)),
249 _ => {
250 if let Some(mut t) = as_linear(arena, cur, true) {
251 if (sign - 1.0).abs() > 0.0 {
252 t.coeffs.iter_mut().for_each(|(_, c)| *c *= sign);
253 t.constant *= sign;
254 }
255 lin.extend(t.coeffs);
256 constant += t.constant;
257 } else {
258 residual.push(SignedExpr { id: cur, neg: sign < 0.0 });
259 }
260 }
261 }
262 }
263 let mut coeffs = lin.into_coeffs();
264 coeffs.retain(|(_, c)| *c != 0.0);
265 (LinearTerms { coeffs, constant }, residual)
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use crate::arena::{ExprArena, ExprNode, VarId};
272
273 #[test]
274 fn param_times_var_stays_symbolic_until_extracted() {
275 let mut arena = ExprArena::new();
278 let pid = arena.new_param(3.0);
279 let price = arena.param(pid);
280 let xnode = arena.push(ExprNode::Var(VarId(0)));
281 let prod = mul_into(&mut arena, price, xnode);
282 assert!(matches!(arena.get(prod), ExprNode::Mul(_)));
283
284 let terms = extract_linear(&arena, prod).expect("linear");
285 assert_eq!(terms.coeffs, vec![(VarId(0), 3.0)]);
286 assert!(terms.constant.abs() < f64::EPSILON);
287 }
288
289 #[test]
290 fn rebinding_param_updates_extracted_coeff() {
291 let mut arena = ExprArena::new();
292 let pid = arena.new_param(3.0);
293 let price = arena.param(pid);
294 let xnode = arena.push(ExprNode::Var(VarId(0)));
295 let prod = mul_into(&mut arena, price, xnode);
296
297 arena.set_param_value(pid, 10.0);
298 let terms = extract_linear(&arena, prod).expect("linear");
299 assert_eq!(terms.coeffs, vec![(VarId(0), 10.0)]);
300 }
301
302 #[test]
303 fn param_plus_var_resolves_constant() {
304 let mut arena = ExprArena::new();
305 let pid = arena.new_param(5.0);
306 let price = arena.param(pid);
307 let xnode = arena.push(ExprNode::Var(VarId(0)));
308 let sum = add_into(&mut arena, price, xnode);
309 let terms = extract_linear(&arena, sum).expect("linear");
310 assert_eq!(terms.coeffs, vec![(VarId(0), 1.0)]);
311 assert!((terms.constant - 5.0).abs() < f64::EPSILON);
312 }
313
314 #[test]
315 fn add_extraction_is_first_seen_ordered_and_merges() {
316 let mut arena = ExprArena::new();
319 let z = arena.push(ExprNode::Var(VarId(2)));
320 let x = arena.push(ExprNode::Var(VarId(0)));
321 let y = arena.push(ExprNode::Var(VarId(1)));
322 let sum = arena.push(ExprNode::Add(smallvec::smallvec![z, x, y, x]));
323
324 let terms = extract_linear(&arena, sum).expect("linear");
325 assert_eq!(terms.coeffs, vec![(VarId(2), 1.0), (VarId(0), 2.0), (VarId(1), 1.0)]);
326 assert!(terms.constant.abs() < f64::EPSILON);
327 assert_eq!(extract_linear(&arena, sum).unwrap().coeffs, terms.coeffs);
328 }
329
330 #[test]
331 fn wide_sum_merges_repeated_vars_in_order() {
332 let mut arena = ExprArena::new();
333 let n = 50u32;
334 let mut ids = Vec::new();
335 for _ in 0..3 {
336 for v in 0..n {
337 ids.push(arena.push(ExprNode::Var(VarId(v))));
338 }
339 }
340 let sum = arena.push(ExprNode::Add(ids.into_iter().collect()));
341 let terms = extract_linear(&arena, sum).expect("linear");
342 let expected: Vec<(VarId, f64)> = (0..n).map(|v| (VarId(v), 3.0)).collect();
343 assert_eq!(terms.coeffs, expected);
344 }
345}