use num_bigint::BigInt;
use num_integer::Integer;
use num_rational::Ratio;
use num_traits::{One, Signed, Zero};
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
use crate::base::arena::Arena;
use crate::base::node::{ExprId, ExprNode};
use crate::base::walk;
use crate::poly::Poly;
use crate::poly::multipoly::{GrevLex, MultiPoly};
pub(crate) fn expr_to_poly(arena: &Arena, expr: ExprId, var: ExprId) -> Option<Poly> {
if !contains_id(arena, expr, var) {
let coeff = expr_to_rational(arena, expr)?;
return Some(Poly::constant(coeff));
}
if expr == var {
return Some(Poly::x());
}
let post_order = walk::post_order_ids(arena, expr);
let mut cache: FxHashMap<ExprId, Poly> = FxHashMap::default();
for &id in &post_order {
let poly = convert_node(arena, id, var, &cache)?;
cache.insert(id, poly);
}
cache.remove(&expr)
}
fn convert_node(
arena: &Arena,
id: ExprId,
var: ExprId,
cache: &FxHashMap<ExprId, Poly>,
) -> Option<Poly> {
if id == var {
return Some(Poly::x());
}
let node = arena.node(id);
match node {
ExprNode::Num(nid) => {
let r = arena.num(*nid).clone();
Some(Poly::constant(r))
}
ExprNode::Symbol(_) => {
if !contains_id(arena, id, var) {
None
} else {
Some(Poly::x())
}
}
ExprNode::Pi
| ExprNode::E
| ExprNode::ImaginaryUnit
| ExprNode::EulerGamma
| ExprNode::Catalan
| ExprNode::GoldenRatio
| ExprNode::PhysicalConstant(_, _) => None,
ExprNode::Infinity | ExprNode::NegInfinity | ExprNode::ComplexInfinity | ExprNode::NaN => {
None
}
ExprNode::Add(children) => {
let mut result = Poly::zero();
for &child in children.iter() {
let child_poly = cache.get(&child)?;
result = &result + child_poly;
}
Some(result)
}
ExprNode::Mul(children) => {
let mut result = Poly::from_int(1);
for &child in children.iter() {
let child_poly = cache.get(&child)?;
result = &result * child_poly;
}
Some(result)
}
ExprNode::Pow(base, exp) => {
let base_poly = cache.get(base)?;
if contains_id(arena, *exp, var) {
return None; }
let exp_val = expr_to_rational(arena, *exp)?;
if !exp_val.is_integer() {
return None; }
let n: i64 = exp_val.to_integer().try_into().ok()?;
if n < 0 {
return None; }
let mut result = Poly::from_int(1);
for _ in 0..n {
result = &result * base_poly;
}
Some(result)
}
ExprNode::Neg(inner) => {
let inner_poly = cache.get(inner)?;
Some(-inner_poly)
}
ExprNode::Sin(_)
| ExprNode::Cos(_)
| ExprNode::Tan(_)
| ExprNode::Asin(_)
| ExprNode::Acos(_)
| ExprNode::Atan(_)
| ExprNode::Atan2(_, _)
| ExprNode::Sinh(_)
| ExprNode::Cosh(_)
| ExprNode::Tanh(_)
| ExprNode::Asinh(_)
| ExprNode::Acosh(_)
| ExprNode::Atanh(_)
| ExprNode::Exp(_)
| ExprNode::Ln(_)
| ExprNode::Abs(_)
| ExprNode::Sign(_)
| ExprNode::Heaviside(_)
| ExprNode::DiracDelta(_)
| ExprNode::Floor(_)
| ExprNode::Ceiling(_) => {
None
}
ExprNode::Apply(_, _)
| ExprNode::Derivative(_, _)
| ExprNode::Integral(_, _)
| ExprNode::DefiniteIntegral(_, _, _, _) => None,
ExprNode::Min(_)
| ExprNode::Max(_)
| ExprNode::Sum(_, _, _, _)
| ExprNode::Product_(_, _, _, _) => None,
ExprNode::Factorial(_) | ExprNode::Binomial(_, _) => None,
ExprNode::Gamma(_)
| ExprNode::LogGamma(_)
| ExprNode::Digamma(_)
| ExprNode::Erf(_)
| ExprNode::Erfc(_)
| ExprNode::LambertW(_)
| ExprNode::Beta(_, _)
| ExprNode::Re(_)
| ExprNode::Im(_)
| ExprNode::Conjugate(_)
| ExprNode::Arg(_)
| ExprNode::Si(_)
| ExprNode::Ci(_)
| ExprNode::Ei(_)
| ExprNode::Li(_)
| ExprNode::Zeta(_)
| ExprNode::Polygamma(_, _)
| ExprNode::KroneckerDelta(_, _) => None,
ExprNode::BoolTrue
| ExprNode::BoolFalse
| ExprNode::Gt(_, _)
| ExprNode::Ge(_, _)
| ExprNode::Eq_(_, _)
| ExprNode::Ne(_, _)
| ExprNode::And(_)
| ExprNode::Or(_)
| ExprNode::Not(_)
| ExprNode::Piecewise(_) => None,
ExprNode::EmptySet
| ExprNode::UniversalSet
| ExprNode::Interval(_, _, _)
| ExprNode::FiniteSet(_)
| ExprNode::SetUnion(_)
| ExprNode::SetIntersection(_)
| ExprNode::SetComplement(_, _) => None,
ExprNode::Limit(_, _, _)
| ExprNode::Series(_, _, _, _)
| ExprNode::LaplaceTransform(_, _, _)
| ExprNode::InverseLaplaceTransform(_, _, _)
| ExprNode::Residue(_, _, _)
| ExprNode::RootOf(_, _)
| ExprNode::DSolve(_, _, _)
| ExprNode::RootSum(_, _, _)
| ExprNode::ConditionSet(_, _) => None,
}
}
fn expr_to_rational(arena: &Arena, id: ExprId) -> Option<Ratio<BigInt>> {
match arena.node(id) {
ExprNode::Num(nid) => Some(arena.num(*nid).clone()),
_ => None,
}
}
fn contains_id(arena: &Arena, haystack: ExprId, needle: ExprId) -> bool {
if haystack == needle {
return true;
}
let mut visited: FxHashMap<ExprId, ()> = FxHashMap::default();
let mut stack: Vec<ExprId> = vec![haystack];
while let Some(id) = stack.pop() {
if id == needle {
return true;
}
if visited.contains_key(&id) {
continue;
}
visited.insert(id, ());
let children = arena.node(id).children();
stack.extend_from_slice(&children);
}
false
}
pub(crate) fn poly_to_expr(arena: &mut Arena, poly: &Poly, var: ExprId) -> ExprId {
if poly.is_zero() {
return arena.zero;
}
let coeffs = poly.coeffs();
let mut terms: SmallVec<[ExprId; 8]> = SmallVec::new();
for (i, c) in coeffs.iter().enumerate() {
if c.is_zero() {
continue;
}
let coeff_id = {
let nid = arena.intern_num(c.clone());
arena.intern(ExprNode::Num(nid))
};
if i == 0 {
terms.push(coeff_id);
} else {
let power = if i == 1 {
var
} else {
let exp = arena.int(i as i64);
arena.pow(var, exp)
};
if c.is_one() {
terms.push(power);
} else {
terms.push(arena.mul(&[coeff_id, power]));
}
}
}
match terms.len() {
0 => arena.zero,
1 => terms[0],
_ => arena.add(&terms),
}
}
pub(crate) fn ratfn_to_expr(
arena: &mut Arena,
rf: &crate::poly::ratfn::RationalFn,
var: ExprId,
) -> ExprId {
let n = poly_to_expr(arena, rf.numer(), var);
if rf.denom().is_constant() {
let d_val = rf.denom().coeff(0);
if d_val.is_one() {
return n;
}
}
let d = poly_to_expr(arena, rf.denom(), var);
arena.div(n, d)
}
pub(crate) fn expr_to_multipoly(
arena: &Arena,
expr: ExprId,
vars: &[ExprId],
) -> Option<MultiPoly<GrevLex>> {
let nv = vars.len();
let var_index: FxHashMap<ExprId, usize> =
vars.iter().enumerate().map(|(i, &v)| (v, i)).collect();
let post_order = walk::post_order_ids(arena, expr);
let mut cache: FxHashMap<ExprId, MultiPoly<GrevLex>> = FxHashMap::default();
for &id in &post_order {
if let Some(&i) = var_index.get(&id) {
cache.insert(id, MultiPoly::var(nv, i));
continue;
}
let poly = match arena.node(id) {
ExprNode::Num(nid) => MultiPoly::constant(nv, arena.num(*nid).clone()),
ExprNode::Add(children) => {
let mut acc = MultiPoly::zero(nv);
for c in children.iter() {
acc = acc.add(cache.get(c)?);
}
acc
}
ExprNode::Mul(children) => {
let mut acc = MultiPoly::from_int(nv, 1);
for c in children.iter() {
acc = acc.mul(cache.get(c)?);
}
acc
}
ExprNode::Pow(base, exp) => {
let base_poly = cache.get(base)?;
let exp_val = expr_to_rational(arena, *exp)?;
if !exp_val.is_integer() {
return None;
}
let n: u32 = exp_val.to_integer().try_into().ok()?;
let mut acc = MultiPoly::from_int(nv, 1);
for _ in 0..n {
acc = acc.mul(base_poly);
}
acc
}
ExprNode::Neg(inner) => cache.get(inner)?.neg(),
_ => return None,
};
cache.insert(id, poly);
}
cache.remove(&expr)
}
pub(crate) fn multipoly_to_expr(
arena: &mut Arena,
poly: &MultiPoly<GrevLex>,
vars: &[ExprId],
) -> ExprId {
let mut terms: Vec<ExprId> = Vec::with_capacity(poly.num_terms());
for (exp, c) in poly.terms() {
let mut factors: SmallVec<[ExprId; 6]> = SmallVec::new();
if !c.is_one() {
let nid = arena.intern_num(c.clone());
factors.push(arena.intern(ExprNode::Num(nid)));
}
for (i, &e) in exp.iter().enumerate() {
if e == 0 {
continue;
}
if e == 1 {
factors.push(vars[i]);
} else {
let e_id = arena.int(e as i64);
factors.push(arena.pow(vars[i], e_id));
}
}
let term = match factors.len() {
0 => arena.one,
1 => factors[0],
_ => arena.mul(&factors),
};
terms.push(term);
}
match terms.len() {
0 => arena.zero,
1 => terms[0],
_ => arena.add(&terms),
}
}
pub(crate) fn symbolic_multipoly_terms(
arena: &mut Arena,
expr: ExprId,
gens: &[ExprId],
) -> Option<Vec<(Vec<u32>, ExprId)>> {
let expanded = crate::transforms::expand::expand(arena, expr);
let terms: Vec<ExprId> = match arena.node(expanded) {
ExprNode::Add(children) => children.to_vec(),
_ => vec![expanded],
};
let mut buckets: std::collections::BTreeMap<Vec<u32>, Vec<ExprId>> =
std::collections::BTreeMap::new();
for term in terms {
let (exps, coeff) = term_exponents_coeff(arena, term, gens)?;
buckets.entry(exps).or_default().push(coeff);
}
let mut out = Vec::with_capacity(buckets.len());
for (exps, bucket) in buckets {
let c = if bucket.len() == 1 {
bucket[0]
} else {
arena.add(&bucket)
};
let c = crate::transforms::eval::eval(arena, c);
if !arena.is_zero_structural(c) {
out.push((exps, c));
}
}
Some(out)
}
fn contains_any(arena: &Arena, expr: ExprId, gens: &[ExprId]) -> bool {
if gens.contains(&expr) {
return true;
}
let mut visited: rustc_hash::FxHashSet<ExprId> = rustc_hash::FxHashSet::default();
let mut stack: Vec<ExprId> = vec![expr];
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
if gens.contains(&id) {
return true;
}
stack.extend(arena.node(id).children());
}
false
}
fn term_exponents_coeff(
arena: &mut Arena,
term: ExprId,
gens: &[ExprId],
) -> Option<(Vec<u32>, ExprId)> {
let mut exps = vec![0u32; gens.len()];
if !contains_any(arena, term, gens) {
return Some((exps, term));
}
let mut consts: SmallVec<[ExprId; 4]> = SmallVec::new();
match arena.node(term).clone() {
ExprNode::Mul(children) => {
for &child in &children {
accumulate_factor(arena, child, gens, &mut exps, &mut consts)?;
}
}
_ => accumulate_factor(arena, term, gens, &mut exps, &mut consts)?,
}
let c = match consts.len() {
0 => arena.one,
1 => consts[0],
_ => arena.mul(&consts),
};
Some((exps, c))
}
fn accumulate_factor(
arena: &Arena,
factor: ExprId,
gens: &[ExprId],
exps: &mut [u32],
consts: &mut SmallVec<[ExprId; 4]>,
) -> Option<()> {
if let Some(i) = gens.iter().position(|&g| g == factor) {
exps[i] = exps[i].checked_add(1)?;
return Some(());
}
if !contains_any(arena, factor, gens) {
consts.push(factor);
return Some(());
}
match arena.node(factor) {
ExprNode::Pow(base, exp) => {
let i = gens.iter().position(|g| g == base)?;
let n = arena.as_num(*exp)?;
if !n.is_integer() || n.is_negative() {
return None;
}
let d: u32 = n.to_integer().try_into().ok()?;
exps[i] = exps[i].checked_add(d)?;
Some(())
}
ExprNode::Neg(inner) => {
consts.push(arena.neg_one);
accumulate_factor(arena, *inner, gens, exps, consts)
}
_ => None,
}
}
pub(crate) fn as_numer_denom(arena: &mut Arena, expr: ExprId) -> (ExprId, ExprId) {
let node = arena.node(expr).clone();
match node {
ExprNode::Pow(base, exp) => {
if let Some(r) = arena.as_num(exp) {
let r = r.clone();
if r.is_negative() {
let pos_exp = {
let neg_r = -r;
let nid = arena.intern_num(neg_r);
arena.intern(ExprNode::Num(nid))
};
let denom = arena.pow(base, pos_exp);
return (arena.one, denom);
}
}
(expr, arena.one)
}
ExprNode::Mul(ref children) => {
let children = children.clone();
let mut numer_factors: SmallVec<[ExprId; 6]> = SmallVec::new();
let mut denom_factors: SmallVec<[ExprId; 6]> = SmallVec::new();
for &child in &children {
let (n, d) = as_numer_denom(arena, child);
if n != arena.one {
numer_factors.push(n);
}
if d != arena.one {
denom_factors.push(d);
}
}
let numer = if numer_factors.is_empty() {
arena.one
} else if numer_factors.len() == 1 {
numer_factors[0]
} else {
arena.mul(&numer_factors)
};
let denom = if denom_factors.is_empty() {
arena.one
} else if denom_factors.len() == 1 {
denom_factors[0]
} else {
arena.mul(&denom_factors)
};
(numer, denom)
}
_ => (expr, arena.one),
}
}
pub(crate) fn cancel(arena: &mut Arena, expr: ExprId, var: ExprId) -> ExprId {
let (numer, denom) = as_numer_denom(arena, expr);
if denom == arena.one {
return expr;
}
let numer_poly = match expr_to_poly(arena, numer, var) {
Some(p) => p,
None => return expr,
};
let denom_poly = match expr_to_poly(arena, denom, var) {
Some(p) => p,
None => return expr,
};
let gcd = Poly::gcd(&numer_poly, &denom_poly);
let (mut new_numer, mut new_denom, mut changed) = if gcd.is_constant() && gcd.coeff(0).is_one()
{
(numer_poly, denom_poly, false)
} else {
(numer_poly.div(&gcd), denom_poly.div(&gcd), true)
};
let n_content = new_numer.content();
let d_content = new_denom.content();
if !n_content.is_zero() && !d_content.is_zero() {
let numer_gcd = n_content.numer().gcd(d_content.numer());
let denom_lcm = n_content.denom().lcm(d_content.denom());
let content_gcd = Ratio::new(numer_gcd, denom_lcm);
if !content_gcd.is_one() {
let inv = Ratio::one() / content_gcd;
new_numer = new_numer.scale(&inv);
new_denom = new_denom.scale(&inv);
changed = true;
}
}
if !changed {
return expr;
}
let new_numer_expr = poly_to_expr(arena, &new_numer, var);
if new_denom.is_zero() {
return expr;
}
if new_denom.degree() == Some(0) && new_denom.coeff(0).is_one() {
return new_numer_expr;
}
let new_denom_expr = poly_to_expr(arena, &new_denom, var);
let neg_one = arena.neg_one;
let denom_inv = arena.pow(new_denom_expr, neg_one);
arena.mul(&[new_numer_expr, denom_inv])
}
pub(crate) fn collect(arena: &mut Arena, expr: ExprId, var: ExprId) -> ExprId {
let poly = match expr_to_poly(arena, expr, var) {
Some(p) => p,
None => return expr,
};
poly_to_expr(arena, &poly, var)
}
pub(crate) fn poly_degree(arena: &Arena, expr: ExprId, var: ExprId) -> Option<usize> {
let poly = expr_to_poly(arena, expr, var)?;
poly.degree()
}
pub(crate) fn poly_coefficients(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
) -> Option<Vec<ExprId>> {
let poly = expr_to_poly(arena, expr, var)?;
let rational_coeffs = poly.coeffs();
let mut result = Vec::with_capacity(rational_coeffs.len());
for c in rational_coeffs {
let nid = arena.intern_num(c.clone());
result.push(arena.intern(crate::base::node::ExprNode::Num(nid)));
}
Some(result)
}
pub(crate) fn together(arena: &mut Arena, expr: ExprId) -> ExprId {
let node = arena.node(expr).clone();
let children = match node {
ExprNode::Add(ref ch) => ch.clone(),
_ => return expr,
};
let mut parts: Vec<(ExprId, ExprId)> = Vec::with_capacity(children.len());
let mut all_denom_one = true;
for &child in &children {
let (n, d) = as_numer_denom(arena, child);
if d != arena.one {
all_denom_one = false;
}
parts.push((n, d));
}
if all_denom_one {
return expr;
}
let mut unique_denoms: Vec<ExprId> = Vec::new();
for &(_, d) in &parts {
if d != arena.one && !unique_denoms.contains(&d) {
unique_denoms.push(d);
}
}
let (n, d) = match try_together_poly_lcm(arena, &parts, &unique_denoms) {
Some(nd) => nd,
None => together_product_fallback(arena, &parts, &unique_denoms),
};
arena.div(n, d)
}
pub(crate) fn fraction_parts(arena: &mut Arena, expr: ExprId) -> (ExprId, ExprId) {
let one = arena.one;
let order = walk::post_order_ids(arena, expr);
let mut cache: FxHashMap<ExprId, (ExprId, ExprId)> = FxHashMap::default();
let lookup = |cache: &FxHashMap<ExprId, (ExprId, ExprId)>, id: ExprId| {
cache.get(&id).copied().unwrap_or((id, one))
};
for &id in &order {
let node = arena.node(id).clone();
let parts = match node {
ExprNode::Num(nid) => {
let r = arena.num(nid).clone();
if r.is_integer() {
(id, one)
} else {
let n = arena.big_int(r.numer().clone());
let d = arena.big_int(r.denom().clone());
(n, d)
}
}
ExprNode::Add(children) => {
let parts: Vec<(ExprId, ExprId)> =
children.iter().map(|&c| lookup(&cache, c)).collect();
combine_fraction_sum(arena, &parts)
}
ExprNode::Mul(children) => {
let mut numers: SmallVec<[ExprId; 6]> = SmallVec::new();
let mut denoms: SmallVec<[ExprId; 6]> = SmallVec::new();
for &c in children.iter() {
let (n, d) = lookup(&cache, c);
if n != one {
numers.push(n);
}
if d != one {
denoms.push(d);
}
}
(
product_or_one(arena, &numers),
product_or_one(arena, &denoms),
)
}
ExprNode::Pow(base, exp) => match arena.as_num(exp).cloned() {
Some(k) if k.is_integer() && !k.is_zero() => {
let (bn, bd) = lookup(&cache, base);
if bn == base && bd == one {
if k.is_negative() {
let m = arena.big_int(-k.to_integer());
(one, arena.pow(base, m))
} else {
(id, one)
}
} else {
let m = arena.big_int(k.to_integer().abs());
let bn_m = arena.pow(bn, m);
let bd_m = arena.pow(bd, m);
if k.is_negative() {
(bd_m, bn_m)
} else {
(bn_m, bd_m)
}
}
}
_ => (id, one),
},
ExprNode::Neg(inner) => {
let (n, d) = lookup(&cache, inner);
(arena.neg(n), d)
}
_ => (id, one),
};
cache.insert(id, parts);
}
lookup(&cache, expr)
}
pub(crate) fn together_deep(arena: &mut Arena, expr: ExprId) -> ExprId {
let (n, d) = fraction_parts(arena, expr);
if d == arena.one { n } else { arena.div(n, d) }
}
fn product_or_one(arena: &mut Arena, factors: &[ExprId]) -> ExprId {
match factors.len() {
0 => arena.one,
1 => factors[0],
_ => arena.mul(factors),
}
}
fn combine_fraction_sum(arena: &mut Arena, parts: &[(ExprId, ExprId)]) -> (ExprId, ExprId) {
let one = arena.one;
if parts.iter().all(|&(_, d)| d == one) {
let numers: SmallVec<[ExprId; 6]> = parts.iter().map(|&(n, _)| n).collect();
return (arena.add(&numers), one);
}
let mut unique_denoms: Vec<ExprId> = Vec::new();
for &(_, d) in parts {
if d != one && !unique_denoms.contains(&d) {
unique_denoms.push(d);
}
}
match try_together_poly_lcm(arena, parts, &unique_denoms) {
Some(nd) => nd,
None => together_product_fallback(arena, parts, &unique_denoms),
}
}
fn try_together_poly_lcm(
arena: &mut Arena,
parts: &[(ExprId, ExprId)],
unique_denoms: &[ExprId],
) -> Option<(ExprId, ExprId)> {
if unique_denoms.is_empty() {
return None;
}
let mut all_syms: Vec<ExprId> = Vec::new();
for &d in unique_denoms {
all_syms.extend(walk::free_symbols(arena, d));
}
all_syms.sort_by_key(|id| id.0);
all_syms.dedup();
if all_syms.len() != 1 {
return None;
}
let var = all_syms[0];
let mut denom_poly_map: Vec<(ExprId, Poly)> = Vec::new();
for &d in unique_denoms {
let p = expr_to_poly(arena, d, var)?;
denom_poly_map.push((d, p));
}
let mut lcm = denom_poly_map[0].1.clone();
for (_, p) in &denom_poly_map[1..] {
let g = Poly::gcd(&lcm, p);
if g.is_zero() {
return None;
}
let a_over_g = lcm.div(&g);
lcm = &a_over_g * p;
}
let common_denom_expr = poly_to_expr(arena, &lcm, var);
let one_poly = Poly::constant(Ratio::one());
let mut scaled_numers: SmallVec<[ExprId; 6]> = SmallVec::new();
for &(n, d) in parts {
let d_poly = if d == arena.one {
&one_poly
} else {
match denom_poly_map.iter().find(|(id, _)| *id == d) {
Some((_, p)) => p,
None => return None,
}
};
let scale_poly = lcm.div(d_poly);
if scale_poly.degree() == Some(0) && scale_poly.coeff(0).is_one() {
scaled_numers.push(n);
} else {
let scale_expr = poly_to_expr(arena, &scale_poly, var);
let scaled = arena.mul(&[n, scale_expr]);
scaled_numers.push(scaled);
}
}
let numer_sum = arena.add(&scaled_numers);
Some((numer_sum, common_denom_expr))
}
fn together_product_fallback(
arena: &mut Arena,
parts: &[(ExprId, ExprId)],
unique_denoms: &[ExprId],
) -> (ExprId, ExprId) {
let common_denom = if unique_denoms.len() == 1 {
unique_denoms[0]
} else {
arena.mul(unique_denoms)
};
let mut scaled_numers: SmallVec<[ExprId; 6]> = SmallVec::new();
for &(n, d) in parts {
if d == arena.one {
let scaled = arena.mul(&[n, common_denom]);
scaled_numers.push(scaled);
} else {
let mut other_denoms: Vec<ExprId> = Vec::new();
for &ud in unique_denoms {
if ud != d {
other_denoms.push(ud);
}
}
if other_denoms.is_empty() {
scaled_numers.push(n);
} else {
let scale = if other_denoms.len() == 1 {
other_denoms[0]
} else {
arena.mul(&other_denoms)
};
let scaled = arena.mul(&[n, scale]);
scaled_numers.push(scaled);
}
}
}
let numer_sum = arena.add(&scaled_numers);
(numer_sum, common_denom)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::arena::Arena;
fn sym(a: &mut Arena, name: &str) -> ExprId {
a.symbol(name)
}
fn display(a: &Arena, id: ExprId) -> String {
a.display(id).to_string()
}
#[test]
fn expr_to_poly_constant() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let five = a.int(5);
let p = expr_to_poly(&a, five, x).unwrap();
assert_eq!(format!("{p}"), "5");
}
#[test]
fn expr_to_poly_variable() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let p = expr_to_poly(&a, x, x).unwrap();
assert_eq!(format!("{p}"), "θ");
}
#[test]
fn expr_to_poly_linear() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let three = a.int(3);
let two_x = a.mul(&[two, x]);
let expr = a.add(&[two_x, three]);
let p = expr_to_poly(&a, expr, x).unwrap();
assert_eq!(p.degree(), Some(1));
assert_eq!(format!("{p}"), "2*θ + 3");
}
#[test]
fn expr_to_poly_quadratic() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let x_sq = a.pow(x, two);
let two_x = a.mul(&[two, x]);
let one = a.one;
let expr = a.add(&[x_sq, two_x, one]);
let p = expr_to_poly(&a, expr, x).unwrap();
assert_eq!(p.degree(), Some(2));
let val = p.eval(&Ratio::from_integer(BigInt::from(3)));
assert_eq!(val, Ratio::from_integer(BigInt::from(16)));
}
#[test]
fn expr_to_poly_fails_for_sin() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let expr = a.sin(x);
assert!(expr_to_poly(&a, expr, x).is_none());
}
#[test]
fn expr_to_poly_fails_for_fractional_power() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let half = a.rational(1, 2);
let expr = a.pow(x, half);
assert!(expr_to_poly(&a, expr, x).is_none());
}
#[test]
fn expr_to_poly_fails_for_negative_power() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let neg_one = a.int(-1);
let expr = a.pow(x, neg_one);
assert!(expr_to_poly(&a, expr, x).is_none());
}
#[test]
fn expr_to_poly_product() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.one;
let two = a.int(2);
let x_sq = a.pow(x, two);
let expr = a.sub(x_sq, one);
let p = expr_to_poly(&a, expr, x).unwrap();
assert_eq!(p.degree(), Some(2));
let val_1 = p.eval(&Ratio::from_integer(BigInt::from(1)));
let val_neg1 = p.eval(&Ratio::from_integer(BigInt::from(-1)));
assert!(val_1.is_zero(), "x²-1 at x=1 should be 0");
assert!(val_neg1.is_zero(), "x²-1 at x=-1 should be 0");
}
#[test]
fn poly_to_expr_constant() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let p = Poly::from_int(7);
let expr = poly_to_expr(&mut a, &p, x);
assert_eq!(display(&a, expr), "7");
}
#[test]
fn poly_to_expr_linear() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let p = Poly::from_coeffs(vec![
Ratio::from_integer(BigInt::from(3)),
Ratio::from_integer(BigInt::from(2)),
]);
let expr = poly_to_expr(&mut a, &p, x);
assert_eq!(display(&a, expr), "2*x + 3");
}
#[test]
fn poly_to_expr_zero() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let p = Poly::zero();
let expr = poly_to_expr(&mut a, &p, x);
assert_eq!(expr, a.zero);
}
#[test]
fn poly_to_expr_quadratic() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let p = Poly::from_coeffs(vec![
Ratio::from_integer(BigInt::from(1)),
Ratio::from_integer(BigInt::from(2)),
Ratio::from_integer(BigInt::from(1)),
]);
let expr = poly_to_expr(&mut a, &p, x);
assert_eq!(display(&a, expr), "x^2 + 2*x + 1");
}
#[test]
fn roundtrip_expr_poly_expr() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let three = a.int(3);
let x_sq = a.pow(x, two);
let three_x = a.mul(&[three, x]);
let orig = a.add(&[x_sq, three_x, two]);
let poly = expr_to_poly(&a, orig, x).unwrap();
let rebuilt = poly_to_expr(&mut a, &poly, x);
assert_eq!(orig, rebuilt, "roundtrip should preserve expression");
}
#[test]
fn numer_denom_plain_symbol() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let (n, d) = as_numer_denom(&mut a, x);
assert_eq!(n, x);
assert_eq!(d, a.one);
}
#[test]
fn numer_denom_inverse() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let neg_one = a.int(-1);
let expr = a.pow(x, neg_one); let (n, d) = as_numer_denom(&mut a, expr);
assert_eq!(n, a.one, "numerator of 1/x should be 1");
assert_eq!(d, x, "denominator of 1/x should be x");
}
#[test]
fn numer_denom_fraction() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let y = sym(&mut a, "y");
let expr = a.div(x, y);
let (n, d) = as_numer_denom(&mut a, expr);
assert_eq!(display(&a, n), "x");
assert_eq!(display(&a, d), "y");
}
#[test]
fn cancel_x_squared_minus_1_over_x_minus_1() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let one = a.one;
let x_sq = a.pow(x, two);
let numer = a.sub(x_sq, one);
let denom = a.sub(x, one);
let expr = a.div(numer, denom);
let result = cancel(&mut a, expr, x);
let s = display(&a, result);
assert_eq!(s, "x + 1", "cancel should give x + 1, got: {s}");
}
#[test]
fn cancel_no_common_factor() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let one = a.one;
let x_sq = a.pow(x, two);
let numer = a.add(&[x_sq, one]);
let denom = a.add(&[x, one]);
let expr = a.div(numer, denom);
let result = cancel(&mut a, expr, x);
assert_eq!(result, expr, "no common factor → unchanged");
}
#[test]
fn cancel_already_simple() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let expr = a.add(&[x, a.one]);
let result = cancel(&mut a, expr, x);
assert_eq!(result, expr, "no denominator → unchanged");
}
#[test]
fn cancel_quadratic_common_factor() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let three = a.int(3);
let six = a.int(6);
let x2 = a.pow(x, two);
let x3 = a.pow(x, three);
let six_x2 = a.mul(&[six, x2]);
let eleven = a.int(11);
let eleven_x = a.mul(&[eleven, x]);
let neg_six_x2 = a.neg(six_x2);
let neg_six = a.neg(six);
let numer = a.add(&[x3, neg_six_x2, eleven_x, neg_six]);
let three_x = a.mul(&[three, x]);
let neg_three_x = a.neg(three_x);
let denom = a.add(&[x2, neg_three_x, two]);
let expr = a.div(numer, denom);
let result = cancel(&mut a, expr, x);
let s = display(&a, result);
assert!(
s.contains('x') && s.contains('3'),
"cancel should give x - 3, got: {s}"
);
let ten = a.int(10);
let val = crate::transforms::subs::subs(&mut a, result, x, ten);
assert_eq!(display(&a, val), "7", "at x=10, x-3 = 7");
}
#[test]
fn cancel_with_coefficients() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let x_sq = a.pow(x, two);
let two_x_sq = a.mul(&[two, x_sq]);
let numer = a.sub(two_x_sq, two);
let two_x = a.mul(&[two, x]);
let denom = a.sub(two_x, two);
let expr = a.div(numer, denom);
let result = cancel(&mut a, expr, x);
let s = display(&a, result);
assert_eq!(s, "x + 1", "cancel should give x + 1, got: {s}");
}
#[test]
fn cancel_non_polynomial_returns_unchanged() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let sin_x = a.sin(x);
let expr = a.div(sin_x, x);
let result = cancel(&mut a, expr, x);
assert_eq!(result, expr, "non-polynomial should be unchanged");
}
#[test]
fn cancel_constant_factor() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let two_x = a.mul(&[two, x]);
let two_x_plus_2 = a.add(&[two_x, two]); let frac = a.div(two_x_plus_2, two); let result = cancel(&mut a, frac, x);
let expected = a.add(&[x, a.one]); assert_eq!(result, expected, "(2x+2)/2 should cancel to x+1");
}
#[test]
fn together_uses_lcm_not_product() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let sym_a = sym(&mut a, "a");
let sym_b = sym(&mut a, "b");
let one = a.one;
let two = a.int(2);
let x_minus_1 = a.sub(x, one);
let x_minus_1_sq = a.pow(x_minus_1, two);
let frac1 = a.div(sym_a, x_minus_1); let frac2 = a.div(sym_b, x_minus_1_sq); let sum = a.add(&[frac1, frac2]);
let result = together(&mut a, sum);
let s = display(&a, result);
assert!(
!s.contains("^3"),
"together should use LCM not product. Got: {s}"
);
}
#[test]
fn together_simplifies_common_factors() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let sym_a = sym(&mut a, "a");
let sym_b = sym(&mut a, "b");
let one = a.one;
let x_minus_1 = a.sub(x, one);
let two = a.int(2);
let x_minus_1_sq = a.pow(x_minus_1, two);
let frac1 = a.div(sym_a, x_minus_1);
let frac2 = a.div(sym_b, x_minus_1_sq);
let sum = a.add(&[frac1, frac2]);
let result = together(&mut a, sum);
let result_str = display(&a, result);
assert!(
!result_str.contains("^3"),
"together should use LCM not product. Got: {result_str}"
);
}
}