use rustc_hash::{FxHashMap, FxHashSet};
use smallvec::SmallVec;
use crate::base::arena::Arena;
use crate::base::dense_f64;
use crate::base::node::{ExprId, ExprNode, SymbolId};
#[must_use]
pub(crate) fn heurisch_integrate(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
var_sym: SymbolId,
) -> Option<ExprId> {
tracing::debug!("heurisch_integrate: entering heuristic Risch integrator");
if !contains_var(arena, expr, var_sym) {
return Some(arena.mul(&[expr, var]));
}
for degree_offset in 0..=2 {
if degree_offset > 0 {
tracing::debug!("heurisch: retry with degree_offset={}", degree_offset);
}
if let Some(result) = heurisch_attempt(arena, expr, var, var_sym, degree_offset) {
return Some(result);
}
}
None
}
fn heurisch_attempt(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
var_sym: SymbolId,
degree_offset: usize,
) -> Option<ExprId> {
let components = collect_components(arena, expr, var_sym);
tracing::debug!("heurisch: collected {} components", components.len());
if components.is_empty() {
return None;
}
let mut comp_syms: Vec<ExprId> = Vec::with_capacity(components.len());
let mut comp_sym_ids: Vec<SymbolId> = Vec::with_capacity(components.len());
for i in 0..components.len() {
let name = format!("__hV{i}");
let sid = arena.symbols.intern(&name);
let eid = arena.intern(ExprNode::Symbol(sid));
comp_syms.push(eid);
comp_sym_ids.push(sid);
}
let comp_derivs: Vec<ExprId> = components
.iter()
.map(|&c| crate::transforms::diff::diff(arena, c, var))
.collect();
let degree_bound = compute_degree_bound(arena, &components, var_sym) + degree_offset;
tracing::debug!(
"heurisch: degree bound total={}, offset={}",
degree_bound,
degree_offset
);
if degree_bound > 12 {
return None;
}
let monomials = generate_monomials(arena, &comp_syms, degree_bound);
if monomials.is_empty() {
return None;
}
let n_unknowns = monomials.len();
tracing::debug!(
"heurisch: {} monomials, {} unknowns",
monomials.len(),
n_unknowns
);
if n_unknowns > 60 {
return None;
}
let n_points = 3 * n_unknowns + 5;
let eval_points: Vec<f64> = (0..n_points)
.map(|i| {
let t = (i as f64 + 1.0) * 0.1317 + 0.2;
t + 0.01 * (t * 7.31).sin()
})
.collect();
let mut matrix_a: Vec<Vec<f64>> = Vec::with_capacity(n_points);
let mut vec_b: Vec<f64> = Vec::with_capacity(n_points);
for &x_val in &eval_points {
let comp_vals: Vec<f64> = components
.iter()
.map(|&c| eval_at_point(arena, c, var, x_val))
.collect();
if comp_vals.iter().any(|v| !v.is_finite()) {
continue;
}
let comp_deriv_vals: Vec<f64> = comp_derivs
.iter()
.map(|&d| eval_at_point(arena, d, var, x_val))
.collect();
if comp_deriv_vals.iter().any(|v| !v.is_finite()) {
continue;
}
let integrand_val = eval_at_point(arena, expr, var, x_val);
if !integrand_val.is_finite() {
continue;
}
let mut row: Vec<f64> = Vec::with_capacity(n_unknowns);
for monomial in monomials.iter().take(n_unknowns) {
let monomial_deriv = eval_monomial_deriv(&comp_vals, &comp_deriv_vals, monomial);
if !monomial_deriv.is_finite() {
row.clear();
break;
}
row.push(monomial_deriv);
}
if row.len() == n_unknowns {
matrix_a.push(row);
vec_b.push(integrand_val);
}
}
let n_rows = matrix_a.len();
tracing::debug!("heurisch: solving {}x{} numeric system", n_rows, n_unknowns);
if n_rows < n_unknowns {
return None;
}
let coeffs = solve_least_squares(&matrix_a, &vec_b, n_unknowns)?;
let rational_coeffs: Vec<(i64, i64)> = coeffs
.iter()
.map(|&c| rationalize(c, 1000))
.collect::<Option<Vec<_>>>()?;
tracing::debug!(
"heurisch: reconstructed {}/{} coefficients",
rational_coeffs.len(),
coeffs.len()
);
let candidate = build_candidate(arena, &monomials, &rational_coeffs, &components, &comp_syms);
let candidate_deriv = crate::transforms::diff::diff(arena, candidate, var);
let test_points = [0.37, 1.13, 2.71, 0.73, 1.89];
let mut verified = true;
for &x_val in &test_points {
let orig_val = eval_at_point(arena, expr, var, x_val);
let deriv_val = eval_at_point(arena, candidate_deriv, var, x_val);
if !orig_val.is_finite() || !deriv_val.is_finite() {
continue;
}
let abs_diff = (orig_val - deriv_val).abs();
let scale = orig_val.abs().max(1.0);
if abs_diff / scale > 1e-6 {
verified = false;
break;
}
}
tracing::debug!(
"heurisch: verification {}",
if verified { "passed" } else { "failed" }
);
if verified { Some(candidate) } else { None }
}
fn collect_components(arena: &Arena, expr: ExprId, var_sym: SymbolId) -> Vec<ExprId> {
let mut components: FxHashSet<ExprId> = FxHashSet::default();
let mut stack: Vec<ExprId> = vec![expr];
let mut visited: FxHashSet<ExprId> = FxHashSet::default();
while let Some(id) = stack.pop() {
if visited.contains(&id) {
continue;
}
visited.insert(id);
let node = arena.node(id).clone();
match node {
ExprNode::Symbol(sid) if sid == var_sym => {
components.insert(id);
}
ExprNode::Sin(inner)
| ExprNode::Cos(inner)
| ExprNode::Tan(inner)
| ExprNode::Exp(inner)
| ExprNode::Ln(inner)
| ExprNode::Asin(inner)
| ExprNode::Acos(inner)
| ExprNode::Atan(inner)
| ExprNode::Sinh(inner)
| ExprNode::Cosh(inner)
| ExprNode::Tanh(inner)
| ExprNode::Asinh(inner)
| ExprNode::Acosh(inner)
| ExprNode::Atanh(inner)
| ExprNode::Abs(inner) => {
if contains_var(arena, inner, var_sym) {
components.insert(id);
stack.push(inner);
}
}
ExprNode::Pow(base, exp) => {
if contains_var(arena, base, var_sym) || contains_var(arena, exp, var_sym) {
if contains_var(arena, exp, var_sym) {
components.insert(id);
}
if contains_var(arena, base, var_sym) {
stack.push(base);
}
stack.push(exp);
}
}
ExprNode::Add(ref children) | ExprNode::Mul(ref children) => {
for &child in children {
stack.push(child);
}
}
ExprNode::Neg(inner) => {
stack.push(inner);
}
_ => {
let children = arena.node(id).children();
for child in children {
stack.push(child);
}
}
}
}
let mut result: Vec<ExprId> = components.into_iter().collect();
result.sort_by_key(|&id| id.0);
result
}
fn compute_degree_bound(arena: &Arena, components: &[ExprId], var_sym: SymbolId) -> usize {
let mut max_poly_degree: usize = 1;
let mut has_transcendental = false;
for &comp in components {
let node = arena.node(comp).clone();
match node {
ExprNode::Symbol(sid) if sid == var_sym => {
max_poly_degree = max_poly_degree.max(1);
}
ExprNode::Pow(base, exp) => {
if let ExprNode::Symbol(sid) = arena.node(base)
&& *sid == var_sym
&& let Some(r) = arena.as_num(exp)
&& r.is_integer()
{
let d = r.to_integer().to_string().parse::<i64>().unwrap_or(1);
max_poly_degree = max_poly_degree.max(d.unsigned_abs() as usize);
}
if contains_var(arena, exp, var_sym) {
has_transcendental = true;
}
}
ExprNode::Sin(_)
| ExprNode::Cos(_)
| ExprNode::Tan(_)
| ExprNode::Exp(_)
| ExprNode::Ln(_)
| ExprNode::Sinh(_)
| ExprNode::Cosh(_)
| ExprNode::Tanh(_) => {
has_transcendental = true;
}
_ => {}
}
}
let a = if has_transcendental { 1 } else { 0 };
let b = max_poly_degree;
a + b + 1
}
type Monomial = Vec<u32>;
fn generate_monomials(arena: &mut Arena, comp_syms: &[ExprId], max_degree: usize) -> Vec<Monomial> {
let n = comp_syms.len();
if n == 0 {
return vec![];
}
let mut monomials: Vec<Monomial> = Vec::new();
let mut stack: Vec<(usize, u32, Monomial)> = Vec::new();
stack.push((0, max_degree as u32, vec![0; n]));
while let Some((pos, remaining, current)) = stack.pop() {
if pos == n {
monomials.push(current);
continue;
}
let max_for_this = remaining;
for e in 0..=max_for_this {
let mut m = current.clone();
m[pos] = e;
stack.push((pos + 1, remaining - e, m));
}
}
if monomials.len() > 100 {
monomials.truncate(100);
}
let _ = arena;
monomials
}
fn eval_monomial(comp_vals: &[f64], monomial: &Monomial) -> f64 {
let mut result = 1.0;
for (i, &e) in monomial.iter().enumerate() {
if e > 0 {
result *= comp_vals[i].powi(e as i32);
}
}
result
}
fn eval_monomial_deriv(comp_vals: &[f64], comp_deriv_vals: &[f64], monomial: &Monomial) -> f64 {
let n = monomial.len();
let full_product = eval_monomial(comp_vals, monomial);
let mut total = 0.0;
for i in 0..n {
let e = monomial[i];
if e == 0 {
continue;
}
if comp_vals[i].abs() < 1e-300 {
let mut partial = 1.0;
for j in 0..n {
if j == i {
partial *= comp_vals[j].powi(monomial[j] as i32 - 1);
} else if monomial[j] > 0 {
partial *= comp_vals[j].powi(monomial[j] as i32);
}
}
total += (e as f64) * partial * comp_deriv_vals[i];
} else {
total += (e as f64) * full_product / comp_vals[i] * comp_deriv_vals[i];
}
}
total
}
fn eval_at_point(arena: &Arena, expr: ExprId, var: ExprId, x_val: f64) -> f64 {
let post_order = crate::base::walk::post_order_ids(arena, expr);
let mut cache: FxHashMap<ExprId, f64> = FxHashMap::default();
cache.insert(var, x_val);
for &id in &post_order {
if cache.contains_key(&id) {
continue;
}
let val = eval_node_f64(arena, id, &cache);
cache.insert(id, val);
}
cache.get(&expr).copied().unwrap_or(f64::NAN)
}
fn eval_node_f64(arena: &Arena, id: ExprId, cache: &FxHashMap<ExprId, f64>) -> f64 {
let node = arena.node(id).clone();
match node {
ExprNode::Num(nid) => {
let r = arena.num(nid);
let numer: f64 = r.numer().to_string().parse().unwrap_or(f64::NAN);
let denom: f64 = r.denom().to_string().parse().unwrap_or(f64::NAN);
if denom == 0.0 {
f64::NAN
} else {
numer / denom
}
}
ExprNode::Symbol(_) => {
cache.get(&id).copied().unwrap_or(f64::NAN)
}
ExprNode::Pi => std::f64::consts::PI,
ExprNode::E => std::f64::consts::E,
ExprNode::ImaginaryUnit => f64::NAN, ExprNode::Infinity => f64::INFINITY,
ExprNode::NegInfinity => f64::NEG_INFINITY,
ExprNode::ComplexInfinity | ExprNode::NaN => f64::NAN,
ExprNode::Add(ref children) => {
let mut sum = 0.0;
for &c in children {
sum += cache.get(&c).copied().unwrap_or(f64::NAN);
}
sum
}
ExprNode::Mul(ref children) => {
let mut prod = 1.0;
for &c in children {
prod *= cache.get(&c).copied().unwrap_or(f64::NAN);
}
prod
}
ExprNode::Pow(base, exp) => {
let b = cache.get(&base).copied().unwrap_or(f64::NAN);
let e = cache.get(&exp).copied().unwrap_or(f64::NAN);
b.powf(e)
}
ExprNode::Neg(inner) => -cache.get(&inner).copied().unwrap_or(f64::NAN),
ExprNode::Sin(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).sin(),
ExprNode::Cos(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).cos(),
ExprNode::Tan(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).tan(),
ExprNode::Exp(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).exp(),
ExprNode::Ln(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).ln(),
ExprNode::Asin(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).asin(),
ExprNode::Acos(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).acos(),
ExprNode::Atan(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).atan(),
ExprNode::Sinh(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).sinh(),
ExprNode::Cosh(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).cosh(),
ExprNode::Tanh(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).tanh(),
ExprNode::Asinh(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).asinh(),
ExprNode::Acosh(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).acosh(),
ExprNode::Atanh(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).atanh(),
ExprNode::Abs(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).abs(),
ExprNode::Sign(inner) => {
let v = cache.get(&inner).copied().unwrap_or(f64::NAN);
if v > 0.0 {
1.0
} else if v < 0.0 {
-1.0
} else {
0.0
}
}
ExprNode::Floor(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).floor(),
ExprNode::Ceiling(inner) => cache.get(&inner).copied().unwrap_or(f64::NAN).ceil(),
ExprNode::Atan2(y, x) => {
let yv = cache.get(&y).copied().unwrap_or(f64::NAN);
let xv = cache.get(&x).copied().unwrap_or(f64::NAN);
yv.atan2(xv)
}
ExprNode::Gamma(inner) => {
let v = cache.get(&inner).copied().unwrap_or(f64::NAN);
gamma_f64(v)
}
ExprNode::Erf(inner) => {
let v = cache.get(&inner).copied().unwrap_or(f64::NAN);
erf_approx_f64(v)
}
ExprNode::Factorial(inner) => {
let v = cache.get(&inner).copied().unwrap_or(f64::NAN);
gamma_f64(v + 1.0)
}
_ => f64::NAN,
}
}
fn gamma_f64(x: f64) -> f64 {
if x <= 0.0 && x == x.floor() {
return f64::NAN; }
if x < 0.5 {
std::f64::consts::PI / ((std::f64::consts::PI * x).sin() * gamma_f64(1.0 - x))
} else {
let g = 7.0;
#[allow(clippy::excessive_precision)]
let c = [
0.99999999999980993,
676.5203681218851,
-1259.1392167224028,
771.32342877765313,
-176.61502916214059,
12.507343278686905,
-0.13857109526572012,
9.9843695780195716e-6,
1.5056327351493116e-7,
];
let x = x - 1.0;
let mut sum = c[0];
for (i, c_val) in c.iter().enumerate().skip(1) {
sum += c_val / (x + i as f64);
}
let t = x + g + 0.5;
(2.0 * std::f64::consts::PI).sqrt() * t.powf(x + 0.5) * (-t).exp() * sum
}
}
fn erf_approx_f64(x: f64) -> f64 {
let sign = x.signum();
let x = x.abs();
let t = 1.0 / (1.0 + 0.3275911 * x);
let poly = t
* (0.254829592
+ t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
sign * (1.0 - poly * (-x * x).exp())
}
fn solve_least_squares(a: &[Vec<f64>], b: &[f64], n: usize) -> Option<Vec<f64>> {
let x = dense_f64::lstsq_householder(&dense_f64::flatten(a), a.len(), n, b)?;
if x.iter().any(|v| !v.is_finite()) {
return None;
}
Some(x)
}
#[must_use]
fn rationalize(x: f64, max_denom: i64) -> Option<(i64, i64)> {
if !x.is_finite() {
return None;
}
if x.abs() < 1e-12 {
return Some((0, 1));
}
let sign = if x < 0.0 { -1 } else { 1 };
let x = x.abs();
let mut p0: i64 = 0;
let mut q0: i64 = 1;
let mut p1: i64 = 1;
let mut q1: i64 = 0;
let mut rem = x;
for _ in 0..50 {
let a = rem.floor() as i64;
let p2 = a.checked_mul(p1)?.checked_add(p0)?;
let q2 = a.checked_mul(q1)?.checked_add(q0)?;
if q2 > max_denom {
break;
}
p0 = p1;
q0 = q1;
p1 = p2;
q1 = q2;
let frac = rem - a as f64;
if frac.abs() < 1e-12 {
break;
}
rem = 1.0 / frac;
if rem > 1e15 {
break;
}
}
if q1 <= 0 || q1 > max_denom {
return None;
}
let approx = (p1 as f64) / (q1 as f64);
let err = (x - approx).abs();
if err > 1e-8 {
return None;
}
Some((sign * p1, q1))
}
fn build_candidate(
arena: &mut Arena,
monomials: &[Monomial],
coeffs: &[(i64, i64)],
components: &[ExprId],
comp_syms: &[ExprId],
) -> ExprId {
let mut terms: SmallVec<[ExprId; 16]> = SmallVec::new();
for (mono, &(p, q)) in monomials.iter().zip(coeffs.iter()) {
if p == 0 {
continue;
}
let mut factors: SmallVec<[ExprId; 8]> = SmallVec::new();
let coeff = arena.rational(p, q);
factors.push(coeff);
for (i, &e) in mono.iter().enumerate() {
if e == 0 {
continue;
}
if e == 1 {
factors.push(comp_syms[i]);
} else {
let exp = arena.int(e as i64);
let pow = arena.pow(comp_syms[i], exp);
factors.push(pow);
}
}
let term = if factors.len() == 1 {
factors[0]
} else {
arena.mul(&factors)
};
terms.push(term);
}
if terms.is_empty() {
return arena.zero;
}
let candidate_in_v = if terms.len() == 1 {
terms[0]
} else {
arena.add(&terms)
};
let back_subs: Vec<(ExprId, ExprId)> = comp_syms
.iter()
.zip(components.iter())
.map(|(&v, &c)| (v, c))
.collect();
crate::transforms::subs::subs_map(arena, candidate_in_v, &back_subs)
}
fn contains_var(arena: &Arena, expr: ExprId, var: SymbolId) -> bool {
let mut stack: Vec<ExprId> = vec![expr];
let mut visited: FxHashSet<ExprId> = FxHashSet::default();
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
if let ExprNode::Symbol(sid) = arena.node(id)
&& *sid == var
{
return true;
}
let children = arena.node(id).children();
for c in children {
stack.push(c);
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::arena::Arena;
fn sym(a: &mut Arena, name: &str) -> ExprId {
a.symbol(name)
}
fn display(a: &Arena, id: ExprId) -> String {
a.display(id).to_string()
}
#[test]
fn rationalize_zero() {
assert_eq!(rationalize(0.0, 1000), Some((0, 1)));
}
#[test]
fn rationalize_integer() {
assert_eq!(rationalize(3.0, 1000), Some((3, 1)));
assert_eq!(rationalize(-5.0, 1000), Some((-5, 1)));
}
#[test]
fn rationalize_half() {
assert_eq!(rationalize(0.5, 1000), Some((1, 2)));
}
#[test]
fn rationalize_third() {
let r = rationalize(1.0 / 3.0, 1000);
assert_eq!(r, Some((1, 3)));
}
#[test]
fn rationalize_neg_quarter() {
assert_eq!(rationalize(-0.25, 1000), Some((-1, 4)));
}
#[test]
fn rationalize_irrational_fails() {
let r = rationalize(std::f64::consts::PI, 1000);
assert!(r.is_some() || r.is_none()); }
#[test]
fn collect_components_poly() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let var_sym = match a.node(x) {
ExprNode::Symbol(s) => *s,
_ => unreachable!(),
};
let two = a.int(2);
let x2 = a.pow(x, two);
let expr = a.add(&[x2, a.one]);
let comps = collect_components(&a, expr, var_sym);
assert!(comps.contains(&x), "should collect x as a component");
}
#[test]
fn collect_components_sin_exp() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let var_sym = match a.node(x) {
ExprNode::Symbol(s) => *s,
_ => unreachable!(),
};
let sin_x = a.sin(x);
let exp_x = a.exp(x);
let expr = a.add(&[sin_x, exp_x]);
let comps = collect_components(&a, expr, var_sym);
assert!(comps.contains(&x), "should contain x");
assert!(comps.contains(&sin_x), "should contain sin(x)");
assert!(comps.contains(&exp_x), "should contain exp(x)");
}
#[test]
fn degree_bound_polynomial() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let var_sym = match a.node(x) {
ExprNode::Symbol(s) => *s,
_ => unreachable!(),
};
let comps = vec![x];
let d = compute_degree_bound(&a, &comps, var_sym);
assert!(d >= 2, "poly degree bound should be >= 2, got {d}");
}
#[test]
fn degree_bound_with_exp() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let var_sym = match a.node(x) {
ExprNode::Symbol(s) => *s,
_ => unreachable!(),
};
let exp_x = a.exp(x);
let comps = vec![x, exp_x];
let d = compute_degree_bound(&a, &comps, var_sym);
assert!(d >= 3, "exp degree bound should be >= 3, got {d}");
}
#[test]
fn monomials_single_var_degree_2() {
let mut a = Arena::new();
let v0 = sym(&mut a, "V0");
let monos = generate_monomials(&mut a, &[v0], 2);
assert_eq!(monos.len(), 3, "expected 3 monomials, got {}", monos.len());
}
#[test]
fn monomials_two_vars_degree_1() {
let mut a = Arena::new();
let v0 = sym(&mut a, "V0");
let v1 = sym(&mut a, "V1");
let monos = generate_monomials(&mut a, &[v0, v1], 1);
assert_eq!(monos.len(), 3, "expected 3 monomials, got {}", monos.len());
}
#[test]
fn eval_at_point_polynomial() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let x2 = a.pow(x, two);
let expr = a.add(&[x2, a.one]);
let val = eval_at_point(&a, expr, x, 3.0);
assert!((val - 10.0).abs() < 1e-10, "expected 10, got {val}");
}
#[test]
fn eval_at_point_trig() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let sin_x = a.sin(x);
let val = eval_at_point(&a, sin_x, x, std::f64::consts::FRAC_PI_2);
assert!((val - 1.0).abs() < 1e-10, "sin(π/2) should be 1, got {val}");
}
#[test]
fn solve_simple_system() {
let a = vec![vec![2.0], vec![2.0], vec![2.0]];
let b = vec![6.0, 6.0, 6.0];
let x = solve_least_squares(&a, &b, 1).unwrap();
assert!((x[0] - 3.0).abs() < 1e-10, "expected x=3, got {}", x[0]);
}
#[test]
fn solve_two_unknowns() {
let a = vec![vec![1.0, 1.0], vec![1.0, -1.0], vec![2.0, 0.0]];
let b = vec![3.0, 1.0, 4.0];
let x = solve_least_squares(&a, &b, 2).unwrap();
assert!((x[0] - 2.0).abs() < 1e-10, "expected x=2, got {}", x[0]);
assert!((x[1] - 1.0).abs() < 1e-10, "expected y=1, got {}", x[1]);
}
#[test]
fn heurisch_constant_times_var() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let var_sym = match a.node(x) {
ExprNode::Symbol(s) => *s,
_ => unreachable!(),
};
let three = a.int(3);
let result = heurisch_integrate(&mut a, three, x, var_sym);
assert!(result.is_some(), "should handle constant");
let r = result.unwrap();
let s = display(&a, r);
assert!(s.contains("3") && s.contains("x"), "expected 3*x, got {s}");
}
#[test]
fn heurisch_exp_x() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let var_sym = match a.node(x) {
ExprNode::Symbol(s) => *s,
_ => unreachable!(),
};
let exp_x = a.exp(x);
let result = heurisch_integrate(&mut a, exp_x, x, var_sym);
if let Some(r) = result {
let deriv = crate::transforms::diff::diff(&mut a, r, x);
let val_orig = eval_at_point(&a, exp_x, x, 1.0);
let val_deriv = eval_at_point(&a, deriv, x, 1.0);
assert!(
(val_orig - val_deriv).abs() < 1e-6,
"FTC failed: f(1)={val_orig}, F'(1)={val_deriv}"
);
}
}
#[test]
fn heurisch_polynomial() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let var_sym = match a.node(x) {
ExprNode::Symbol(s) => *s,
_ => unreachable!(),
};
let two = a.int(2);
let x2 = a.pow(x, two);
let result = heurisch_integrate(&mut a, x2, x, var_sym);
if let Some(r) = result {
let deriv = crate::transforms::diff::diff(&mut a, r, x);
let val_orig = eval_at_point(&a, x2, x, 2.0);
let val_deriv = eval_at_point(&a, deriv, x, 2.0);
assert!(
(val_orig - val_deriv).abs() < 1e-6,
"FTC failed: f(2)={val_orig}, F'(2)={val_deriv}"
);
}
}
#[test]
fn monomial_deriv_single_var() {
let comp_vals = vec![3.0];
let comp_deriv_vals = vec![1.0];
let mono = vec![2];
let d = eval_monomial_deriv(&comp_vals, &comp_deriv_vals, &mono);
assert!((d - 6.0).abs() < 1e-10, "expected 6, got {d}");
}
#[test]
fn monomial_deriv_two_vars() {
let comp_vals = vec![2.0, 3.0];
let comp_deriv_vals = vec![1.0, 0.5];
let mono = vec![1, 1];
let d = eval_monomial_deriv(&comp_vals, &comp_deriv_vals, &mono);
assert!((d - 4.0).abs() < 1e-10, "expected 4, got {d}");
}
}