use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Signed, Zero};
use super::dense::Poly;
use super::generic::GenPoly;
use super::sturm::SturmChain;
fn isolate_root_near(poly: &Poly, approx: f64) -> Option<(Ratio<BigInt>, Ratio<BigInt>)> {
let chain = SturmChain::new(poly);
if chain.has_no_real_roots() {
return None;
}
let cauchy = cauchy_bound(poly);
let neg_bound = -cauchy.clone();
let intervals = chain.isolate_roots_in(&neg_bound, &cauchy, 60);
if intervals.is_empty() {
return None;
}
let approx_rat = f64_to_rational_approx(approx);
let mut best = &intervals[0];
let mut best_dist = rational_dist_to_interval(&approx_rat, &best.0, &best.1);
for interval in &intervals[1..] {
let dist = rational_dist_to_interval(&approx_rat, &interval.0, &interval.1);
if dist < best_dist {
best = interval;
best_dist = dist;
}
}
Some(best.clone())
}
fn cauchy_bound(p: &Poly) -> Ratio<BigInt> {
let n = match p.degree() {
Some(d) if d >= 1 => d,
_ => return Ratio::from_integer(BigInt::from(1)),
};
let lc = p.coeff(n);
if lc.is_zero() {
return Ratio::from_integer(BigInt::from(1));
}
let mut max_ratio = Ratio::from_integer(BigInt::from(0));
for i in 0..n {
let ratio = Ratio::new(
p.coeff(i).numer().clone().abs(),
p.coeff(i).denom().clone() * lc.numer().clone().abs(),
) * Ratio::new(lc.denom().clone(), BigInt::from(1));
if ratio > max_ratio {
max_ratio = ratio;
}
}
max_ratio + Ratio::from_integer(BigInt::from(1))
}
pub fn minimal_polynomial(
arena: &mut crate::base::arena::Arena,
expr: crate::base::node::ExprId,
) -> Option<Poly> {
use crate::base::node::ExprNode;
let node = arena.node(expr).clone();
match node {
ExprNode::Num(nid) => {
let r = arena.num(nid).clone();
Some(Poly::from_coeffs(vec![-r, rat(1, 1)]))
}
ExprNode::Pi | ExprNode::E => None,
ExprNode::ImaginaryUnit => {
Some(Poly::from_coeffs(vec![rat(1, 1), rat(0, 1), rat(1, 1)]))
}
ExprNode::GoldenRatio => {
Some(Poly::from_coeffs(vec![rat(-1, 1), rat(-1, 1), rat(1, 1)]))
}
ExprNode::Neg(inner) => {
let mp = minimal_polynomial(arena, inner)?;
Some(negate_variable(&mp))
}
ExprNode::Pow(base, exp) => {
if let Some(base_r) = arena.as_num(base)
&& let Some(exp_r) = arena.as_num(exp)
&& base_r.is_positive()
&& !exp_r.is_integer()
{
let p_int = exp_r.numer().clone();
let q_int = exp_r.denom().clone();
let q_usize: usize = (&q_int).try_into().ok()?;
let n_pow_p = rational_pow(base_r, &p_int)?;
let mut coeffs = vec![Ratio::zero(); q_usize + 1];
coeffs[0] = -n_pow_p;
coeffs[q_usize] = rat(1, 1);
let mp = Poly::from_coeffs(coeffs);
return pick_irreducible_factor(&mp, base_r, exp_r);
}
if arena.as_num(base).is_none()
&& let Some(exp_r) = arena.as_num(exp).cloned()
{
return minpoly_pow(arena, base, &exp_r);
}
None
}
ExprNode::Add(ref children) if children.len() == 2 => {
let mp_a = minimal_polynomial(arena, children[0])?;
let mp_b = minimal_polynomial(arena, children[1])?;
minpoly_add(&mp_a, &mp_b, arena, children[0], children[1])
}
ExprNode::Add(ref children) if children.len() > 2 => {
let mut acc_expr = children[0];
let mut acc_mp = minimal_polynomial(arena, acc_expr)?;
for &child in &children[1..] {
let child_mp = minimal_polynomial(arena, child)?;
acc_mp = minpoly_add(&acc_mp, &child_mp, arena, acc_expr, child)?;
acc_expr = arena.add(&[acc_expr, child]);
}
Some(acc_mp)
}
ExprNode::Mul(ref children) if children.len() == 2 => {
if let Some(r) = arena.as_num(children[0]).cloned() {
let mp_b = minimal_polynomial(arena, children[1])?;
return Some(minpoly_rational_mul(&mp_b, &r));
}
if let Some(r) = arena.as_num(children[1]).cloned() {
let mp_a = minimal_polynomial(arena, children[0])?;
return Some(minpoly_rational_mul(&mp_a, &r));
}
let mp_a = minimal_polynomial(arena, children[0])?;
let mp_b = minimal_polynomial(arena, children[1])?;
minpoly_mul(&mp_a, &mp_b, arena, children[0], children[1])
}
ExprNode::Mul(ref children) if children.len() > 2 => {
let mut rational_coeff = rat(1, 1);
let mut symbolic: Vec<crate::base::node::ExprId> = Vec::new();
for &child in children.iter() {
if let Some(r) = arena.as_num(child) {
rational_coeff *= r.clone();
} else {
symbolic.push(child);
}
}
if symbolic.is_empty() {
return Some(Poly::from_coeffs(vec![-rational_coeff, rat(1, 1)]));
}
let mut acc_expr = symbolic[0];
let mut acc_mp = minimal_polynomial(arena, acc_expr)?;
for &child in &symbolic[1..] {
let child_mp = minimal_polynomial(arena, child)?;
acc_mp = minpoly_mul(&acc_mp, &child_mp, arena, acc_expr, child)?;
acc_expr = arena.mul(&[acc_expr, child]);
}
if !rational_coeff.is_one() {
acc_mp = minpoly_rational_mul(&acc_mp, &rational_coeff);
}
Some(acc_mp)
}
ExprNode::Symbol(_) => None,
_ => None, }
}
fn minpoly_add(
mp_a: &Poly,
mp_b: &Poly,
arena: &mut crate::base::arena::Arena,
expr_a: crate::base::node::ExprId,
expr_b: crate::base::node::ExprId,
) -> Option<Poly> {
let deg_a = mp_a.degree()?;
let deg_b = mp_b.degree()?;
let result_degree = deg_a * deg_b;
let mut points: Vec<(i64, Ratio<BigInt>)> = Vec::new();
for k in 0..=result_degree {
let x_val = rat(k as i64, 1);
let mb_shifted = substitute_shift(mp_b, &x_val);
let res = GenPoly::<Ratio<BigInt>>::resultant(mp_a, &mb_shifted);
points.push((k as i64, res));
}
let r_poly = super::dense::lagrange_interpolate_rational(&points);
if r_poly.is_zero() {
return None;
}
pick_factor_by_numerical_eval(
&r_poly,
arena,
expr_a,
Some(expr_b),
true, )
}
fn minpoly_mul(
mp_a: &Poly,
mp_b: &Poly,
arena: &mut crate::base::arena::Arena,
expr_a: crate::base::node::ExprId,
expr_b: crate::base::node::ExprId,
) -> Option<Poly> {
let deg_a = mp_a.degree()?;
let deg_b = mp_b.degree()?;
let result_degree = deg_a * deg_b;
let mut points: Vec<(i64, Ratio<BigInt>)> = Vec::new();
for k in 0..=result_degree {
let x_val = rat(k as i64, 1);
let mb_scaled = reciprocal_scale(mp_b, &x_val);
let res = GenPoly::<Ratio<BigInt>>::resultant(mp_a, &mb_scaled);
points.push((k as i64, res));
}
let r_poly = super::dense::lagrange_interpolate_rational(&points);
if r_poly.is_zero() {
return None;
}
pick_factor_by_numerical_eval(
&r_poly,
arena,
expr_a,
Some(expr_b),
false, )
}
fn minpoly_pow(
arena: &mut crate::base::arena::Arena,
base: crate::base::node::ExprId,
exp: &Ratio<BigInt>,
) -> Option<Poly> {
let p: i64 = exp.numer().try_into().ok()?;
let q: usize = exp.denom().try_into().ok()?;
let mp_base = minimal_polynomial(arena, base)?;
let (mut mp, mut alpha) = if q == 1 {
(mp_base, base)
} else {
if !crate::transforms::evalf::eval_const_f64(arena, base).is_some_and(|v| v > 0.0) {
return None;
}
let deg = mp_base.degree()?;
let mut coeffs = vec![Ratio::zero(); deg * q + 1];
for (i, c) in mp_base.coeffs().iter().enumerate() {
coeffs[i * q] = c.clone();
}
let one_over_q = {
let nid = arena.intern_num(Ratio::new(BigInt::one(), BigInt::from(q)));
arena.intern(crate::base::node::ExprNode::Num(nid))
};
let root = arena.pow(base, one_over_q);
let mp =
pick_factor_by_numerical_eval(&Poly::from_coeffs(coeffs), arena, root, None, false)?;
(mp, root)
};
if p == 0 {
return Some(Poly::from_coeffs(vec![rat(-1, 1), rat(1, 1)]));
}
if p < 0 {
if mp.coeff(0).is_zero() {
return None;
}
let reversed: Vec<Ratio<BigInt>> = mp.coeffs().iter().rev().cloned().collect();
mp = Poly::from_coeffs(reversed).make_monic();
let minus_one = arena.int(-1);
alpha = arena.pow(alpha, minus_one);
}
let n = p.unsigned_abs();
let mut acc = mp.clone();
let mut acc_expr = alpha;
for _ in 1..n {
acc = minpoly_mul(&acc, &mp, arena, acc_expr, alpha)?;
acc_expr = arena.mul(&[acc_expr, alpha]);
}
Some(acc)
}
fn minpoly_rational_mul(mp_alpha: &Poly, r: &Ratio<BigInt>) -> Poly {
if r.is_zero() {
return Poly::x();
}
let deg = mp_alpha.degree().unwrap_or(0);
let mut coeffs = Vec::with_capacity(deg + 1);
let mut r_power = rat(1, 1);
let r_inv = Ratio::new(r.denom().clone(), r.numer().clone());
for k in 0..=deg {
let c = mp_alpha.coeff(k);
coeffs.push(c * r_power.clone());
r_power *= r_inv.clone();
}
let result = Poly::from_coeffs(coeffs);
result.make_monic()
}
pub fn exact_is_zero(
arena: &mut crate::base::arena::Arena,
expr: crate::base::node::ExprId,
) -> Option<bool> {
if expr == arena.zero {
return Some(true);
}
if let Some(r) = arena.as_num(expr) {
return Some(r.is_zero());
}
let mp = minimal_polynomial(arena, expr)?;
let m_at_zero = mp.eval(&rat(0, 1));
if !m_at_zero.is_zero() {
tracing::trace!("exact_is_zero: m(0) ≠ 0 → definitely nonzero");
return Some(false);
}
let approx = crate::transforms::evalf::eval_const_f64(arena, expr)?;
if approx.abs() > 1e-10 {
tracing::trace!("exact_is_zero: m(0)=0 but |expr|={approx} > 1e-10 → nonzero root");
return Some(false);
}
if let Some(interval) = isolate_root_near(&mp, approx) {
if interval.0.is_positive() || interval.1.is_negative() {
tracing::trace!("exact_is_zero: isolated root interval excludes 0 → nonzero");
return Some(false);
}
let chain = SturmChain::new(&mp);
let zero_rat = rat(0, 1);
let roots_in_neg = chain.count_roots_in(&interval.0, &zero_rat);
let roots_in_pos = chain.count_roots_in(&zero_rat, &interval.1);
if roots_in_neg == 0 && roots_in_pos == 0 {
tracing::trace!("exact_is_zero: isolated interval contains only 0 → zero");
return Some(true);
}
tracing::trace!("exact_is_zero: interval has roots besides 0, relying on numerical approx");
}
tracing::trace!("exact_is_zero: m(0)=0 and |expr| < 1e-10 → zero (numerical fallback)");
Some(true)
}
pub fn exact_sign(
arena: &mut crate::base::arena::Arena,
expr: crate::base::node::ExprId,
) -> Option<i8> {
if expr == arena.zero {
return Some(0);
}
if let Some(r) = arena.as_num(expr) {
return Some(if r.is_positive() {
1
} else if r.is_negative() {
-1
} else {
0
});
}
let mp = minimal_polynomial(arena, expr)?;
let approx = crate::transforms::evalf::eval_const_f64(arena, expr)?;
let interval = isolate_root_near(&mp, approx)?;
if interval.0.is_positive() {
Some(1)
} else if interval.1.is_negative() {
Some(-1)
} else {
let m_at_zero = mp.eval(&rat(0, 1));
if m_at_zero.is_zero() {
let chain = SturmChain::new(&mp);
let zero_rat = rat(0, 1);
let roots_left = chain.count_roots_in(&interval.0, &zero_rat);
let roots_right = chain.count_roots_in(&zero_rat, &interval.1);
if roots_left == 0 && roots_right == 0 {
Some(0)
} else {
if approx > 1e-15 {
Some(1)
} else if approx < -1e-15 {
Some(-1)
} else {
Some(0)
}
}
} else {
if approx > 0.0 { Some(1) } else { Some(-1) }
}
}
}
pub fn is_zero_checked(
arena: &mut crate::base::arena::Arena,
expr: crate::base::node::ExprId,
) -> Option<bool> {
if expr == arena.zero {
return Some(true);
}
if let Some(r) = arena.as_num(expr) {
return Some(r.is_zero());
}
let f64_val = crate::transforms::evalf::eval_const_f64(arena, expr);
match f64_val {
Some(v) if v.abs() >= 1e-10 => {
Some(false)
}
Some(v) => {
let f64_says_zero = v.abs() < 1e-14;
match exact_is_zero(arena, expr) {
Some(exact_answer) => {
if exact_answer != f64_says_zero {
tracing::warn!(
f64_val = v,
exact_answer,
"is_zero_checked: eval_const_f64 and exact_is_zero DISAGREE — trusting exact"
);
}
Some(exact_answer)
}
None => {
tracing::trace!(
f64_val = v,
"is_zero_checked: exact_is_zero returned None, using f64 tolerance"
);
Some(f64_says_zero)
}
}
}
None => {
exact_is_zero(arena, expr)
}
}
}
pub fn sign_checked(
arena: &mut crate::base::arena::Arena,
expr: crate::base::node::ExprId,
) -> Option<i8> {
if expr == arena.zero {
return Some(0);
}
if let Some(r) = arena.as_num(expr) {
return Some(if r.is_positive() {
1
} else if r.is_negative() {
-1
} else {
0
});
}
let f64_val = crate::transforms::evalf::eval_const_f64(arena, expr);
match f64_val {
Some(v) if v > 1e-10 => Some(1),
Some(v) if v < -1e-10 => Some(-1),
Some(v) => {
let f64_sign: i8 = if v > 1e-14 {
1
} else if v < -1e-14 {
-1
} else {
0
};
match exact_sign(arena, expr) {
Some(exact_answer) => {
if exact_answer != f64_sign {
tracing::warn!(
f64_val = v,
exact_sign = exact_answer,
f64_sign,
"sign_checked: eval_const_f64 and exact_sign DISAGREE — trusting exact"
);
}
Some(exact_answer)
}
None => {
tracing::trace!(
f64_val = v,
"sign_checked: exact_sign returned None, using f64"
);
Some(f64_sign)
}
}
}
None => {
exact_sign(arena, expr)
}
}
}
fn negate_variable(p: &Poly) -> Poly {
let deg = p.degree().unwrap_or(0);
let mut coeffs = Vec::with_capacity(deg + 1);
for k in 0..=deg {
let c = p.coeff(k);
if k % 2 == 0 {
coeffs.push(c);
} else {
coeffs.push(-c);
}
}
Poly::from_coeffs(coeffs)
}
fn substitute_shift(p: &Poly, x_val: &Ratio<BigInt>) -> Poly {
let deg = p.degree().unwrap_or(0);
let mut result = Poly::zero();
for k in 0..=deg {
let a_k = p.coeff(k);
if a_k.is_zero() {
continue;
}
for j in 0..=k {
let binom = binomial_rational(k, j);
let sign = if j % 2 == 0 { rat(1, 1) } else { rat(-1, 1) };
let x_power = rational_pow_usize(x_val, k - j);
let coeff_j = &a_k * &binom * &sign * &x_power;
let mut result_coeffs: Vec<Ratio<BigInt>> =
(0..=std::cmp::max(result.degree().unwrap_or(0), j))
.map(|i| result.coeff(i))
.collect();
while result_coeffs.len() <= j {
result_coeffs.push(Ratio::zero());
}
result_coeffs[j] = result_coeffs[j].clone() + coeff_j;
result = Poly::from_coeffs(result_coeffs);
}
}
result
}
fn reciprocal_scale(p: &Poly, x_val: &Ratio<BigInt>) -> Poly {
let deg = p.degree().unwrap_or(0);
let mut coeffs = vec![Ratio::zero(); deg + 1];
for i in 0..=deg {
let b_i = p.coeff(i);
if b_i.is_zero() {
continue;
}
let x_pow_i = rational_pow_usize(x_val, i);
coeffs[deg - i] = &b_i * &x_pow_i;
}
Poly::from_coeffs(coeffs)
}
fn pick_factor_by_numerical_eval(
r_poly: &Poly,
arena: &mut crate::base::arena::Arena,
expr_a: crate::base::node::ExprId,
expr_b: Option<crate::base::node::ExprId>,
is_addition: bool,
) -> Option<Poly> {
let (_content, factors) = r_poly.factor_over_z();
if factors.is_empty() {
return None;
}
if factors.len() == 1 {
return Some(factors[0].0.make_monic());
}
let target_f64 = if let Some(eb) = expr_b {
let combined = if is_addition {
arena.add(&[expr_a, eb])
} else {
arena.mul(&[expr_a, eb])
};
crate::transforms::evalf::eval_const_f64(arena, combined)
} else {
crate::transforms::evalf::eval_const_f64(arena, expr_a)
};
if let Some(target) = target_f64 {
let target_rat = f64_to_rational_approx(target);
let mut best_factor = &factors[0].0;
let mut best_val = factors[0].0.eval(&target_rat).abs();
for (factor, _) in &factors[1..] {
let val = factor.eval(&target_rat).abs();
if val < best_val {
best_factor = factor;
best_val = val;
}
}
Some(best_factor.make_monic())
} else {
tracing::debug!(
"pick_factor_by_numerical_eval: eval_const_f64 failed, using degree heuristic"
);
let mut best = &factors[0];
for factor in &factors[1..] {
if factor.0.degree().unwrap_or(usize::MAX) < best.0.degree().unwrap_or(usize::MAX) {
best = factor;
}
}
Some(best.0.make_monic())
}
}
fn pick_irreducible_factor(
mp: &Poly,
base_r: &Ratio<BigInt>,
exp_r: &Ratio<BigInt>,
) -> Option<Poly> {
let (_content, factors) = mp.factor_over_z();
if factors.is_empty() {
return None;
}
if factors.len() == 1 {
return Some(factors[0].0.make_monic());
}
let base_f64: f64 = base_r.numer().to_string().parse().ok()?;
let base_f64 = base_f64 / base_r.denom().to_string().parse::<f64>().ok()?;
let exp_f64: f64 = exp_r.numer().to_string().parse().ok()?;
let exp_f64 = exp_f64 / exp_r.denom().to_string().parse::<f64>().ok()?;
let target = base_f64.powf(exp_f64);
let target_rat = f64_to_rational_approx(target);
let mut best_factor = &factors[0].0;
let mut best_val = factors[0].0.eval(&target_rat).abs();
for (factor, _) in &factors[1..] {
let val = factor.eval(&target_rat).abs();
if val < best_val {
best_factor = factor;
best_val = val;
}
}
Some(best_factor.make_monic())
}
fn rat(n: i64, d: i64) -> Ratio<BigInt> {
Ratio::new(BigInt::from(n), BigInt::from(d))
}
fn rational_pow(base: &Ratio<BigInt>, exp: &BigInt) -> Option<Ratio<BigInt>> {
let exp_i64: i64 = exp.try_into().ok()?;
if exp_i64 >= 0 {
let e = exp_i64 as u32;
let n = num_traits::Pow::pow(base.numer(), e);
let d = num_traits::Pow::pow(base.denom(), e);
Some(Ratio::new(n, d))
} else {
let e = (-exp_i64) as u32;
let n = num_traits::Pow::pow(base.denom(), e);
let d = num_traits::Pow::pow(base.numer(), e);
Some(Ratio::new(n, d))
}
}
fn rational_pow_usize(base: &Ratio<BigInt>, exp: usize) -> Ratio<BigInt> {
if exp == 0 {
return rat(1, 1);
}
let mut result = base.clone();
for _ in 1..exp {
result = &result * base;
}
result
}
fn binomial_rational(n: usize, k: usize) -> Ratio<BigInt> {
if k > n {
return Ratio::zero();
}
let k = k.min(n - k);
let mut result = rat(1, 1);
for i in 0..k {
result *= Ratio::from_integer(BigInt::from(n - i));
result /= Ratio::from_integer(BigInt::from(i + 1));
}
result
}
fn f64_to_rational_approx(x: f64) -> Ratio<BigInt> {
crate::base::numeric::f64_to_ratio_exact(x).unwrap_or_else(Ratio::zero)
}
fn rational_dist_to_interval(
point: &Ratio<BigInt>,
lo: &Ratio<BigInt>,
hi: &Ratio<BigInt>,
) -> Ratio<BigInt> {
if point < lo {
lo - point
} else if point > hi {
point - hi
} else {
Ratio::zero()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn r(n: i64, d: i64) -> Ratio<BigInt> {
Ratio::new(BigInt::from(n), BigInt::from(d))
}
#[test]
fn minpoly_rational() {
let mut arena = crate::base::arena::Arena::new();
let expr = arena.rational(3, 4);
let mp = minimal_polynomial(&mut arena, expr).unwrap();
assert_eq!(mp.degree(), Some(1));
assert!(
num_traits::Zero::is_zero(&mp.eval(&r(3, 4))),
"3/4 should be a root"
);
}
#[test]
fn minpoly_sqrt2() {
let mut arena = crate::base::arena::Arena::new();
let two = arena.int(2);
let half = arena.rational(1, 2);
let sqrt2 = arena.pow(two, half);
let mp = minimal_polynomial(&mut arena, sqrt2).unwrap();
assert_eq!(mp.degree(), Some(2));
assert_eq!(mp.coeff(0), r(-2, 1));
assert_eq!(mp.coeff(2), r(1, 1));
}
#[test]
fn minpoly_cbrt2() {
let mut arena = crate::base::arena::Arena::new();
let two = arena.int(2);
let third = arena.rational(1, 3);
let cbrt2 = arena.pow(two, third);
let mp = minimal_polynomial(&mut arena, cbrt2).unwrap();
assert_eq!(mp.degree(), Some(3));
assert_eq!(mp.coeff(0), r(-2, 1));
assert_eq!(mp.coeff(3), r(1, 1));
}
#[test]
fn minpoly_imaginary_unit() {
let mut arena = crate::base::arena::Arena::new();
let i_unit = arena.i_unit;
let mp = minimal_polynomial(&mut arena, i_unit).unwrap();
assert_eq!(mp.degree(), Some(2));
assert_eq!(mp.coeff(0), r(1, 1));
assert_eq!(mp.coeff(2), r(1, 1));
}
#[test]
fn exact_is_zero_on_zero() {
let mut arena = crate::base::arena::Arena::new();
let zero = arena.zero;
assert_eq!(exact_is_zero(&mut arena, zero), Some(true));
}
#[test]
fn exact_is_zero_on_nonzero_rational() {
let mut arena = crate::base::arena::Arena::new();
let expr = arena.rational(1, 3);
assert_eq!(exact_is_zero(&mut arena, expr), Some(false));
}
#[test]
fn exact_is_zero_sqrt5_squared_minus_5() {
let mut arena = crate::base::arena::Arena::new();
let five = arena.int(5);
let half = arena.rational(1, 2);
let sqrt5 = arena.pow(five, half);
let two = arena.int(2);
let sqrt5_sq = arena.pow(sqrt5, two); let diff = arena.sub(sqrt5_sq, five);
assert_eq!(exact_is_zero(&mut arena, diff), Some(true));
}
#[test]
fn exact_sign_positive_rational() {
let mut arena = crate::base::arena::Arena::new();
let expr = arena.rational(7, 3);
assert_eq!(exact_sign(&mut arena, expr), Some(1));
}
#[test]
fn exact_sign_negative_rational() {
let mut arena = crate::base::arena::Arena::new();
let expr = arena.rational(-2, 5);
assert_eq!(exact_sign(&mut arena, expr), Some(-1));
}
#[test]
fn exact_sign_zero() {
let mut arena = crate::base::arena::Arena::new();
let zero = arena.zero;
assert_eq!(exact_sign(&mut arena, zero), Some(0));
}
fn assert_zero_check_agrees(
arena: &mut crate::base::arena::Arena,
expr: crate::base::node::ExprId,
label: &str,
) {
let f64_val = crate::transforms::evalf::eval_const_f64(arena, expr);
let checked = is_zero_checked(arena, expr);
let f64_says_zero = f64_val.map(|v| v.abs() < 1e-14);
if let (Some(c), Some(f)) = (checked, f64_says_zero) {
assert_eq!(
c, f,
"cross-check MISMATCH for {label}: is_zero_checked={c}, f64_says_zero={f}, f64_val={f64_val:?}"
);
}
}
fn assert_sign_check_agrees(
arena: &mut crate::base::arena::Arena,
expr: crate::base::node::ExprId,
label: &str,
) {
let f64_val = crate::transforms::evalf::eval_const_f64(arena, expr);
let checked = sign_checked(arena, expr);
let f64_sign: Option<i8> = f64_val.map(|v| {
if v > 1e-14 {
1
} else if v < -1e-14 {
-1
} else {
0
}
});
if let (Some(c), Some(f)) = (checked, f64_sign) {
assert_eq!(
c, f,
"cross-check MISMATCH for {label}: sign_checked={c}, f64_sign={f}, f64_val={f64_val:?}"
);
}
}
#[test]
fn cross_check_zero_rational() {
let mut arena = crate::base::arena::Arena::new();
let zero = arena.zero;
let one_third = arena.rational(1, 3);
let neg_seven = arena.int(-7);
assert_zero_check_agrees(&mut arena, zero, "0");
assert_zero_check_agrees(&mut arena, one_third, "1/3");
assert_zero_check_agrees(&mut arena, neg_seven, "-7");
}
#[test]
fn cross_check_sign_rational() {
let mut arena = crate::base::arena::Arena::new();
let zero = arena.zero;
let pos = arena.rational(7, 3);
let neg = arena.rational(-2, 5);
assert_sign_check_agrees(&mut arena, zero, "0");
assert_sign_check_agrees(&mut arena, pos, "7/3");
assert_sign_check_agrees(&mut arena, neg, "-2/5");
}
#[test]
fn cross_check_sqrt2() {
let mut arena = crate::base::arena::Arena::new();
let two = arena.int(2);
let half = arena.rational(1, 2);
let sqrt2 = arena.pow(two, half);
assert_zero_check_agrees(&mut arena, sqrt2, "√2");
assert_sign_check_agrees(&mut arena, sqrt2, "√2");
}
#[test]
fn cross_check_sqrt3() {
let mut arena = crate::base::arena::Arena::new();
let three = arena.int(3);
let half = arena.rational(1, 2);
let sqrt3 = arena.pow(three, half);
assert_zero_check_agrees(&mut arena, sqrt3, "√3");
assert_sign_check_agrees(&mut arena, sqrt3, "√3");
}
#[test]
fn cross_check_cbrt2() {
let mut arena = crate::base::arena::Arena::new();
let two = arena.int(2);
let third = arena.rational(1, 3);
let cbrt2 = arena.pow(two, third);
assert_zero_check_agrees(&mut arena, cbrt2, "∛2");
assert_sign_check_agrees(&mut arena, cbrt2, "∛2");
}
#[test]
fn cross_check_sqrt5_sq_minus_5() {
let mut arena = crate::base::arena::Arena::new();
let five = arena.int(5);
let half = arena.rational(1, 2);
let sqrt5 = arena.pow(five, half);
let two = arena.int(2);
let sqrt5_sq = arena.pow(sqrt5, two);
let diff = arena.sub(sqrt5_sq, five);
assert_zero_check_agrees(&mut arena, diff, "(√5)²-5");
assert_sign_check_agrees(&mut arena, diff, "(√5)²-5");
}
#[test]
fn cross_check_negative_sqrt() {
let mut arena = crate::base::arena::Arena::new();
let two = arena.int(2);
let half = arena.rational(1, 2);
let sqrt2 = arena.pow(two, half);
let neg_sqrt2 = arena.neg(sqrt2);
assert_zero_check_agrees(&mut arena, neg_sqrt2, "-√2");
assert_sign_check_agrees(&mut arena, neg_sqrt2, "-√2");
}
#[test]
fn cross_check_rational_times_sqrt() {
let mut arena = crate::base::arena::Arena::new();
let two = arena.int(2);
let half = arena.rational(1, 2);
let sqrt2 = arena.pow(two, half);
let three_fourths = arena.rational(3, 4);
let expr = arena.mul(&[three_fourths, sqrt2]);
assert_zero_check_agrees(&mut arena, expr, "(3/4)·√2");
assert_sign_check_agrees(&mut arena, expr, "(3/4)·√2");
}
#[test]
fn cross_check_one_over_sqrt2() {
let mut arena = crate::base::arena::Arena::new();
let one = arena.int(1);
let two = arena.int(2);
let half = arena.rational(1, 2);
let sqrt2 = arena.pow(two, half);
let expr = arena.div(one, sqrt2);
assert_zero_check_agrees(&mut arena, expr, "1/√2");
assert_sign_check_agrees(&mut arena, expr, "1/√2");
}
#[test]
fn cross_check_small_positive_rational() {
let mut arena = crate::base::arena::Arena::new();
let expr = arena.rational(1, 1000000);
assert_zero_check_agrees(&mut arena, expr, "1/1000000");
assert_sign_check_agrees(&mut arena, expr, "1/1000000");
assert_eq!(is_zero_checked(&mut arena, expr), Some(false));
assert_eq!(sign_checked(&mut arena, expr), Some(1));
}
#[test]
fn probe_arena_add_arity() {
use crate::base::node::ExprNode;
let mut arena = crate::base::arena::Arena::new();
let n2 = arena.int(2);
let n3 = arena.int(3);
let n5 = arena.int(5);
let half = arena.rational(1, 2);
let sqrt2 = arena.pow(n2, half);
let sqrt3 = arena.pow(n3, half);
let sqrt5 = arena.pow(n5, half);
let sum = arena.add(&[sqrt2, sqrt3, sqrt5]);
let arity = match arena.node(sum) {
ExprNode::Add(children) => children.len(),
_ => 0,
};
eprintln!("probe_arena_add_arity: Add node has {arity} children");
assert!(arity >= 2, "Add node should have at least 2 children");
}
#[test]
fn probe_arena_mul_arity() {
use crate::base::node::ExprNode;
let mut arena = crate::base::arena::Arena::new();
let n2 = arena.int(2);
let n3 = arena.int(3);
let n5 = arena.int(5);
let half = arena.rational(1, 2);
let sqrt2 = arena.pow(n2, half);
let sqrt3 = arena.pow(n3, half);
let sqrt5 = arena.pow(n5, half);
let prod = arena.mul(&[sqrt2, sqrt3, sqrt5]);
let node = arena.node(prod).clone();
eprintln!("probe_arena_mul_arity: node = {node:?}");
match &node {
ExprNode::Mul(children) => {
eprintln!(" Mul with {} children", children.len());
}
ExprNode::Pow(_, _) => {
eprintln!(" Simplified to a Pow (likely √30)");
}
ExprNode::Num(_) => {
eprintln!(" Simplified to a number");
}
_ => {
eprintln!(" Other node type");
}
}
}
#[test]
fn probe_sqrt2_times_sqrt3_simplification() {
let mut arena = crate::base::arena::Arena::new();
let n2 = arena.int(2);
let n3 = arena.int(3);
let n6 = arena.int(6);
let half = arena.rational(1, 2);
let sqrt2 = arena.pow(n2, half);
let sqrt3 = arena.pow(n3, half);
let sqrt6 = arena.pow(n6, half);
let prod = arena.mul(&[sqrt2, sqrt3]);
let structurally_same = prod == sqrt6;
eprintln!("probe: √2·√3 == √6 structurally? {structurally_same}");
eprintln!("probe: √2·√3 node = {:?}", arena.node(prod));
eprintln!("probe: √6 node = {:?}", arena.node(sqrt6));
let diff = arena.sub(prod, sqrt6);
let diff_is_zero = diff == arena.zero;
eprintln!("probe: √2·√3 - √6 == 0 structurally? {diff_is_zero}");
}
#[test]
fn probe_what_expressions_reach_exact_is_zero() {
let mut arena = crate::base::arena::Arena::new();
let r = arena.rational(1, 6);
assert_eq!(is_zero_checked(&mut arena, r), Some(false));
let n3 = arena.int(3);
let half = arena.rational(1, 2);
let sqrt3 = arena.pow(n3, half);
assert_eq!(is_zero_checked(&mut arena, sqrt3), Some(false));
let sixth = arena.rational(1, 6);
let scaled = arena.mul(&[sixth, sqrt3]);
assert_eq!(is_zero_checked(&mut arena, scaled), Some(false));
let neg_sqrt3 = arena.neg(sqrt3);
assert_eq!(is_zero_checked(&mut arena, neg_sqrt3), Some(false));
assert_eq!(sign_checked(&mut arena, neg_sqrt3), Some(-1));
}
#[test]
fn minpoly_sqrt2_plus_sqrt3_exact_coefficients() {
let mut arena = crate::base::arena::Arena::new();
let n2 = arena.int(2);
let n3 = arena.int(3);
let half = arena.rational(1, 2);
let sqrt2 = arena.pow(n2, half);
let sqrt3 = arena.pow(n3, half);
let sum = arena.add(&[sqrt2, sqrt3]);
let mp =
minimal_polynomial(&mut arena, sum).expect("minimal_polynomial must succeed for √2+√3");
assert_eq!(mp.degree(), Some(4), "degree must be exactly 4");
assert_eq!(mp.coeff(0), r(1, 1), "constant term must be 1");
assert_eq!(mp.coeff(1), r(0, 1), "t¹ coefficient must be 0");
assert_eq!(mp.coeff(2), r(-10, 1), "t² coefficient must be -10");
assert_eq!(mp.coeff(3), r(0, 1), "t³ coefficient must be 0");
assert_eq!(mp.coeff(4), r(1, 1), "t⁴ coefficient must be 1");
}
#[test]
fn minpoly_sqrt2_times_sqrt3_exact() {
let mut arena = crate::base::arena::Arena::new();
let n2 = arena.int(2);
let n3 = arena.int(3);
let half = arena.rational(1, 2);
let sqrt2 = arena.pow(n2, half);
let sqrt3 = arena.pow(n3, half);
let prod = arena.mul(&[sqrt2, sqrt3]);
let mp = minimal_polynomial(&mut arena, prod)
.expect("minimal_polynomial must succeed for √2·√3");
assert_eq!(mp.degree(), Some(2), "degree must be 2");
assert_eq!(mp.coeff(0), r(-6, 1), "constant term must be -6");
assert_eq!(mp.coeff(2), r(1, 1), "leading coefficient must be 1");
let val = crate::transforms::evalf::eval_const_f64(&mut arena, prod)
.expect("must evaluate numerically");
assert!((val - 6.0_f64.sqrt()).abs() < 1e-10, "√2·√3 must equal √6");
}
fn eval_poly_at_f64(p: &Poly, x: f64) -> f64 {
let x_rat = f64_to_rational_approx(x);
let result = p.eval(&x_rat);
let n: f64 = result.numer().to_string().parse().unwrap_or(f64::NAN);
let d: f64 = result.denom().to_string().parse().unwrap_or(1.0);
n / d
}
#[test]
fn minpoly_nary_add_must_vanish_at_value() {
let mut arena = crate::base::arena::Arena::new();
let half = arena.rational(1, 2);
let n2 = arena.int(2);
let n3 = arena.int(3);
let n5 = arena.int(5);
let sqrt2 = arena.pow(n2, half);
let sqrt3 = arena.pow(n3, half);
let sqrt5 = arena.pow(n5, half);
let sum = arena.add(&[sqrt2, sqrt3, sqrt5]);
let val = crate::transforms::evalf::eval_const_f64(&mut arena, sum)
.expect("√2+√3+√5 must evaluate");
assert!(
(val - 5.382).abs() < 0.01,
"sanity: √2+√3+√5 ≈ 5.382, got {val}"
);
let mp = minimal_polynomial(&mut arena, sum);
if let Some(ref mp) = mp {
let residual = eval_poly_at_f64(mp, val);
assert!(
residual.abs() < 1.0,
"WRONG MINIMAL POLYNOMIAL: p(√2+√3+√5) = {residual} (should be ≈ 0), \
degree={:?}, poly coeffs: {:?}",
mp.degree(),
(0..=mp.degree().unwrap_or(0))
.map(|i| mp.coeff(i).to_string())
.collect::<Vec<_>>()
);
}
}
#[test]
fn minpoly_nary_mul_must_vanish_at_value() {
let mut arena = crate::base::arena::Arena::new();
let half = arena.rational(1, 2);
let n2 = arena.int(2);
let n3 = arena.int(3);
let n5 = arena.int(5);
let sqrt2 = arena.pow(n2, half);
let sqrt3 = arena.pow(n3, half);
let sqrt5 = arena.pow(n5, half);
let prod = arena.mul(&[sqrt2, sqrt3, sqrt5]);
let val = crate::transforms::evalf::eval_const_f64(&mut arena, prod)
.expect("√2·√3·√5 must evaluate");
assert!(
(val - 30.0_f64.sqrt()).abs() < 1e-10,
"sanity: √2·√3·√5 = √30"
);
let mp = minimal_polynomial(&mut arena, prod);
if let Some(ref mp) = mp {
let residual = eval_poly_at_f64(mp, val);
assert!(
residual.abs() < 1.0,
"WRONG MINIMAL POLYNOMIAL: p(√30) = {residual}, degree={:?}",
mp.degree()
);
}
}
#[test]
fn zero_detection_structural_subtraction() {
let mut arena = crate::base::arena::Arena::new();
let half = arena.rational(1, 2);
let n2 = arena.int(2);
let n3 = arena.int(3);
let sqrt2 = arena.pow(n2, half);
let sqrt3 = arena.pow(n3, half);
let sum = arena.add(&[sqrt2, sqrt3]);
let diff = arena.sub(sum, sum);
assert_eq!(
is_zero_checked(&mut arena, diff),
Some(true),
"(√2+√3)-(√2+√3) must be zero"
);
}
#[test]
fn zero_detection_sqrt_product_identity() {
let mut arena = crate::base::arena::Arena::new();
let half = arena.rational(1, 2);
let n2 = arena.int(2);
let n3 = arena.int(3);
let n6 = arena.int(6);
let sqrt2 = arena.pow(n2, half);
let sqrt3 = arena.pow(n3, half);
let sqrt6 = arena.pow(n6, half);
let prod = arena.mul(&[sqrt2, sqrt3]);
let diff = arena.sub(prod, sqrt6);
let result = is_zero_checked(&mut arena, diff);
assert_eq!(
result,
Some(true),
"√2·√3 - √6 is exactly zero — is_zero_checked must detect it. \
If Some(false), that's BAD MATH."
);
}
#[test]
fn zero_detection_sqrt5_squared() {
let mut arena = crate::base::arena::Arena::new();
let n5 = arena.int(5);
let half = arena.rational(1, 2);
let two = arena.int(2);
let sqrt5 = arena.pow(n5, half);
let sq = arena.pow(sqrt5, two);
let diff = arena.sub(sq, n5);
assert_eq!(
is_zero_checked(&mut arena, diff),
Some(true),
"(√5)² - 5 must be detected as zero"
);
}
#[test]
fn nonzero_radical_sum_clearly_positive() {
let mut arena = crate::base::arena::Arena::new();
let half = arena.rational(1, 2);
let n2 = arena.int(2);
let n3 = arena.int(3);
let n5 = arena.int(5);
let sqrt2 = arena.pow(n2, half);
let sqrt3 = arena.pow(n3, half);
let sqrt5 = arena.pow(n5, half);
let neg_sqrt5 = arena.neg(sqrt5);
let expr = arena.add(&[sqrt2, sqrt3, neg_sqrt5]);
let val =
crate::transforms::evalf::eval_const_f64(&mut arena, expr).expect("must evaluate");
assert!(val > 0.5, "√2+√3-√5 ≈ 0.728, got {val}");
assert_eq!(
is_zero_checked(&mut arena, expr),
Some(false),
"is_zero_checked must say √2+√3-√5 is nonzero"
);
assert_eq!(
sign_checked(&mut arena, expr),
Some(1),
"sign_checked must say √2+√3-√5 is positive"
);
}
#[test]
fn nonzero_sqrt2_minus_sqrt3() {
let mut arena = crate::base::arena::Arena::new();
let half = arena.rational(1, 2);
let n2 = arena.int(2);
let n3 = arena.int(3);
let sqrt2 = arena.pow(n2, half);
let sqrt3 = arena.pow(n3, half);
let diff = arena.sub(sqrt2, sqrt3);
assert_eq!(
is_zero_checked(&mut arena, diff),
Some(false),
"√2-√3 is not zero"
);
assert_eq!(
sign_checked(&mut arena, diff),
Some(-1),
"√2-√3 is negative"
);
}
#[test]
fn nonzero_tiny_rational_in_ambiguous_zone() {
let mut arena = crate::base::arena::Arena::new();
let expr = arena.rational(1, 1_000_000_000_000_000); assert_eq!(
is_zero_checked(&mut arena, expr),
Some(false),
"1e-15 is tiny but nonzero — MUST NOT be called zero"
);
assert_eq!(sign_checked(&mut arena, expr), Some(1));
}
#[test]
fn nonzero_tiny_negative_rational_in_ambiguous_zone() {
let mut arena = crate::base::arena::Arena::new();
let expr = arena.rational(-1, 1_000_000_000_000_000);
assert_eq!(
is_zero_checked(&mut arena, expr),
Some(false),
"-1e-15 is tiny but nonzero"
);
assert_eq!(sign_checked(&mut arena, expr), Some(-1));
}
#[test]
fn nonzero_very_small_rational_1e_20() {
let mut arena = crate::base::arena::Arena::new();
let a = arena.rational(1, 10_000_000_000); let b = arena.rational(1, 10_000_000_000);
let expr = arena.mul(&[a, b]);
let expr = crate::transforms::eval::eval(&mut arena, expr);
let result = is_zero_checked(&mut arena, expr);
assert!(
result != Some(true),
"1e-20 is nonzero — is_zero_checked MUST NOT say it's zero. Got {result:?}"
);
}
#[test]
fn nested_radical_no_crash_no_wrong_answer() {
let mut arena = crate::base::arena::Arena::new();
let half = arena.rational(1, 2);
let n3 = arena.int(3);
let n2 = arena.int(2);
let sqrt3 = arena.pow(n3, half);
let inner = arena.add(&[n2, sqrt3]);
let outer = arena.pow(inner, half);
let result = is_zero_checked(&mut arena, outer);
assert!(
result != Some(true),
"√(2+√3) ≈ 1.93 — must NOT be called zero! Got {result:?}"
);
let sign = sign_checked(&mut arena, outer);
assert!(
sign == Some(1) || sign.is_none(),
"√(2+√3) is positive — sign must be 1 or unknown, not {sign:?}"
);
}
#[test]
fn transcendental_pi_no_crash() {
let mut arena = crate::base::arena::Arena::new();
let pi = arena.pi;
let result = is_zero_checked(&mut arena, pi);
assert!(result != Some(true), "π is not zero!");
}
#[test]
fn free_symbol_no_crash() {
let mut arena = crate::base::arena::Arena::new();
let x = arena.symbol("x");
let result = is_zero_checked(&mut arena, x);
assert!(
result.is_none(),
"Free symbol x: is_zero_checked must return None, got {result:?}"
);
}
#[test]
fn nary_fold_bug_direct_detection() {
use crate::base::node::ExprNode;
let mut arena = crate::base::arena::Arena::new();
let half = arena.rational(1, 2);
let n2 = arena.int(2);
let n3 = arena.int(3);
let n5 = arena.int(5);
let sqrt2 = arena.pow(n2, half);
let sqrt3 = arena.pow(n3, half);
let sqrt5 = arena.pow(n5, half);
let sum3 = arena.add(&[sqrt2, sqrt3, sqrt5]);
let is_nary = matches!(arena.node(sum3), ExprNode::Add(c) if c.len() > 2);
if is_nary {
let mp = minimal_polynomial(&mut arena, sum3);
let val =
crate::transforms::evalf::eval_const_f64(&mut arena, sum3).expect("must evaluate");
if let Some(ref mp) = mp {
let residual = eval_poly_at_f64(mp, val);
assert!(
residual.abs() < 1.0,
"N-ARY FOLD BUG TRIGGERED: minimal polynomial does NOT vanish \
at √2+√3+√5 = {val}. Residual = {residual}, degree = {:?}. \
The factor selection likely picked the wrong factor due to the \
acc_expr = child bug in the N-ary Add fold.",
mp.degree()
);
assert_eq!(
is_zero_checked(&mut arena, sum3),
Some(false),
"√2+√3+√5 is nonzero"
);
}
} else {
assert_eq!(
is_zero_checked(&mut arena, sum3),
Some(false),
"√2+√3+√5 is nonzero (binary path)"
);
}
}
#[test]
fn nary_fold_bug_zero_expression() {
let mut arena = crate::base::arena::Arena::new();
let half = arena.rational(1, 2);
let n2 = arena.int(2);
let n3 = arena.int(3);
let sqrt2 = arena.pow(n2, half);
let sqrt3 = arena.pow(n3, half);
let neg_sqrt2 = arena.neg(sqrt2);
let neg_sqrt3 = arena.neg(sqrt3);
let sum = arena.add(&[sqrt2, sqrt3, neg_sqrt2, neg_sqrt3]);
let result = is_zero_checked(&mut arena, sum);
assert_eq!(result, Some(true), "√2+√3-√2-√3 must be zero");
}
#[test]
fn binary_add_sqrt2_plus_sqrt3_exact_minpoly() {
let mut arena = crate::base::arena::Arena::new();
let half = arena.rational(1, 2);
let n2 = arena.int(2);
let n3 = arena.int(3);
let sqrt2 = arena.pow(n2, half);
let sqrt3 = arena.pow(n3, half);
let sum = arena.add(&[sqrt2, sqrt3]);
let mp = minimal_polynomial(&mut arena, sum).expect("must succeed for √2+√3");
assert_eq!(mp.degree(), Some(4));
assert_eq!(mp.coeff(0), r(1, 1));
assert_eq!(mp.coeff(1), r(0, 1));
assert_eq!(mp.coeff(2), r(-10, 1));
assert_eq!(mp.coeff(3), r(0, 1));
assert_eq!(mp.coeff(4), r(1, 1));
let val = crate::transforms::evalf::eval_const_f64(&mut arena, sum).unwrap();
let residual = eval_poly_at_f64(&mp, val);
assert!(residual.abs() < 1e-6, "p(√2+√3) = {residual}, expected ≈ 0");
let other_roots = [
2.0_f64.sqrt() - 3.0_f64.sqrt(), -2.0_f64.sqrt() + 3.0_f64.sqrt(), -2.0_f64.sqrt() - 3.0_f64.sqrt(), ];
for root in &other_roots {
let res = eval_poly_at_f64(&mp, *root);
assert!(
res.abs() < 1e-6,
"p({root}) = {res}, expected ≈ 0 (it's a root too)"
);
}
assert_eq!(is_zero_checked(&mut arena, sum), Some(false));
assert_eq!(sign_checked(&mut arena, sum), Some(1));
}
#[test]
fn binary_add_sqrt2_minus_1_exact_minpoly() {
let mut arena = crate::base::arena::Arena::new();
let half = arena.rational(1, 2);
let n2 = arena.int(2);
let one = arena.int(1);
let sqrt2 = arena.pow(n2, half);
let expr = arena.sub(sqrt2, one);
let mp = minimal_polynomial(&mut arena, expr).expect("must succeed for √2-1");
assert_eq!(mp.degree(), Some(2));
assert_eq!(mp.coeff(0), r(-1, 1));
assert_eq!(mp.coeff(1), r(2, 1));
assert_eq!(mp.coeff(2), r(1, 1));
assert_eq!(is_zero_checked(&mut arena, expr), Some(false));
assert_eq!(sign_checked(&mut arena, expr), Some(1));
}
#[test]
fn sign_checked_negative_radical_difference() {
let mut arena = crate::base::arena::Arena::new();
let half = arena.rational(1, 2);
let n2 = arena.int(2);
let n3 = arena.int(3);
let sqrt2 = arena.pow(n2, half);
let sqrt3 = arena.pow(n3, half);
let diff = arena.sub(sqrt2, sqrt3);
assert_eq!(sign_checked(&mut arena, diff), Some(-1));
}
#[test]
fn sign_checked_zero_expression() {
let mut arena = crate::base::arena::Arena::new();
let zero = arena.zero;
assert_eq!(sign_checked(&mut arena, zero), Some(0));
}
#[test]
fn sign_checked_positive_cbrt() {
let mut arena = crate::base::arena::Arena::new();
let n2 = arena.int(2);
let third = arena.rational(1, 3);
let cbrt2 = arena.pow(n2, third);
assert_eq!(sign_checked(&mut arena, cbrt2), Some(1));
}
#[test]
fn sign_checked_negative_cbrt() {
let mut arena = crate::base::arena::Arena::new();
let n2 = arena.int(2);
let third = arena.rational(1, 3);
let cbrt2 = arena.pow(n2, third);
let neg = arena.neg(cbrt2);
assert_eq!(sign_checked(&mut arena, neg), Some(-1));
}
#[test]
fn stress_cross_check_many_expressions() {
let mut arena = crate::base::arena::Arena::new();
let half = arena.rational(1, 2);
let bases: Vec<i64> = vec![2, 3, 5, 7, 11];
for &b in &bases {
let base = arena.int(b);
let sqrt_b = arena.pow(base, half);
assert_zero_check_agrees(&mut arena, sqrt_b, &format!("√{b}"));
assert_sign_check_agrees(&mut arena, sqrt_b, &format!("√{b}"));
let neg = arena.neg(sqrt_b);
assert_zero_check_agrees(&mut arena, neg, &format!("-√{b}"));
assert_sign_check_agrees(&mut arena, neg, &format!("-√{b}"));
let one = arena.int(1);
let inv = arena.div(one, sqrt_b);
assert_zero_check_agrees(&mut arena, inv, &format!("1/√{b}"));
assert_sign_check_agrees(&mut arena, inv, &format!("1/√{b}"));
}
for i in 0..bases.len() {
for j in (i + 1)..bases.len() {
let bi = arena.int(bases[i]);
let bj = arena.int(bases[j]);
let si = arena.pow(bi, half);
let sj = arena.pow(bj, half);
let sum = arena.add(&[si, sj]);
let diff = arena.sub(si, sj);
let sum_label = format!("√{}+√{}", bases[i], bases[j]);
let diff_label = format!("√{}-√{}", bases[i], bases[j]);
assert_zero_check_agrees(&mut arena, sum, &sum_label);
assert_sign_check_agrees(&mut arena, sum, &sum_label);
assert_zero_check_agrees(&mut arena, diff, &diff_label);
assert_sign_check_agrees(&mut arena, diff, &diff_label);
assert_eq!(
is_zero_checked(&mut arena, sum),
Some(false),
"{sum_label} must be nonzero"
);
assert_eq!(
sign_checked(&mut arena, sum),
Some(1),
"{sum_label} must be positive"
);
let expected_sign: i8 = if bases[i] < bases[j] { -1 } else { 1 };
assert_eq!(
is_zero_checked(&mut arena, diff),
Some(false),
"{diff_label} must be nonzero"
);
let s = sign_checked(&mut arena, diff);
assert!(
s == Some(expected_sign) || s.is_none(),
"{diff_label}: expected sign {expected_sign}, got {s:?}"
);
}
}
}
#[test]
fn katya_sqrt2_plus_sqrt3_squared_minus_5_minus_2sqrt6() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n2 = a.int(2);
let n3 = a.int(3);
let n5 = a.int(5);
let n6 = a.int(6);
let sqrt2 = a.pow(n2, half);
let sqrt3 = a.pow(n3, half);
let sqrt6 = a.pow(n6, half);
let sum = a.add(&[sqrt2, sqrt3]);
let two = a.int(2);
let sq = a.pow(sum, two);
let sq = crate::transforms::eval::eval(&mut a, sq);
let two_sqrt6 = a.mul(&[two, sqrt6]);
let rhs = a.add(&[n5, two_sqrt6]);
let rhs = crate::transforms::eval::eval(&mut a, rhs);
let diff = a.sub(sq, rhs);
let diff = crate::transforms::eval::eval(&mut a, diff);
let result = is_zero_checked(&mut a, diff);
assert_eq!(
result,
Some(true),
"BUG: (√2+√3)²-5-2√6 should be zero. Got {result:?}, node={:?}",
a.node(diff)
);
}
#[test]
fn katya_golden_ratio_phi_sq_minus_phi_minus_1() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n5 = a.int(5);
let n1 = a.int(1);
let n2 = a.int(2);
let sqrt5 = a.pow(n5, half);
let num = a.add(&[n1, sqrt5]);
let phi = a.div(num, n2);
let phi_sq = a.pow(phi, n2);
let phi_sq = crate::transforms::eval::eval(&mut a, phi_sq);
let phi_sq_minus_phi = a.sub(phi_sq, phi);
let diff = a.sub(phi_sq_minus_phi, n1);
let diff = crate::transforms::eval::eval(&mut a, diff);
let result = is_zero_checked(&mut a, diff);
assert_eq!(
result,
Some(true),
"BUG: φ²-φ-1 should be zero. Got {result:?}"
);
}
#[test]
fn katya_2sqrt2_minus_sqrt8() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n2 = a.int(2);
let n8 = a.int(8);
let sqrt2 = a.pow(n2, half);
let sqrt8 = a.pow(n8, half);
let two_sqrt2 = a.mul(&[n2, sqrt2]);
let diff = a.sub(two_sqrt2, sqrt8);
let diff = crate::transforms::eval::eval(&mut a, diff);
let result = is_zero_checked(&mut a, diff);
assert_eq!(
result,
Some(true),
"BUG: 2√2 - √8 = 0. Got {result:?}, node={:?}",
a.node(diff)
);
}
#[test]
fn katya_sqrt50_minus_5sqrt2() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n2 = a.int(2);
let n5 = a.int(5);
let n50 = a.int(50);
let sqrt2 = a.pow(n2, half);
let sqrt50 = a.pow(n50, half);
let five_sqrt2 = a.mul(&[n5, sqrt2]);
let diff = a.sub(sqrt50, five_sqrt2);
let diff = crate::transforms::eval::eval(&mut a, diff);
let result = is_zero_checked(&mut a, diff);
assert_eq!(result, Some(true), "BUG: √50 - 5√2 = 0. Got {result:?}");
}
#[test]
fn katya_cbrt4_minus_cbrt2_squared() {
let mut a = crate::base::arena::Arena::new();
let n2 = a.int(2);
let n4 = a.int(4);
let third = a.rational(1, 3);
let cbrt4 = a.pow(n4, third);
let cbrt2 = a.pow(n2, third);
let cbrt2_sq = a.pow(cbrt2, n2);
let cbrt2_sq = crate::transforms::eval::eval(&mut a, cbrt2_sq);
let diff = a.sub(cbrt4, cbrt2_sq);
let diff = crate::transforms::eval::eval(&mut a, diff);
let result = is_zero_checked(&mut a, diff);
assert_eq!(result, Some(true), "BUG: ∛4 - (∛2)² = 0. Got {result:?}");
}
#[test]
fn katya_sqrt2_times_sqrt2_minus_2() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n2 = a.int(2);
let sqrt2 = a.pow(n2, half);
let prod = a.mul(&[sqrt2, sqrt2]);
let prod = crate::transforms::eval::eval(&mut a, prod);
let diff = a.sub(prod, n2);
let diff = crate::transforms::eval::eval(&mut a, diff);
let result = is_zero_checked(&mut a, diff);
assert_eq!(result, Some(true), "BUG: √2·√2-2 = 0. Got {result:?}");
}
#[test]
fn katya_sqrt2_plus_sqrt3_times_sqrt2_minus_sqrt3_plus_1() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n1 = a.int(1);
let n2 = a.int(2);
let n3 = a.int(3);
let sqrt2 = a.pow(n2, half);
let sqrt3 = a.pow(n3, half);
let sum = a.add(&[sqrt2, sqrt3]);
let diff = a.sub(sqrt2, sqrt3);
let prod = a.mul(&[sum, diff]);
let prod = crate::transforms::eval::eval(&mut a, prod);
let expr = a.add(&[prod, n1]);
let expr = crate::transforms::eval::eval(&mut a, expr);
let result = is_zero_checked(&mut a, expr);
assert_eq!(
result,
Some(true),
"BUG: (√2+√3)(√2-√3)+1 = 0. Got {result:?}"
);
}
#[test]
fn katya_1_over_sqrt2_minus_sqrt2_over_2() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n1 = a.int(1);
let n2 = a.int(2);
let sqrt2 = a.pow(n2, half);
let inv_sqrt2 = a.div(n1, sqrt2);
let sqrt2_over_2 = a.div(sqrt2, n2);
let diff = a.sub(inv_sqrt2, sqrt2_over_2);
let diff = crate::transforms::eval::eval(&mut a, diff);
let result = is_zero_checked(&mut a, diff);
assert_eq!(
result,
Some(true),
"BUG: 1/√2 - √2/2 = 0. Got {result:?}, node={:?}",
a.node(diff)
);
}
#[test]
fn katya_rationalize_1_over_sqrt2_plus_1() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n1 = a.int(1);
let n2 = a.int(2);
let sqrt2 = a.pow(n2, half);
let sqrt2_plus_1 = a.add(&[sqrt2, n1]);
let lhs = a.div(n1, sqrt2_plus_1);
let rhs = a.sub(sqrt2, n1);
let diff = a.sub(lhs, rhs);
let diff = crate::transforms::eval::eval(&mut a, diff);
let result = is_zero_checked(&mut a, diff);
assert_eq!(
result,
Some(true),
"BUG: 1/(√2+1)-(√2-1) = 0. Got {result:?}, node={:?}",
a.node(diff)
);
}
#[test]
fn tomas_integrate_1_over_x2_plus_1() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let one = a.int(1);
let two = a.int(2);
let x2 = a.pow(x, two);
let denom = a.add(&[x2, one]);
let integrand = a.div(one, denom);
let result = a.integrate_expr(integrand, x);
let result = crate::transforms::eval::eval(&mut a, result);
let at_1 = crate::transforms::subs::subs(&mut a, result, x, one);
let at_1 = crate::transforms::eval::eval(&mut a, at_1);
let val = crate::transforms::evalf::eval_const_f64(&mut a, at_1);
assert!(val.is_some(), "BUG: ∫1/(x²+1) must evaluate at x=1");
let v = val.unwrap();
assert!(
(v - std::f64::consts::FRAC_PI_4).abs() < 1e-10,
"BUG: ∫1/(x²+1) at x=1 should be π/4 ≈ 0.7854, got {v}"
);
}
#[test]
fn tomas_integrate_1_over_x2_plus_2x_plus_2() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let one = a.int(1);
let two = a.int(2);
let x2 = a.pow(x, two);
let two_x = a.mul(&[two, x]);
let denom = a.add(&[x2, two_x, two]);
let integrand = a.div(one, denom);
let result = a.integrate_expr(integrand, x);
let result = crate::transforms::eval::eval(&mut a, result);
let zero = a.zero;
let at_0 = crate::transforms::subs::subs(&mut a, result, x, zero);
let at_0 = crate::transforms::eval::eval(&mut a, at_0);
let val = crate::transforms::evalf::eval_const_f64(&mut a, at_0);
assert!(val.is_some(), "BUG: ∫1/(x²+2x+2) must evaluate at x=0");
let v = val.unwrap();
assert!(
(v - std::f64::consts::FRAC_PI_4).abs() < 1e-10,
"BUG: ∫1/(x²+2x+2) at x=0 should be π/4, got {v}"
);
}
#[test]
fn tomas_integrate_2x_over_x2_plus_1() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let one = a.int(1);
let two = a.int(2);
let x2 = a.pow(x, two);
let denom = a.add(&[x2, one]);
let numer = a.mul(&[two, x]);
let integrand = a.div(numer, denom);
let result = a.integrate_expr(integrand, x);
let result = crate::transforms::eval::eval(&mut a, result);
let at_1 = crate::transforms::subs::subs(&mut a, result, x, one);
let at_1 = crate::transforms::eval::eval(&mut a, at_1);
let val = crate::transforms::evalf::eval_const_f64(&mut a, at_1);
assert!(val.is_some(), "BUG: ∫2x/(x²+1) must evaluate at x=1");
let v = val.unwrap();
assert!(
(v - 2.0_f64.ln()).abs() < 1e-10,
"BUG: ∫2x/(x²+1) at x=1 should be ln(2) ≈ 0.6931, got {v}"
);
}
#[test]
fn tomas_simplify_sin_sq_plus_cos_sq() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let two = a.int(2);
let sinx = a.sin(x);
let cosx = a.cos(x);
let sin2 = a.pow(sinx, two);
let cos2 = a.pow(cosx, two);
let sum = a.add(&[sin2, cos2]);
let simplified = a.trigsimp_expr(sum);
let simplified = crate::transforms::eval::eval(&mut a, simplified);
assert_eq!(
simplified, a.one,
"BUG: sin²(x)+cos²(x) should simplify to 1"
);
}
#[test]
fn tomas_derivative_of_integral_is_identity() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let two = a.int(2);
let x2 = a.pow(x, two);
let integral = a.integrate_expr(x2, x);
let integral = crate::transforms::eval::eval(&mut a, integral);
let deriv = a.diff_wrt(integral, x);
let deriv = crate::transforms::eval::eval(&mut a, deriv);
let three = a.int(3);
let at_3 = crate::transforms::subs::subs(&mut a, deriv, x, three);
let at_3 = crate::transforms::eval::eval(&mut a, at_3);
let val = crate::transforms::evalf::eval_const_f64(&mut a, at_3);
assert!(val.is_some(), "BUG: d/dx(∫x²dx) must evaluate at x=3");
assert!(
(val.unwrap() - 9.0).abs() < 1e-10,
"BUG: d/dx(∫x²dx) at x=3 should be 9, got {:?}",
val
);
}
#[test]
fn tomas_integrate_exp_x() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let ex = a.exp(x);
let result = a.integrate_expr(ex, x);
let result = crate::transforms::eval::eval(&mut a, result);
let one = a.int(1);
let at_1 = crate::transforms::subs::subs(&mut a, result, x, one);
let at_1 = crate::transforms::eval::eval(&mut a, at_1);
let val = crate::transforms::evalf::eval_const_f64(&mut a, at_1);
assert!(val.is_some(), "BUG: ∫e^x must evaluate at x=1");
let v = val.unwrap();
assert!(
(v - std::f64::consts::E).abs() < 1e-10,
"BUG: ∫e^x at x=1 should be e ≈ 2.7183, got {v}"
);
}
#[test]
fn tomas_expand_then_factor() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let one = a.int(1);
let two = a.int(2);
let f1 = a.add(&[x, one]);
let f2 = a.add(&[x, two]);
let prod = a.mul(&[f1, f2]);
let expanded = a.expand_expr(prod);
let expanded = crate::transforms::eval::eval(&mut a, expanded);
let ten = a.int(10);
let at_10 = crate::transforms::subs::subs(&mut a, expanded, x, ten);
let at_10 = crate::transforms::eval::eval(&mut a, at_10);
let val = crate::transforms::evalf::eval_const_f64(&mut a, at_10);
assert_eq!(
val,
Some(132.0),
"BUG: (x+1)(x+2) at x=10 should be 132, got {val:?}"
);
}
#[test]
fn tomas_diff_sin_is_cos() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let sinx = a.sin(x);
let deriv = a.diff_wrt(sinx, x);
let deriv = crate::transforms::eval::eval(&mut a, deriv);
let zero = a.zero;
let at_0 = crate::transforms::subs::subs(&mut a, deriv, x, zero);
let at_0 = crate::transforms::eval::eval(&mut a, at_0);
let val = crate::transforms::evalf::eval_const_f64(&mut a, at_0);
assert!(val.is_some(), "BUG: d/dx(sin(x)) must evaluate at x=0");
assert!(
(val.unwrap() - 1.0).abs() < 1e-10,
"BUG: d/dx(sin(x)) at x=0 should be cos(0)=1, got {:?}",
val
);
}
#[test]
fn tomas_diff_ln_is_1_over_x() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let lnx = a.ln(x);
let deriv = a.diff_wrt(lnx, x);
let deriv = crate::transforms::eval::eval(&mut a, deriv);
let two = a.int(2);
let at_2 = crate::transforms::subs::subs(&mut a, deriv, x, two);
let at_2 = crate::transforms::eval::eval(&mut a, at_2);
let val = crate::transforms::evalf::eval_const_f64(&mut a, at_2);
assert!(val.is_some(), "BUG: d/dx(ln(x)) must evaluate at x=2");
assert!(
(val.unwrap() - 0.5).abs() < 1e-10,
"BUG: d/dx(ln(x)) at x=2 should be 0.5, got {:?}",
val
);
}
#[test]
fn tomas_second_derivative_x_cubed() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let three = a.int(3);
let x3 = a.pow(x, three);
let d1 = a.diff_wrt(x3, x);
let d1 = crate::transforms::eval::eval(&mut a, d1);
let d2 = a.diff_wrt(d1, x);
let d2 = crate::transforms::eval::eval(&mut a, d2);
let five = a.int(5);
let at_5 = crate::transforms::subs::subs(&mut a, d2, x, five);
let at_5 = crate::transforms::eval::eval(&mut a, at_5);
let val = crate::transforms::evalf::eval_const_f64(&mut a, at_5);
assert_eq!(
val,
Some(30.0),
"BUG: d²/dx²(x³) at x=5 should be 30, got {val:?}"
);
}
#[test]
fn lin_sign_sqrt2_minus_near_rational_positive() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n2 = a.int(2);
let sqrt2 = a.pow(n2, half);
let approx = a.rational(14142, 10000);
let diff = a.sub(sqrt2, approx);
let diff = crate::transforms::eval::eval(&mut a, diff);
let sign = sign_checked(&mut a, diff);
assert_eq!(
sign,
Some(1),
"BUG: √2 - 14142/10000 is small positive. Got sign={sign:?}"
);
}
#[test]
fn lin_sign_sqrt2_minus_near_rational_negative() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n2 = a.int(2);
let sqrt2 = a.pow(n2, half);
let approx = a.rational(141422, 100000);
let diff = a.sub(sqrt2, approx);
let diff = crate::transforms::eval::eval(&mut a, diff);
let sign = sign_checked(&mut a, diff);
assert_eq!(
sign,
Some(-1),
"BUG: √2 - 141422/100000 is small negative. Got sign={sign:?}"
);
}
#[test]
fn lin_is_zero_sqrt2_minus_rational_not_zero() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n2 = a.int(2);
let sqrt2 = a.pow(n2, half);
let approx = a.rational(7071, 5000);
let diff = a.sub(sqrt2, approx);
let diff = crate::transforms::eval::eval(&mut a, diff);
let result = is_zero_checked(&mut a, diff);
assert_eq!(
result,
Some(false),
"BUG: √2 - 7071/5000 is not zero. Got {result:?}"
);
}
#[test]
fn lin_evalf_pi() {
let mut a = crate::base::arena::Arena::new();
let pi = a.pi;
let val = crate::transforms::evalf::eval_const_f64(&mut a, pi);
assert!(val.is_some(), "BUG: π must evaluate");
let v = val.unwrap();
assert!(
(v - std::f64::consts::PI).abs() < 1e-10,
"BUG: π should be 3.14159..., got {v}"
);
}
#[test]
fn lin_evalf_e() {
let mut a = crate::base::arena::Arena::new();
let e = a.e_const;
let val = crate::transforms::evalf::eval_const_f64(&mut a, e);
assert!(val.is_some(), "BUG: e must evaluate");
let v = val.unwrap();
assert!(
(v - std::f64::consts::E).abs() < 1e-10,
"BUG: e should be 2.71828..., got {v}"
);
}
#[test]
fn lin_limit_sinx_over_x() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let sinx = a.sin(x);
let ratio = a.div(sinx, x);
let zero = a.zero;
let lim = a
.limit_expr(ratio, x, zero)
.expect("BUG: limit_expr failed for sin(x)/x");
let lim = crate::transforms::eval::eval(&mut a, lim);
assert_eq!(
lim,
a.one,
"BUG: lim sin(x)/x → 0 should be 1, got node={:?}",
a.node(lim)
);
}
#[test]
fn lin_chain_rule_sin_x_squared() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let two = a.int(2);
let x2 = a.pow(x, two);
let sin_x2 = a.sin(x2);
let deriv = a.diff_wrt(sin_x2, x);
let deriv = crate::transforms::eval::eval(&mut a, deriv);
let one = a.int(1);
let at_1 = crate::transforms::subs::subs(&mut a, deriv, x, one);
let at_1 = crate::transforms::eval::eval(&mut a, at_1);
let val = crate::transforms::evalf::eval_const_f64(&mut a, at_1);
assert!(val.is_some(), "BUG: d/dx sin(x²) must evaluate at x=1");
let expected = 2.0 * 1.0_f64.cos();
assert!(
(val.unwrap() - expected).abs() < 1e-10,
"BUG: d/dx sin(x²) at x=1 should be 2cos(1) ≈ {expected}, got {:?}",
val
);
}
#[test]
fn lin_series_exp_x_order_4() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let ex = a.exp(x);
let zero = a.zero;
let series = a
.series_expr(ex, x, zero, 4u32)
.expect("BUG: series_expr failed for e^x");
let series = crate::transforms::eval::eval(&mut a, series);
let tenth = a.rational(1, 10);
let at_01 = crate::transforms::subs::subs(&mut a, series, x, tenth);
let at_01 = crate::transforms::eval::eval(&mut a, at_01);
let val = crate::transforms::evalf::eval_const_f64(&mut a, at_01);
assert!(val.is_some(), "BUG: Taylor e^x must evaluate at x=0.1");
let v = val.unwrap();
assert!(
(v - 0.1_f64.exp()).abs() < 1e-4,
"BUG: Taylor e^x(4th order) at x=0.1 should be ≈ 1.10517, got {v}"
);
}
#[test]
fn lin_solve_quadratic() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let two = a.int(2);
let six = a.int(6);
let x2 = a.pow(x, two);
let neg5 = a.int(-5);
let neg5x = a.mul(&[neg5, x]);
let poly = a.add(&[x2, neg5x, six]);
let roots = crate::transforms::solve::solve(&mut a, poly, x);
assert_eq!(
roots.len(),
2,
"BUG: x²-5x+6 should have 2 roots, got {}",
roots.len()
);
let mut vals: Vec<f64> = roots
.iter()
.filter_map(|r| crate::transforms::evalf::eval_const_f64(&mut a, r.value))
.collect();
vals.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert!(
(vals[0] - 2.0).abs() < 1e-10,
"BUG: first root should be 2, got {}",
vals[0]
);
assert!(
(vals[1] - 3.0).abs() < 1e-10,
"BUG: second root should be 3, got {}",
vals[1]
);
}
#[test]
fn lin_matrix_det_2x2() {
let mut a = crate::base::arena::Arena::new();
let n1 = a.int(1);
let n2 = a.int(2);
let n3 = a.int(3);
let n4 = a.int(4);
let ad = a.mul(&[n1, n4]);
let bc = a.mul(&[n2, n3]);
let det = a.sub(ad, bc);
let det = crate::transforms::eval::eval(&mut a, det);
let val = crate::transforms::evalf::eval_const_f64(&mut a, det);
assert_eq!(
val,
Some(-2.0),
"BUG: det([[1,2],[3,4]]) should be -2, got {val:?}"
);
}
#[test]
fn lin_matrix_det_3x3_singular() {
let mut a = crate::base::arena::Arena::new();
let n1 = a.int(1);
let n2 = a.int(2);
let n3 = a.int(3);
let n4 = a.int(4);
let n5 = a.int(5);
let n6 = a.int(6);
let n7 = a.int(7);
let n8 = a.int(8);
let n9 = a.int(9);
let p59 = a.mul(&[n5, n9]);
let p68 = a.mul(&[n6, n8]);
let m11 = a.sub(p59, p68);
let p49 = a.mul(&[n4, n9]);
let p67 = a.mul(&[n6, n7]);
let m12 = a.sub(p49, p67);
let p48 = a.mul(&[n4, n8]);
let p57 = a.mul(&[n5, n7]);
let m13 = a.sub(p48, p57);
let t1 = a.mul(&[n1, m11]);
let t2 = a.mul(&[n2, m12]);
let t3 = a.mul(&[n3, m13]);
let det = a.sub(t1, t2);
let det = a.add(&[det, t3]);
let det = crate::transforms::eval::eval(&mut a, det);
let val = crate::transforms::evalf::eval_const_f64(&mut a, det);
assert_eq!(
val,
Some(0.0),
"BUG: det of singular 3x3 should be 0, got {val:?}"
);
}
#[test]
fn lin_solve_linear_system_2x2() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let y = a.symbol("y");
let n3 = a.int(3);
let n2 = a.int(2);
let eq1 = a.add(&[x, y]);
let eq1 = a.sub(eq1, n3);
let two_x = a.mul(&[n2, x]);
let eq2 = a.sub(two_x, y);
let roots_x = crate::transforms::solve::solve(&mut a, eq1, x);
if !roots_x.is_empty() {
let x_val = roots_x[0].value; let eq2_sub = crate::transforms::subs::subs(&mut a, eq2, x, x_val);
let eq2_sub = crate::transforms::eval::eval(&mut a, eq2_sub);
let roots_y = crate::transforms::solve::solve(&mut a, eq2_sub, y);
if !roots_y.is_empty() {
let y_val = crate::transforms::evalf::eval_const_f64(&mut a, roots_y[0].value);
assert!(
y_val.is_some_and(|v| (v - 2.0).abs() < 1e-10),
"BUG: y should be 2, got {y_val:?}"
);
let x_final = crate::transforms::subs::subs(&mut a, x_val, y, roots_y[0].value);
let x_final = crate::transforms::eval::eval(&mut a, x_final);
let x_val_f64 = crate::transforms::evalf::eval_const_f64(&mut a, x_final);
assert!(
x_val_f64.is_some_and(|v| (v - 1.0).abs() < 1e-10),
"BUG: x should be 1, got {x_val_f64:?}"
);
}
}
}
#[test]
fn lin_product_rule_derivative() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let sinx = a.sin(x);
let prod = a.mul(&[x, sinx]);
let deriv = a.diff_wrt(prod, x);
let deriv = crate::transforms::eval::eval(&mut a, deriv);
let two = a.int(2);
let pi = a.pi;
let pi_half = a.div(pi, two);
let at_pi2 = crate::transforms::subs::subs(&mut a, deriv, x, pi_half);
let at_pi2 = crate::transforms::eval::eval(&mut a, at_pi2);
let val = crate::transforms::evalf::eval_const_f64(&mut a, at_pi2);
assert!(val.is_some(), "BUG: d/dx(x·sin(x)) must evaluate at x=π/2");
assert!(
(val.unwrap() - 1.0).abs() < 1e-10,
"BUG: d/dx(x·sin(x)) at x=π/2 should be 1, got {:?}",
val
);
}
#[test]
fn lin_integrate_then_diff_sinx() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let sinx = a.sin(x);
let integral = a.integrate_expr(sinx, x);
let integral = crate::transforms::eval::eval(&mut a, integral);
let deriv = a.diff_wrt(integral, x);
let deriv = crate::transforms::eval::eval(&mut a, deriv);
let six = a.int(6);
let pi = a.pi;
let pi_6 = a.div(pi, six);
let at_val = crate::transforms::subs::subs(&mut a, deriv, x, pi_6);
let at_val = crate::transforms::eval::eval(&mut a, at_val);
let val = crate::transforms::evalf::eval_const_f64(&mut a, at_val);
assert!(val.is_some(), "BUG: d/dx(∫sin(x)dx) must evaluate at x=π/6");
assert!(
(val.unwrap() - 0.5).abs() < 1e-10,
"BUG: d/dx(∫sin(x)dx) at x=π/6 should be 0.5, got {:?}",
val
);
}
#[test]
fn lin_evalf_sqrt2_high_precision() {
let mut a = crate::base::arena::Arena::new();
let n2 = a.int(2);
let half = a.rational(1, 2);
let sqrt2 = a.pow(n2, half);
let val = crate::transforms::evalf::eval_const_f64(&mut a, sqrt2);
assert!(val.is_some(), "BUG: √2 must evaluate");
let v = val.unwrap();
assert!(
(v - std::f64::consts::SQRT_2).abs() < 1e-14,
"BUG: √2 should be 1.41421356237..., got {v}"
);
}
#[test]
fn lin_negative_exponent() {
let mut a = crate::base::arena::Arena::new();
let n2 = a.int(2);
let neg1 = a.int(-1);
let result = a.pow(n2, neg1);
let result = crate::transforms::eval::eval(&mut a, result);
let val = crate::transforms::evalf::eval_const_f64(&mut a, result);
assert_eq!(val, Some(0.5), "BUG: 2^(-1) should be 0.5, got {val:?}");
}
#[test]
fn lin_zero_to_the_zero() {
let mut a = crate::base::arena::Arena::new();
let zero = a.zero;
let result = a.pow(zero, zero);
let result = crate::transforms::eval::eval(&mut a, result);
let val = crate::transforms::evalf::eval_const_f64(&mut a, result);
assert!(
val == Some(1.0) || result == a.one,
"BUG: 0^0 should be 1, got val={val:?}, node={:?}",
a.node(result)
);
}
#[test]
fn lin_large_integer_arithmetic() {
let mut a = crate::base::arena::Arena::new();
let n2 = a.int(2);
let n64 = a.int(64);
let big = a.pow(n2, n64);
let big = crate::transforms::eval::eval(&mut a, big);
let one = a.int(1);
let result = a.sub(big, one);
let result = crate::transforms::eval::eval(&mut a, result);
if let Some(r) = a.as_num(result) {
let expected: u64 = u64::MAX; assert_eq!(
r.to_string(),
expected.to_string(),
"BUG: 2^64-1 should be {expected}, got {r}"
);
} else {
panic!(
"BUG: 2^64-1 should be a number, got node={:?}",
a.node(result)
);
}
}
#[test]
fn lin_gcd_polynomial() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let one = a.int(1);
let two = a.int(2);
let x2 = a.pow(x, two);
let p1 = a.sub(x2, one);
let neg2 = a.int(-2);
let neg2x = a.mul(&[neg2, x]);
let p2 = a.add(&[x2, neg2x, one]);
let gcd_expr = a.poly_gcd_expr(p1, p2, x);
let gcd_expr = gcd_expr.expect("BUG: poly_gcd_expr returned None for x²-1 and x²-2x+1");
let gcd_expr = crate::transforms::eval::eval(&mut a, gcd_expr);
let five = a.int(5);
let at_5 = crate::transforms::subs::subs(&mut a, gcd_expr, x, five);
let at_5 = crate::transforms::eval::eval(&mut a, at_5);
let val = crate::transforms::evalf::eval_const_f64(&mut a, at_5);
assert!(val.is_some(), "BUG: poly gcd must evaluate");
let v = val.unwrap();
assert!(
v.abs() > 0.1,
"BUG: gcd(x²-1, x²-2x+1) at x=5 should be nonzero, got {v}"
);
let p1_at_5 = crate::transforms::subs::subs(&mut a, p1, x, five);
let p1_at_5 = crate::transforms::eval::eval(&mut a, p1_at_5);
let p1_val = crate::transforms::evalf::eval_const_f64(&mut a, p1_at_5).unwrap();
let remainder = p1_val / v;
assert!(
(remainder - remainder.round()).abs() < 1e-10,
"BUG: gcd should divide p1. p1(5)={p1_val}, gcd(5)={v}, ratio={remainder}"
);
}
#[test]
fn hard_near_zero_radical_minus_close_rational() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n2 = a.int(2);
let sqrt2 = a.pow(n2, half);
let close_approx = a.rational(665857, 470832);
let diff = a.sub(sqrt2, close_approx);
let diff = crate::transforms::eval::eval(&mut a, diff);
let f64_val = crate::transforms::evalf::eval_const_f64(&mut a, diff);
eprintln!("hard_near_zero: √2 - 665857/470832 f64 = {f64_val:?}");
let result = is_zero_checked(&mut a, diff);
assert_eq!(
result,
Some(false),
"BUG: √2 - 665857/470832 ≈ -1.6e-12 is NONZERO but is_zero_checked says {result:?}. \
f64={f64_val:?}. This is a critical failure in the ambiguous zone."
);
}
#[test]
fn hard_near_zero_radical_minus_close_rational_sign() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n2 = a.int(2);
let sqrt2 = a.pow(n2, half);
let close_approx = a.rational(665857, 470832);
let diff = a.sub(sqrt2, close_approx);
let diff = crate::transforms::eval::eval(&mut a, diff);
let sign = sign_checked(&mut a, diff);
assert_eq!(
sign,
Some(-1),
"BUG: √2 - 665857/470832 is tiny negative. sign_checked says {sign:?}"
);
}
#[test]
fn hard_near_zero_positive_radical_minus_rational() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n2 = a.int(2);
let sqrt2 = a.pow(n2, half);
let approx = a.rational(1393, 985);
let diff = a.sub(sqrt2, approx);
let diff = crate::transforms::eval::eval(&mut a, diff);
let result = is_zero_checked(&mut a, diff);
assert_eq!(result, Some(false), "BUG: √2 - 1393/985 is nonzero");
let sign = sign_checked(&mut a, diff);
assert_eq!(sign, Some(1), "BUG: √2 - 1393/985 is positive");
}
#[test]
fn hard_sqrt_n2_plus_1_minus_n_large() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n = a.int(10000);
let n2 = a.int(100000001); let sqrt_n2p1 = a.pow(n2, half);
let diff = a.sub(sqrt_n2p1, n);
let diff = crate::transforms::eval::eval(&mut a, diff);
let result = is_zero_checked(&mut a, diff);
assert_eq!(result, Some(false), "BUG: √(10000²+1)-10000 is nonzero");
let sign = sign_checked(&mut a, diff);
assert_eq!(sign, Some(1), "BUG: √(10000²+1)-10000 is positive");
}
#[test]
fn hard_minpoly_identity_as_zero_test() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n2 = a.int(2);
let n3 = a.int(3);
let n4 = a.int(4);
let n10 = a.int(10);
let n1 = a.int(1);
let sqrt2 = a.pow(n2, half);
let sqrt3 = a.pow(n3, half);
let s = a.add(&[sqrt2, sqrt3]);
let s2 = a.pow(s, n2);
let s2 = crate::transforms::eval::eval(&mut a, s2);
let s4 = a.pow(s, n4);
let s4 = crate::transforms::eval::eval(&mut a, s4);
let ten_s2 = a.mul(&[n10, s2]);
let ten_s2 = crate::transforms::eval::eval(&mut a, ten_s2);
let neg_ten_s2 = a.neg(ten_s2);
let expr = a.add(&[s4, neg_ten_s2, n1]);
let expr = crate::transforms::eval::eval(&mut a, expr);
let f64_val = crate::transforms::evalf::eval_const_f64(&mut a, expr);
eprintln!("hard_minpoly_identity: (√2+√3)⁴-10(√2+√3)²+1 f64 = {f64_val:?}");
let result = is_zero_checked(&mut a, expr);
assert_eq!(
result,
Some(true),
"BUG: (√2+√3)⁴-10(√2+√3)²+1 = 0 (minimal poly identity). \
is_zero_checked says {result:?}, f64={f64_val:?}"
);
}
#[test]
fn hard_rationalize_denominator_identity() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n1 = a.int(1);
let n2 = a.int(2);
let n3 = a.int(3);
let sqrt2 = a.pow(n2, half);
let sqrt3 = a.pow(n3, half);
let denom = a.add(&[sqrt3, sqrt2]);
let lhs = a.div(n1, denom);
let rhs = a.sub(sqrt3, sqrt2);
let diff = a.sub(lhs, rhs);
let diff = crate::transforms::eval::eval(&mut a, diff);
let f64_val = crate::transforms::evalf::eval_const_f64(&mut a, diff);
eprintln!(
"hard_rationalize: 1/(√3+√2)-(√3-√2) f64 = {f64_val:?}, node = {:?}",
a.node(diff)
);
let result = is_zero_checked(&mut a, diff);
assert_eq!(
result,
Some(true),
"BUG: 1/(√3+√2)-(√3-√2) = 0. Got {result:?}, f64={f64_val:?}"
);
}
#[test]
fn hard_exact_is_zero_on_symbol_plus_radical() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let half = a.rational(1, 2);
let n2 = a.int(2);
let sqrt2 = a.pow(n2, half);
let expr = a.add(&[x, sqrt2]);
let result = is_zero_checked(&mut a, expr);
assert!(
result.is_none(),
"BUG: x+√2 has a free variable — is_zero_checked must return None, got {result:?}"
);
}
#[test]
fn hard_sign_of_near_zero_difference_of_radicals() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n1 = a.int(1);
let n2 = a.int(2);
let n3 = a.int(3);
let n5 = a.int(5);
let sqrt2 = a.pow(n2, half);
let sqrt3 = a.pow(n3, half);
let sqrt5 = a.pow(n5, half);
let neg_sqrt3 = a.neg(sqrt3);
let neg_sqrt2 = a.neg(sqrt2);
let expr = a.add(&[sqrt5, neg_sqrt3, neg_sqrt2, n1]);
let expr = crate::transforms::eval::eval(&mut a, expr);
let f64_val = crate::transforms::evalf::eval_const_f64(&mut a, expr);
eprintln!("hard_sign_near_zero: √5-√3-√2+1 f64 = {f64_val:?}");
let result = is_zero_checked(&mut a, expr);
assert_eq!(
result,
Some(false),
"BUG: √5-√3-√2+1 ≈ 0.09, nonzero. Got {result:?}"
);
let sign = sign_checked(&mut a, expr);
assert_eq!(
sign,
Some(1),
"BUG: √5-√3-√2+1 ≈ 0.09, positive. Got {sign:?}"
);
}
#[test]
fn hard_integrate_1_over_x4_plus_1() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let one = a.int(1);
let four = a.int(4);
let x4 = a.pow(x, four);
let denom = a.add(&[x4, one]);
let integrand = a.div(one, denom);
let result = a.integrate_expr(integrand, x);
let result = crate::transforms::eval::eval(&mut a, result);
let at_1 = crate::transforms::subs::subs(&mut a, result, x, one);
let at_1 = crate::transforms::eval::eval(&mut a, at_1);
let zero = a.zero;
let at_0 = crate::transforms::subs::subs(&mut a, result, x, zero);
let at_0 = crate::transforms::eval::eval(&mut a, at_0);
let definite = a.sub(at_1, at_0);
let definite = crate::transforms::eval::eval(&mut a, definite);
let val = crate::transforms::evalf::eval_const_f64(&mut a, definite);
if let Some(v) = val {
assert!(
(v - 0.86697298).abs() < 1e-4,
"BUG: ∫₀¹ 1/(x⁴+1)dx should be ≈ 0.8670, got {v}"
);
} else {
eprintln!(
"hard_integrate_x4+1: could not evaluate definite integral numerically. \
Result node: {:?}",
a.node(definite)
);
}
}
#[test]
fn hard_integrate_1_over_x3_minus_1() {
let mut a = crate::base::arena::Arena::new();
let x = a.symbol("x");
let one = a.int(1);
let three = a.int(3);
let x3 = a.pow(x, three);
let denom = a.sub(x3, one);
let integrand = a.div(one, denom);
let result = a.integrate_expr(integrand, x);
let result = crate::transforms::eval::eval(&mut a, result);
let two = a.int(2);
let at_2 = crate::transforms::subs::subs(&mut a, result, x, two);
let at_2 = crate::transforms::eval::eval(&mut a, at_2);
let val = crate::transforms::evalf::eval_const_f64(&mut a, at_2);
if let Some(v) = val {
assert!(
v.is_finite(),
"BUG: ∫1/(x³-1) at x=2 should be finite, got {v}"
);
assert!(
v.abs() < 100.0,
"BUG: ∫1/(x³-1) at x=2 should be reasonable, got {v}"
);
}
}
#[test]
fn hard_eval_const_f64_in_ambiguous_zone_is_not_trusted() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n2 = a.int(2);
let sqrt2 = a.pow(n2, half);
let close = a.rational(14142135623731i64, 10000000000000i64);
let diff = a.sub(sqrt2, close);
let diff = crate::transforms::eval::eval(&mut a, diff);
let f64_val = crate::transforms::evalf::eval_const_f64(&mut a, diff);
eprintln!("hard_ambiguous: √2 - 14142135623731/10000000000000 f64 = {f64_val:?}");
let result = is_zero_checked(&mut a, diff);
assert_eq!(
result,
Some(false),
"CRITICAL BUG: √2 - 14142135623731/10000000000000 is nonzero, but \
is_zero_checked says {result:?}. f64={f64_val:?}. \
The exact method must override the f64 tolerance in the ambiguous zone."
);
}
#[test]
fn hard_eval_const_f64_ambiguous_zone_sign() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n2 = a.int(2);
let sqrt2 = a.pow(n2, half);
let close = a.rational(14142135623731i64, 10000000000000i64);
let diff = a.sub(sqrt2, close);
let diff = crate::transforms::eval::eval(&mut a, diff);
let sign = sign_checked(&mut a, diff);
assert_eq!(
sign,
Some(-1),
"BUG: √2 - 14142135623731/10000000000000 is tiny negative, \
sign_checked says {sign:?}"
);
}
#[test]
fn hard_double_rationalization_zero() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let n1 = a.int(1);
let n2 = a.int(2);
let n3 = a.int(3);
let n5 = a.int(5);
let sqrt3 = a.pow(n3, half);
let sqrt5 = a.pow(n5, half);
let denom = a.add(&[sqrt5, sqrt3]);
let lhs = a.div(n1, denom);
let numer = a.sub(sqrt5, sqrt3);
let rhs = a.div(numer, n2);
let diff = a.sub(lhs, rhs);
let diff = crate::transforms::eval::eval(&mut a, diff);
let f64_val = crate::transforms::evalf::eval_const_f64(&mut a, diff);
eprintln!("hard_double_rat: 1/(√5+√3)-(√5-√3)/2 f64 = {f64_val:?}");
let result = is_zero_checked(&mut a, diff);
assert_eq!(
result,
Some(true),
"BUG: 1/(√5+√3)-(√5-√3)/2 = 0. Got {result:?}, f64={f64_val:?}"
);
}
#[test]
fn hard_cube_root_identity() {
let mut a = crate::base::arena::Arena::new();
let n2 = a.int(2);
let n3 = a.int(3);
let third = a.rational(1, 3);
let cbrt2 = a.pow(n2, third);
let cubed = a.pow(cbrt2, n3);
let cubed = crate::transforms::eval::eval(&mut a, cubed);
let diff = a.sub(cubed, n2);
let diff = crate::transforms::eval::eval(&mut a, diff);
let result = is_zero_checked(&mut a, diff);
assert_eq!(result, Some(true), "BUG: (∛2)³ - 2 = 0. Got {result:?}");
}
#[test]
fn hard_fourth_root_identity() {
let mut a = crate::base::arena::Arena::new();
let n4 = a.int(4);
let n5 = a.int(5);
let quarter = a.rational(1, 4);
let root = a.pow(n5, quarter);
let power = a.pow(root, n4);
let power = crate::transforms::eval::eval(&mut a, power);
let diff = a.sub(power, n5);
let diff = crate::transforms::eval::eval(&mut a, diff);
let result = is_zero_checked(&mut a, diff);
assert_eq!(result, Some(true), "BUG: (⁴√5)⁴ - 5 = 0. Got {result:?}");
}
#[test]
fn hard_mixed_power_zero() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let neg_half = a.rational(-1, 2);
let n1 = a.int(1);
let n2 = a.int(2);
let n3 = a.int(3);
let n6 = a.int(6);
let sqrt2 = a.pow(n2, half);
let sqrt3 = a.pow(n3, half);
let inv_sqrt6 = a.pow(n6, neg_half);
let prod = a.mul(&[sqrt2, sqrt3, inv_sqrt6]);
let prod = crate::transforms::eval::eval(&mut a, prod);
let diff = a.sub(prod, n1);
let diff = crate::transforms::eval::eval(&mut a, diff);
let result = is_zero_checked(&mut a, diff);
assert_eq!(
result,
Some(true),
"BUG: √2·√3/√6 - 1 = 0. Got {result:?}, node={:?}",
a.node(diff)
);
}
#[test]
fn hard_stress_all_signs_of_radical_differences() {
let mut a = crate::base::arena::Arena::new();
let half = a.rational(1, 2);
let primes = [2i64, 3, 5, 7, 11, 13];
for i in 0..primes.len() {
for j in (i + 1)..primes.len() {
let pi = a.int(primes[i]);
let pj = a.int(primes[j]);
let si = a.pow(pi, half);
let sj = a.pow(pj, half);
let diff = a.sub(si, sj);
let sign = sign_checked(&mut a, diff);
assert_eq!(
sign,
Some(-1),
"BUG: √{} - √{} should be negative (√{} < √{}), got {sign:?}",
primes[i],
primes[j],
primes[i],
primes[j]
);
}
}
}
}