use crate::dag::builder::DagBuilder;
use crate::dag::node::DagNodeId;
use crate::dag::symbol::{OpKind, SymbolKind};
#[must_use]
pub fn approximate_simplify(
builder: &mut DagBuilder,
root: DagNodeId,
aggressiveness: f64,
) -> DagNodeId {
if aggressiveness < 0.1 || root.is_none() {
return root;
}
let threshold = coefficient_threshold(aggressiveness);
prune_additive_chain(builder, root, threshold)
}
fn coefficient_threshold(aggressiveness: f64) -> f64 {
let clamped = aggressiveness.clamp(0.0, 1.0);
let exponent = -4.0 + clamped * 3.0; 10.0_f64.powf(exponent)
}
fn prune_additive_chain(builder: &mut DagBuilder, root: DagNodeId, threshold: f64) -> DagNodeId {
let Some(node) = builder.arena().get(root) else {
return root;
};
if !matches!(node.kind, SymbolKind::Operator(OpKind::Add)) {
return root;
}
let mut terms: Vec<DagNodeId> = Vec::new();
let mut stack: Vec<DagNodeId> = vec![root];
while let Some(id) = stack.pop() {
match builder.arena().get(id) {
Some(n) if matches!(n.kind, SymbolKind::Operator(OpKind::Add)) => {
for child in n.children.as_slice().iter().rev() {
stack.push(*child);
}
}
_ => terms.push(id),
}
}
let kept: Vec<DagNodeId> = terms
.into_iter()
.filter(|&id| {
let coef = magnitude_of_term(builder, id);
coef.abs() >= threshold
})
.collect();
match kept.len() {
0 => builder.constant(0.0),
1 => kept[0],
_ => balanced_add(builder, &kept),
}
}
fn balanced_add(builder: &mut DagBuilder, terms: &[DagNodeId]) -> DagNodeId {
match terms.len() {
0 => builder.constant(0.0),
1 => terms[0],
2 => builder.add(terms[0], terms[1]),
n => {
let mid = n / 2;
let left = balanced_add(builder, &terms[..mid]);
let right = balanced_add(builder, &terms[mid..]);
builder.add(left, right)
}
}
}
fn magnitude_of_term(builder: &DagBuilder, id: DagNodeId) -> f64 {
builder.arena().get(id).map_or(0.0, |n| match n.kind {
SymbolKind::Constant(v) => v.abs(),
_ => n.meta.coefficient.abs(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_op_below_threshold() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let y = b.variable("y");
let sum = b.add(x, y);
let result = approximate_simplify(&mut b, sum, 0.05);
assert_eq!(result, sum, "aggressiveness < 0.1 should be a no-op");
}
#[test]
fn prunes_tiny_constants() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let tiny = b.constant(1e-6);
let sum = b.add(x, tiny);
let result = approximate_simplify(&mut b, sum, 0.6);
assert_eq!(result, x, "tiny constant term should be pruned");
}
#[test]
fn keeps_meaningful_terms() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let y = b.variable("y");
let sum = b.add(x, y);
let result = approximate_simplify(&mut b, sum, 1.0);
assert_eq!(result, sum, "non-tiny terms must survive");
}
#[test]
fn all_pruned_returns_zero() {
let mut b = DagBuilder::new();
let tiny1 = b.constant(1e-6);
let tiny2 = b.constant(2e-6);
let sum = b.add(tiny1, tiny2);
let result = approximate_simplify(&mut b, sum, 1.0);
let node = b.arena().get(result).expect("constant 0 expected");
let v: f64 = if let crate::dag::symbol::SymbolKind::Constant(v) = node.kind {
v
} else {
panic!("expected Constant kind")
};
assert!(v.abs() < f64::EPSILON);
}
#[test]
fn non_additive_root_is_passthrough() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let y = b.variable("y");
let prod = b.mul(x, y);
let result = approximate_simplify(&mut b, prod, 1.0);
assert_eq!(result, prod);
}
}