1use std::collections::HashMap;
6use std::fmt;
7use std::sync::Arc;
8
9use antecedent_core::VariableId;
10
11use crate::{CausalExprArena, DerivationMeta, ExprId, ExprNode, VarSetId};
12
13#[derive(Clone, Debug, Eq, PartialEq)]
25#[non_exhaustive]
26pub enum SimplifyError {
27 DeadSumOut {
30 variables: Vec<VariableId>,
32 },
33 DeadIntegralOut {
36 variables: Vec<VariableId>,
38 },
39}
40
41impl fmt::Display for SimplifyError {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 Self::DeadSumOut { variables } => {
45 write!(f, "SumOut binds variable(s) not free in its body: ")?;
46 write_var_list(f, variables)
47 }
48 Self::DeadIntegralOut { variables } => {
49 write!(f, "IntegralOut binds variable(s) not free in its body: ")?;
50 write_var_list(f, variables)
51 }
52 }
53 }
54}
55
56impl std::error::Error for SimplifyError {}
57
58fn write_var_list(f: &mut fmt::Formatter<'_>, variables: &[VariableId]) -> fmt::Result {
59 for (i, v) in variables.iter().enumerate() {
60 if i > 0 {
61 write!(f, ", ")?;
62 }
63 write!(f, "V{}", v.raw())?;
64 }
65 Ok(())
66}
67
68pub(crate) fn simplify(arena: &mut CausalExprArena, root: ExprId) -> Result<ExprId, SimplifyError> {
75 let mut memo: HashMap<ExprId, ExprId> = HashMap::new();
76 let mut free_memo: HashMap<ExprId, VarSetId> = HashMap::new();
77 simplify_rec(arena, root, &mut memo, &mut free_memo)
78}
79
80fn simplify_rec(
81 arena: &mut CausalExprArena,
82 id: ExprId,
83 memo: &mut HashMap<ExprId, ExprId>,
84 free_memo: &mut HashMap<ExprId, VarSetId>,
85) -> Result<ExprId, SimplifyError> {
86 if let Some(&cached) = memo.get(&id) {
87 return Ok(cached);
88 }
89 let rebuilt = rebuild_children(arena, id, memo, free_memo)?;
90 let simplified = apply_rules_fixpoint(arena, rebuilt, free_memo)?;
91 memo.insert(id, simplified);
92 Ok(simplified)
93}
94
95fn rebuild_children(
96 arena: &mut CausalExprArena,
97 id: ExprId,
98 memo: &mut HashMap<ExprId, ExprId>,
99 free_memo: &mut HashMap<ExprId, VarSetId>,
100) -> Result<ExprId, SimplifyError> {
101 let node = arena.node(id).clone();
102 let rebuilt = match node {
103 ExprNode::Distribution { .. } => id,
104 ExprNode::Product(list) => {
105 let children_ids: Vec<ExprId> = arena.list(list).to_vec();
106 let mut children: Vec<ExprId> = Vec::with_capacity(children_ids.len());
107 for c in children_ids {
108 children.push(simplify_rec(arena, c, memo, free_memo)?);
109 }
110 let list_id = arena.intern_list(children);
111 arena.intern(ExprNode::Product(list_id))
112 }
113 ExprNode::SumOut { variables, expr } => {
114 let body = simplify_rec(arena, expr, memo, free_memo)?;
115 arena.intern(ExprNode::SumOut { variables, expr: body })
116 }
117 ExprNode::IntegralOut { variables, expr } => {
118 let body = simplify_rec(arena, expr, memo, free_memo)?;
119 arena.intern(ExprNode::IntegralOut { variables, expr: body })
120 }
121 ExprNode::Ratio { numerator, denominator } => {
122 let num = simplify_rec(arena, numerator, memo, free_memo)?;
123 let den = simplify_rec(arena, denominator, memo, free_memo)?;
124 arena.intern(ExprNode::Ratio { numerator: num, denominator: den })
125 }
126 ExprNode::Expectation { function, distribution } => {
127 let dist = simplify_rec(arena, distribution, memo, free_memo)?;
128 arena.intern(ExprNode::Expectation { function, distribution: dist })
129 }
130 ExprNode::Contrast { left, right, op } => {
131 let l = simplify_rec(arena, left, memo, free_memo)?;
132 let r = simplify_rec(arena, right, memo, free_memo)?;
133 arena.intern(ExprNode::Contrast { left: l, right: r, op })
134 }
135 };
136 Ok(rebuilt)
137}
138
139fn apply_rules_fixpoint(
140 arena: &mut CausalExprArena,
141 mut id: ExprId,
142 free_memo: &mut HashMap<ExprId, VarSetId>,
143) -> Result<ExprId, SimplifyError> {
144 loop {
146 let next = apply_local_rules(arena, id, free_memo)?;
147 if next == id {
148 return Ok(id);
149 }
150 id = next;
151 }
152}
153
154fn apply_local_rules(
155 arena: &mut CausalExprArena,
156 id: ExprId,
157 free_memo: &mut HashMap<ExprId, VarSetId>,
158) -> Result<ExprId, SimplifyError> {
159 match arena.node(id).clone() {
160 ExprNode::SumOut { variables, expr } => {
161 rewrite_sum_out(arena, id, variables, expr, free_memo)
162 }
163 ExprNode::IntegralOut { variables, expr } => {
164 rewrite_integral_out(arena, id, variables, expr, free_memo)
165 }
166 ExprNode::Product(list) => Ok(rewrite_product(arena, id, list)),
167 ExprNode::Ratio { numerator, denominator } => {
168 Ok(rewrite_ratio(arena, id, numerator, denominator))
169 }
170 _ => Ok(id),
171 }
172}
173
174fn rewrite_sum_out(
175 arena: &mut CausalExprArena,
176 id: ExprId,
177 variables: VarSetId,
178 expr: ExprId,
179 free_memo: &mut HashMap<ExprId, VarSetId>,
180) -> Result<ExprId, SimplifyError> {
181 if arena.var_set(variables).is_empty() {
182 return Ok(tag_if_new(arena, expr, id, "simplify.empty_sum_out"));
183 }
184 if let ExprNode::SumOut { variables: inner_v, expr: inner_e } = arena.node(expr).clone() {
185 let merged: Vec<VariableId> = arena
186 .var_set(variables)
187 .iter()
188 .copied()
189 .chain(arena.var_set(inner_v).iter().copied())
190 .collect();
191 let union = arena.intern_var_set(merged);
192 let node = ExprNode::SumOut { variables: union, expr: inner_e };
193 return Ok(intern_derived(arena, node, "simplify.merge_sum_out"));
194 }
195 let free = free_vars(arena, expr, free_memo);
196 if !intersects(arena, variables, free) {
197 return Err(SimplifyError::DeadSumOut { variables: arena.var_set(variables).to_vec() });
200 }
201 Ok(id)
202}
203
204fn rewrite_integral_out(
205 arena: &mut CausalExprArena,
206 id: ExprId,
207 variables: VarSetId,
208 expr: ExprId,
209 free_memo: &mut HashMap<ExprId, VarSetId>,
210) -> Result<ExprId, SimplifyError> {
211 if arena.var_set(variables).is_empty() {
212 return Ok(tag_if_new(arena, expr, id, "simplify.empty_integral_out"));
213 }
214 if let ExprNode::IntegralOut { variables: inner_v, expr: inner_e } = arena.node(expr).clone() {
215 let merged: Vec<VariableId> = arena
216 .var_set(variables)
217 .iter()
218 .copied()
219 .chain(arena.var_set(inner_v).iter().copied())
220 .collect();
221 let union = arena.intern_var_set(merged);
222 let node = ExprNode::IntegralOut { variables: union, expr: inner_e };
223 return Ok(intern_derived(arena, node, "simplify.merge_integral_out"));
224 }
225 let free = free_vars(arena, expr, free_memo);
226 if !intersects(arena, variables, free) {
227 return Err(SimplifyError::DeadIntegralOut {
230 variables: arena.var_set(variables).to_vec(),
231 });
232 }
233 Ok(id)
234}
235
236fn rewrite_product(arena: &mut CausalExprArena, id: ExprId, list: crate::ExprListId) -> ExprId {
237 let children = arena.list(list).to_vec();
238 if children.len() == 1 {
239 return tag_if_new(arena, children[0], id, "simplify.singleton_product");
240 }
241 let mut flat: Vec<ExprId> = Vec::with_capacity(children.len());
242 let mut flattened = false;
243 for c in &children {
244 if let ExprNode::Product(inner) = arena.node(*c) {
245 flat.extend_from_slice(arena.list(*inner));
246 flattened = true;
247 } else {
248 flat.push(*c);
249 }
250 }
251 flat.sort_unstable();
252 let sorted_changed = flat.as_slice() != children.as_slice();
253 if flattened || sorted_changed {
254 if flat.len() == 1 {
255 return tag_if_new(arena, flat[0], id, "simplify.singleton_product");
256 }
257 let list_id = arena.intern_list(flat);
258 let rule =
259 if flattened { "simplify.flatten_product" } else { "simplify.canonical_product" };
260 return intern_derived(arena, ExprNode::Product(list_id), rule);
261 }
262 id
263}
264
265fn rewrite_ratio(
266 arena: &mut CausalExprArena,
267 id: ExprId,
268 numerator: ExprId,
269 denominator: ExprId,
270) -> ExprId {
271 if let ExprNode::Ratio { numerator: a, denominator: b } = arena.node(numerator).clone() {
273 let bc = {
274 let mut kids = vec![b, denominator];
275 kids.sort_unstable();
276 let list = arena.intern_list(kids);
277 arena.intern(ExprNode::Product(list))
278 };
279 return intern_derived(
280 arena,
281 ExprNode::Ratio { numerator: a, denominator: bc },
282 "simplify.ratio_assoc_left",
283 );
284 }
285 if let ExprNode::Ratio { numerator: b, denominator: c } = arena.node(denominator).clone() {
287 let ac = {
288 let mut kids = vec![numerator, c];
289 kids.sort_unstable();
290 let list = arena.intern_list(kids);
291 arena.intern(ExprNode::Product(list))
292 };
293 return intern_derived(
294 arena,
295 ExprNode::Ratio { numerator: ac, denominator: b },
296 "simplify.ratio_assoc_right",
297 );
298 }
299 id
300}
301
302fn tag_if_new(arena: &mut CausalExprArena, result: ExprId, _from: ExprId, _rule: &str) -> ExprId {
303 let _ = arena;
305 result
306}
307
308fn intern_derived(arena: &mut CausalExprArena, node: ExprNode, rule: &str) -> ExprId {
309 let before = arena.len();
310 let id = arena.intern(node);
311 if arena.len() > before {
312 arena.set_derivation_if_absent(id, DerivationMeta { rule: Arc::from(rule), note: None });
313 }
314 id
315}
316
317fn intersects(arena: &CausalExprArena, a: VarSetId, b: VarSetId) -> bool {
318 let av = arena.var_set(a);
319 let bv = arena.var_set(b);
320 let mut i = 0;
321 let mut j = 0;
322 while i < av.len() && j < bv.len() {
323 match av[i].raw().cmp(&bv[j].raw()) {
324 std::cmp::Ordering::Equal => return true,
325 std::cmp::Ordering::Less => i += 1,
326 std::cmp::Ordering::Greater => j += 1,
327 }
328 }
329 false
330}
331
332fn free_vars(
333 arena: &mut CausalExprArena,
334 id: ExprId,
335 memo: &mut HashMap<ExprId, VarSetId>,
336) -> VarSetId {
337 if let Some(&cached) = memo.get(&id) {
338 return cached;
339 }
340 let result = match arena.node(id).clone() {
341 ExprNode::Distribution { variables, conditioned_on, intervention, .. } => {
342 let mut vars: Vec<VariableId> = arena.var_set(variables).to_vec();
343 let bound: Vec<VariableId> =
355 arena.intervention_assignments(intervention).iter().map(|a| a.variable).collect();
356 for &v in arena.var_set(conditioned_on) {
357 if !bound.iter().any(|b| *b == v) {
358 vars.push(v);
359 }
360 }
361 arena.intern_var_set(vars)
362 }
363 ExprNode::Product(list) => {
364 let children: Vec<ExprId> = arena.list(list).to_vec();
365 let mut vars = Vec::new();
366 for c in children {
367 let fv = free_vars(arena, c, memo);
368 vars.extend_from_slice(arena.var_set(fv));
369 }
370 arena.intern_var_set(vars)
371 }
372 ExprNode::SumOut { variables, expr } | ExprNode::IntegralOut { variables, expr } => {
373 let body = free_vars(arena, expr, memo);
374 let bound = arena.var_set(variables);
375 let remaining: Vec<VariableId> = arena
376 .var_set(body)
377 .iter()
378 .copied()
379 .filter(|v| !bound.iter().any(|b| b == v))
380 .collect();
381 arena.intern_var_set(remaining)
382 }
383 ExprNode::Ratio { numerator, denominator } => {
384 let n = free_vars(arena, numerator, memo);
385 let d = free_vars(arena, denominator, memo);
386 let mut vars = arena.var_set(n).to_vec();
387 vars.extend_from_slice(arena.var_set(d));
388 arena.intern_var_set(vars)
389 }
390 ExprNode::Expectation { function, distribution } => {
391 let dist = free_vars(arena, distribution, memo);
392 let mut vars = arena.var_set(dist).to_vec();
393 vars.push(function.variable());
394 arena.intern_var_set(vars)
395 }
396 ExprNode::Contrast { left, right, .. } => {
397 let l = free_vars(arena, left, memo);
398 let r = free_vars(arena, right, memo);
399 let mut vars = arena.var_set(l).to_vec();
400 vars.extend_from_slice(arena.var_set(r));
401 arena.intern_var_set(vars)
402 }
403 };
404 memo.insert(id, result);
405 result
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411 use crate::{ContrastOp, DomainRef, OutcomeExprId};
412 use antecedent_core::Value;
413
414 #[test]
415 fn empty_sum_out_eliminates() {
416 let mut a = CausalExprArena::new();
417 let empty = a.empty_var_set();
418 let empty_i = a.empty_intervention_set();
419 let dist = a.intern(ExprNode::Distribution {
420 variables: empty,
421 conditioned_on: empty,
422 intervention: empty_i,
423 domain: DomainRef::Observational,
424 });
425 let summed = a.intern(ExprNode::SumOut { variables: empty, expr: dist });
426 assert_eq!(simplify(&mut a, summed).unwrap(), dist);
427 }
428
429 #[test]
430 fn merge_nested_sum_out() {
431 let mut a = CausalExprArena::new();
432 let empty = a.empty_var_set();
433 let empty_i = a.empty_intervention_set();
434 let v1 = a.intern_var_set([VariableId::from_raw(1)]);
435 let v2 = a.intern_var_set([VariableId::from_raw(2)]);
436 let vars12 = a.intern_var_set([VariableId::from_raw(1), VariableId::from_raw(2)]);
437 let dist = a.intern(ExprNode::Distribution {
438 variables: vars12,
439 conditioned_on: empty,
440 intervention: empty_i,
441 domain: DomainRef::Observational,
442 });
443 let inner = a.intern(ExprNode::SumOut { variables: v2, expr: dist });
444 let outer = a.intern(ExprNode::SumOut { variables: v1, expr: inner });
445 let s = simplify(&mut a, outer).unwrap();
446 match a.node(s) {
447 ExprNode::SumOut { variables, expr } => {
448 assert_eq!(
449 a.var_set(*variables),
450 &[VariableId::from_raw(1), VariableId::from_raw(2)]
451 );
452 assert_eq!(*expr, dist);
453 }
454 other => panic!("expected merged SumOut, got {other:?}"),
455 }
456 }
457
458 #[test]
459 fn dead_sum_out_rejected() {
460 let mut a = CausalExprArena::new();
466 let empty = a.empty_var_set();
467 let empty_i = a.empty_intervention_set();
468 let y = a.intern_var_set([VariableId::from_raw(0)]);
469 let z = a.intern_var_set([VariableId::from_raw(1)]);
470 let dist = a.intern(ExprNode::Distribution {
471 variables: y,
472 conditioned_on: empty,
473 intervention: empty_i,
474 domain: DomainRef::Observational,
475 });
476 let summed = a.intern(ExprNode::SumOut { variables: z, expr: dist });
477 let err = simplify(&mut a, summed).unwrap_err();
478 assert_eq!(err, SimplifyError::DeadSumOut { variables: vec![VariableId::from_raw(1)] });
479 }
480
481 #[test]
482 fn dead_integral_out_rejected() {
483 let mut a = CausalExprArena::new();
487 let empty = a.empty_var_set();
488 let empty_i = a.empty_intervention_set();
489 let y = a.intern_var_set([VariableId::from_raw(0)]);
490 let z = a.intern_var_set([VariableId::from_raw(1)]);
491 let dist = a.intern(ExprNode::Distribution {
492 variables: y,
493 conditioned_on: empty,
494 intervention: empty_i,
495 domain: DomainRef::Observational,
496 });
497 let integrated = a.intern(ExprNode::IntegralOut { variables: z, expr: dist });
498 let err = simplify(&mut a, integrated).unwrap_err();
499 assert_eq!(
500 err,
501 SimplifyError::DeadIntegralOut { variables: vec![VariableId::from_raw(1)] }
502 );
503 }
504
505 #[test]
506 fn singleton_and_flatten_product() {
507 let mut a = CausalExprArena::new();
508 let empty = a.empty_var_set();
509 let empty_i = a.empty_intervention_set();
510 let v0 = a.intern_var_set([VariableId::from_raw(0)]);
511 let v1 = a.intern_var_set([VariableId::from_raw(1)]);
512 let d1 = a.intern(ExprNode::Distribution {
513 variables: v0,
514 conditioned_on: empty,
515 intervention: empty_i,
516 domain: DomainRef::Observational,
517 });
518 let d2 = a.intern(ExprNode::Distribution {
519 variables: v1,
520 conditioned_on: empty,
521 intervention: empty_i,
522 domain: DomainRef::Observational,
523 });
524 let inner = {
525 let list = a.intern_list([d1]);
526 a.intern(ExprNode::Product(list))
527 };
528 assert_eq!(simplify(&mut a, inner).unwrap(), d1);
529
530 let nest = {
531 let list_inner = a.intern_list([d1, d2]);
532 let p_inner = a.intern(ExprNode::Product(list_inner));
533 let list_outer = a.intern_list([p_inner, d1]);
534 a.intern(ExprNode::Product(list_outer))
535 };
536 let s = simplify(&mut a, nest).unwrap();
537 match a.node(s) {
538 ExprNode::Product(list) => {
539 let kids = a.list(*list);
540 assert_eq!(kids.len(), 3);
541 let mut sorted = kids.to_vec();
542 sorted.sort_unstable();
543 assert_eq!(kids, sorted.as_slice());
544 }
545 other => panic!("expected product, got {other:?}"),
546 }
547 }
548
549 #[test]
550 fn product_order_independent() {
551 let mut a = CausalExprArena::new();
552 let empty = a.empty_var_set();
553 let empty_i = a.empty_intervention_set();
554 let v0 = a.intern_var_set([VariableId::from_raw(0)]);
555 let v1 = a.intern_var_set([VariableId::from_raw(1)]);
556 let d1 = a.intern(ExprNode::Distribution {
557 variables: v0,
558 conditioned_on: empty,
559 intervention: empty_i,
560 domain: DomainRef::Observational,
561 });
562 let d2 = a.intern(ExprNode::Distribution {
563 variables: v1,
564 conditioned_on: empty,
565 intervention: empty_i,
566 domain: DomainRef::Observational,
567 });
568 let p1 = {
569 let list = a.intern_list([d1, d2]);
570 a.intern(ExprNode::Product(list))
571 };
572 let p2 = {
573 let list = a.intern_list([d2, d1]);
574 a.intern(ExprNode::Product(list))
575 };
576 assert_eq!(simplify(&mut a, p1).unwrap(), simplify(&mut a, p2).unwrap());
577 }
578
579 #[test]
580 fn simplify_idempotent() {
581 let mut a = CausalExprArena::new();
582 let id = a.backdoor_ate(
583 VariableId::from_raw(0),
584 VariableId::from_raw(1),
585 &[VariableId::from_raw(2)],
586 Value::f64(1.0),
587 Value::f64(0.0),
588 );
589 let s1 = simplify(&mut a, id).unwrap();
590 let s2 = simplify(&mut a, s1).unwrap();
591 assert_eq!(s1, s2);
592 }
593
594 #[test]
595 fn ratio_assoc_left() {
596 let mut a = CausalExprArena::new();
597 let empty = a.empty_var_set();
598 let empty_i = a.empty_intervention_set();
599 let v0 = a.intern_var_set([VariableId::from_raw(0)]);
600 let v1 = a.intern_var_set([VariableId::from_raw(1)]);
601 let v2 = a.intern_var_set([VariableId::from_raw(2)]);
602 let da = a.intern(ExprNode::Distribution {
603 variables: v0,
604 conditioned_on: empty,
605 intervention: empty_i,
606 domain: DomainRef::Observational,
607 });
608 let db = a.intern(ExprNode::Distribution {
609 variables: v1,
610 conditioned_on: empty,
611 intervention: empty_i,
612 domain: DomainRef::Observational,
613 });
614 let dc = a.intern(ExprNode::Distribution {
615 variables: v2,
616 conditioned_on: empty,
617 intervention: empty_i,
618 domain: DomainRef::Observational,
619 });
620 let ab = a.intern(ExprNode::Ratio { numerator: da, denominator: db });
621 let nested = a.intern(ExprNode::Ratio { numerator: ab, denominator: dc });
622 let s = simplify(&mut a, nested).unwrap();
623 match a.node(s) {
624 ExprNode::Ratio { numerator, denominator } => {
625 assert_eq!(*numerator, da);
626 match a.node(*denominator) {
627 ExprNode::Product(list) => {
628 let kids = a.list(*list);
629 assert_eq!(kids.len(), 2);
630 assert!(kids.contains(&db) && kids.contains(&dc));
631 }
632 other => panic!("expected product denom, got {other:?}"),
633 }
634 }
635 other => panic!("expected ratio, got {other:?}"),
636 }
637 }
638
639 #[test]
640 fn contrast_rebuilds_children() {
641 let mut a = CausalExprArena::new();
642 let empty = a.empty_var_set();
643 let empty_i = a.empty_intervention_set();
644 let dist = a.intern(ExprNode::Distribution {
645 variables: empty,
646 conditioned_on: empty,
647 intervention: empty_i,
648 domain: DomainRef::Observational,
649 });
650 let summed = a.intern(ExprNode::SumOut { variables: empty, expr: dist });
651 let exp = a.intern(ExprNode::Expectation {
652 function: OutcomeExprId::identity(VariableId::from_raw(0)),
653 distribution: summed,
654 });
655 let contrast =
656 a.intern(ExprNode::Contrast { left: exp, right: exp, op: ContrastOp::Difference });
657 let s = simplify(&mut a, contrast).unwrap();
658 match a.node(s) {
659 ExprNode::Contrast { left, right, .. } => {
660 match a.node(*left) {
661 ExprNode::Expectation { distribution, .. } => assert_eq!(*distribution, dist),
662 other => panic!("expected expectation, got {other:?}"),
663 }
664 assert_eq!(left, right);
665 }
666 other => panic!("expected contrast, got {other:?}"),
667 }
668 }
669}