Skip to main content

clifford_codegen/symbolic/
simplify.rs

1//! Expression simplification utilities.
2//!
3//! This module provides tools for simplifying symbolic expressions before
4//! converting them to Rust code.
5
6use symbolica::atom::{Atom, AtomCore, AtomView};
7use symbolica::coefficient::CoefficientView;
8
9/// Utilities for simplifying Symbolica expressions.
10///
11/// This struct provides methods for simplifying mathematical expressions,
12/// which reduces the number of terms and operations in generated code.
13pub struct ExpressionSimplifier;
14
15impl ExpressionSimplifier {
16    /// Creates a new expression simplifier.
17    pub fn new() -> Self {
18        Self
19    }
20
21    /// Simplifies an expression by expanding and collecting like terms.
22    ///
23    /// This is the main simplification entry point. It:
24    /// 1. Expands all products and powers
25    /// 2. Collects like terms
26    /// 3. Simplifies numeric coefficients
27    ///
28    /// # Example
29    ///
30    /// ```ignore
31    /// let simplifier = ExpressionSimplifier::new();
32    /// let expr = Atom::parse("(a + b) * (a + b)").unwrap();
33    /// let simplified = simplifier.simplify(&expr);
34    /// // Result: a*a + 2*a*b + b*b
35    /// ```
36    pub fn simplify(&self, expr: &Atom) -> Atom {
37        expr.expand()
38    }
39
40    /// Checks if an expression is zero.
41    ///
42    /// Returns true if the expression simplifies to zero.
43    pub fn is_zero(&self, expr: &Atom) -> bool {
44        let expanded = expr.expand();
45        match expanded.as_atom_view() {
46            AtomView::Num(n) => {
47                let coeff = n.get_coeff_view();
48                matches!(coeff, CoefficientView::Natural(0, _, 0, _))
49            }
50            _ => false,
51        }
52    }
53
54    /// Counts the number of terms in an expression.
55    ///
56    /// This is useful for comparing simplified vs unsimplified expressions.
57    pub fn term_count(&self, expr: &Atom) -> usize {
58        let expanded = expr.expand();
59        match expanded.as_atom_view() {
60            AtomView::Add(a) => a.iter().count(),
61            AtomView::Num(n) => {
62                let coeff = n.get_coeff_view();
63                if matches!(coeff, CoefficientView::Natural(0, _, 0, _)) {
64                    0
65                } else {
66                    1
67                }
68            }
69            _ => 1,
70        }
71    }
72
73    /// Counts the total number of operations in an expression.
74    ///
75    /// This counts additions, multiplications, and other operations.
76    pub fn operation_count(&self, expr: &Atom) -> usize {
77        self.count_ops_view(expr.as_atom_view())
78    }
79
80    /// Counts operations recursively for an AtomView.
81    fn count_ops_view(&self, view: AtomView<'_>) -> usize {
82        match view {
83            AtomView::Num(_) | AtomView::Var(_) => 0,
84            AtomView::Add(a) => {
85                let children: usize = a.iter().map(|c| self.count_ops_view(c)).sum();
86                let additions = a.iter().count().saturating_sub(1);
87                children + additions
88            }
89            AtomView::Mul(m) => {
90                let children: usize = m.iter().map(|c| self.count_ops_view(c)).sum();
91                let multiplications = m.iter().count().saturating_sub(1);
92                children + multiplications
93            }
94            AtomView::Pow(p) => {
95                let (base, exp) = p.get_base_exp();
96                self.count_ops_view(base) + self.count_ops_view(exp) + 1
97            }
98            AtomView::Fun(f) => {
99                let children: usize = f.iter().map(|c| self.count_ops_view(c)).sum();
100                children + 1
101            }
102        }
103    }
104}
105
106impl Default for ExpressionSimplifier {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use std::sync::Mutex;
116
117    // Symbolica uses global state that conflicts when tests run in parallel.
118    // Tests prefixed with `symbolica_` are configured to run serially via nextest.
119    // The mutex provides a fallback for `cargo test` users.
120    static SYMBOLICA_LOCK: Mutex<()> = Mutex::new(());
121
122    #[test]
123    fn symbolica_simplify_expands_product() {
124        let _guard = SYMBOLICA_LOCK.lock().unwrap();
125        let simplifier = ExpressionSimplifier::new();
126
127        // Create (1 + 1) which should simplify to 2
128        let one = Atom::num(1);
129        let sum = &one + &one;
130        let simplified = simplifier.simplify(&sum);
131
132        // Should be the number 2
133        assert!(!simplifier.is_zero(&simplified));
134    }
135
136    #[test]
137    fn symbolica_is_zero_detects_zero() {
138        let _guard = SYMBOLICA_LOCK.lock().unwrap();
139        let simplifier = ExpressionSimplifier::new();
140
141        let zero = Atom::num(0);
142        assert!(simplifier.is_zero(&zero));
143
144        let one = Atom::num(1);
145        assert!(!simplifier.is_zero(&one));
146    }
147
148    #[test]
149    fn symbolica_term_count_for_sum() {
150        let _guard = SYMBOLICA_LOCK.lock().unwrap();
151        let simplifier = ExpressionSimplifier::new();
152
153        // a + b has 2 terms
154        let a = Atom::num(1);
155        let b = Atom::num(2);
156        let sum = &a + &b;
157
158        // After simplification, 1 + 2 = 3, which is 1 term
159        assert_eq!(simplifier.term_count(&sum), 1);
160    }
161}