1use std::collections::HashMap;
6use std::sync::Arc;
7
8use antecedent_core::VariableId;
9
10use crate::{CausalExprArena, DerivationMeta, ExprId, ExprNode, VarSetId};
11
12pub(crate) fn simplify(arena: &mut CausalExprArena, root: ExprId) -> ExprId {
14 let mut memo: HashMap<ExprId, ExprId> = HashMap::new();
15 let mut free_memo: HashMap<ExprId, VarSetId> = HashMap::new();
16 simplify_rec(arena, root, &mut memo, &mut free_memo)
17}
18
19fn simplify_rec(
20 arena: &mut CausalExprArena,
21 id: ExprId,
22 memo: &mut HashMap<ExprId, ExprId>,
23 free_memo: &mut HashMap<ExprId, VarSetId>,
24) -> ExprId {
25 if let Some(&cached) = memo.get(&id) {
26 return cached;
27 }
28 let rebuilt = rebuild_children(arena, id, memo, free_memo);
29 let simplified = apply_rules_fixpoint(arena, rebuilt, free_memo);
30 memo.insert(id, simplified);
31 simplified
32}
33
34fn rebuild_children(
35 arena: &mut CausalExprArena,
36 id: ExprId,
37 memo: &mut HashMap<ExprId, ExprId>,
38 free_memo: &mut HashMap<ExprId, VarSetId>,
39) -> ExprId {
40 let node = arena.node(id).clone();
41 match node {
42 ExprNode::Distribution { .. } => id,
43 ExprNode::Product(list) => {
44 let children_ids: Vec<ExprId> = arena.list(list).to_vec();
45 let children: Vec<ExprId> =
46 children_ids.into_iter().map(|c| simplify_rec(arena, c, memo, free_memo)).collect();
47 let list_id = arena.intern_list(children);
48 arena.intern(ExprNode::Product(list_id))
49 }
50 ExprNode::SumOut { variables, expr } => {
51 let body = simplify_rec(arena, expr, memo, free_memo);
52 arena.intern(ExprNode::SumOut { variables, expr: body })
53 }
54 ExprNode::IntegralOut { variables, expr } => {
55 let body = simplify_rec(arena, expr, memo, free_memo);
56 arena.intern(ExprNode::IntegralOut { variables, expr: body })
57 }
58 ExprNode::Ratio { numerator, denominator } => {
59 let num = simplify_rec(arena, numerator, memo, free_memo);
60 let den = simplify_rec(arena, denominator, memo, free_memo);
61 arena.intern(ExprNode::Ratio { numerator: num, denominator: den })
62 }
63 ExprNode::Expectation { function, distribution } => {
64 let dist = simplify_rec(arena, distribution, memo, free_memo);
65 arena.intern(ExprNode::Expectation { function, distribution: dist })
66 }
67 ExprNode::Contrast { left, right, op } => {
68 let l = simplify_rec(arena, left, memo, free_memo);
69 let r = simplify_rec(arena, right, memo, free_memo);
70 arena.intern(ExprNode::Contrast { left: l, right: r, op })
71 }
72 }
73}
74
75fn apply_rules_fixpoint(
76 arena: &mut CausalExprArena,
77 mut id: ExprId,
78 free_memo: &mut HashMap<ExprId, VarSetId>,
79) -> ExprId {
80 loop {
82 let next = apply_local_rules(arena, id, free_memo);
83 if next == id {
84 return id;
85 }
86 id = next;
87 }
88}
89
90fn apply_local_rules(
91 arena: &mut CausalExprArena,
92 id: ExprId,
93 free_memo: &mut HashMap<ExprId, VarSetId>,
94) -> ExprId {
95 match arena.node(id).clone() {
96 ExprNode::SumOut { variables, expr } => {
97 rewrite_sum_out(arena, id, variables, expr, free_memo)
98 }
99 ExprNode::IntegralOut { variables, expr } => {
100 rewrite_integral_out(arena, id, variables, expr, free_memo)
101 }
102 ExprNode::Product(list) => rewrite_product(arena, id, list),
103 ExprNode::Ratio { numerator, denominator } => {
104 rewrite_ratio(arena, id, numerator, denominator)
105 }
106 _ => id,
107 }
108}
109
110fn rewrite_sum_out(
111 arena: &mut CausalExprArena,
112 id: ExprId,
113 variables: VarSetId,
114 expr: ExprId,
115 free_memo: &mut HashMap<ExprId, VarSetId>,
116) -> ExprId {
117 if arena.var_set(variables).is_empty() {
118 return tag_if_new(arena, expr, id, "simplify.empty_sum_out");
119 }
120 if let ExprNode::SumOut { variables: inner_v, expr: inner_e } = arena.node(expr).clone() {
121 let merged: Vec<VariableId> = arena
122 .var_set(variables)
123 .iter()
124 .copied()
125 .chain(arena.var_set(inner_v).iter().copied())
126 .collect();
127 let union = arena.intern_var_set(merged);
128 let node = ExprNode::SumOut { variables: union, expr: inner_e };
129 return intern_derived(arena, node, "simplify.merge_sum_out");
130 }
131 let free = free_vars(arena, expr, free_memo);
132 if !intersects(arena, variables, free) {
133 return tag_if_new(arena, expr, id, "simplify.dead_sum_out");
134 }
135 id
136}
137
138fn rewrite_integral_out(
139 arena: &mut CausalExprArena,
140 id: ExprId,
141 variables: VarSetId,
142 expr: ExprId,
143 free_memo: &mut HashMap<ExprId, VarSetId>,
144) -> ExprId {
145 if arena.var_set(variables).is_empty() {
146 return tag_if_new(arena, expr, id, "simplify.empty_integral_out");
147 }
148 if let ExprNode::IntegralOut { variables: inner_v, expr: inner_e } = arena.node(expr).clone() {
149 let merged: Vec<VariableId> = arena
150 .var_set(variables)
151 .iter()
152 .copied()
153 .chain(arena.var_set(inner_v).iter().copied())
154 .collect();
155 let union = arena.intern_var_set(merged);
156 let node = ExprNode::IntegralOut { variables: union, expr: inner_e };
157 return intern_derived(arena, node, "simplify.merge_integral_out");
158 }
159 let free = free_vars(arena, expr, free_memo);
160 if !intersects(arena, variables, free) {
161 return tag_if_new(arena, expr, id, "simplify.dead_integral_out");
162 }
163 id
164}
165
166fn rewrite_product(arena: &mut CausalExprArena, id: ExprId, list: crate::ExprListId) -> ExprId {
167 let children = arena.list(list).to_vec();
168 if children.len() == 1 {
169 return tag_if_new(arena, children[0], id, "simplify.singleton_product");
170 }
171 let mut flat: Vec<ExprId> = Vec::with_capacity(children.len());
172 let mut flattened = false;
173 for c in &children {
174 if let ExprNode::Product(inner) = arena.node(*c) {
175 flat.extend_from_slice(arena.list(*inner));
176 flattened = true;
177 } else {
178 flat.push(*c);
179 }
180 }
181 flat.sort_unstable();
182 let sorted_changed = flat.as_slice() != children.as_slice();
183 if flattened || sorted_changed {
184 if flat.len() == 1 {
185 return tag_if_new(arena, flat[0], id, "simplify.singleton_product");
186 }
187 let list_id = arena.intern_list(flat);
188 let rule =
189 if flattened { "simplify.flatten_product" } else { "simplify.canonical_product" };
190 return intern_derived(arena, ExprNode::Product(list_id), rule);
191 }
192 id
193}
194
195fn rewrite_ratio(
196 arena: &mut CausalExprArena,
197 id: ExprId,
198 numerator: ExprId,
199 denominator: ExprId,
200) -> ExprId {
201 if let ExprNode::Ratio { numerator: a, denominator: b } = arena.node(numerator).clone() {
203 let bc = {
204 let mut kids = vec![b, denominator];
205 kids.sort_unstable();
206 let list = arena.intern_list(kids);
207 arena.intern(ExprNode::Product(list))
208 };
209 return intern_derived(
210 arena,
211 ExprNode::Ratio { numerator: a, denominator: bc },
212 "simplify.ratio_assoc_left",
213 );
214 }
215 if let ExprNode::Ratio { numerator: b, denominator: c } = arena.node(denominator).clone() {
217 let ac = {
218 let mut kids = vec![numerator, c];
219 kids.sort_unstable();
220 let list = arena.intern_list(kids);
221 arena.intern(ExprNode::Product(list))
222 };
223 return intern_derived(
224 arena,
225 ExprNode::Ratio { numerator: ac, denominator: b },
226 "simplify.ratio_assoc_right",
227 );
228 }
229 id
230}
231
232fn tag_if_new(arena: &mut CausalExprArena, result: ExprId, _from: ExprId, _rule: &str) -> ExprId {
233 let _ = arena;
235 result
236}
237
238fn intern_derived(arena: &mut CausalExprArena, node: ExprNode, rule: &str) -> ExprId {
239 let before = arena.len();
240 let id = arena.intern(node);
241 if arena.len() > before {
242 arena.set_derivation_if_absent(id, DerivationMeta { rule: Arc::from(rule), note: None });
243 }
244 id
245}
246
247fn intersects(arena: &CausalExprArena, a: VarSetId, b: VarSetId) -> bool {
248 let av = arena.var_set(a);
249 let bv = arena.var_set(b);
250 let mut i = 0;
251 let mut j = 0;
252 while i < av.len() && j < bv.len() {
253 match av[i].raw().cmp(&bv[j].raw()) {
254 std::cmp::Ordering::Equal => return true,
255 std::cmp::Ordering::Less => i += 1,
256 std::cmp::Ordering::Greater => j += 1,
257 }
258 }
259 false
260}
261
262fn free_vars(
263 arena: &mut CausalExprArena,
264 id: ExprId,
265 memo: &mut HashMap<ExprId, VarSetId>,
266) -> VarSetId {
267 if let Some(&cached) = memo.get(&id) {
268 return cached;
269 }
270 let result = match arena.node(id).clone() {
271 ExprNode::Distribution { variables, conditioned_on, intervention, .. } => {
272 let mut vars: Vec<VariableId> = arena.var_set(variables).to_vec();
273 vars.extend_from_slice(arena.var_set(conditioned_on));
274 let _ = intervention;
276 arena.intern_var_set(vars)
277 }
278 ExprNode::Product(list) => {
279 let children: Vec<ExprId> = arena.list(list).to_vec();
280 let mut vars = Vec::new();
281 for c in children {
282 let fv = free_vars(arena, c, memo);
283 vars.extend_from_slice(arena.var_set(fv));
284 }
285 arena.intern_var_set(vars)
286 }
287 ExprNode::SumOut { variables, expr } | ExprNode::IntegralOut { variables, expr } => {
288 let body = free_vars(arena, expr, memo);
289 let bound = arena.var_set(variables);
290 let remaining: Vec<VariableId> = arena
291 .var_set(body)
292 .iter()
293 .copied()
294 .filter(|v| !bound.iter().any(|b| b == v))
295 .collect();
296 arena.intern_var_set(remaining)
297 }
298 ExprNode::Ratio { numerator, denominator } => {
299 let n = free_vars(arena, numerator, memo);
300 let d = free_vars(arena, denominator, memo);
301 let mut vars = arena.var_set(n).to_vec();
302 vars.extend_from_slice(arena.var_set(d));
303 arena.intern_var_set(vars)
304 }
305 ExprNode::Expectation { function, distribution } => {
306 let dist = free_vars(arena, distribution, memo);
307 let mut vars = arena.var_set(dist).to_vec();
308 vars.push(function.variable());
309 arena.intern_var_set(vars)
310 }
311 ExprNode::Contrast { left, right, .. } => {
312 let l = free_vars(arena, left, memo);
313 let r = free_vars(arena, right, memo);
314 let mut vars = arena.var_set(l).to_vec();
315 vars.extend_from_slice(arena.var_set(r));
316 arena.intern_var_set(vars)
317 }
318 };
319 memo.insert(id, result);
320 result
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use crate::{ContrastOp, DomainRef, OutcomeExprId};
327 use antecedent_core::Value;
328
329 #[test]
330 fn empty_sum_out_eliminates() {
331 let mut a = CausalExprArena::new();
332 let empty = a.empty_var_set();
333 let empty_i = a.empty_intervention_set();
334 let dist = a.intern(ExprNode::Distribution {
335 variables: empty,
336 conditioned_on: empty,
337 intervention: empty_i,
338 domain: DomainRef::Observational,
339 });
340 let summed = a.intern(ExprNode::SumOut { variables: empty, expr: dist });
341 assert_eq!(simplify(&mut a, summed), dist);
342 }
343
344 #[test]
345 fn merge_nested_sum_out() {
346 let mut a = CausalExprArena::new();
347 let empty = a.empty_var_set();
348 let empty_i = a.empty_intervention_set();
349 let v1 = a.intern_var_set([VariableId::from_raw(1)]);
350 let v2 = a.intern_var_set([VariableId::from_raw(2)]);
351 let vars12 = a.intern_var_set([VariableId::from_raw(1), VariableId::from_raw(2)]);
352 let dist = a.intern(ExprNode::Distribution {
353 variables: vars12,
354 conditioned_on: empty,
355 intervention: empty_i,
356 domain: DomainRef::Observational,
357 });
358 let inner = a.intern(ExprNode::SumOut { variables: v2, expr: dist });
359 let outer = a.intern(ExprNode::SumOut { variables: v1, expr: inner });
360 let s = simplify(&mut a, outer);
361 match a.node(s) {
362 ExprNode::SumOut { variables, expr } => {
363 assert_eq!(
364 a.var_set(*variables),
365 &[VariableId::from_raw(1), VariableId::from_raw(2)]
366 );
367 assert_eq!(*expr, dist);
368 }
369 other => panic!("expected merged SumOut, got {other:?}"),
370 }
371 }
372
373 #[test]
374 fn dead_sum_out_eliminates() {
375 let mut a = CausalExprArena::new();
376 let empty = a.empty_var_set();
377 let empty_i = a.empty_intervention_set();
378 let y = a.intern_var_set([VariableId::from_raw(0)]);
379 let z = a.intern_var_set([VariableId::from_raw(1)]);
380 let dist = a.intern(ExprNode::Distribution {
381 variables: y,
382 conditioned_on: empty,
383 intervention: empty_i,
384 domain: DomainRef::Observational,
385 });
386 let summed = a.intern(ExprNode::SumOut { variables: z, expr: dist });
387 assert_eq!(simplify(&mut a, summed), dist);
388 }
389
390 #[test]
391 fn singleton_and_flatten_product() {
392 let mut a = CausalExprArena::new();
393 let empty = a.empty_var_set();
394 let empty_i = a.empty_intervention_set();
395 let v0 = a.intern_var_set([VariableId::from_raw(0)]);
396 let v1 = a.intern_var_set([VariableId::from_raw(1)]);
397 let d1 = a.intern(ExprNode::Distribution {
398 variables: v0,
399 conditioned_on: empty,
400 intervention: empty_i,
401 domain: DomainRef::Observational,
402 });
403 let d2 = a.intern(ExprNode::Distribution {
404 variables: v1,
405 conditioned_on: empty,
406 intervention: empty_i,
407 domain: DomainRef::Observational,
408 });
409 let inner = {
410 let list = a.intern_list([d1]);
411 a.intern(ExprNode::Product(list))
412 };
413 assert_eq!(simplify(&mut a, inner), d1);
414
415 let nest = {
416 let list_inner = a.intern_list([d1, d2]);
417 let p_inner = a.intern(ExprNode::Product(list_inner));
418 let list_outer = a.intern_list([p_inner, d1]);
419 a.intern(ExprNode::Product(list_outer))
420 };
421 let s = simplify(&mut a, nest);
422 match a.node(s) {
423 ExprNode::Product(list) => {
424 let kids = a.list(*list);
425 assert_eq!(kids.len(), 3);
426 let mut sorted = kids.to_vec();
427 sorted.sort_unstable();
428 assert_eq!(kids, sorted.as_slice());
429 }
430 other => panic!("expected product, got {other:?}"),
431 }
432 }
433
434 #[test]
435 fn product_order_independent() {
436 let mut a = CausalExprArena::new();
437 let empty = a.empty_var_set();
438 let empty_i = a.empty_intervention_set();
439 let v0 = a.intern_var_set([VariableId::from_raw(0)]);
440 let v1 = a.intern_var_set([VariableId::from_raw(1)]);
441 let d1 = a.intern(ExprNode::Distribution {
442 variables: v0,
443 conditioned_on: empty,
444 intervention: empty_i,
445 domain: DomainRef::Observational,
446 });
447 let d2 = a.intern(ExprNode::Distribution {
448 variables: v1,
449 conditioned_on: empty,
450 intervention: empty_i,
451 domain: DomainRef::Observational,
452 });
453 let p1 = {
454 let list = a.intern_list([d1, d2]);
455 a.intern(ExprNode::Product(list))
456 };
457 let p2 = {
458 let list = a.intern_list([d2, d1]);
459 a.intern(ExprNode::Product(list))
460 };
461 assert_eq!(simplify(&mut a, p1), simplify(&mut a, p2));
462 }
463
464 #[test]
465 fn simplify_idempotent() {
466 let mut a = CausalExprArena::new();
467 let id = a.backdoor_ate(
468 VariableId::from_raw(0),
469 VariableId::from_raw(1),
470 &[VariableId::from_raw(2)],
471 Value::f64(1.0),
472 Value::f64(0.0),
473 );
474 let s1 = simplify(&mut a, id);
475 let s2 = simplify(&mut a, s1);
476 assert_eq!(s1, s2);
477 }
478
479 #[test]
480 fn ratio_assoc_left() {
481 let mut a = CausalExprArena::new();
482 let empty = a.empty_var_set();
483 let empty_i = a.empty_intervention_set();
484 let v0 = a.intern_var_set([VariableId::from_raw(0)]);
485 let v1 = a.intern_var_set([VariableId::from_raw(1)]);
486 let v2 = a.intern_var_set([VariableId::from_raw(2)]);
487 let da = a.intern(ExprNode::Distribution {
488 variables: v0,
489 conditioned_on: empty,
490 intervention: empty_i,
491 domain: DomainRef::Observational,
492 });
493 let db = a.intern(ExprNode::Distribution {
494 variables: v1,
495 conditioned_on: empty,
496 intervention: empty_i,
497 domain: DomainRef::Observational,
498 });
499 let dc = a.intern(ExprNode::Distribution {
500 variables: v2,
501 conditioned_on: empty,
502 intervention: empty_i,
503 domain: DomainRef::Observational,
504 });
505 let ab = a.intern(ExprNode::Ratio { numerator: da, denominator: db });
506 let nested = a.intern(ExprNode::Ratio { numerator: ab, denominator: dc });
507 let s = simplify(&mut a, nested);
508 match a.node(s) {
509 ExprNode::Ratio { numerator, denominator } => {
510 assert_eq!(*numerator, da);
511 match a.node(*denominator) {
512 ExprNode::Product(list) => {
513 let kids = a.list(*list);
514 assert_eq!(kids.len(), 2);
515 assert!(kids.contains(&db) && kids.contains(&dc));
516 }
517 other => panic!("expected product denom, got {other:?}"),
518 }
519 }
520 other => panic!("expected ratio, got {other:?}"),
521 }
522 }
523
524 #[test]
525 fn contrast_rebuilds_children() {
526 let mut a = CausalExprArena::new();
527 let empty = a.empty_var_set();
528 let empty_i = a.empty_intervention_set();
529 let dist = a.intern(ExprNode::Distribution {
530 variables: empty,
531 conditioned_on: empty,
532 intervention: empty_i,
533 domain: DomainRef::Observational,
534 });
535 let summed = a.intern(ExprNode::SumOut { variables: empty, expr: dist });
536 let exp = a.intern(ExprNode::Expectation {
537 function: OutcomeExprId::identity(VariableId::from_raw(0)),
538 distribution: summed,
539 });
540 let contrast =
541 a.intern(ExprNode::Contrast { left: exp, right: exp, op: ContrastOp::Difference });
542 let s = simplify(&mut a, contrast);
543 match a.node(s) {
544 ExprNode::Contrast { left, right, .. } => {
545 match a.node(*left) {
546 ExprNode::Expectation { distribution, .. } => assert_eq!(*distribution, dist),
547 other => panic!("expected expectation, got {other:?}"),
548 }
549 assert_eq!(left, right);
550 }
551 other => panic!("expected contrast, got {other:?}"),
552 }
553 }
554}