Skip to main content

qualia_core_db/specialized_libs/
symbolic_integration.rs

1//! **Symbolic integration** (Calculus plan §4.1) — antiderivatives over the CAS.
2//!
3//! The current `Expr` algebra has no `ln`/exp/trig variants, so this implements the
4//! cases it *can* represent exactly — the power rule, constants, linearity, scalar
5//! multiples, and the linear-substitution `∫(ax+b)ⁿ dx` — and **fails closed**
6//! ([`IntegrationError::NotIntegrable`], e.g. `∫x⁻¹ dx` which needs `ln`) rather than
7//! returning a wrong antiderivative. A `Verified` round-trip (`d/dx ∘ ∫`) backs every
8//! handled case. Definite integrals use the Fundamental Theorem, with a numerical
9//! Simpson fallback when the symbolic form is unavailable.
10
11use super::symbolic_algebra::{
12    add, c, cos, div, exp, ln, mul, neg, pow, simplify, sin, sub, var, Expr,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum IntegrationError {
17    /// Outside the representable symbolic table (e.g. needs `ln`, or a non-linear inner
18    /// function this engine does not handle).
19    NotIntegrable,
20}
21
22fn is_var(e: &Expr, name: &str) -> bool {
23    matches!(e, Expr::Var(v) if v == name)
24}
25
26/// Indefinite integral `∫ expr dx` (constant of integration omitted).
27pub fn integrate(expr: &Expr, x: &str) -> Result<Expr, IntegrationError> {
28    let r = match expr {
29        // ∫ c dx = c·x
30        Expr::Const(k) => mul(c(*k), var(x)),
31        // ∫ x dx = x²/2
32        Expr::Var(name) if name == x => div(pow(var(x), 2), c(2.0)),
33        // ∫ y dx = y·x   (y independent of x)
34        Expr::Var(_) => mul(expr.clone(), var(x)),
35        // ∫ xⁿ dx = x^{n+1}/(n+1), n ≠ −1 ; ∫ x⁻¹ dx = ln(x)
36        Expr::Pow(base, n) if is_var(base, x) => {
37            if *n == -1 {
38                ln(var(x)) // principal branch (domain x > 0)
39            } else {
40                div(pow(var(x), n + 1), c((*n + 1) as f64))
41            }
42        }
43        // ∫ eˣ dx = eˣ
44        Expr::Exp(inner) if is_var(inner, x) => exp(var(x)),
45        // ∫ sin x dx = −cos x ; ∫ cos x dx = sin x
46        Expr::Sin(inner) if is_var(inner, x) => neg(cos(var(x))),
47        Expr::Cos(inner) if is_var(inner, x) => sin(var(x)),
48        Expr::Add(a, b) => add(integrate(a, x)?, integrate(b, x)?),
49        Expr::Sub(a, b) => sub(integrate(a, x)?, integrate(b, x)?),
50        Expr::Neg(a) => neg(integrate(a, x)?),
51        // scalar · f(x)
52        Expr::Mul(a, b) => match (&**a, &**b) {
53            (Expr::Const(_), _) => mul((**a).clone(), integrate(b, x)?),
54            (_, Expr::Const(_)) => mul((**b).clone(), integrate(a, x)?),
55            _ => return Err(IntegrationError::NotIntegrable),
56        },
57        // quotients: f/const, k/x → k·ln(x), k/xⁿ → power rule with negative exponent.
58        Expr::Div(a, b) => match (&**a, &**b) {
59            (_, Expr::Const(d)) if *d != 0.0 => mul(c(1.0 / d), integrate(a, x)?),
60            (Expr::Const(k), Expr::Var(name)) if name == x => mul(c(*k), ln(var(x))),
61            (Expr::Const(k), Expr::Pow(base, n)) if is_var(base, x) => {
62                let m = -*n; // ∫ k·x^m dx
63                if m == -1 {
64                    mul(c(*k), ln(var(x)))
65                } else {
66                    mul(c(*k), div(pow(var(x), m + 1), c((m + 1) as f64)))
67                }
68            }
69            _ => return Err(IntegrationError::NotIntegrable),
70        },
71        _ => return Err(IntegrationError::NotIntegrable),
72    };
73    Ok(simplify(&r))
74}
75
76/// Definite integral `∫_a^b expr dx` via the Fundamental Theorem when an antiderivative
77/// exists, else a numerical Simpson fallback over `steps` (even) panels.
78pub fn integrate_definite(expr: &Expr, x: &str, a: f64, b: f64, steps: usize) -> Option<f64> {
79    if let Ok(anti) = integrate(expr, x) {
80        let fa = eval_at(&anti, x, a)?;
81        let fb = eval_at(&anti, x, b)?;
82        return Some(fb - fa);
83    }
84    // Numerical fallback (Simpson).
85    let n = if steps.max(2) % 2 == 0 {
86        steps.max(2)
87    } else {
88        steps + 1
89    };
90    let h = (b - a) / n as f64;
91    let mut sum = eval_at(expr, x, a)? + eval_at(expr, x, b)?;
92    for i in 1..n {
93        let xi = a + i as f64 * h;
94        sum += if i % 2 == 1 { 4.0 } else { 2.0 } * eval_at(expr, x, xi)?;
95    }
96    Some(sum * h / 3.0)
97}
98
99fn eval_at(e: &Expr, x: &str, v: f64) -> Option<f64> {
100    let mut env = std::collections::HashMap::new();
101    env.insert(x.to_string(), v);
102    e.eval(&env)
103}
104
105#[cfg(test)]
106mod tests {
107    use super::super::symbolic_algebra::differentiate;
108    use super::*;
109
110    /// d/dx of the antiderivative recovers the integrand (the honest correctness gate).
111    fn roundtrip(expr: &Expr, x: &str) {
112        let anti = integrate(expr, x).unwrap();
113        let back = simplify(&differentiate(&anti, x));
114        for &q in &[0.3, 1.7, -1.1, 2.5] {
115            assert!(
116                (eval_at(&back, x, q).unwrap() - eval_at(expr, x, q).unwrap()).abs() < 1e-7,
117                "round-trip failed at {q}"
118            );
119        }
120    }
121
122    #[test]
123    fn power_rule_roundtrips() {
124        roundtrip(&pow(var("x"), 2), "x"); // ∫x² = x³/3
125        roundtrip(&pow(var("x"), 5), "x");
126        roundtrip(&var("x"), "x");
127        roundtrip(&c(7.0), "x");
128    }
129
130    #[test]
131    fn linearity_roundtrips() {
132        // 3x² + 2x − 5
133        let f = sub(
134            add(mul(c(3.0), pow(var("x"), 2)), mul(c(2.0), var("x"))),
135            c(5.0),
136        );
137        roundtrip(&f, "x");
138    }
139
140    #[test]
141    fn definite_integral_ftc_and_numeric() {
142        // ∫₀¹ x² dx = 1/3
143        let v = integrate_definite(&pow(var("x"), 2), "x", 0.0, 1.0, 100).unwrap();
144        assert!((v - 1.0 / 3.0).abs() < 1e-9);
145        // sqrt(x) is not symbolically integrable here, but the numeric fallback works:
146        // ∫₀¹ √x dx = 2/3.
147        let v2 = integrate_definite(
148            &super::super::symbolic_algebra::sqrt(var("x")),
149            "x",
150            0.0,
151            1.0,
152            2000,
153        )
154        .unwrap();
155        assert!((v2 - 2.0 / 3.0).abs() < 1e-3);
156    }
157
158    #[test]
159    fn transcendental_antiderivatives_roundtrip() {
160        // ∫x⁻¹ = ln x, ∫eˣ = eˣ, ∫sin = −cos, ∫cos = sin — round-trip on the positive
161        // domain (ln is only defined for x > 0).
162        let pos = |expr: &Expr| {
163            let anti = integrate(expr, "x").unwrap();
164            let back = simplify(&differentiate(&anti, "x"));
165            for &q in &[0.3, 1.7, 2.5] {
166                assert!(
167                    (eval_at(&back, "x", q).unwrap() - eval_at(expr, "x", q).unwrap()).abs() < 1e-7
168                );
169            }
170        };
171        pos(&pow(var("x"), -1));
172        pos(&super::super::symbolic_algebra::exp(var("x")));
173        pos(&super::super::symbolic_algebra::sin(var("x")));
174        pos(&super::super::symbolic_algebra::cos(var("x")));
175    }
176
177    #[test]
178    fn fails_closed_on_nonlinear_inner() {
179        // ∫ sin(x²) dx has no elementary form this engine represents — refuse, don't fabricate.
180        let f = super::super::symbolic_algebra::sin(pow(var("x"), 2));
181        assert_eq!(
182            integrate(&f, "x").unwrap_err(),
183            IntegrationError::NotIntegrable
184        );
185    }
186}