pub mod hermite;
pub mod integrate;
pub mod log_to_real;
mod param_rational;
pub(crate) use param_rational::polynomial_sqrt;
pub mod rde;
pub mod rothstein_trager;
pub mod tower;
pub mod tower_integrate;
use std::cell::Cell;
use num_traits::{One, Signed};
use crate::base::arena::Arena;
use crate::base::node::{ExprId, ExprNode};
use crate::base::numeric::Q;
use crate::base::stage::stage;
use crate::poly::dense::Poly;
use crate::poly::generic::GenPoly;
use crate::poly::ratfn::RationalFn;
thread_local! {
static RISCH_GUARD: Cell<bool> = const { Cell::new(false) };
}
struct RischRecursionGuard;
impl RischRecursionGuard {
fn enter() -> Option<Self> {
RISCH_GUARD.with(|g| {
if g.get() {
None } else {
g.set(true);
Some(RischRecursionGuard)
}
})
}
}
impl Drop for RischRecursionGuard {
fn drop(&mut self) {
RISCH_GUARD.with(|g| g.set(false));
}
}
pub(crate) fn rational_guard_active() -> bool {
RISCH_GUARD.with(Cell::get)
}
#[derive(Clone, Debug)]
pub enum LogTerm {
Rational { coeff: Q, argument: Poly },
Algebraic {
min_poly: Poly,
log_arg: Option<GenPoly<RationalFn>>,
},
}
#[derive(Clone, Debug)]
pub enum RischResult {
Elementary {
rational_numer: Poly,
rational_denom: Poly,
log_terms: Vec<LogTerm>,
arena_expr: Option<crate::base::node::ExprId>,
},
NonElementary,
Failed(String),
}
pub(crate) enum TowerResult {
Elementary(crate::base::node::ExprId),
NonElementary,
NotApplicable,
}
pub(crate) fn try_risch_tower(
arena: &mut crate::base::arena::Arena,
expr: crate::base::node::ExprId,
var: crate::base::node::ExprId,
) -> TowerResult {
if crate::base::walk::post_order_ids(arena, expr)
.iter()
.any(|&id| matches!(arena.node(id), ExprNode::RootSum(..)))
{
return TowerResult::NotApplicable;
}
let mut de = match tower::build_tower(arena, expr, var) {
Ok(de) => de,
Err(_reason) => {
tracing::debug!(reason = _reason.as_str(), "Risch tower: build_tower failed");
return TowerResult::NotApplicable;
}
};
if de.is_base_level() {
return TowerResult::NotApplicable;
}
let result = integrate::risch_integrate(arena, &mut de);
match result {
RischResult::Elementary {
arena_expr: Some(id),
rational_numer: _rn,
rational_denom: _rd,
log_terms: _lt,
} => {
tracing::debug!(
tower_depth = de.depth(),
"Risch tower: elementary antiderivative found"
);
TowerResult::Elementary(id)
}
RischResult::Elementary {
arena_expr: None,
rational_numer: _rn,
rational_denom: _rd,
log_terms: _lt,
} => {
TowerResult::NotApplicable
}
RischResult::NonElementary => TowerResult::NonElementary,
RischResult::Failed(_msg) => {
tracing::debug!(
reason = _msg.as_str(),
"Risch tower integration failed, falling through"
);
TowerResult::NotApplicable
}
}
}
const MAX_RATIONAL_EXPONENT: i64 = 1000;
fn is_rational_function_over_q(arena: &Arena, e: ExprId, var: ExprId) -> bool {
crate::base::walk::post_order_ids(arena, e)
.into_iter()
.all(|id| match arena.node(id) {
ExprNode::Num(_) | ExprNode::Add(_) | ExprNode::Mul(_) | ExprNode::Neg(_) => true,
ExprNode::Pow(_, exp) => arena.as_num(*exp).is_some_and(|r| {
r.is_integer()
&& i64::try_from(r.to_integer()).is_ok_and(|n| n.abs() <= MAX_RATIONAL_EXPONENT)
}),
_ => id == var,
})
}
fn has_negative_power_of(arena: &Arena, e: ExprId, var: ExprId) -> bool {
crate::base::walk::post_order_ids(arena, e)
.into_iter()
.any(|id| match arena.node(id) {
ExprNode::Pow(base, exp) => {
arena
.as_num(*exp)
.is_some_and(|r| r.is_integer() && r.is_negative())
&& crate::base::walk::contains(arena, *base, var)
}
_ => false,
})
}
pub fn try_risch_rational(arena: &mut Arena, expr: ExprId, var: ExprId) -> Option<ExprId> {
if !is_rational_function_over_q(arena, expr, var) {
return None;
}
let _guard = RischRecursionGuard::enter()?;
let (mut numer_id, mut denom_id) = crate::poly::polybridge::as_numer_denom(arena, expr);
if denom_id == arena.one() && has_negative_power_of(arena, expr, var) {
let combined = crate::poly::polybridge::together_deep(arena, expr);
(numer_id, denom_id) = crate::poly::polybridge::as_numer_denom(arena, combined);
if denom_id == arena.one() {
let n = crate::transforms::expand::expand(arena, numer_id);
let n = crate::transforms::eval::eval(arena, n);
let p = crate::poly::polybridge::expr_to_poly(arena, n, var)?;
let hr = hermite::hermite_reduce(&p, &Poly::from_int(1))?;
return Some(crate::poly::polybridge::poly_to_expr(
arena,
&hr.g_numer,
var,
));
}
}
if denom_id == arena.one() {
return None;
}
let numer_exp = stage!(
arena,
"expand_numer",
numer_id,
crate::transforms::expand::expand(arena, numer_id)
);
let numer_expanded = crate::transforms::eval::eval(arena, numer_exp);
let denom_exp = stage!(
arena,
"expand_denom",
denom_id,
crate::transforms::expand::expand(arena, denom_id)
);
let denom_expanded = crate::transforms::eval::eval(arena, denom_exp);
let (numer_poly, denom_poly) = match (
crate::poly::polybridge::expr_to_poly(arena, numer_expanded, var),
crate::poly::polybridge::expr_to_poly(arena, denom_expanded, var),
) {
(Some(n), Some(d)) => (n, d),
_ if has_negative_power_of(arena, numer_expanded, var)
|| has_negative_power_of(arena, denom_expanded, var) =>
{
let combined = crate::poly::polybridge::together_deep(arena, expr);
let (n, d) = crate::poly::polybridge::as_numer_denom(arena, combined);
let n = crate::transforms::expand::expand(arena, n);
let n = crate::transforms::eval::eval(arena, n);
let d = crate::transforms::expand::expand(arena, d);
let d = crate::transforms::eval::eval(arena, d);
(
crate::poly::polybridge::expr_to_poly(arena, n, var)?,
crate::poly::polybridge::expr_to_poly(arena, d, var)?,
)
}
_ => return None,
};
if denom_poly.is_constant() {
return None;
}
integrate_rational_function(arena, &numer_poly, &denom_poly, var, expr)
}
pub(crate) fn try_risch_rational_params(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
) -> Option<ExprId> {
let _guard = RischRecursionGuard::enter()?;
param_rational::integrate_param_rational(arena, expr, var)
}
pub(crate) fn reduced_fraction_terms(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
) -> Option<ExprId> {
let ExprNode::Symbol(var_sym) = *arena.node(var) else {
return None;
};
let ExprNode::Mul(factors) = arena.node(expr) else {
return None;
};
let depends =
|arena: &Arena, id: ExprId| crate::base::walk::has_free_symbol(arena, id, var_sym);
let has_sum = factors
.iter()
.any(|&f| matches!(arena.node(f), ExprNode::Add(_)) && depends(arena, f));
let has_denominator = factors.iter().any(|&f| {
matches!(arena.node(f), ExprNode::Pow(b, e)
if arena.as_num(*e).is_some_and(|q| q.is_integer() && q.is_negative())
&& depends(arena, *b))
});
if !has_sum || !has_denominator {
return None;
}
let transcendental = |arena: &Arena, g: ExprId| {
!matches!(arena.node(g), ExprNode::Symbol(_))
&& crate::base::walk::has_free_symbol(arena, g, var_sym)
};
crate::simplify::ratsimp::collect_reduced_terms(arena, expr, transcendental)
}
fn power_substitution_degree(a: &Poly, d: &Poly) -> Option<usize> {
let mut g = 0usize;
for (e, c) in d.coeffs().iter().enumerate() {
if !num_traits::Zero::is_zero(c) {
g = num_integer::gcd(g, e);
}
}
for (e, c) in a.coeffs().iter().enumerate() {
if !num_traits::Zero::is_zero(c) {
g = num_integer::gcd(g, e + 1);
}
}
(g >= 2).then_some(g)
}
fn compress_exponents(p: &Poly, k: usize, shift: usize) -> Poly {
Poly::from_coeffs(p.coeffs().iter().skip(shift).step_by(k).cloned().collect())
}
pub(super) fn integrate_rational_function(
arena: &mut Arena,
numer_poly: &Poly,
denom_poly: &Poly,
var: ExprId,
label: ExprId,
) -> Option<ExprId> {
let expr = label;
if let Some(k) = power_substitution_degree(numer_poly, denom_poly) {
let k_q = Q::from_integer(k.into());
let a_u = compress_exponents(numer_poly, k, k - 1).scale(&k_q.recip());
let d_u = compress_exponents(denom_poly, k, 0);
let u = arena.symbol("__rr_u");
let f_u = integrate_rational_function(arena, &a_u, &d_u, u, label)?;
let k_id = arena.int(i64::try_from(k).ok()?);
let x_k = arena.pow(var, k_id);
let f_x = crate::transforms::subs::subs(arena, f_u, u, x_k);
return Some(crate::transforms::eval::eval(arena, f_x));
}
let hr = stage!(
arena,
"hermite",
expr,
hermite::hermite_reduce(numer_poly, denom_poly)
)?;
let log_result = if hr.h_numer.is_zero() {
rothstein_trager::LogPartResult { terms: vec![] }
} else {
stage!(
arena,
"rothstein_trager",
expr,
rothstein_trager::logarithmic_part(&hr.h_numer, &hr.h_denom)
)?
};
let mut terms: Vec<ExprId> = Vec::new();
if !hr.g_numer.is_zero() {
let g_num_id = crate::poly::polybridge::poly_to_expr(arena, &hr.g_numer, var);
let g_den_id = crate::poly::polybridge::poly_to_expr(arena, &hr.g_denom, var);
if g_den_id == arena.one() {
terms.push(g_num_id);
} else {
terms.push(arena.div(g_num_id, g_den_id));
}
}
let mut rational_log_parts: Vec<(Q, Poly)> = Vec::new();
let mut has_algebraic = false;
let mut n_algebraic = 0usize;
for term in &log_result.terms {
match term {
LogTerm::Rational { coeff, argument } => {
tracing::debug!(
coeff = %coeff,
argument_degree = ?argument.degree(),
"try_risch_rational: found rational log term"
);
rational_log_parts.push((coeff.clone(), argument.clone()));
}
LogTerm::Algebraic { min_poly, .. } => {
tracing::debug!(
min_poly_degree = ?min_poly.degree(),
"try_risch_rational: found algebraic log term"
);
has_algebraic = true;
n_algebraic += 1;
}
}
}
tracing::debug!(
n_rational = rational_log_parts.len(),
n_algebraic,
has_algebraic,
"try_risch_rational: Rothstein-Trager classification"
);
for (coeff, argument) in &rational_log_parts {
let arg_id = crate::poly::polybridge::poly_to_expr(arena, argument, var);
let abs_arg = arena.abs(arg_id);
let ln_arg = arena.ln(abs_arg);
if coeff.is_one() {
terms.push(ln_arg);
} else if (-coeff.clone()).is_one() {
terms.push(arena.neg(ln_arg));
} else {
let coeff_id = arena.num_ratio(coeff.clone());
terms.push(arena.mul(&[coeff_id, ln_arg]));
}
}
if has_algebraic && !hr.h_numer.is_zero() {
type Factor<'a> = (
&'a Poly,
Option<&'a GenPoly<RationalFn>>,
Option<Vec<ExprId>>,
);
let mut per_factor: Vec<Factor<'_>> = Vec::new();
for term in &log_result.terms {
let LogTerm::Algebraic { min_poly, log_arg } = term else {
continue;
};
let log_arg = log_arg.as_ref();
let real_terms = match log_arg {
Some(s) if min_poly.degree() == Some(2) => stage!(
arena,
"quadratic_log_to_real",
expr,
log_to_real::quadratic_log_to_real(arena, var, min_poly, s)
),
Some(s) if s.degree() == Some(1) => stage!(
arena,
"log_to_real",
expr,
log_to_real::log_to_real(arena, var, min_poly, s)
),
_ => None,
};
if real_terms.is_none() {
tracing::debug!(
min_poly_degree = ?min_poly.degree(),
log_arg_degree = ?log_arg.and_then(GenPoly::degree),
"try_risch_rational: no real form for algebraic factor"
);
}
per_factor.push((min_poly, log_arg, real_terms));
}
if per_factor.iter().all(|(_, _, t)| t.is_some()) {
for (_, _, real_terms) in per_factor {
terms.extend(real_terms.into_iter().flatten());
}
} else {
tracing::debug!(
h_numer_degree = ?hr.h_numer.degree(),
h_denom_degree = ?hr.h_denom.degree(),
n_rational_to_subtract = rational_log_parts.len(),
"try_risch_rational: trying the algebraic remainder path"
);
let mut a_alg = hr.h_numer.clone();
for (coeff, v_i) in &rational_log_parts {
let v_i_prime = v_i.derivative();
let cofactor = hr.h_denom.div(v_i);
debug_assert!(
{
let product = &cofactor * v_i;
product == hr.h_denom
},
"h_denom / v_i must be exact polynomial division"
);
let contribution = (&v_i_prime * &cofactor).scale(coeff);
a_alg = &a_alg - &contribution;
}
if a_alg.is_zero() {
tracing::debug!(
"try_risch_rational: A_alg is zero — rational terms fully account for the integrand"
);
} else {
let g = Poly::gcd(&a_alg, &hr.h_denom);
let a_reduced = a_alg.div(&g);
let d_reduced = hr.h_denom.div(&g);
let alg_num_id = crate::poly::polybridge::poly_to_expr(arena, &a_reduced, var);
let alg_den_id = crate::poly::polybridge::poly_to_expr(arena, &d_reduced, var);
let algebraic_remainder = arena.div(alg_num_id, alg_den_id);
let alg_integral = if heuristic_cannot_help(
arena, &a_reduced, &d_reduced, alg_den_id, var,
) {
tracing::debug!(
d_degree = ?d_reduced.degree(),
"try_risch_rational: no explicit root of the algebraic denominator, skipping the heuristic integrator"
);
arena.intern(ExprNode::Integral(algebraic_remainder, var))
} else {
crate::transforms::integrate::integrate(arena, algebraic_remainder, var)
};
let root_sums_available = per_factor.iter().all(|(_, s, _)| s.is_some());
let oversized = root_sums_available
&& !crate::base::walk::has_unevaluated(arena, alg_integral)
&& {
let size = tree_size(arena, alg_integral);
tracing::debug!(
size,
"try_risch_rational: algebraic remainder's closed form"
);
size > MAX_REMAINDER_SIZE
};
if !crate::base::walk::has_unevaluated(arena, alg_integral) && !oversized {
terms.push(alg_integral);
} else if root_sums_available {
for (min_poly, log_arg, real_terms) in per_factor {
match (real_terms, log_arg) {
(Some(real_terms), _) => terms.extend(real_terms),
(None, Some(s)) => {
tracing::debug!(
min_poly_degree = ?min_poly.degree(),
"try_risch_rational: emitting RootSum for algebraic factor"
);
let linear = s.degree().is_some_and(|k| k >= 2).then(|| {
linear_root_sum_term(
arena,
var,
min_poly,
&hr.h_numer,
&hr.h_denom,
)
});
match linear.flatten() {
Some(term) => terms.push(term),
None => terms.push(root_sum_term(arena, var, min_poly, s)),
}
}
(None, None) => {}
}
}
} else {
tracing::debug!(
"try_risch_rational: no log argument for RootSum, keeping the unevaluated algebraic integral"
);
terms.push(alg_integral);
}
}
}
}
if terms.is_empty() {
Some(arena.zero())
} else if terms.len() == 1 {
Some(terms[0])
} else {
Some(arena.add(&terms))
}
}
fn tree_size(arena: &Arena, e: ExprId) -> usize {
let mut size: rustc_hash::FxHashMap<ExprId, usize> = rustc_hash::FxHashMap::default();
for id in crate::base::walk::post_order_ids(arena, e) {
let mut s: usize = 1;
arena
.node(id)
.for_each_child(|c| s = s.saturating_add(size.get(&c).copied().unwrap_or(1)));
size.insert(id, s);
}
size.get(&e).copied().unwrap_or(1)
}
const MAX_REMAINDER_SIZE: usize = 2_000;
fn heuristic_cannot_help(arena: &mut Arena, n: &Poly, d: &Poly, d_id: ExprId, var: ExprId) -> bool {
if d.degree().is_none_or(|deg| deg < 3) || admits_substitution(n, d) {
return false;
}
let roots = crate::transforms::solve::solve(arena, d_id, var);
!roots.is_empty()
&& roots.iter().all(|s| {
crate::base::walk::post_order_ids(arena, s.value)
.iter()
.any(|&id| matches!(arena.node(id), ExprNode::RootOf(..)))
})
}
fn admits_substitution(n: &Poly, d: &Poly) -> bool {
let components = d.decompose();
(1..components.len()).any(|j| {
let Some((inner, rest)) = components[j..].split_last() else {
return false;
};
let h = rest.iter().rev().fold(inner.clone(), |h, c| c.compose(&h));
let h_prime = h.derivative();
let (mut m, rem) = n.div_rem(&h_prime);
if !rem.is_zero() {
return false;
}
while !m.is_zero() {
let (q, digit) = m.div_rem(&h);
if digit.degree().is_some_and(|deg| deg > 0) {
return false;
}
m = q;
}
true
})
}
const MAX_LINEAR_ROOT_SUM_DEGREE: usize = 24;
fn linear_root_sum_term(
arena: &mut Arena,
var: ExprId,
q: &Poly,
a: &Poly,
d: &Poly,
) -> Option<ExprId> {
let eg = Poly::extended_gcd(&d.derivative(), d);
if eg.gcd.degree() != Some(0) {
return None;
}
let r = (a * &eg.x).rem(d);
let mut q_of_r = Poly::zero();
for c in q.coeffs().iter().rev() {
q_of_r = (&(&q_of_r * &r) + &Poly::constant(c.clone())).rem(d);
}
let d_q = Poly::gcd(d, &q_of_r);
let deg = d_q.degree()?;
if !(2..=MAX_LINEAR_ROOT_SUM_DEGREE).contains(°) {
return None;
}
if crate::poly::sturm::SturmChain::new(&d_q).count_real_roots() != 0
&& q.degree()
.is_some_and(|n| crate::poly::sturm::SturmChain::new(q).count_real_roots() >= n)
{
return None;
}
let r_q = r.rem(&d_q);
let s = arena.symbol("__rs_t");
let d_q_expr = crate::poly::polybridge::poly_to_expr(arena, &d_q, s);
let r_q_expr = crate::poly::polybridge::poly_to_expr(arena, &r_q, s);
let lin = arena.sub(var, s);
let ln = arena.ln(lin);
let body = arena.mul(&[r_q_expr, ln]);
Some(arena.intern(ExprNode::RootSum(d_q_expr, body, s)))
}
fn root_sum_term(arena: &mut Arena, var: ExprId, q: &Poly, s: &GenPoly<RationalFn>) -> ExprId {
let t = arena.symbol("__rs_t");
let mut s_terms: Vec<ExprId> = Vec::new();
for (k, c) in s.coeffs.iter().enumerate() {
if c.numer().is_zero() {
continue;
}
let c_expr = crate::poly::polybridge::ratfn_to_expr(arena, c, t);
let x_k = match k {
0 => arena.one(),
1 => var,
_ => {
let k_id = arena.int(i64::try_from(k).unwrap_or(i64::MAX));
arena.pow(var, k_id)
}
};
s_terms.push(arena.mul(&[c_expr, x_k]));
}
let s_expr = arena.add(&s_terms);
let ln_s = arena.ln(s_expr);
let body = arena.mul(&[t, ln_s]);
let q_expr = crate::poly::polybridge::poly_to_expr(arena, q, t);
arena.intern(ExprNode::RootSum(q_expr, body, t))
}
#[cfg(test)]
mod tests {
use super::*;
use num_bigint::BigInt;
use num_rational::Ratio;
fn sym(arena: &mut Arena, name: &str) -> ExprId {
arena.symbol(name)
}
fn display(arena: &Arena, id: ExprId) -> String {
arena.display(id).to_string()
}
#[test]
fn from_ratio_integer() {
let mut arena = Arena::new();
let expr = arena.num_ratio(Ratio::from_integer(BigInt::from(42)));
assert_eq!(display(&arena, expr), "42");
}
#[test]
fn from_ratio_fraction() {
let mut arena = Arena::new();
let expr = arena.num_ratio(Ratio::new(BigInt::from(3), BigInt::from(4)));
assert_eq!(display(&arena, expr), "3/4");
}
#[test]
fn try_risch_rational_on_non_rational_returns_none() {
let mut arena = Arena::new();
let x = sym(&mut arena, "x");
let expr = arena.sin(x);
assert!(try_risch_rational(&mut arena, expr, x).is_none());
}
#[test]
fn try_risch_rational_on_polynomial_returns_none() {
let mut arena = Arena::new();
let x = sym(&mut arena, "x");
let two = arena.int(2);
let one = arena.one();
let x_sq = arena.pow(x, two);
let expr = arena.add(&[x_sq, one]);
assert!(try_risch_rational(&mut arena, expr, x).is_none());
}
#[test]
fn try_risch_rational_one_over_x() {
let mut arena = Arena::new();
let x = sym(&mut arena, "x");
let neg1 = arena.int(-1);
let expr = arena.pow(x, neg1);
let result = try_risch_rational(&mut arena, expr, x);
assert!(
result.is_some(),
"∫ 1/x dx should succeed via Risch rational"
);
let s = display(&arena, result.unwrap());
assert!(
s.contains("ln") && s.contains("x"),
"∫ 1/x dx should contain ln(x), got: {s}"
);
}
#[test]
fn try_risch_rational_one_over_x_sq_plus_1_squared() {
let mut arena = Arena::new();
let x = sym(&mut arena, "x");
let two = arena.int(2);
let x_sq = arena.pow(x, two);
let one = arena.one();
let x_sq_plus_1 = arena.add(&[x_sq, one]);
let neg2 = arena.int(-2);
let expr = arena.pow(x_sq_plus_1, neg2);
let result = try_risch_rational(&mut arena, expr, x);
assert!(result.is_some(), "∫ 1/(x²+1)² dx should succeed");
let result_expr = result.unwrap();
let s = display(&arena, result_expr);
assert!(
s.contains("x") && s.contains("atan"),
"result should have rational part + arctan, got: {s}"
);
assert!(
!s.contains("Integral"),
"result should be fully evaluated, got: {s}"
);
}
#[test]
fn try_risch_rational_one_over_x_squared() {
let mut arena = Arena::new();
let x = sym(&mut arena, "x");
let neg2 = arena.int(-2);
let expr = arena.pow(x, neg2);
let result = try_risch_rational(&mut arena, expr, x);
assert!(
result.is_some(),
"∫ 1/x^2 dx should succeed via Risch rational"
);
let s = display(&arena, result.unwrap());
assert!(s.contains("x"), "∫ 1/x^2 dx should be -1/x, got: {s}");
}
#[test]
fn prs_x3_minus_1_has_degree_1_member() {
use crate::poly::dense::Poly;
use crate::poly::generic::GenPoly;
use crate::poly::ratfn::RationalFn;
fn r(n: i64, d: i64) -> Q {
Ratio::new(BigInt::from(n), BigInt::from(d))
}
let d_gp: GenPoly<RationalFn> = GenPoly::from_coeffs(vec![
RationalFn::from_rational(r(-1, 1)),
RationalFn::from_rational(r(0, 1)),
RationalFn::from_rational(r(0, 1)),
RationalFn::from_rational(r(1, 1)),
]);
let neg_3t = RationalFn::from_poly(Poly::from_coeffs(vec![r(0, 1), r(-3, 1)]));
let b_gp: GenPoly<RationalFn> = GenPoly::from_coeffs(vec![
RationalFn::from_rational(r(1, 1)),
RationalFn::from_rational(r(0, 1)),
neg_3t,
]);
let prs = GenPoly::<RationalFn>::euclidean_prs(&d_gp, &b_gp);
assert!(
prs.contains_key(&1),
"PRS should contain a degree-1 member, got degrees: {:?}",
prs.keys().collect::<Vec<_>>()
);
let h = prs.get(&1).unwrap();
let h_monic = h.make_monic();
let c1 = h_monic.coeff(1);
assert!(
c1.numer().is_constant() && c1.denom().is_constant(),
"x coefficient should be a constant RationalFn"
);
let c1_val = c1.to_rational().expect("should be rational");
assert_eq!(c1_val, r(1, 1), "x coefficient should be 1");
let c0 = h_monic.coeff(0);
assert!(
c0.denom().is_constant(),
"constant term denominator should be 1"
);
let c0_numer = c0.numer();
assert_eq!(
c0_numer.degree(),
Some(1),
"constant term should be linear in t"
);
assert_eq!(c0_numer.coeff(0), r(0, 1), "constant of -3t should be 0");
assert_eq!(c0_numer.coeff(1), r(-3, 1), "slope of -3t should be -3");
}
#[test]
fn solve_quadratic_factor_produces_conjugate_roots() {
let mut arena = Arena::new();
let t = sym(&mut arena, "t");
let nine = arena.int(9);
let three = arena.int(3);
let one = arena.one();
let two = arena.int(2);
let t_sq = arena.pow(t, two);
let term_9t2 = arena.mul(&[nine, t_sq]);
let term_3t = arena.mul(&[three, t]);
let q_expr = arena.add(&[term_9t2, term_3t, one]);
let roots = crate::transforms::solve::solve(&mut arena, q_expr, t);
assert_eq!(
roots.len(),
2,
"quadratic should have 2 roots, got {}",
roots.len()
);
let mut pos_im_found = false;
let mut neg_im_found = false;
for root in &roots {
let (re, im) = crate::base::complex::as_real_imag(&mut arena, root.value);
let re = crate::transforms::eval::eval(&mut arena, re);
let im = crate::transforms::eval::eval(&mut arena, im);
let re_f64 = crate::transforms::evalf::eval_const_f64(&mut arena, re);
let im_f64 = crate::transforms::evalf::eval_const_f64(&mut arena, im);
if let (Some(re_v), Some(im_v)) = (re_f64, im_f64) {
assert!(
(re_v - (-1.0 / 6.0)).abs() < 1e-10,
"Re should be -1/6, got {re_v}"
);
let expected_im = 3.0_f64.sqrt() / 6.0;
assert!(
(im_v.abs() - expected_im).abs() < 1e-10,
"|Im| should be √3/6 ≈ {expected_im}, got {}",
im_v.abs()
);
if im_v > 0.0 {
pos_im_found = true;
} else {
neg_im_found = true;
}
} else {
panic!(
"Could not evaluate root to f64: {}",
display(&arena, root.value)
);
}
}
assert!(
pos_im_found,
"should have a root with positive imaginary part"
);
assert!(
neg_im_found,
"should have a root with negative imaginary part"
);
}
#[test]
fn try_risch_rational_one_over_x_cubed_minus_1_numerical() {
let mut arena = Arena::new();
let x = sym(&mut arena, "x");
let three = arena.int(3);
let one = arena.one();
let x_cubed = arena.pow(x, three);
let denom = arena.sub(x_cubed, one);
let neg1 = arena.int(-1);
let expr = arena.pow(denom, neg1);
let result = try_risch_rational(&mut arena, expr, x);
assert!(result.is_some(), "∫ 1/(x³-1) dx should succeed");
let anti = result.unwrap();
assert!(
!crate::base::walk::has_unevaluated(&arena, anti),
"result should not contain unevaluated integrals: {}",
display(&arena, anti)
);
let val_3 = arena.int(3);
let val_2 = arena.int(2);
let f3 = crate::transforms::subs::subs(&mut arena, anti, x, val_3);
let f3 = crate::transforms::eval::eval(&mut arena, f3);
let f2 = crate::transforms::subs::subs(&mut arena, anti, x, val_2);
let f2 = crate::transforms::eval::eval(&mut arena, f2);
let f3_f64 = crate::transforms::evalf::eval_const_f64(&mut arena, f3);
let f2_f64 = crate::transforms::evalf::eval_const_f64(&mut arena, f2);
if let (Some(f3v), Some(f2v)) = (f3_f64, f2_f64) {
let integral = f3v - f2v;
assert!(
(integral - 0.07539).abs() < 0.001,
"∫₂³ 1/(x³-1) dx ≈ 0.07539, got {integral}"
);
}
}
}