use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Signed, Zero};
use crate::base::arena::Arena;
use crate::base::node::{ExprId, ExprNode, SymbolId};
use crate::base::numeric::Q;
use crate::poly::Poly;
use crate::poly::polybridge;
#[derive(Clone, Debug)]
pub struct Solution {
pub value: ExprId,
}
#[derive(Clone, Debug)]
pub(crate) enum SolveOutcome {
Solutions(Vec<Solution>),
Identity,
NoSolution(String),
Unresolved(String),
}
impl SolveOutcome {
pub(crate) fn into_solutions(self) -> Vec<Solution> {
match self {
SolveOutcome::Solutions(s) => s,
SolveOutcome::Identity | SolveOutcome::NoSolution(_) | SolveOutcome::Unresolved(_) => {
Vec::new()
}
}
}
}
pub(crate) fn solve(arena: &mut Arena, expr: ExprId, var: ExprId) -> Vec<Solution> {
solve_classified(arena, expr, var).into_solutions()
}
pub(crate) fn solve_classified(arena: &mut Arena, expr: ExprId, var: ExprId) -> SolveOutcome {
solve_impl(arena, expr, var, None)
}
pub(crate) fn solve_general(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
param: ExprId,
) -> SolveOutcome {
solve_impl(arena, expr, var, Some(param))
}
fn solve_impl(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
period: Option<ExprId>,
) -> SolveOutcome {
match solve_raw(arena, expr, var, period) {
SolveOutcome::Solutions(s) => {
let had_candidates = !s.is_empty();
let s = match drop_certain_non_roots(arena, expr, var, s, period) {
Ok(s) => s,
Err(reason) => return SolveOutcome::Unresolved(reason),
};
if had_candidates && s.is_empty() {
if period.is_none()
&& let Some(outcome) = other_branches_outcome(arena, expr, var)
{
return outcome;
}
return SolveOutcome::NoSolution(
"every candidate solution fails the equation (a principal branch does not reach the right-hand side)"
.into(),
);
}
finalize_solutions(arena, s)
}
other => other,
}
}
fn other_branches_outcome(arena: &mut Arena, expr: ExprId, var: ExprId) -> Option<SolveOutcome> {
let member = root_off_principal_branch(arena, expr, var)?;
let shown = arena.display(member).to_string();
Some(SolveOutcome::Unresolved(format!(
"every principal-branch candidate fails the equation (a pole or a branch the \
right-hand side is not on), but other branches solve it (e.g. {shown}); \
see solve_general"
)))
}
fn branch_parameter(arena: &mut Arena, avoid: &[ExprId]) -> ExprId {
use crate::base::assumptions::{Assumption, Assumptions};
let mut declared = Assumptions::default().with(Assumption::Integer);
declared.normalize_declared();
let mut k = 0usize;
loop {
let name = if k == 0 {
"_n".to_owned()
} else {
format!("_n_{k}")
};
k += 1;
match arena.symbols.get(&name) {
None => {
let s = arena.symbol(&name);
if let ExprNode::Symbol(sid) = *arena.node(s) {
arena.set_symbol_assumptions(sid, declared);
}
return s;
}
Some(sid) if arena.symbol_assumptions(sid) == declared => {
let s = arena.intern(ExprNode::Symbol(sid));
if !avoid
.iter()
.any(|&e| crate::base::walk::contains(arena, e, s))
{
return s;
}
}
Some(_) => {}
}
}
}
pub(crate) struct PeriodicCandidates {
pub(crate) plain: Vec<ExprId>,
pub(crate) families: Vec<ExprId>,
pub(crate) param: ExprId,
}
fn raw_families(arena: &mut Arena, expr: ExprId, var: ExprId) -> Option<PeriodicCandidates> {
let n = branch_parameter(arena, &[expr, var]);
let SolveOutcome::Solutions(cands) = solve_raw(arena, expr, var, Some(n)) else {
return None;
};
let mut plain = Vec::new();
let mut families = Vec::new();
for c in cands {
let v = crate::transforms::eval::eval(arena, c.value);
if crate::base::walk::contains(arena, v, n) {
families.push(v);
} else {
plain.push(v);
}
}
Some(PeriodicCandidates {
plain,
families,
param: n,
})
}
pub(crate) fn periodic_candidates(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
) -> Option<PeriodicCandidates> {
raw_families(arena, expr, var).filter(|c| !c.families.is_empty())
}
fn root_off_principal_branch(arena: &mut Arena, expr: ExprId, var: ExprId) -> Option<ExprId> {
let PeriodicCandidates {
families, param: n, ..
} = raw_families(arena, expr, var)?;
for fam in families {
for k in [1i64, -1, 2, -2, 3, -3] {
let kk = arena.int(k);
let member = crate::transforms::subs::subs(arena, fam, n, kk);
let member = crate::transforms::eval::eval(arena, member);
if member_is_root(arena, expr, var, member) {
return Some(member);
}
}
}
None
}
fn member_is_root(arena: &mut Arena, expr: ExprId, var: ExprId, candidate: ExprId) -> bool {
use crate::base::walk;
if !walk::free_symbols(arena, candidate).is_empty() || walk::has_unevaluated(arena, candidate) {
return false;
}
let at = crate::transforms::subs::subs(arena, expr, var, candidate);
let at = crate::transforms::eval::eval(arena, at);
if arena.is_zero_structural(at) {
return true;
}
if !walk::free_symbols(arena, at).is_empty() || walk::has_unevaluated(arena, at) {
return false;
}
let undefined = walk::post_order_ids(arena, at).into_iter().any(|id| {
matches!(
arena.node(id),
ExprNode::Infinity | ExprNode::NegInfinity | ExprNode::ComplexInfinity | ExprNode::NaN
)
});
if undefined {
return false;
}
use crate::transforms::evalf::{Settled, ZeroSearch};
match crate::transforms::evalf::evalf_settled(arena, at, ROOT_CHECK_DIGITS, ZeroSearch::Cap) {
Ok((z, Settled::Certified)) => z.0.is_zero() && z.1.is_zero(),
Ok((_, Settled::ZeroToPrecision)) => true,
Err(_) => false,
}
}
fn drop_certain_non_roots(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
solutions: Vec<Solution>,
period: Option<ExprId>,
) -> Result<Vec<Solution>, String> {
if solutions.is_empty() || polybridge::expr_to_poly(arena, expr, var).is_some() {
return Ok(solutions);
}
let mut out = Vec::with_capacity(solutions.len());
for s in solutions {
match period {
Some(n) if crate::base::walk::contains(arena, s.value, n) => {
for value in check_family(arena, expr, var, s.value, n)? {
out.push(Solution { value });
}
}
_ => {
if !certainly_not_a_root(arena, expr, var, s.value) {
out.push(s);
}
}
}
}
Ok(out)
}
fn check_family(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
candidate: ExprId,
n: ExprId,
) -> Result<Vec<ExprId>, String> {
let member = |arena: &mut Arena, k: i64| -> ExprId {
let kk = arena.int(k);
let m = crate::transforms::subs::subs(arena, candidate, n, kk);
crate::transforms::eval::eval(arena, m)
};
let step_over_pi = {
let s0 = member(arena, 0);
let s1 = member(arena, 1);
let s2 = member(arena, 2);
let d = arena.sub(s1, s0);
let d = crate::transforms::eval::eval(arena, d);
let d2 = arena.sub(s2, s1);
let d2 = crate::transforms::eval::eval(arena, d2);
let ratio = arena.div(d, arena.pi);
let ratio = crate::transforms::eval::eval(arena, ratio);
if d == d2 {
arena.as_num(ratio).cloned()
} else {
None
}
};
if let (Some(t), Some(r)) = (structural_period(arena, expr, var), step_over_pi)
&& !r.is_zero()
{
let m = t / r.abs();
if m.is_integer() && m.is_positive() && m <= Ratio::from_integer(BigInt::from(12)) {
let m = m.to_integer();
let m_i64: i64 = num_traits::ToPrimitive::to_i64(&m).unwrap_or(1);
let mut keep = Vec::new();
for j in 0..m_i64 {
let mj = member(arena, j);
if certainly_not_a_root(arena, expr, var, mj) {
continue;
}
if m_i64 == 1 {
keep.push(candidate);
continue;
}
let m_id = arena.big_int(m.clone());
let mn = arena.mul(&[m_id, n]);
let j_id = arena.int(j);
let shifted = arena.add(&[mn, j_id]);
let sub = crate::transforms::subs::subs(arena, candidate, n, shifted);
let sub = crate::transforms::expand::expand(arena, sub);
keep.push(crate::transforms::eval::eval(arena, sub));
}
return Ok(keep);
}
}
let mut failing: Option<ExprId> = None;
let mut passing = false;
for k in [0i64, 1, -1, 2, -2, 3, -3] {
let mk = member(arena, k);
if certainly_not_a_root(arena, expr, var, mk) {
failing.get_or_insert(mk);
} else {
passing = true;
}
}
if !passing {
return Ok(Vec::new());
}
if failing.is_none() {
failing = singular_member(arena, expr, var, candidate, n);
}
match failing {
None => Ok(vec![candidate]),
Some(bad) => Err(format!(
"the solution family {} contains {}, which is not a solution (a pole, or a branch the \
equation is not on); a family over all integers {} cannot exclude it",
arena.display(candidate),
arena.display(bad),
arena.display(n),
)),
}
}
fn structural_period(arena: &mut Arena, expr: ExprId, var: ExprId) -> Option<Q> {
let mut period: Option<Q> = None;
let mut stack = vec![expr];
while let Some(id) = stack.pop() {
if !expr_contains_var(arena, id, var) {
continue;
}
if id == var {
return None;
}
let node = arena.node(id).clone();
let atom = match node {
ExprNode::Sin(arg) | ExprNode::Cos(arg) => Some((arg, 2i64)),
ExprNode::Tan(arg) => Some((arg, 1i64)),
ExprNode::Exp(arg) => Some((arg, -2i64)),
_ => None,
};
match atom {
Some((arg, half_turns)) => {
let (a, _) = linear_rate(arena, arg, var)?;
let a = if half_turns < 0 {
let i = arena.i_unit;
let r = arena.div(a, i);
crate::transforms::eval::eval(arena, r)
} else {
a
};
let half_turns = half_turns.abs();
let a = arena.as_num(a)?.clone();
if a.is_zero() {
return None;
}
let p = Ratio::from_integer(BigInt::from(half_turns)) / a.abs();
period = Some(match period {
None => p,
Some(q) => rational_lcm(&q, &p),
});
}
None => stack.extend(arena.children(id)),
}
}
period
}
fn rational_lcm(x: &Q, y: &Q) -> Q {
use num_integer::Integer;
let num = x.numer().lcm(y.numer());
let den = x.denom().gcd(y.denom());
Ratio::new(num, den)
}
fn singular_member(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
candidate: ExprId,
n: ExprId,
) -> Option<ExprId> {
let mut points: Vec<ExprId> = Vec::new();
for id in crate::base::walk::post_order_ids(arena, expr) {
let base = match *arena.node(id) {
ExprNode::Pow(b, e) if arena.as_num(e).is_some_and(|r| r.is_negative()) => b,
ExprNode::Ln(b) => b,
_ => continue,
};
if !expr_contains_var(arena, base, var) {
continue;
}
if let Some(p) = polybridge::expr_to_poly(arena, base, var) {
if !p.is_constant() {
points.extend(
solve_rational_poly(arena, var, &p)
.into_iter()
.map(|s| s.value),
);
}
continue;
}
let Some(coeffs) = symbolic_poly_coeffs(arena, base, var) else {
continue;
};
if coeffs.len() > 3 || coeffs.iter().any(|&c| expr_contains_var(arena, c, var)) {
continue;
}
if let Some(sols) = solve_symbolic_coeffs(arena, &coeffs) {
points.extend(sols.into_iter().map(|s| s.value));
}
}
let k0 = {
let z = arena.zero;
let m = crate::transforms::subs::subs(arena, candidate, n, z);
crate::transforms::eval::eval(arena, m)
};
let k1 = {
let o = arena.one;
let m = crate::transforms::subs::subs(arena, candidate, n, o);
crate::transforms::eval::eval(arena, m)
};
let d = arena.sub(k1, k0);
let d = crate::transforms::eval::eval(arena, d);
for p in points {
let ratio = arena.sub(p, k0);
let ratio = arena.div(ratio, d);
let ratio = crate::transforms::eval::eval(arena, ratio);
let Ok(s) = crate::transforms::evalf::evalf(arena, ratio, 20) else {
continue;
};
let Ok(v) = s.trim().parse::<f64>() else {
continue;
};
if !v.is_finite() || (v - v.round()).abs() > 1e-9 || v.abs() > 1e12 {
continue;
}
let kk = arena.int(v.round() as i64);
let m = crate::transforms::subs::subs(arena, candidate, n, kk);
let m = crate::transforms::eval::eval(arena, m);
if certainly_not_a_root(arena, expr, var, m) {
return Some(m);
}
}
None
}
pub(crate) fn certainly_not_a_root(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
candidate: ExprId,
) -> bool {
use crate::base::walk;
if !walk::free_symbols(arena, candidate).is_empty() || walk::has_unevaluated(arena, candidate) {
return false;
}
let at = crate::transforms::subs::subs(arena, expr, var, candidate);
let at = crate::transforms::eval::eval(arena, at);
if !walk::free_symbols(arena, at).is_empty() || walk::has_unevaluated(arena, at) {
return false;
}
let undefined = walk::post_order_ids(arena, at).into_iter().any(|id| {
matches!(
arena.node(id),
ExprNode::Infinity | ExprNode::NegInfinity | ExprNode::ComplexInfinity | ExprNode::NaN
)
});
if undefined {
return true;
}
use crate::transforms::evalf::{Settled, ZeroSearch};
match crate::transforms::evalf::evalf_settled(arena, at, ROOT_CHECK_DIGITS, ZeroSearch::Cap) {
Ok((z, Settled::Certified)) => !(z.0.is_zero() && z.1.is_zero()),
Ok((_, Settled::ZeroToPrecision)) | Err(_) => false,
}
}
const ROOT_CHECK_DIGITS: u32 = 20;
fn solve_raw(arena: &mut Arena, expr: ExprId, var: ExprId, period: Option<ExprId>) -> SolveOutcome {
if arena.is_zero_structural(expr) {
return SolveOutcome::Identity;
}
if !expr_contains_var(arena, expr, var) {
return classify_constant(arena, expr, var);
}
let parametric_rational = matches!(arena.node(expr), ExprNode::Mul(_))
&& is_rational_function_of(arena, expr, var)
&& crate::base::walk::free_symbols(arena, expr)
.iter()
.any(|&s| s != var)
&& rational_over_q_in_all_symbols(arena, expr);
if let ExprNode::Mul(ref children) = arena.node(expr).clone()
&& !parametric_rational
{
let mut solutions: Vec<Solution> = Vec::new();
let mut any_identity = false;
let mut unsolved = false;
let mut denominator = Poly::one();
for &child in children {
if let ExprNode::Pow(b, e) = *arena.node(child)
&& arena
.as_num(e)
.is_some_and(|r| r.is_integer() && r.is_negative())
&& let Some(q) = polybridge::expr_to_poly(arena, b, var)
&& !q.is_constant()
{
denominator = denominator.mul(&q);
}
}
for &child in children {
if !expr_contains_var(arena, child, var) {
continue;
}
let outcome = match polybridge::expr_to_poly(arena, child, var) {
Some(p) if !denominator.is_constant() && !p.is_constant() => {
let p = without_common_factors(p, &denominator);
if p.is_constant() {
SolveOutcome::NoSolution("every root of the factor is a pole".into())
} else {
SolveOutcome::Solutions(solve_rational_poly(arena, var, &p))
}
}
_ => solve_raw(arena, child, var, period),
};
match outcome {
SolveOutcome::Solutions(child_solutions) => {
unsolved |= child_solutions.is_empty();
for sol in child_solutions {
if !solutions.iter().any(|s| s.value == sol.value) {
solutions.push(sol);
}
}
}
SolveOutcome::Identity => any_identity = true,
SolveOutcome::NoSolution(_) => {}
SolveOutcome::Unresolved(_) => unsolved = true,
}
}
if any_identity {
return SolveOutcome::Identity;
}
if !solutions.is_empty() && !unsolved {
match drop_poles_of_product(arena, expr, var, solutions) {
Ok(s) if !s.is_empty() => return SolveOutcome::Solutions(s),
Ok(_) => {
if period.is_none()
&& let Some(outcome) = other_branches_outcome(arena, expr, var)
{
return outcome;
}
return SolveOutcome::NoSolution(
"every root of a factor is a pole of another factor".into(),
);
}
Err(reason) => return SolveOutcome::Unresolved(reason),
}
}
}
if let Some(poly) = polybridge::expr_to_poly(arena, expr, var) {
if poly.is_zero() {
return SolveOutcome::Identity;
}
if poly.is_constant() {
let c = arena.display(expr).to_string();
return SolveOutcome::NoSolution(format!("equation reduces to {c} = 0"));
}
return SolveOutcome::Solutions(solve_rational_poly(arena, var, &poly));
}
if let Some(solutions) = try_solve_linear_symbolic(arena, expr, var)
&& !solutions.is_empty()
{
return SolveOutcome::Solutions(solutions);
}
if let Some(solutions) = try_solve_symbolic_poly(arena, expr, var)
&& !solutions.is_empty()
{
return SolveOutcome::Solutions(solutions);
}
if let Some(solutions) = try_solve_parametric_factors(arena, expr, var, period) {
return SolveOutcome::Solutions(solutions);
}
match try_solve_rational(arena, expr, var, period) {
Some(SolveOutcome::Solutions(solutions)) if !solutions.is_empty() => {
return SolveOutcome::Solutions(solutions);
}
Some(SolveOutcome::NoSolution(reason)) => return SolveOutcome::NoSolution(reason),
Some(SolveOutcome::Unresolved(reason)) => return SolveOutcome::Unresolved(reason),
_ => {}
}
let mut domain_empty = false;
match try_solve_by_inversion(arena, expr, var, period) {
Some(solutions) if !solutions.is_empty() => {
return SolveOutcome::Solutions(solutions);
}
Some(_) => domain_empty = true, None => {}
}
if let Some(solutions) = try_change_of_variable(arena, expr, var, period)
&& !solutions.is_empty()
{
return SolveOutcome::Solutions(solutions);
}
if let Some(solutions) = try_radical_substitution(arena, expr, var)
&& !solutions.is_empty()
{
return SolveOutcome::Solutions(solutions);
}
if let Some(solutions) = try_square_root_elimination(arena, expr, var)
&& !solutions.is_empty()
{
return SolveOutcome::Solutions(solutions);
}
if let Some(solutions) = try_exponential_substitution(arena, expr, var, period)
&& !solutions.is_empty()
{
return SolveOutcome::Solutions(solutions);
}
if let Some(solutions) = try_half_angle_substitution(arena, expr, var, period)
&& !solutions.is_empty()
{
return SolveOutcome::Solutions(solutions);
}
if let Some(solutions) = try_log_sum(arena, expr, var, period)
&& !solutions.is_empty()
{
return SolveOutcome::Solutions(solutions);
}
let var_sym_opt = match arena.node(var) {
ExprNode::Symbol(sid) => Some(*sid),
_ => None,
};
if let Some(var_sym) = var_sym_opt
&& let Some(solutions) = try_solve_lambert(arena, expr, var, var_sym)
&& !solutions.is_empty()
{
return SolveOutcome::Solutions(solutions);
}
if domain_empty {
return SolveOutcome::NoSolution(
"equation has no real solutions (range restriction of exp/sin/cos/cosh/abs)".into(),
);
}
SolveOutcome::Solutions(Vec::new())
}
fn finalize_solutions(arena: &mut Arena, solutions: Vec<Solution>) -> SolveOutcome {
let had_candidates = !solutions.is_empty();
let mut out: Vec<Solution> = Vec::with_capacity(solutions.len());
for s in solutions {
let v = crate::transforms::eval::eval(arena, s.value);
let infinite = matches!(
arena.node(v),
ExprNode::Infinity | ExprNode::NegInfinity | ExprNode::ComplexInfinity | ExprNode::NaN
);
if infinite {
continue;
}
if !out.iter().any(|o| o.value == v) {
out.push(Solution { value: v });
}
}
if had_candidates && out.is_empty() {
return SolveOutcome::NoSolution(
"every candidate solution is infinite or undefined (e.g. 1/x = 0)".into(),
);
}
SolveOutcome::Solutions(out)
}
fn solve_rational_poly(arena: &mut Arena, var: ExprId, poly: &Poly) -> Vec<Solution> {
let degree = poly.degree().unwrap_or(0);
tracing::debug!(degree = degree, "polynomial degree determined");
match degree {
0 => Vec::new(),
1 => solve_linear(arena, poly),
2 => solve_quadratic(arena, poly),
3 => solve_cubic(arena, var, poly),
4 => solve_quartic(arena, var, poly),
_ => solve_rational_roots(arena, var, poly),
}
}
fn classify_constant(arena: &mut Arena, expr: ExprId, var: ExprId) -> SolveOutcome {
let var_name = arena.display(var).to_string();
let evaled = crate::transforms::eval::eval(arena, expr);
if arena.is_zero_structural(evaled) {
return SolveOutcome::Identity;
}
if let Some(r) = arena.as_num(evaled)
&& !r.is_zero()
{
return SolveOutcome::NoSolution(format!("equation reduces to {r} = 0"));
}
let expanded = crate::transforms::expand::expand(arena, evaled);
let expanded = crate::transforms::eval::eval(arena, expanded);
if arena.is_zero_structural(expanded) {
return SolveOutcome::Identity;
}
if let Some(r) = arena.as_num(expanded)
&& !r.is_zero()
{
return SolveOutcome::NoSolution(format!("equation reduces to {r} = 0"));
}
if crate::base::walk::free_symbols(arena, expanded).is_empty() {
match crate::poly::algebraic::is_zero_checked(arena, expanded) {
Some(false) => {
let shown = arena.display(expr).to_string();
return SolveOutcome::NoSolution(format!(
"equation reduces to the nonzero constant {shown} = 0"
));
}
Some(true) => return SolveOutcome::Identity,
None => {
use crate::transforms::evalf::{Settled, ZeroSearch};
if let Ok((_, Settled::ZeroToPrecision)) =
crate::transforms::evalf::evalf_settled(arena, expanded, 20, ZeroSearch::Deep)
{
return SolveOutcome::Identity;
}
}
}
}
let shown = arena.display(expr).to_string();
SolveOutcome::NoSolution(format!(
"expression {shown} does not depend on {var_name} and is not identically zero"
))
}
fn expr_contains_var(arena: &Arena, expr: ExprId, var: ExprId) -> bool {
crate::base::walk::contains(arena, expr, var)
}
pub(crate) fn symbolic_poly_coeffs(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
) -> Option<Vec<ExprId>> {
let expanded = crate::transforms::expand::expand(arena, expr);
let terms: Vec<ExprId> = match arena.node(expanded).clone() {
ExprNode::Add(children) => children.to_vec(),
_ => vec![expanded],
};
let mut buckets: Vec<Vec<ExprId>> = Vec::new();
for term in terms {
let (deg, coeff) = term_degree_coeff(arena, term, var)?;
if buckets.len() <= deg {
buckets.resize_with(deg + 1, Vec::new);
}
buckets[deg].push(coeff);
}
let mut coeffs = Vec::with_capacity(buckets.len());
for bucket in buckets {
let c = match bucket.len() {
0 => arena.zero,
1 => bucket[0],
_ => arena.add(&bucket),
};
coeffs.push(crate::transforms::eval::eval(arena, c));
}
while coeffs.len() > 1 && arena.is_zero_structural(*coeffs.last()?) {
coeffs.pop();
}
Some(coeffs)
}
fn term_degree_coeff(arena: &mut Arena, term: ExprId, var: ExprId) -> Option<(usize, ExprId)> {
if term == var {
return Some((1, arena.one));
}
if !expr_contains_var(arena, term, var) {
return Some((0, term));
}
match arena.node(term).clone() {
ExprNode::Pow(base, exp) if base == var => {
let n = arena.as_num(exp)?.clone();
if !n.is_integer() || n.is_negative() {
return None;
}
let d: usize = n.to_integer().try_into().ok()?;
Some((d, arena.one))
}
ExprNode::Neg(inner) => {
let (d, c) = term_degree_coeff(arena, inner, var)?;
Some((d, arena.neg(c)))
}
ExprNode::Mul(children) => {
let mut deg = 0usize;
let mut consts: Vec<ExprId> = Vec::new();
for &child in &children {
if !expr_contains_var(arena, child, var) {
consts.push(child);
} else if child == var {
deg += 1;
} else if let ExprNode::Pow(base, exp) = arena.node(child).clone()
&& base == var
{
let n = arena.as_num(exp)?.clone();
if !n.is_integer() || n.is_negative() {
return None;
}
let d: usize = n.to_integer().try_into().ok()?;
deg += d;
} else {
return None;
}
}
let c = match consts.len() {
0 => arena.one,
1 => consts[0],
_ => arena.mul(&consts),
};
Some((deg, c))
}
_ => None,
}
}
fn try_solve_symbolic_poly(arena: &mut Arena, expr: ExprId, var: ExprId) -> Option<Vec<Solution>> {
let coeffs = symbolic_poly_coeffs(arena, expr, var)?;
solve_symbolic_coeffs(arena, &coeffs)
}
fn solve_symbolic_coeffs(arena: &mut Arena, coeffs: &[ExprId]) -> Option<Vec<Solution>> {
let degree = coeffs.len().checked_sub(1)?;
match degree {
0 => None,
1 => {
let a = coeffs[1];
let b = coeffs[0];
let neg_b = arena.neg(b);
let v = arena.div(neg_b, a);
let v = crate::transforms::eval::eval(arena, v);
let v = crate::simplify::ratsimp::ratsimp(arena, v);
Some(vec![Solution { value: v }])
}
2 => {
let a = coeffs[2];
let b = coeffs[1];
let c = coeffs[0];
if arena.is_zero_structural(b) {
let neg_c = arena.neg(c);
let ratio = arena.div(neg_c, a);
let ratio = crate::transforms::eval::eval(arena, ratio);
let root = arena.sqrt(ratio);
let root = crate::transforms::eval::eval(arena, root);
let neg_root = arena.neg(root);
return Some(vec![Solution { value: root }, Solution { value: neg_root }]);
}
let two = arena.int(2);
let four = arena.int(4);
let b_sq = arena.pow(b, two);
let four_ac = arena.mul(&[four, a, c]);
let disc = arena.sub(b_sq, four_ac);
let disc = crate::transforms::eval::eval(arena, disc);
let disc = crate::simplify::ratsimp::ratsimp(arena, disc);
let sqrt_disc = arena.sqrt(disc);
let neg_b = arena.neg(b);
let two_a = arena.mul(&[two, a]);
let num1 = arena.add(&[neg_b, sqrt_disc]);
let num2 = arena.sub(neg_b, sqrt_disc);
let x1 = arena.div(num1, two_a);
let x2 = arena.div(num2, two_a);
let x1 = crate::transforms::eval::eval(arena, x1);
let x2 = crate::transforms::eval::eval(arena, x2);
if x1 == x2 {
Some(vec![Solution { value: x1 }])
} else {
Some(vec![Solution { value: x1 }, Solution { value: x2 }])
}
}
_ => {
let middle_zero = coeffs[1..degree]
.iter()
.all(|&c| arena.is_zero_structural(c));
if !middle_zero {
return None;
}
let a = coeffs[degree];
let b = coeffs[0];
let neg_b = arena.neg(b);
let ratio = arena.div(neg_b, a);
let ratio = crate::transforms::eval::eval(arena, ratio);
Some(binomial_roots(arena, ratio, degree))
}
}
}
fn binomial_roots(arena: &mut Arena, c: ExprId, n: usize) -> Vec<Solution> {
if arena.is_zero_structural(c) {
return vec![Solution { value: arena.zero }];
}
let negative_real = arena.as_num(c).is_some_and(|r| r.is_negative());
let (radicand, angle_offset) = if negative_real {
(arena.neg(c), 1i64)
} else {
(c, 0i64)
};
let inv_n = arena.rational(1, n as i64);
let magnitude = arena.pow(radicand, inv_n);
let magnitude = crate::transforms::eval::eval(arena, magnitude);
let pi = arena.pi;
let i_unit = arena.i_unit;
let mut roots: Vec<Solution> = Vec::with_capacity(n);
for k in 0..n {
let numer = 2 * k as i64 + angle_offset;
let root = if numer == 0 {
magnitude
} else {
let frac = arena.rational(numer, n as i64);
let angle = arena.mul(&[frac, pi]);
let cos_a = arena.cos(angle);
let sin_a = arena.sin(angle);
let i_sin = arena.mul(&[i_unit, sin_a]);
let omega = arena.add(&[cos_a, i_sin]);
let prod = arena.mul(&[magnitude, omega]);
let prod = crate::transforms::eval::eval(arena, prod);
let prod = crate::transforms::expand::expand(arena, prod);
crate::transforms::eval::eval(arena, prod)
};
if !roots.iter().any(|r| r.value == root) {
roots.push(Solution { value: root });
}
}
roots
}
fn try_solve_binomial_rational(arena: &mut Arena, poly: &Poly) -> Option<Vec<Solution>> {
let n = poly.degree()?;
if n < 3 {
return None;
}
for i in 1..n {
if !poly.coeff(i).is_zero() {
return None;
}
}
let a = poly.coeff(n);
let b = poly.coeff(0);
if a.is_zero() {
return None;
}
let ratio = -b / a;
let c = arena.num_ratio(ratio.clone());
Some(binomial_roots(arena, c, n))
}
fn real_for_real_var(arena: &mut Arena, e: ExprId, var: ExprId) -> bool {
use crate::base::assumptions::{AssumptionCache, Assumptions, Props};
let mut cache = AssumptionCache::new();
for s in crate::base::walk::free_symbols(arena, e) {
let ExprNode::Symbol(sid) = *arena.node(s) else {
continue;
};
let declared = arena.symbol_assumptions(sid);
if s == var || declared == Assumptions::default() {
let mut real = declared;
real.known_true |= Props::REAL;
if !real.is_contradictory() {
cache.set_symbol_assumptions(s, real);
}
}
}
cache.query(arena, e, Props::REAL) == Some(true)
}
fn two_pi_i_n(arena: &mut Arena, n: ExprId) -> ExprId {
let two = arena.int(2);
let pi = arena.pi;
let i = arena.i_unit;
arena.mul(&[two, pi, i, n])
}
fn try_solve_by_inversion(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
period: Option<ExprId>,
) -> Option<Vec<Solution>> {
if !expr_contains_var(arena, expr, var) {
return None;
}
let zero = arena.zero;
solve_by_peeling(arena, expr, zero, var, period)
}
fn solve_by_peeling(
arena: &mut Arena,
lhs: ExprId,
rhs: ExprId,
var: ExprId,
period: Option<ExprId>,
) -> Option<Vec<Solution>> {
if lhs == var {
return Some(vec![Solution { value: rhs }]);
}
let node = arena.node(lhs).clone();
match node {
ExprNode::Add(ref children) => {
let mut dep = Vec::new();
let mut indep = Vec::new();
for &child in children {
if expr_contains_var(arena, child, var) {
dep.push(child);
} else {
indep.push(child);
}
}
if dep.len() == 1 {
let new_rhs = if indep.is_empty() {
rhs
} else {
let sum_indep = arena.add(&indep);
arena.sub(rhs, sum_indep)
};
return solve_by_peeling(arena, dep[0], new_rhs, var, period);
}
let diff = arena.sub(lhs, rhs);
let diff = crate::transforms::eval::eval(arena, diff);
if let Some(poly) = polybridge::expr_to_poly(arena, diff, var) {
if poly.is_zero() || poly.is_constant() {
return None;
}
return Some(solve_rational_poly(arena, var, &poly));
}
if let Some(sols) = try_solve_linear_symbolic(arena, diff, var) {
return Some(sols);
}
try_solve_symbolic_poly(arena, diff, var)
}
ExprNode::Mul(ref children) => {
let mut dep = Vec::new();
let mut indep = Vec::new();
for &child in children {
if expr_contains_var(arena, child, var) {
dep.push(child);
} else {
indep.push(child);
}
}
if dep.len() != 1 || indep.is_empty() {
return None;
}
let coeff = arena.mul(&indep);
let mut new_rhs = arena.div(rhs, coeff);
if crate::base::walk::contains(arena, coeff, arena.i_unit) {
let e = crate::transforms::eval::eval(arena, new_rhs);
let e = crate::transforms::expand::expand(arena, e);
new_rhs = crate::transforms::eval::eval(arena, e);
}
solve_by_peeling(arena, dep[0], new_rhs, var, period)
}
ExprNode::Exp(inner) => {
if rhs == arena.zero {
tracing::debug!("solve_by_peeling: exp domain error, rhs = 0");
return Some(vec![]);
}
let real_inner = real_for_real_var(arena, inner, var);
if real_inner
&& let Some(c) = arena.as_num(rhs)
&& !c.is_positive()
{
tracing::debug!("solve_by_peeling: exp domain error, rhs <= 0");
return Some(vec![]);
}
let ln_rhs = principal_ln(arena, rhs);
let new_rhs = match period {
Some(n) if !real_inner => {
let p = two_pi_i_n(arena, n);
arena.add(&[ln_rhs, p])
}
_ => ln_rhs,
};
solve_by_peeling(arena, inner, new_rhs, var, period)
}
ExprNode::Ln(inner) => {
let new_rhs = arena.exp(rhs);
solve_by_peeling(arena, inner, new_rhs, var, period)
}
ExprNode::Sin(inner) => {
if let Some(c) = arena.as_num(rhs)
&& c.abs() > Ratio::one()
&& real_for_real_var(arena, inner, var)
{
tracing::debug!("solve_by_peeling: sin domain error, |c| > 1");
return Some(vec![]);
}
tracing::debug!("solve_by_peeling: inverting sin, two branches");
let asin_rhs = arena.asin(rhs);
let pi = arena.pi;
let pi_minus_asin = arena.sub(pi, asin_rhs);
let (b1, b2) = match period {
Some(n) => {
let two = arena.int(2);
let two_pi_n = arena.mul(&[two, pi, n]);
(
arena.add(&[asin_rhs, two_pi_n]),
arena.add(&[pi_minus_asin, two_pi_n]),
)
}
None => (asin_rhs, pi_minus_asin),
};
peel_two_branches(arena, inner, b1, b2, var, period)
}
ExprNode::Cos(inner) => {
if let Some(c) = arena.as_num(rhs)
&& c.abs() > Ratio::one()
&& real_for_real_var(arena, inner, var)
{
tracing::debug!("solve_by_peeling: cos domain error, |c| > 1");
return Some(vec![]);
}
tracing::debug!("solve_by_peeling: inverting cos, two branches");
let acos_rhs = arena.acos(rhs);
let neg_acos = arena.neg(acos_rhs);
let (b1, b2) = match period {
Some(n) => {
let two = arena.int(2);
let pi = arena.pi;
let two_pi_n = arena.mul(&[two, pi, n]);
(
arena.add(&[acos_rhs, two_pi_n]),
arena.add(&[neg_acos, two_pi_n]),
)
}
None => (acos_rhs, neg_acos),
};
peel_two_branches(arena, inner, b1, b2, var, period)
}
ExprNode::Tan(inner) => {
let atan_rhs = arena.atan(rhs);
let new_rhs = match period {
Some(n) => {
let pi = arena.pi;
let pi_n = arena.mul(&[pi, n]);
arena.add(&[atan_rhs, pi_n])
}
None => atan_rhs,
};
solve_by_peeling(arena, inner, new_rhs, var, period)
}
ExprNode::Pow(inner_base, inner_exp) => {
if let Some(n) = arena.as_num(inner_exp) {
let n = n.clone();
if !n.is_zero() {
let branches = power_preimages(arena, rhs, &n)?;
return peel_branches(arena, inner_base, &branches, var, period);
}
}
if !expr_contains_var(arena, inner_base, var)
&& expr_contains_var(arena, inner_exp, var)
{
tracing::debug!("solve_by_peeling: constant-base exponential a^f(x) = rhs");
if let Some(n) = period {
let ln_base = arena.ln(inner_base);
let ln_base = crate::transforms::eval::eval(arena, ln_base);
let prod = arena.mul(&[inner_exp, ln_base]);
if !real_for_real_var(arena, prod, var) {
let ln_rhs = principal_ln(arena, rhs);
let p = two_pi_i_n(arena, n);
let num = arena.add(&[ln_rhs, p]);
let new_rhs = arena.div(num, ln_base);
let new_rhs = crate::transforms::expand::expand(arena, new_rhs);
let new_rhs = crate::transforms::eval::eval(arena, new_rhs);
return solve_by_peeling(arena, inner_exp, new_rhs, var, period);
}
}
if let (Some(b), Some(r)) = (
arena.as_num(inner_base).cloned(),
arena.as_num(rhs).cloned(),
) && b.is_integer()
&& r.is_integer()
&& b > Ratio::one()
&& r.is_positive()
{
let b_int = b.to_integer();
let r_int = r.to_integer();
let mut power = BigInt::one();
for k in 0u32..65 {
if power == r_int {
let k_expr = arena.int(k as i64);
tracing::debug!(
"solve_by_peeling: integer log shortcut, base^{k} = rhs"
);
return solve_by_peeling(arena, inner_exp, k_expr, var, period);
}
if power > r_int {
break;
}
power *= &b_int;
}
}
let ln_base = arena.ln(inner_base);
let ln_rhs = principal_ln(arena, rhs);
let new_rhs = arena.div(ln_rhs, ln_base);
return solve_by_peeling(arena, inner_exp, new_rhs, var, period);
}
None
}
ExprNode::Neg(inner) => {
let new_rhs = arena.neg(rhs);
solve_by_peeling(arena, inner, new_rhs, var, period)
}
ExprNode::Asin(inner) => {
let new_rhs = arena.sin(rhs);
solve_by_peeling(arena, inner, new_rhs, var, period)
}
ExprNode::Acos(inner) => {
let new_rhs = arena.cos(rhs);
solve_by_peeling(arena, inner, new_rhs, var, period)
}
ExprNode::Atan(inner) => {
let new_rhs = arena.tan(rhs);
solve_by_peeling(arena, inner, new_rhs, var, period)
}
ExprNode::Sinh(inner) => {
let asinh_rhs = arena.asinh(rhs);
match period {
Some(n) if !real_for_real_var(arena, inner, var) => {
let p = two_pi_i_n(arena, n);
let i_pi = arena.mul(&[arena.i_unit, arena.pi]);
let other = arena.sub(i_pi, asinh_rhs);
let b1 = arena.add(&[asinh_rhs, p]);
let b2 = arena.add(&[other, p]);
peel_two_branches(arena, inner, b1, b2, var, period)
}
_ => solve_by_peeling(arena, inner, asinh_rhs, var, period),
}
}
ExprNode::Cosh(inner) => {
let real_inner = real_for_real_var(arena, inner, var);
if real_inner
&& let Some(c) = arena.as_num(rhs)
&& *c < Ratio::one()
{
tracing::debug!("solve_by_peeling: cosh domain error, rhs < 1");
return Some(vec![]);
}
let acosh_rhs = arena.acosh(rhs);
let neg_acosh = arena.neg(acosh_rhs);
let (b1, b2) = match period {
Some(n) if !real_inner => {
let p = two_pi_i_n(arena, n);
(arena.add(&[acosh_rhs, p]), arena.add(&[neg_acosh, p]))
}
_ => (acosh_rhs, neg_acosh),
};
peel_two_branches(arena, inner, b1, b2, var, period)
}
ExprNode::Tanh(inner) => {
let atanh_rhs = arena.atanh(rhs);
let new_rhs = match period {
Some(n) if !real_for_real_var(arena, inner, var) => {
let i_pi_n = arena.mul(&[arena.i_unit, arena.pi, n]);
arena.add(&[atanh_rhs, i_pi_n])
}
_ => atanh_rhs,
};
solve_by_peeling(arena, inner, new_rhs, var, period)
}
ExprNode::Asinh(inner) => {
let new_rhs = arena.sinh(rhs);
solve_by_peeling(arena, inner, new_rhs, var, period)
}
ExprNode::Acosh(inner) => {
let new_rhs = arena.cosh(rhs);
solve_by_peeling(arena, inner, new_rhs, var, period)
}
ExprNode::Atanh(inner) => {
let new_rhs = arena.tanh(rhs);
solve_by_peeling(arena, inner, new_rhs, var, period)
}
ExprNode::Abs(inner) => {
if let Some(r) = arena.as_num(rhs)
&& r.is_negative()
{
return Some(vec![]);
}
if !real_for_real_var(arena, inner, var) {
return None;
}
let neg_rhs = arena.neg(rhs);
peel_two_branches(arena, inner, rhs, neg_rhs, var, period)
}
_ => None,
}
}
const MAX_POWER_PREIMAGES: i64 = 24;
fn power_preimages(arena: &mut Arena, c: ExprId, n: &Q) -> Option<Vec<ExprId>> {
let p = n.numer().clone();
let q = n.denom().clone();
let c_zero = arena.is_zero_structural(c);
if c_zero {
return Some(if n.is_positive() {
vec![arena.zero]
} else {
vec![]
});
}
let symbolic = !crate::base::walk::free_symbols(arena, c).is_empty();
let p_abs: i64 = num_traits::ToPrimitive::to_i64(&p.abs()).unwrap_or(i64::MAX);
if q.is_one() || (!symbolic && p_abs <= MAX_POWER_PREIMAGES) {
let base = if p.is_negative() {
let m1 = arena.neg_one;
let inv = arena.pow(c, m1);
crate::transforms::eval::eval(arena, inv)
} else {
c
};
if p_abs > MAX_INT_POWER_ROOTS {
return None;
}
let roots = binomial_roots(arena, base, p_abs as usize);
let exact_arg = if q.is_one() {
None
} else {
gaussian_rational_arg(arena, c).map(|t| {
if p.is_negative() && t != Ratio::one() {
-t
} else {
t
}
})
};
let aligned = roots.len() as i64 == p_abs;
let q_id = arena.big_int(q.clone());
let mut out = Vec::with_capacity(roots.len());
for (k, r) in roots.into_iter().enumerate() {
if let Some(t) = &exact_arg
&& aligned
&& !in_principal_sector(t, k, p_abs, &q)
{
continue;
}
let f = arena.pow(r.value, q_id);
let f = crate::transforms::expand::expand(arena, f);
let f = crate::transforms::eval::eval(arena, f);
if !out.contains(&f) {
out.push(f);
}
}
return Some(out);
}
let inv_n = Ratio::one() / n.clone();
let inv_n_id = arena.num_ratio(inv_n);
Some(vec![arena.pow(c, inv_n_id)])
}
const MAX_INT_POWER_ROOTS: i64 = 64;
fn principal_ln(arena: &mut Arena, c: ExprId) -> ExprId {
let Some(t) = gaussian_rational_arg(arena, c) else {
return arena.ln(c);
};
if t.is_zero() || t.is_one() {
return arena.ln(c);
}
let (re, im) = crate::base::complex::as_real_imag(arena, c);
let re = crate::transforms::eval::eval(arena, re);
let im = crate::transforms::eval::eval(arena, im);
let (Some(a), Some(b)) = (arena.as_num(re).cloned(), arena.as_num(im).cloned()) else {
return arena.ln(c);
};
let modulus_ln = if a.is_zero() {
let m = arena.num_ratio(b.abs());
arena.ln(m)
} else {
let m = arena.num_ratio(a.abs());
let ln_m = arena.ln(m);
let two = arena.int(2);
let ln2 = arena.ln(two);
let half = arena.rational(1, 2);
let half_ln2 = arena.mul(&[half, ln2]);
arena.add(&[ln_m, half_ln2])
};
let t_id = arena.num_ratio(t);
let angle = arena.mul(&[t_id, arena.pi, arena.i_unit]);
let sum = arena.add(&[modulus_ln, angle]);
crate::transforms::eval::eval(arena, sum)
}
fn gaussian_rational_arg(arena: &mut Arena, c: ExprId) -> Option<Q> {
let (re, im) = crate::base::complex::as_real_imag(arena, c);
let re = crate::transforms::eval::eval(arena, re);
let im = crate::transforms::eval::eval(arena, im);
let a = arena.as_num(re)?.clone();
let b = arena.as_num(im)?.clone();
let q = |n: i64, d: i64| Ratio::new(BigInt::from(n), BigInt::from(d));
Some(match (a.signum(), b.signum()) {
(sa, sb) if sa.is_zero() && sb.is_zero() => return None,
(sa, sb) if sb.is_zero() => {
if sa.is_positive() {
q(0, 1)
} else {
q(1, 1)
}
}
(sa, sb) if sa.is_zero() => {
if sb.is_positive() {
q(1, 2)
} else {
q(-1, 2)
}
}
(sa, sb) if a.abs() == b.abs() => match (sa.is_positive(), sb.is_positive()) {
(true, true) => q(1, 4),
(false, true) => q(3, 4),
(false, false) => q(-3, 4),
(true, false) => q(-1, 4),
},
_ => return None,
})
}
fn in_principal_sector(t: &Q, k: usize, n: i64, q: &BigInt) -> bool {
let two = Ratio::from_integer(BigInt::from(2));
let mut s = (t + Ratio::from_integer(BigInt::from(2 * k as i64)))
/ Ratio::from_integer(BigInt::from(n));
while s > Ratio::one() {
s -= &two;
}
while s <= -Ratio::<BigInt>::one() {
s += &two;
}
let bound = Ratio::new(BigInt::one(), q.clone());
-bound.clone() < s && s <= bound
}
fn peel_branches(
arena: &mut Arena,
inner: ExprId,
rhss: &[ExprId],
var: ExprId,
period: Option<ExprId>,
) -> Option<Vec<Solution>> {
let mut solutions: Vec<Solution> = Vec::new();
let mut saw_some = rhss.is_empty();
for &rhs in rhss {
if let Some(sols) = solve_by_peeling(arena, inner, rhs, var, period) {
saw_some = true;
for sol in sols {
if !solutions.iter().any(|s| s.value == sol.value) {
solutions.push(sol);
}
}
}
}
if solutions.is_empty() {
if saw_some { Some(vec![]) } else { None }
} else {
Some(solutions)
}
}
fn peel_two_branches(
arena: &mut Arena,
inner: ExprId,
rhs1: ExprId,
rhs2: ExprId,
var: ExprId,
period: Option<ExprId>,
) -> Option<Vec<Solution>> {
let mut solutions = Vec::new();
let mut saw_some = false;
if let Some(sols) = solve_by_peeling(arena, inner, rhs1, var, period) {
saw_some = true;
solutions.extend(sols);
}
if let Some(sols) = solve_by_peeling(arena, inner, rhs2, var, period) {
saw_some = true;
for sol in sols {
if !solutions.iter().any(|s| s.value == sol.value) {
solutions.push(sol);
}
}
}
if solutions.is_empty() {
if saw_some { Some(vec![]) } else { None }
} else {
Some(solutions)
}
}
fn solve_linear(arena: &mut Arena, poly: &Poly) -> Vec<Solution> {
let a = poly.coeff(1); let b = poly.coeff(0);
if a.is_zero() {
return Vec::new(); }
let value = -b / a;
let value_id = arena.num_ratio(value.clone());
vec![Solution { value: value_id }]
}
fn solve_quadratic(arena: &mut Arena, poly: &Poly) -> Vec<Solution> {
let a = poly.coeff(2);
let b = poly.coeff(1);
let c = poly.coeff(0);
if a.is_zero() {
let linear = Poly::from_coeffs(vec![c, b]);
return solve_linear(arena, &linear);
}
let discriminant = &b * &b - Ratio::from_integer(BigInt::from(4)) * &a * &c;
if discriminant.is_zero() {
let two_a = Ratio::from_integer(BigInt::from(2)) * &a;
let value = -b / two_a;
let value_id = arena.num_ratio(value.clone());
return vec![Solution { value: value_id }];
}
if discriminant.is_negative() {
let abs_disc = -discriminant;
let neg_b = arena.num_ratio((-&b).clone());
let abs_disc_id = arena.num_ratio(abs_disc.clone());
let sqrt_abs_disc = if let Some(s) = rational_sqrt(&abs_disc) {
arena.num_ratio(s.clone())
} else {
arena.sqrt(abs_disc_id)
};
let i_sqrt = arena.mul(&[arena.i_unit, sqrt_abs_disc]);
let two_a_val = Ratio::from_integer(BigInt::from(2)) * &a;
let two_a_id = arena.num_ratio(two_a_val.clone());
let two_a_inv = {
let neg_one = arena.neg_one;
arena.pow(two_a_id, neg_one)
};
let sum1 = arena.add(&[neg_b, i_sqrt]);
let x1 = arena.mul(&[sum1, two_a_inv]);
let neg_i_sqrt = arena.neg(i_sqrt);
let sum2 = arena.add(&[neg_b, neg_i_sqrt]);
let x2 = arena.mul(&[sum2, two_a_inv]);
return vec![Solution { value: x1 }, Solution { value: x2 }];
}
if let Some(sqrt_disc) = rational_sqrt(&discriminant) {
let two_a = Ratio::from_integer(BigInt::from(2)) * &a;
let x1 = (-&b + &sqrt_disc) / &two_a;
let x2 = (-&b - &sqrt_disc) / &two_a;
let x1_id = arena.num_ratio(x1.clone());
let x2_id = arena.num_ratio(x2.clone());
if x1 == x2 {
vec![Solution { value: x1_id }]
} else {
vec![Solution { value: x1_id }, Solution { value: x2_id }]
}
} else {
let neg_b = {
let neg_b_val = -&b;
arena.num_ratio(neg_b_val.clone())
};
let sqrt_disc = {
let disc_id = arena.num_ratio(discriminant.clone());
arena.sqrt(disc_id)
};
let two_a_val = Ratio::from_integer(BigInt::from(2)) * &a;
let two_a_id = arena.num_ratio(two_a_val.clone());
let two_a_inv = {
let neg_one = arena.neg_one;
arena.pow(two_a_id, neg_one)
};
let sum1 = arena.add(&[neg_b, sqrt_disc]);
let x1 = arena.mul(&[sum1, two_a_inv]);
let neg_sqrt = arena.neg(sqrt_disc);
let sum2 = arena.add(&[neg_b, neg_sqrt]);
let x2 = arena.mul(&[sum2, two_a_inv]);
vec![Solution { value: x1 }, Solution { value: x2 }]
}
}
fn solve_cubic(arena: &mut Arena, var: ExprId, poly: &Poly) -> Vec<Solution> {
let a = poly.coeff(3);
if a.is_zero() {
let quadratic = Poly::from_coeffs(vec![poly.coeff(0), poly.coeff(1), poly.coeff(2)]);
return solve_quadratic(arena, &quadratic);
}
let rational_attempt = solve_rational_roots(arena, var, poly);
if !rational_attempt.is_empty() {
return rational_attempt;
}
solve_cubic_cardano(arena, poly)
}
fn solve_cubic_cardano(arena: &mut Arena, poly: &Poly) -> Vec<Solution> {
let a = poly.coeff(3);
let b = poly.coeff(2);
let c = poly.coeff(1);
let d = poly.coeff(0);
if a.is_zero() {
let quadratic = Poly::from_coeffs(vec![d, c, b]);
return solve_quadratic(arena, &quadratic);
}
if let Some(roots) = try_solve_binomial_rational(arena, poly) {
return roots;
}
let a_id = arena.num_ratio(a.clone());
let b_id = arena.num_ratio(b.clone());
let c_id = arena.num_ratio(c.clone());
let d_id = arena.num_ratio(d.clone());
let three = arena.int(3);
let nine = arena.int(9);
let twenty_seven = arena.int(27);
let two = arena.int(2);
let four = arena.int(4);
let tmp1 = arena.mul(&[three, a_id, c_id]);
let tmp2 = arena.mul(&[b_id, b_id]);
let p_num = arena.sub(tmp1, tmp2);
let p_den = arena.mul(&[three, a_id, a_id]);
let p = arena.div(p_num, p_den);
let tmp3 = arena.mul(&[two, b_id, b_id, b_id]);
let tmp4 = arena.mul(&[nine, a_id, b_id, c_id]);
let tmp4n = arena.neg(tmp4);
let tmp5 = arena.mul(&[twenty_seven, a_id, a_id, d_id]);
let q_num = arena.add(&[tmp3, tmp4n, tmp5]);
let q_den = arena.mul(&[twenty_seven, a_id, a_id, a_id]);
let q = arena.div(q_num, q_den);
let qq = arena.mul(&[q, q]);
let qq_over4 = arena.div(qq, four);
let ppp = arena.mul(&[p, p, p]);
let ppp_over27 = arena.div(ppp, twenty_seven);
let disc = arena.add(&[qq_over4, ppp_over27]);
let half = arena.rational(1, 2);
let sqrt_disc = arena.pow(disc, half);
let neg_q = arena.neg(q);
let neg_q_half = arena.div(neg_q, two);
let u_arg = arena.add(&[neg_q_half, sqrt_disc]);
let v_arg = arena.sub(neg_q_half, sqrt_disc);
let (p_rat, q_rat, disc_rat) = {
let three_r = Ratio::from_integer(BigInt::from(3));
let nine_r = Ratio::from_integer(BigInt::from(9));
let two_r = Ratio::from_integer(BigInt::from(2));
let four_r = Ratio::from_integer(BigInt::from(4));
let twenty_seven_r = Ratio::from_integer(BigInt::from(27));
let p_r = (&three_r * &a * &c - &b * &b) / (&three_r * &a * &a);
let q_r = (&two_r * &b * &b * &b - &nine_r * &a * &b * &c + &twenty_seven_r * &a * &a * &d)
/ (&twenty_seven_r * &a * &a * &a);
let disc_r = &q_r * &q_r / &four_r + &p_r * &p_r * &p_r / &twenty_seven_r;
(p_r, q_r, disc_r)
};
let (u_sign, v_sign) = cardano_radicand_signs(&p_rat, &q_rat, &disc_rat);
let u = real_cbrt_with_sign(arena, u_arg, u_sign);
let v = real_cbrt_with_sign(arena, v_arg, v_sign);
let t1 = arena.add(&[u, v]);
let three_a = arena.mul(&[three, a_id]);
let shift = arena.div(b_id, three_a);
let x1 = arena.sub(t1, shift);
let omega_re = arena.rational(-1, 2);
let sqrt3 = arena.pow(three, half);
let sqrt3_half = arena.div(sqrt3, two);
let i_unit = arena.i_unit;
let omega_im = arena.mul(&[sqrt3_half, i_unit]);
let omega = arena.add(&[omega_re, omega_im]);
let omega2 = arena.sub(omega_re, omega_im);
let ou = arena.mul(&[omega, u]);
let o2v = arena.mul(&[omega2, v]);
let t2 = arena.add(&[ou, o2v]);
let o2u = arena.mul(&[omega2, u]);
let ov = arena.mul(&[omega, v]);
let t3 = arena.add(&[o2u, ov]);
let x2 = arena.sub(t2, shift);
let x3 = arena.sub(t3, shift);
let x1s = crate::transforms::eval::eval(arena, x1);
let x2s = crate::transforms::eval::eval(arena, x2);
let x3s = crate::transforms::eval::eval(arena, x3);
vec![
Solution { value: x1s },
Solution { value: x2s },
Solution { value: x3s },
]
}
fn cardano_radicand_signs(p: &Q, q: &Q, disc: &Q) -> (i8, i8) {
use std::cmp::Ordering;
if disc.is_negative() {
return (0, 0);
}
let neg_q_sign: i8 = match q.cmp(&Ratio::zero()) {
Ordering::Less => 1,
Ordering::Equal => 0,
Ordering::Greater => -1,
};
if disc.is_zero() {
return (neg_q_sign, neg_q_sign);
}
let p_sign = match p.cmp(&Ratio::zero()) {
Ordering::Less => -1i8,
Ordering::Equal => 0,
Ordering::Greater => 1,
};
let u_sign = if neg_q_sign >= 0 {
1
} else {
p_sign
};
let v_sign = if neg_q_sign <= 0 {
-1
} else {
-p_sign
};
(u_sign, v_sign)
}
fn real_cbrt_with_sign(arena: &mut Arena, arg: ExprId, sign: i8) -> ExprId {
let third = arena.rational(1, 3);
match sign {
1 => arena.pow(arg, third),
-1 => {
let neg_arg = arena.neg(arg);
let neg_arg = crate::transforms::eval::eval(arena, neg_arg);
let root = arena.pow(neg_arg, third);
arena.neg(root)
}
_ => {
let ev = crate::transforms::eval::eval(arena, arg);
if arena.is_zero_structural(ev) {
return arena.zero;
}
arena.pow(arg, third)
}
}
}
fn solve_quartic(arena: &mut Arena, var: ExprId, poly: &Poly) -> Vec<Solution> {
let a = poly.coeff(4);
if a.is_zero() {
let cubic = Poly::from_coeffs(vec![
poly.coeff(0),
poly.coeff(1),
poly.coeff(2),
poly.coeff(3),
]);
return solve_cubic(arena, var, &cubic);
}
let rational_attempt = solve_rational_roots(arena, var, poly);
if !rational_attempt.is_empty() {
return rational_attempt;
}
solve_quartic_ferrari(arena, poly)
}
fn solve_quartic_ferrari(arena: &mut Arena, poly: &Poly) -> Vec<Solution> {
let a = poly.coeff(4);
let b = poly.coeff(3);
let c = poly.coeff(2);
let d = poly.coeff(1);
let e = poly.coeff(0);
if a.is_zero() {
let cubic = Poly::from_coeffs(vec![e.clone(), d, c, b]);
return solve_cubic_cardano(arena, &cubic);
}
if let Some(roots) = try_solve_binomial_rational(arena, poly) {
return roots;
}
let (p_rat, q_rat, r_rat) = {
let r3 = Ratio::from_integer(BigInt::from(3));
let r4 = Ratio::from_integer(BigInt::from(4));
let r8 = Ratio::from_integer(BigInt::from(8));
let r16 = Ratio::from_integer(BigInt::from(16));
let r64 = Ratio::from_integer(BigInt::from(64));
let r256 = Ratio::from_integer(BigInt::from(256));
let a2 = &a * &a;
let a3 = &a2 * &a;
let a4 = &a3 * &a;
let b2 = &b * &b;
let p_r = (&r8 * &a * &c - &r3 * &b2) / (&r8 * &a2);
let q_r = (&b2 * &b - &r4 * &a * &b * &c + &r8 * &a2 * &d) / (&r8 * &a3);
let r_r = (-&r3 * &b2 * &b2 + &r256 * &a3 * &e - &r64 * &a2 * &b * &d
+ &r16 * &a * &b2 * &c)
/ (&r256 * &a4);
(p_r, q_r, r_r)
};
if q_rat.is_zero() {
let quad = Poly::from_coeffs(vec![r_rat.clone(), p_rat.clone(), Ratio::one()]);
let s_roots = solve_quadratic(arena, &quad);
let four_a = Ratio::from_integer(BigInt::from(4)) * &a;
let shift_rat = &b / &four_a;
let shift = arena.num_ratio(shift_rat.clone());
let half = arena.rational(1, 2);
let mut out: Vec<Solution> = Vec::new();
for s in s_roots {
let s_ev = crate::transforms::eval::eval(arena, s.value);
let t_pos = arena.pow(s_ev, half);
let t_neg = arena.neg(t_pos);
for t in [t_pos, t_neg] {
let x = arena.sub(t, shift);
let x = crate::transforms::eval::eval(arena, x);
if !out.iter().any(|o| o.value == x) {
out.push(Solution { value: x });
}
}
}
return out;
}
let a_id = arena.num_ratio(a.clone());
let b_id = arena.num_ratio(b.clone());
let c_id = arena.num_ratio(c.clone());
let d_id = arena.num_ratio(d.clone());
let e_id = arena.num_ratio(e.clone());
let two = arena.int(2);
let three = arena.int(3);
let four = arena.int(4);
let eight = arena.int(8);
let sixteen = arena.int(16);
let sixty_four = arena.int(64);
let two_fifty_six = arena.int(256);
let tmp_8ac = arena.mul(&[eight, a_id, c_id]);
let tmp_3bb = arena.mul(&[three, b_id, b_id]);
let p_num = arena.sub(tmp_8ac, tmp_3bb);
let p_den = arena.mul(&[eight, a_id, a_id]);
let p = arena.div(p_num, p_den);
let tmp_bbb = arena.mul(&[b_id, b_id, b_id]);
let tmp_4abc = arena.mul(&[four, a_id, b_id, c_id]);
let tmp_4abc_n = arena.neg(tmp_4abc);
let tmp_8aad = arena.mul(&[eight, a_id, a_id, d_id]);
let q_num = arena.add(&[tmp_bbb, tmp_4abc_n, tmp_8aad]);
let q_den = arena.mul(&[eight, a_id, a_id, a_id]);
let q = arena.div(q_num, q_den);
let tmp_3b4 = arena.mul(&[three, b_id, b_id, b_id, b_id]);
let tmp_3b4_n = arena.neg(tmp_3b4);
let tmp_256a3e = arena.mul(&[two_fifty_six, a_id, a_id, a_id, e_id]);
let tmp_64a2bd = arena.mul(&[sixty_four, a_id, a_id, b_id, d_id]);
let tmp_64a2bd_n = arena.neg(tmp_64a2bd);
let tmp_16ab2c = arena.mul(&[sixteen, a_id, b_id, b_id, c_id]);
let r_num = arena.add(&[tmp_3b4_n, tmp_256a3e, tmp_64a2bd_n, tmp_16ab2c]);
let r_den = arena.mul(&[two_fifty_six, a_id, a_id, a_id, a_id]);
let r = arena.div(r_num, r_den);
let resolvent_c3 = eight;
let neg_four = arena.neg(four);
let resolvent_c2 = arena.mul(&[neg_four, p]);
let neg_eight = arena.neg(eight);
let resolvent_c1 = arena.mul(&[neg_eight, r]);
let tmp_4pr = arena.mul(&[four, p, r]);
let tmp_qq = arena.mul(&[q, q]);
let tmp_qq_n = arena.neg(tmp_qq);
let resolvent_c0 = arena.add(&[tmp_4pr, tmp_qq_n]);
let resolvent_c3_e = crate::transforms::eval::eval(arena, resolvent_c3);
let resolvent_c2_e = crate::transforms::eval::eval(arena, resolvent_c2);
let resolvent_c1_e = crate::transforms::eval::eval(arena, resolvent_c1);
let resolvent_c0_e = crate::transforms::eval::eval(arena, resolvent_c0);
let rc3 = match arena.as_num(resolvent_c3_e) {
Some(v) => v.clone(),
None => return Vec::new(),
};
let rc2 = match arena.as_num(resolvent_c2_e) {
Some(v) => v.clone(),
None => return Vec::new(),
};
let rc1 = match arena.as_num(resolvent_c1_e) {
Some(v) => v.clone(),
None => return Vec::new(),
};
let rc0 = match arena.as_num(resolvent_c0_e) {
Some(v) => v.clone(),
None => return Vec::new(),
};
let resolvent_poly = Poly::from_coeffs(vec![rc0, rc1, rc2, rc3]);
debug_assert!(!q_rat.is_zero());
let m = if let Some(m_rat) = find_preferred_resolvent_root(&resolvent_poly, &p_rat) {
arena.num_ratio(m_rat.clone())
} else {
let m_solutions = solve_cubic_cardano(arena, &resolvent_poly);
if m_solutions.is_empty() {
return Vec::new();
}
m_solutions[0].value
};
let half = arena.rational(1, 2);
let two_m = arena.mul(&[two, m]);
let two_m_minus_p = arena.sub(two_m, p);
let k = arena.pow(two_m_minus_p, half);
let two_k = arena.mul(&[two, k]);
let q_over_2k = arena.div(q, two_k);
let four_a = arena.mul(&[four, a_id]);
let shift = arena.div(b_id, four_a);
let s1 = arena.sub(m, q_over_2k);
let kk = arena.mul(&[k, k]);
let four_s1 = arena.mul(&[four, s1]);
let disc1 = arena.sub(kk, four_s1);
let sqrt_disc1 = arena.pow(disc1, half);
let neg_k = arena.neg(k);
let sum1a = arena.add(&[neg_k, sqrt_disc1]);
let t1a = arena.div(sum1a, two);
let diff1b = arena.sub(neg_k, sqrt_disc1);
let t1b = arena.div(diff1b, two);
let x1 = arena.sub(t1a, shift);
let x2 = arena.sub(t1b, shift);
let s2 = arena.add(&[m, q_over_2k]);
let kk2 = arena.mul(&[k, k]);
let four_s2 = arena.mul(&[four, s2]);
let disc2 = arena.sub(kk2, four_s2);
let sqrt_disc2 = arena.pow(disc2, half);
let sum2a = arena.add(&[k, sqrt_disc2]);
let t2a = arena.div(sum2a, two);
let diff2b = arena.sub(k, sqrt_disc2);
let t2b = arena.div(diff2b, two);
let x3 = arena.sub(t2a, shift);
let x4 = arena.sub(t2b, shift);
let x1s = crate::transforms::eval::eval(arena, x1);
let x2s = crate::transforms::eval::eval(arena, x2);
let x3s = crate::transforms::eval::eval(arena, x3);
let x4s = crate::transforms::eval::eval(arena, x4);
vec![
Solution { value: x1s },
Solution { value: x2s },
Solution { value: x3s },
Solution { value: x4s },
]
}
fn find_preferred_resolvent_root(resolvent: &Poly, p_rat: &Q) -> Option<Q> {
let (int_poly, _scale) = clear_denominators(resolvent);
let a0 = int_poly.coeff(0).to_integer();
let an = {
let v = int_poly.leading_coeff()?;
v.to_integer()
};
let two_r = Ratio::from_integer(BigInt::from(2));
let mut fallback: Option<Q> = None;
if a0.is_zero() {
let zero = Ratio::zero();
if &two_r * &zero - p_rat != Ratio::zero() {
return Some(zero);
}
fallback = Some(zero);
let reduced_coeffs: Vec<Q> = resolvent.coeffs().iter().skip(1).cloned().collect();
let reduced = Poly::from_coeffs(reduced_coeffs);
let aq = reduced.coeff(2);
let bq = reduced.coeff(1);
let cq = reduced.coeff(0);
if !aq.is_zero() {
let disc = &bq * &bq - Ratio::from_integer(BigInt::from(4)) * &aq * &cq;
if let Some(sqrt_d) = rational_sqrt(&disc) {
let two_aq = Ratio::from_integer(BigInt::from(2)) * &aq;
for candidate in [(-&bq + &sqrt_d) / &two_aq, (-&bq - &sqrt_d) / &two_aq] {
if &two_r * &candidate - p_rat != Ratio::zero() {
return Some(candidate);
}
if fallback.is_none() {
fallback = Some(candidate);
}
}
}
}
} else {
let divs_a0 = divisors(&a0.abs());
let divs_an = divisors(&an.abs());
for p_div in &divs_a0 {
for q_div in &divs_an {
for &sign in &[1i64, -1i64] {
let candidate = Ratio::new(p_div * BigInt::from(sign), q_div.clone());
if resolvent.eval(&candidate).is_zero() {
if &two_r * &candidate - p_rat != Ratio::zero() {
return Some(candidate);
}
if fallback.is_none() {
fallback = Some(candidate);
}
}
}
}
}
}
fallback
}
fn solve_rational_roots(arena: &mut Arena, var: ExprId, poly: &Poly) -> Vec<Solution> {
let degree = match poly.degree() {
Some(d) if d >= 1 => d,
_ => return Vec::new(),
};
if degree > 4
&& let Some(roots) = try_solve_binomial_rational(arena, poly)
{
return roots;
}
let (_content, factors) = poly.factor_over_z();
let mut roots: Vec<Solution> = Vec::new();
for (factor, _multiplicity) in &factors {
let found = match factor.degree() {
None | Some(0) => Vec::new(),
Some(1) => solve_linear(arena, factor),
Some(2) => solve_quadratic(arena, factor),
Some(3) => solve_cubic_cardano(arena, factor),
Some(4) => solve_quartic_ferrari(arena, factor),
Some(d) => {
if let Some(explicit) = try_solve_binomial_rational(arena, factor) {
explicit
} else {
let factor_expr = polybridge::poly_to_expr(arena, factor, var);
(0..d)
.map(|i| {
let idx = arena.int(i as i64);
Solution {
value: arena.intern(ExprNode::RootOf(factor_expr, var, idx)),
}
})
.collect()
}
}
};
for sol in found {
if !roots.iter().any(|s| s.value == sol.value) {
roots.push(sol);
}
}
}
tracing::debug!(
factors = factors.len(),
roots = roots.len(),
"roots via factorisation over ℤ"
);
roots
}
fn rational_sqrt(r: &Q) -> Option<Q> {
if r.is_negative() {
return None;
}
if r.is_zero() {
return Some(Ratio::zero());
}
let n = r.numer().abs();
let d = r.denom().abs();
let sqrt_n = n.sqrt();
let sqrt_d = d.sqrt();
if &sqrt_n * &sqrt_n == n && &sqrt_d * &sqrt_d == d {
Some(Ratio::new(sqrt_n, sqrt_d))
} else {
None
}
}
fn clear_denominators(poly: &Poly) -> (Poly, BigInt) {
if poly.is_zero() {
return (Poly::zero(), BigInt::one());
}
let mut lcm = BigInt::one();
for c in poly.coeffs() {
let d = c.denom().abs();
lcm = num_integer::lcm(lcm, d);
}
let scale = Ratio::from_integer(lcm.clone());
let scaled = poly.scale(&scale);
(scaled, lcm)
}
fn divisors(n: &BigInt) -> Vec<BigInt> {
if n.is_zero() {
return vec![BigInt::one()];
}
let n_abs = n.abs();
let limit: u64 = match (&n_abs).try_into() {
Ok(v) if v <= 1_000_000u64 => v,
_ => {
return vec![BigInt::one(), n_abs];
}
};
let mut result = Vec::new();
let mut i = 1u64;
while i * i <= limit {
if limit.is_multiple_of(i) {
result.push(BigInt::from(i));
if i * i != limit {
result.push(BigInt::from(limit / i));
}
}
i += 1;
}
result.sort();
result
}
fn try_change_of_variable(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
period: Option<ExprId>,
) -> Option<Vec<Solution>> {
let var_sym = match arena.node(var) {
ExprNode::Symbol(sid) => *sid,
_ => return None,
};
let candidates = collect_generators(arena, expr, var, var_sym);
for generator in candidates {
let t = arena.symbol("__t_subst");
let substituted = arena.subs_structural(expr, generator, t);
if !expr_contains_var(arena, substituted, var) {
let t_solutions = solve(arena, substituted, t);
if !t_solutions.is_empty() {
let mut var_solutions: Vec<Solution> = Vec::new();
for t_sol in &t_solutions {
if let Some(back_sols) =
solve_by_peeling(arena, generator, t_sol.value, var, period)
{
for s in back_sols {
if !var_solutions.iter().any(|v| v.value == s.value) {
var_solutions.push(s);
}
}
}
}
if !var_solutions.is_empty() {
return Some(var_solutions);
}
}
}
if let ExprNode::Exp(inner) = arena.node(generator).clone()
&& inner == var
{
let rewritten = rewrite_exp_powers(arena, expr, var, generator);
if rewritten != expr {
let substituted2 = arena.subs_structural(rewritten, generator, t);
if !expr_contains_var(arena, substituted2, var) {
let t_solutions = solve(arena, substituted2, t);
if !t_solutions.is_empty() {
let mut var_solutions: Vec<Solution> = Vec::new();
for t_sol in &t_solutions {
if let Some(back_sols) =
solve_by_peeling(arena, generator, t_sol.value, var, period)
{
for s in back_sols {
if !var_solutions.iter().any(|v| v.value == s.value) {
var_solutions.push(s);
}
}
}
}
if !var_solutions.is_empty() {
return Some(var_solutions);
}
}
}
}
}
}
None
}
fn try_radical_substitution(arena: &mut Arena, expr: ExprId, var: ExprId) -> Option<Vec<Solution>> {
let post = crate::base::walk::post_order_ids(arena, expr);
let mut radicand: Option<ExprId> = None;
for &id in &post {
if let ExprNode::Pow(b, e) = *arena.node(id)
&& expr_contains_var(arena, b, var)
{
let r = arena.as_num(e)?;
if !r.is_integer() {
match radicand {
None => radicand = Some(b),
Some(u) if u == b => {}
Some(_) => return None,
}
}
}
}
let u = radicand?;
let rate = if u == var {
None
} else {
Some(linear_rate(arena, u, var)?)
};
let mut lcm = BigInt::one();
let mut powers: Vec<(ExprId, Q)> = Vec::new();
for &id in &post {
if !expr_contains_var(arena, id, var) {
continue;
}
match arena.node(id) {
ExprNode::Pow(b, e) if *b == u => {
let r = arena.as_num(*e)?.clone();
if !r.is_integer() {
lcm = num_integer::Integer::lcm(&lcm, r.denom());
}
powers.push((id, r));
}
ExprNode::Pow(_, e) if expr_contains_var(arena, *e, var) => return None,
ExprNode::Symbol(_)
| ExprNode::Add(_)
| ExprNode::Mul(_)
| ExprNode::Neg(_)
| ExprNode::Pow(..) => {}
_ => return None,
}
}
if lcm.is_one() || lcm > BigInt::from(MAX_RADICAL_INDEX) {
return None;
}
let t = arena.symbol("__t_radical");
let l_id = arena.big_int(lcm.clone());
let mut substituted = expr;
for (pow_id, r) in &powers {
let k = r * Ratio::from_integer(lcm.clone());
let k_id = arena.num_ratio(k);
let tk = arena.pow(t, k_id);
substituted = arena.subs_structural(substituted, *pow_id, tk);
}
let t_l = arena.pow(t, l_id);
let x_of_t = match rate {
None => t_l,
Some((a, b)) => {
let num = arena.sub(t_l, b);
arena.div(num, a)
}
};
substituted = crate::transforms::subs::subs(arena, substituted, var, x_of_t);
if rate.is_some() {
substituted = crate::transforms::expand::expand(arena, substituted);
}
let substituted = crate::transforms::eval::eval(arena, substituted);
if expr_contains_var(arena, substituted, var) {
return None;
}
let t_solutions = solve(arena, substituted, t);
let mut out: Vec<Solution> = Vec::new();
for ts in t_solutions {
let x0 = crate::transforms::subs::subs(arena, x_of_t, t, ts.value);
let x0 = crate::transforms::expand::expand(arena, x0);
let x0 = crate::transforms::eval::eval(arena, x0);
if !out.iter().any(|s| s.value == x0) {
out.push(Solution { value: x0 });
}
}
if rate.is_some() {
return strictly_checked(arena, expr, var, out);
}
Some(out)
}
const MAX_RADICAL_INDEX: i64 = 12;
fn try_square_root_elimination(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
) -> Option<Vec<Solution>> {
let mut radicands: Vec<ExprId> = Vec::new();
for id in crate::base::walk::post_order_ids(arena, expr) {
if !expr_contains_var(arena, id, var) {
continue;
}
match arena.node(id).clone() {
ExprNode::Pow(b, e) => {
let r = arena.as_num(e)?.clone();
if r.is_integer() {
continue;
}
if *r.denom() != BigInt::from(2) || expr_contains_var(arena, e, var) {
return None;
}
polybridge::expr_to_poly(arena, b, var)?;
if !radicands.contains(&b) {
radicands.push(b);
}
}
ExprNode::Symbol(_) | ExprNode::Add(_) | ExprNode::Mul(_) | ExprNode::Neg(_) => {}
_ => return None,
}
}
if radicands.is_empty() || radicands.len() > 2 {
return None;
}
let s = arena.symbol("__s_unrad");
let r = arena.symbol("__r_unrad");
let syms = [s, r];
let mut replaced = expr;
for id in crate::base::walk::post_order_ids(arena, expr) {
let ExprNode::Pow(b, e) = *arena.node(id) else {
continue;
};
let Some(k) = arena.as_num(e).cloned() else {
continue;
};
let Some(which) = radicands.iter().position(|&u| u == b) else {
continue;
};
if k.is_integer() {
continue;
}
let m = k.floor();
let m_id = arena.num_ratio(m);
let bm = arena.pow(b, m_id);
let repl = arena.mul(&[bm, syms[which]]);
replaced = arena.subs_structural(replaced, id, repl);
}
let replaced = crate::transforms::expand::expand(arena, replaced);
let replaced = crate::transforms::eval::eval(arena, replaced);
let (a_rad, b_rad) = (
radicands[0],
radicands.get(1).copied().unwrap_or(arena.zero),
);
let by_s = symbolic_poly_coeffs(arena, replaced, s)?;
if by_s.len() > 2 {
return None;
}
let zero = arena.zero;
let s0 = by_s.first().copied().unwrap_or(zero);
let s1 = by_s.get(1).copied().unwrap_or(zero);
let split_r = |arena: &mut Arena, e: ExprId| -> Option<(ExprId, ExprId)> {
let c = symbolic_poly_coeffs(arena, e, r)?;
if c.len() > 2 {
return None;
}
Some((
c.first().copied().unwrap_or(zero),
c.get(1).copied().unwrap_or(zero),
))
};
let (a, c) = split_r(arena, s0)?;
let (b, d) = split_r(arena, s1)?;
for &coef in &[a, b, c, d] {
if crate::base::walk::contains(arena, coef, s)
|| crate::base::walk::contains(arena, coef, r)
{
return None;
}
}
let two = arena.int(2);
let sq = |arena: &mut Arena, e: ExprId| arena.pow(e, two);
let ab = arena.mul(&[a_rad, b_rad]);
let a2 = sq(arena, a);
let d2 = sq(arena, d);
let b2 = sq(arena, b);
let c2 = sq(arena, c);
let d2ab = arena.mul(&[d2, ab]);
let b2a = arena.mul(&[b2, a_rad]);
let c2b = arena.mul(&[c2, b_rad]);
let neg_b2a = arena.neg(b2a);
let neg_c2b = arena.neg(c2b);
let e_big = arena.add(&[a2, d2ab, neg_b2a, neg_c2b]);
let ad = arena.mul(&[a, d]);
let bc = arena.mul(&[b, c]);
let ad_bc = arena.sub(ad, bc);
let f_big = arena.mul(&[two, ad_bc]);
let e2 = sq(arena, e_big);
let f2 = sq(arena, f_big);
let f2ab = arena.mul(&[f2, ab]);
let norm = arena.sub(e2, f2ab);
let norm = crate::transforms::expand::expand(arena, norm);
let norm = crate::transforms::eval::eval(arena, norm);
let poly = polybridge::expr_to_poly(arena, norm, var)?;
if poly.is_constant() {
return None;
}
let candidates = solve_rational_poly(arena, var, &poly);
strictly_checked(arena, expr, var, candidates)
}
fn strictly_checked(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
candidates: Vec<Solution>,
) -> Option<Vec<Solution>> {
if crate::base::walk::free_symbols(arena, expr)
.iter()
.any(|&s| s != var)
{
return None;
}
let mut out = Vec::new();
for c in candidates {
if member_is_root(arena, expr, var, c.value) {
out.push(c);
} else if !certainly_not_a_root(arena, expr, var, c.value) {
return None;
}
}
Some(out)
}
fn walk_var_atoms(
arena: &Arena,
expr: ExprId,
var: ExprId,
mut atom: impl FnMut(ExprId) -> bool,
) -> Option<()> {
let mut stack = vec![expr];
let mut seen = rustc_hash::FxHashSet::default();
while let Some(id) = stack.pop() {
if !seen.insert(id) || !expr_contains_var(arena, id, var) {
continue;
}
match arena.node(id) {
ExprNode::Add(ch) | ExprNode::Mul(ch) => stack.extend(ch.iter().copied()),
ExprNode::Neg(b) => stack.push(*b),
ExprNode::Pow(b, e)
if !expr_contains_var(arena, *e, var)
&& arena.as_num(*e).is_some_and(|r| r.is_integer()) =>
{
stack.push(*b)
}
_ => {
if !atom(id) {
return None;
}
}
}
}
Some(())
}
fn try_solve_rational(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
period: Option<ExprId>,
) -> Option<SolveOutcome> {
if !is_rational_function_of(arena, expr, var) {
return None;
}
let (num, den) = arena.as_numer_denom_expr(expr);
let num = crate::transforms::expand::expand(arena, num);
let num = crate::transforms::eval::eval(arena, num);
if !expr_contains_var(arena, num, var) {
return None;
}
let still_rational = crate::base::walk::post_order_ids(arena, num)
.into_iter()
.any(|id| {
matches!(arena.node(id), ExprNode::Pow(b, e)
if expr_contains_var(arena, *b, var)
&& arena.as_num(*e).is_some_and(|r| r.is_negative()))
});
if still_rational {
return None;
}
if let (Some(p), Some(q)) = (
polybridge::expr_to_poly(arena, num, var),
polybridge::expr_to_poly(arena, den, var),
) && !q.is_zero()
&& !p.is_zero()
{
let p = without_common_factors(p, &q);
if p.is_constant() {
return Some(SolveOutcome::NoSolution(
"every root of the numerator is a pole (a root of the denominator)".into(),
));
}
return Some(SolveOutcome::Solutions(solve_rational_poly(arena, var, &p)));
}
let num = match without_common_factors_multi(arena, num, den, var) {
Some(reduced) if !expr_contains_var(arena, reduced, var) => {
return Some(SolveOutcome::NoSolution(
"every root of the numerator is a pole (a root of the denominator)".into(),
));
}
Some(reduced) => reduced,
None => num,
};
let candidates = solve_raw(arena, num, var, period).into_solutions();
let mut out = Vec::with_capacity(candidates.len());
for s in candidates {
let d = crate::transforms::subs::subs(arena, den, var, s.value);
let d = crate::transforms::expand::expand(arena, d);
let d = crate::transforms::eval::eval(arena, d);
if arena.is_zero_structural(d) {
continue;
}
if crate::base::walk::free_symbols(arena, d).is_empty() {
match crate::poly::algebraic::is_zero_checked(arena, d) {
Some(true) => continue,
Some(false) => {}
None => {
return Some(SolveOutcome::Unresolved(format!(
"cannot decide whether the root {} of the numerator is a pole",
arena.display(s.value)
)));
}
}
}
out.push(s);
}
Some(SolveOutcome::Solutions(out))
}
fn try_solve_parametric_factors(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
period: Option<ExprId>,
) -> Option<Vec<Solution>> {
let coeffs = symbolic_poly_coeffs(arena, expr, var)?;
if coeffs.len() < 4 || coeffs.len() > 9 {
return None;
}
let factored = arena.factor_expr(expr, var);
if factored == expr {
return None;
}
let parts: Vec<ExprId> = match arena.node(factored) {
ExprNode::Mul(ch) => ch.to_vec(),
_ => vec![factored],
};
let mut with_var: Vec<ExprId> = Vec::new();
for p in parts {
let base = match *arena.node(p) {
ExprNode::Pow(b, e)
if arena
.as_num(e)
.is_some_and(|r| r.is_integer() && r.is_positive()) =>
{
b
}
_ => p,
};
if expr_contains_var(arena, base, var) && !with_var.contains(&base) {
with_var.push(base);
}
}
let mut out: Vec<Solution> = Vec::new();
for f in with_var {
if f == expr {
return None;
}
let SolveOutcome::Solutions(sols) = solve_raw(arena, f, var, period) else {
return None;
};
if sols.is_empty() {
return None;
}
for s in sols {
if !out.iter().any(|o| o.value == s.value) {
out.push(s);
}
}
}
(!out.is_empty()).then_some(out)
}
fn rational_over_q_in_all_symbols(arena: &Arena, expr: ExprId) -> bool {
let ExprNode::Mul(children) = arena.node(expr) else {
return false;
};
let mut syms = crate::base::walk::free_symbols(arena, expr);
syms.sort_by_key(|id| id.0);
syms.dedup();
if syms.len() > 4 {
return false;
}
children.iter().all(|&c| {
let base = match arena.node(c) {
ExprNode::Pow(b, e) if arena.as_num(*e).is_some_and(|r| r.is_integer()) => *b,
_ => c,
};
polybridge::expr_to_multipoly(arena, base, &syms).is_some()
})
}
fn drop_poles_of_product(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
candidates: Vec<Solution>,
) -> Result<Vec<Solution>, String> {
let ExprNode::Mul(children) = arena.node(expr).clone() else {
return Ok(candidates);
};
let dens: Vec<ExprId> = children
.iter()
.filter_map(|&c| match *arena.node(c) {
ExprNode::Pow(b, e)
if arena.as_num(e).is_some_and(|r| r.is_negative())
&& expr_contains_var(arena, b, var) =>
{
Some(b)
}
_ => None,
})
.collect();
if dens.is_empty() {
return Ok(candidates);
}
let mut out = Vec::with_capacity(candidates.len());
'cand: for s in candidates {
if !crate::base::walk::free_symbols(arena, s.value).is_empty() {
out.push(s);
continue;
}
for &b in &dens {
let d = crate::transforms::subs::subs(arena, b, var, s.value);
let d = crate::transforms::expand::expand(arena, d);
let d = crate::transforms::eval::eval(arena, d);
if arena.is_zero_structural(d) {
continue 'cand;
}
match crate::poly::algebraic::is_zero_checked(arena, d) {
Some(true) => continue 'cand,
Some(false) => {}
None => {
return Err(format!(
"cannot decide whether {} is a pole",
arena.display(s.value)
));
}
}
}
out.push(s);
}
Ok(out)
}
fn is_rational_function_of(arena: &Arena, expr: ExprId, var: ExprId) -> bool {
let mut negative = false;
for id in crate::base::walk::post_order_ids(arena, expr) {
if !expr_contains_var(arena, id, var) {
continue;
}
match arena.node(id) {
ExprNode::Symbol(_) | ExprNode::Add(_) | ExprNode::Mul(_) | ExprNode::Neg(_) => {}
ExprNode::Pow(_, e) => match arena.as_num(*e) {
Some(r) if r.is_integer() => negative |= r.is_negative(),
_ => return false,
},
_ => return false,
}
}
negative
}
fn without_common_factors_multi(
arena: &mut Arena,
num: ExprId,
den: ExprId,
var: ExprId,
) -> Option<ExprId> {
let mut syms = crate::base::walk::free_symbols(arena, num);
syms.extend(crate::base::walk::free_symbols(arena, den));
syms.sort_by_key(|id| id.0);
syms.dedup();
if !syms.contains(&var) || syms.len() < 2 || syms.len() > 4 {
return None;
}
let mut n = polybridge::expr_to_multipoly(arena, num, &syms)?;
let d = polybridge::expr_to_multipoly(arena, den, &syms)?;
if d.is_zero() || n.is_zero() {
return None;
}
let mut changed = false;
for _ in 0..64 {
let g = crate::poly::multipoly::MultiPoly::gcd(&n, &d);
if g.total_degree().unwrap_or(0) == 0 {
break;
}
n = n.div_exact(&g)?;
changed = true;
}
changed.then(|| polybridge::multipoly_to_expr(arena, &n, &syms))
}
fn without_common_factors(mut p: Poly, q: &Poly) -> Poly {
if q.is_zero() {
return p;
}
loop {
let g = Poly::gcd(&p, q);
if g.degree().unwrap_or(0) == 0 || p.is_zero() {
return p;
}
p = p.div(&g);
}
}
fn linear_rate(arena: &mut Arena, arg: ExprId, var: ExprId) -> Option<(ExprId, ExprId)> {
let coeffs = symbolic_poly_coeffs(arena, arg, var)?;
if coeffs.len() != 2 || arena.is_zero_structural(coeffs[1]) {
return None;
}
Some((coeffs[1], coeffs[0]))
}
fn perfect_power_base(c: &Q) -> Option<(BigInt, i64)> {
if !c.is_positive() || c.is_one() {
return None;
}
let (n, sign) = if c.is_integer() {
(c.to_integer(), 1i64)
} else if c.numer().is_one() {
(c.denom().clone(), -1i64)
} else {
return None;
};
let bits = n.bits();
for k in (2..=bits.min(64)).rev() {
let k32 = u32::try_from(k).ok()?;
let r = n.nth_root(k32);
if num_traits::Pow::pow(&r, k32) == n {
return Some((r, sign * i64::try_from(k).ok()?));
}
}
Some((n, sign))
}
fn try_exponential_substitution(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
period: Option<ExprId>,
) -> Option<Vec<Solution>> {
let mut atoms: Vec<ExprId> = Vec::new();
walk_var_atoms(arena, expr, var, |id| {
let ok = match arena.node(id) {
ExprNode::Exp(_) => true,
ExprNode::Pow(b, _) => !expr_contains_var(arena, *b, var),
_ => false,
};
if ok && !atoms.contains(&id) {
atoms.push(id);
}
ok
})?;
let mut rated: Vec<(ExprId, Q, ExprId)> = Vec::with_capacity(atoms.len());
let mut unit: Option<ExprId> = None; let mut base: Option<Option<BigInt>> = None; for &id in &atoms {
let (this_base, arg, scale, cbase) = match arena.node(id).clone() {
ExprNode::Exp(arg) => (None, arg, Q::one(), None),
ExprNode::Pow(b, arg) => {
let c = arena.as_num(b)?.clone();
let (r, k) = perfect_power_base(&c)?;
(Some(r), arg, Q::from_integer(BigInt::from(k)), Some(b))
}
_ => return None,
};
match &base {
None => base = Some(this_base.clone()),
Some(b) if *b == this_base => {}
Some(_) => return None,
}
let (a, b0) = linear_rate(arena, arg, var)?;
let a0 = *unit.get_or_insert(a);
let ratio = arena.div(a, a0);
let ratio = crate::transforms::eval::eval(arena, ratio);
let ratio = arena.as_num(ratio)?.clone();
let factor = match cbase {
None => arena.exp(b0),
Some(cb) => arena.pow(cb, b0),
};
rated.push((id, ratio * scale, factor));
}
let unit = unit?;
let base = base?;
if rated.is_empty() {
return None;
}
let mut gn = BigInt::zero();
let mut gd = BigInt::one();
for (_, r, _) in &rated {
gn = num_integer::Integer::gcd(&gn, r.numer());
gd = num_integer::Integer::lcm(&gd, r.denom());
}
let g = Q::new(gn, gd);
if g.is_zero() {
return None;
}
let t = arena.symbol("__t_exp");
let mut sub = expr;
for (id, r, factor) in &rated {
let n = (r / &g).to_integer();
if n.abs() > BigInt::from(MAX_SUBSTITUTION_DEGREE) {
return None;
}
let n_id = arena.big_int(n);
let tn = arena.pow(t, n_id);
let repl = arena.mul(&[*factor, tn]);
sub = arena.subs_structural(sub, *id, repl);
}
let sub = crate::transforms::eval::eval(arena, sub);
if expr_contains_var(arena, sub, var) {
return None;
}
let g_id = arena.num_ratio(g);
let gx = arena.mul(&[g_id, unit, var]);
let generator = match base {
None => arena.exp(gx),
Some(r) => {
let r_id = arena.big_int(r);
arena.pow(r_id, gx)
}
};
let mut out: Vec<Solution> = Vec::new();
for ts in solutions_unless_identity(arena, sub, t)? {
if let Some(back) = solve_by_peeling(arena, generator, ts.value, var, period) {
for s in back {
if !out.iter().any(|o| o.value == s.value) {
out.push(s);
}
}
}
}
Some(out)
}
fn solutions_unless_identity(arena: &mut Arena, sub: ExprId, t: ExprId) -> Option<Vec<Solution>> {
let combined = polybridge::together(arena, sub);
let (num, _) = polybridge::as_numer_denom(arena, combined);
let num = crate::transforms::expand::expand(arena, num);
let num = crate::transforms::eval::eval(arena, num);
if arena.is_zero_structural(num) {
return None;
}
match solve_classified(arena, sub, t) {
SolveOutcome::Identity => None,
other => Some(other.into_solutions()),
}
}
const MAX_SUBSTITUTION_DEGREE: i64 = 64;
fn try_half_angle_substitution(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
period: Option<ExprId>,
) -> Option<Vec<Solution>> {
let mut atoms: Vec<ExprId> = Vec::new();
let mut multiple = false;
walk_var_atoms(arena, expr, var, |id| {
let arg = match arena.node(id) {
ExprNode::Sin(a) | ExprNode::Cos(a) | ExprNode::Tan(a) => *a,
_ => return false,
};
let ok = if arg == var {
true
} else if let ExprNode::Mul(ch) = arena.node(arg)
&& ch.len() == 2
&& ch.contains(&var)
&& ch
.iter()
.any(|&c| arena.as_num(c).is_some_and(|r| r.is_integer()))
{
multiple = true;
true
} else {
false
};
if ok && !atoms.contains(&id) {
atoms.push(id);
}
ok
})?;
if atoms.len() < 2 && !multiple {
return None;
}
let expanded = if multiple {
let e = arena.expand_trig_expr(expr);
crate::transforms::eval::eval(arena, e)
} else {
expr
};
let (s, c, tn) = (arena.sin(var), arena.cos(var), arena.tan(var));
walk_var_atoms(arena, expanded, var, |id| id == s || id == c || id == tn)?;
let t = arena.symbol("__t_half");
let one = arena.one;
let two = arena.int(2);
let t2 = arena.pow(t, two);
let one_p = arena.add(&[one, t2]);
let one_m = arena.sub(one, t2);
let two_t = arena.mul(&[two, t]);
let sin_t = arena.div(two_t, one_p);
let cos_t = arena.div(one_m, one_p);
let tan_t = arena.div(two_t, one_m);
let mut sub = arena.subs_structural(expanded, s, sin_t);
sub = arena.subs_structural(sub, c, cos_t);
sub = arena.subs_structural(sub, tn, tan_t);
let sub = crate::transforms::eval::eval(arena, sub);
if expr_contains_var(arena, sub, var) {
return None;
}
let half = arena.rational(1, 2);
let half_x = arena.mul(&[half, var]);
let generator = arena.tan(half_x);
let mut out: Vec<Solution> = Vec::new();
for ts in solutions_unless_identity(arena, sub, t)? {
if let Some(back) = solve_by_peeling(arena, generator, ts.value, var, period) {
for s in back {
if !out.iter().any(|o| o.value == s.value) {
out.push(s);
}
}
}
}
let pi = arena.pi;
let at_pi = crate::transforms::subs::subs(arena, expr, var, pi);
let at_pi = crate::transforms::eval::eval(arena, at_pi);
let zero_at_pi = arena.is_zero_structural(at_pi)
|| (crate::base::walk::free_symbols(arena, at_pi).is_empty()
&& crate::poly::algebraic::is_zero_checked(arena, at_pi) == Some(true));
if zero_at_pi {
let value = match period {
Some(n) => {
let two_pi_n = arena.mul(&[two, pi, n]);
arena.add(&[pi, two_pi_n])
}
None => pi,
};
if !out.iter().any(|o| o.value == value) {
out.push(Solution { value });
}
}
Some(out)
}
fn try_log_sum(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
period: Option<ExprId>,
) -> Option<Vec<Solution>> {
let terms: Vec<ExprId> = match arena.node(expr) {
ExprNode::Add(ch) => ch.to_vec(),
_ => return None,
};
let mut factors: Vec<ExprId> = Vec::new();
let mut rest: Vec<ExprId> = Vec::new();
for term in terms {
if !expr_contains_var(arena, term, var) {
rest.push(term);
continue;
}
let (c, f) = match arena.node(term).clone() {
ExprNode::Ln(f) => (arena.one, f),
ExprNode::Mul(ch) if ch.len() == 2 => {
let (k, l) = if arena.as_num(ch[0]).is_some() {
(ch[0], ch[1])
} else {
(ch[1], ch[0])
};
match (arena.as_num(k), arena.node(l)) {
(Some(_), ExprNode::Ln(f)) => (k, *f),
_ => return None,
}
}
_ => return None,
};
factors.push(arena.pow(f, c));
}
if factors.len() < 2 {
return None;
}
let lhs = arena.mul(&factors);
let g = arena.add(&rest);
let minus_g = arena.neg(g);
let rhs = arena.exp(minus_g);
let eq = arena.sub(lhs, rhs);
let eq = crate::transforms::eval::eval(arena, eq);
if !expr_contains_var(arena, eq, var) {
return None;
}
Some(solve_raw(arena, eq, var, period).into_solutions())
}
fn collect_generators(arena: &Arena, expr: ExprId, var: ExprId, var_sym: SymbolId) -> Vec<ExprId> {
let mut generators = Vec::new();
let mut visited = std::collections::HashSet::new();
collect_gens_recursive(arena, expr, var, var_sym, &mut generators, &mut visited);
generators
}
#[allow(clippy::only_used_in_recursion)]
fn collect_gens_recursive(
arena: &Arena,
expr: ExprId,
var: ExprId,
var_sym: SymbolId,
gens: &mut Vec<ExprId>,
visited: &mut std::collections::HashSet<ExprId>,
) {
if !visited.insert(expr) {
return;
}
match arena.node(expr).clone() {
ExprNode::Exp(inner) if inner == var && !gens.contains(&expr) => {
gens.push(expr);
}
ExprNode::Sin(inner) | ExprNode::Cos(inner) | ExprNode::Tan(inner)
if inner == var && !gens.contains(&expr) =>
{
gens.push(expr);
}
ExprNode::Ln(inner) if inner == var && !gens.contains(&expr) => {
gens.push(expr);
}
ExprNode::Pow(base, exp)
if base == var && !expr_contains_var(arena, exp, var)
&& !gens.contains(&expr) =>
{
gens.push(expr);
}
ExprNode::Add(ref children) | ExprNode::Mul(ref children) => {
for &c in children {
collect_gens_recursive(arena, c, var, var_sym, gens, visited);
}
}
ExprNode::Pow(base, exp) => {
collect_gens_recursive(arena, base, var, var_sym, gens, visited);
collect_gens_recursive(arena, exp, var, var_sym, gens, visited);
}
ExprNode::Neg(inner)
| ExprNode::Exp(inner)
| ExprNode::Ln(inner)
| ExprNode::Sin(inner)
| ExprNode::Cos(inner)
| ExprNode::Tan(inner) => {
collect_gens_recursive(arena, inner, var, var_sym, gens, visited);
}
_ => {}
}
}
fn rewrite_exp_powers(arena: &mut Arena, expr: ExprId, var: ExprId, gen_exp_x: ExprId) -> ExprId {
let mut result = expr;
for k in 2i64..=6 {
let k_id = arena.int(k);
let k_var = arena.mul(&[k_id, var]);
let exp_k_var = arena.exp(k_var);
let gen_pow_k = arena.pow(gen_exp_x, k_id);
result = arena.subs_structural(result, exp_k_var, gen_pow_k);
}
for k in [-1i64, -2, -3] {
let k_id = arena.int(k);
let k_var = arena.mul(&[k_id, var]);
let exp_k_var = arena.exp(k_var);
let gen_pow_k = arena.pow(gen_exp_x, k_id);
result = arena.subs_structural(result, exp_k_var, gen_pow_k);
}
result
}
fn try_solve_lambert(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
_var_sym: SymbolId,
) -> Option<Vec<Solution>> {
tracing::debug!("solve: trying LambertW");
let terms: Vec<ExprId> = match arena.node(expr).clone() {
ExprNode::Add(children) => children.to_vec(),
_ => vec![expr],
};
let mut constant_terms: Vec<ExprId> = Vec::new();
let mut linear_coeffs: Vec<ExprId> = Vec::new(); let mut exp_terms: Vec<LambertExp> = Vec::new(); let mut var_exp_terms: Vec<LambertExp> = Vec::new(); let mut var_ln_terms: Vec<LambertLn> = Vec::new(); let mut ln_terms: Vec<LambertLn> = Vec::new(); let mut pow_terms: Vec<ExprId> = Vec::new();
for &term in &terms {
if !expr_contains_var(arena, term, var) {
constant_terms.push(term);
continue;
}
match classify_lambert_term(arena, term, var) {
Some(LambertTermClass::Linear(coeff)) => linear_coeffs.push(coeff),
Some(LambertTermClass::ExpVar(e)) => exp_terms.push(e),
Some(LambertTermClass::VarExpVar(e)) => var_exp_terms.push(e),
Some(LambertTermClass::VarLnVar(ln)) => var_ln_terms.push(ln),
Some(LambertTermClass::LnVar(ln)) => ln_terms.push(ln),
Some(LambertTermClass::PowVarVar(coeff)) => pow_terms.push(coeff),
None => return None,
}
}
let d = match constant_terms.len() {
0 => arena.zero,
1 => constant_terms[0],
_ => arena.add(&constant_terms),
};
let c = match linear_coeffs.len() {
0 => None,
1 => Some(linear_coeffs[0]),
_ => Some(arena.add(&linear_coeffs)),
};
let counts = (
exp_terms.len(),
var_exp_terms.len(),
var_ln_terms.len(),
ln_terms.len(),
pow_terms.len(),
);
let solutions: Vec<ExprId> = match (counts, c) {
((0, 1, 0, 0, 0), None) => {
let LambertExp { coeff: a, rate: b } = var_exp_terms[0];
let neg_d = arena.neg(d);
let neg_d_over_a = arena.div(neg_d, a);
let b_arg = arena.mul(&[b, neg_d_over_a]);
lambert_branches(arena, b_arg)
.into_iter()
.map(|w| arena.div(w, b))
.collect()
}
((1, 0, 0, 0, 0), Some(c)) => {
let LambertExp {
coeff: a_exp,
rate: b,
} = exp_terms[0];
let b_d = arena.mul(&[b, d]);
let b_d_over_c = arena.div(b_d, c);
let exp_bd_c = arena.exp(b_d_over_c);
let c_exp_bd_c = arena.mul(&[c, exp_bd_c]);
let a_b = arena.mul(&[a_exp, b]);
let w_arg = arena.div(a_b, c_exp_bd_c);
let d_over_c = arena.div(d, c);
lambert_branches(arena, w_arg)
.into_iter()
.map(|w| {
let neg_w = arena.neg(w);
let neg_w_over_b = arena.div(neg_w, b);
arena.sub(neg_w_over_b, d_over_c)
})
.collect()
}
((0, 0, 1, 0, 0), c) => {
let LambertLn { coeff: a, scale: b } = var_ln_terms[0];
let c_over_a = match c {
Some(c) => arena.div(c, a),
None => arena.zero,
};
let neg_bd = arena.mul(&[arena.neg_one, b, d]);
let neg_bd_over_a = arena.div(neg_bd, a);
let exp_c_a = arena.exp(c_over_a);
let w_arg = arena.mul(&[neg_bd_over_a, exp_c_a]);
lambert_branches(arena, w_arg)
.into_iter()
.map(|w| {
let t = arena.sub(w, c_over_a);
let y = arena.exp(t);
arena.div(y, b)
})
.collect()
}
((0, 0, 0, 1, 0), Some(c)) => {
let LambertLn { coeff: a, scale: b } = ln_terms[0];
let ab = arena.mul(&[a, b]);
let c_over_ab = arena.div(c, ab);
let a_over_c = arena.div(a, c);
let neg_d = arena.neg(d);
let neg_d_over_a = arena.div(neg_d, a);
let exp_part = arena.exp(neg_d_over_a);
let w_arg = arena.mul(&[c_over_ab, exp_part]);
lambert_branches(arena, w_arg)
.into_iter()
.map(|w| arena.mul(&[a_over_c, w]))
.collect()
}
((0, 0, 0, 0, 1), None) => {
let a = pow_terms[0];
let neg_d = arena.neg(d);
let r = arena.div(neg_d, a);
let ln_r = arena.ln(r);
lambert_branches(arena, ln_r)
.into_iter()
.map(|w| arena.exp(w))
.collect()
}
_ => return None,
};
Some(
solutions
.into_iter()
.map(|s| Solution {
value: crate::transforms::eval::eval(arena, s),
})
.collect(),
)
}
fn lambert_branches(arena: &mut Arena, arg: ExprId) -> Vec<ExprId> {
let arg = crate::transforms::eval::eval(arena, arg);
let principal = arena.lambertw(arg);
let mut out = vec![principal];
if crate::base::walk::free_symbols(arena, arg).is_empty()
&& crate::poly::algebraic::sign_checked(arena, arg) == Some(-1)
{
let neg_one = arena.neg_one;
let inv_e = arena.exp(neg_one);
let shifted = arena.add(&[arg, inv_e]);
if crate::poly::algebraic::sign_checked(arena, shifted) == Some(1) {
out.push(arena.lambertw_branch(arg, neg_one));
}
}
out
}
#[derive(Clone, Copy)]
struct LambertExp {
coeff: ExprId,
rate: ExprId,
}
#[derive(Clone, Copy)]
struct LambertLn {
coeff: ExprId,
scale: ExprId,
}
enum LambertTermClass {
Linear(ExprId),
ExpVar(LambertExp),
VarExpVar(LambertExp),
VarLnVar(LambertLn),
LnVar(LambertLn),
PowVarVar(ExprId),
}
fn classify_lambert_term(arena: &mut Arena, term: ExprId, var: ExprId) -> Option<LambertTermClass> {
let (term, negate) = match arena.node(term).clone() {
ExprNode::Neg(inner) => (inner, true),
_ => (term, false),
};
let factors: Vec<ExprId> = match arena.node(term).clone() {
ExprNode::Mul(children) => children.to_vec(),
_ => vec![term],
};
let mut const_factors: Vec<ExprId> = Vec::new();
let mut has_var = false;
let mut exp_inner: Option<ExprId> = None;
let mut ln_inner: Option<ExprId> = None;
let mut pow_var_var = false;
for &child in &factors {
if !expr_contains_var(arena, child, var) {
const_factors.push(child);
continue;
}
if child == var {
if has_var {
return None; }
has_var = true;
continue;
}
match arena.node(child).clone() {
ExprNode::Exp(inner) if exp_inner.is_none() => exp_inner = Some(inner),
ExprNode::Pow(base, e)
if exp_inner.is_none()
&& !expr_contains_var(arena, base, var)
&& !arena.is_zero_structural(base) =>
{
let ln_base = arena.ln(base);
let ln_base = crate::transforms::eval::eval(arena, ln_base);
exp_inner = Some(arena.mul(&[e, ln_base]));
}
ExprNode::Ln(inner) if ln_inner.is_none() => ln_inner = Some(inner),
ExprNode::Pow(base, exp) if base == var && exp == var && !pow_var_var => {
pow_var_var = true;
}
_ => return None, }
}
let mut coeff = match const_factors.len() {
0 => arena.one,
1 => const_factors[0],
_ => arena.mul(&const_factors),
};
if negate {
coeff = arena.neg(coeff);
}
let class = match (has_var, exp_inner, ln_inner, pow_var_var) {
(true, None, None, false) => LambertTermClass::Linear(coeff),
(has_var, Some(inner), None, false) => {
let rate = extract_var_coeff_in_product(arena, inner, var)?;
let e = LambertExp { coeff, rate };
if has_var {
LambertTermClass::VarExpVar(e)
} else {
LambertTermClass::ExpVar(e)
}
}
(has_var, None, Some(inner), false) => {
let scale = extract_var_coeff_in_product(arena, inner, var)?;
let l = LambertLn { coeff, scale };
if has_var {
LambertTermClass::VarLnVar(l)
} else {
LambertTermClass::LnVar(l)
}
}
(false, None, None, true) => LambertTermClass::PowVarVar(coeff),
_ => return None,
};
Some(class)
}
pub(crate) fn try_solve_linear_symbolic(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
) -> Option<Vec<Solution>> {
let terms: Vec<ExprId> = match arena.node(expr).clone() {
ExprNode::Add(children) => children.to_vec(),
_ => vec![expr],
};
let mut coeff_parts: Vec<ExprId> = Vec::new(); let mut const_parts: Vec<ExprId> = Vec::new();
for &term in &terms {
if !expr_contains_var(arena, term, var) {
const_parts.push(term);
continue;
}
{
let coeff = extract_var_coeff_in_product(arena, term, var)?;
coeff_parts.push(coeff);
}
}
if coeff_parts.is_empty() {
return None;
}
let coeff = if coeff_parts.len() == 1 {
coeff_parts[0]
} else {
arena.add(&coeff_parts)
};
let constant = if const_parts.is_empty() {
arena.zero
} else if const_parts.len() == 1 {
const_parts[0]
} else {
arena.add(&const_parts)
};
let neg_const = arena.neg(constant);
let solution = arena.div(neg_const, coeff);
let solution = crate::simplify::ratsimp::ratsimp(arena, solution);
Some(vec![Solution { value: solution }])
}
fn extract_var_coeff_in_product(arena: &mut Arena, expr: ExprId, var: ExprId) -> Option<ExprId> {
if expr == var {
return Some(arena.one);
}
match arena.node(expr).clone() {
ExprNode::Mul(children) => {
let mut const_parts: Vec<ExprId> = Vec::new();
let mut found_var = false;
for &child in &children {
if child == var {
if found_var {
return None;
}
found_var = true;
} else if !expr_contains_var(arena, child, var) {
const_parts.push(child);
} else {
return None;
}
}
if !found_var {
return None;
}
match const_parts.len() {
0 => Some(arena.one),
1 => Some(const_parts[0]),
_ => Some(arena.mul(&const_parts)),
}
}
_ => None,
}
}
#[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()
}
fn solution_strings(a: &Arena, solutions: &[Solution]) -> Vec<String> {
solutions.iter().map(|s| display(a, s.value)).collect()
}
#[test]
fn solve_linear_simple() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let three = a.int(3);
let expr = a.sub(x, three);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1);
assert_eq!(display(&a, solutions[0].value), "3");
}
#[test]
fn solve_linear_with_coefficient() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let six = a.int(6);
let two_x = a.mul(&[two, x]);
let expr = a.sub(two_x, six);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1);
assert_eq!(display(&a, solutions[0].value), "3");
}
#[test]
fn solve_linear_rational_solution() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let three = a.int(3);
let one = a.one;
let three_x = a.mul(&[three, x]);
let expr = a.sub(three_x, one);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1);
assert_eq!(display(&a, solutions[0].value), "1/3");
}
#[test]
fn solve_linear_negative_solution() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let five = a.int(5);
let expr = a.add(&[x, five]);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1);
assert_eq!(display(&a, solutions[0].value), "-5");
}
#[test]
fn solve_quadratic_two_roots() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let five = a.int(5);
let six = a.int(6);
let x_sq = a.pow(x, two);
let five_x = a.mul(&[five, x]);
let neg_five_x = a.neg(five_x);
let expr = a.add(&[x_sq, neg_five_x, six]);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 2);
let vals: Vec<String> = solution_strings(&a, &solutions);
assert!(
vals.contains(&"2".to_string()),
"should have root 2: {vals:?}"
);
assert!(
vals.contains(&"3".to_string()),
"should have root 3: {vals:?}"
);
}
#[test]
fn solve_quadratic_double_root() {
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 two_x = a.mul(&[two, x]);
let neg_two_x = a.neg(two_x);
let expr = a.add(&[x_sq, neg_two_x, one]);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1);
assert_eq!(display(&a, solutions[0].value), "1");
}
#[test]
fn solve_quadratic_no_real_roots() {
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 expr = a.add(&[x_sq, one]);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 2, "x²+1=0 should have 2 complex roots");
let vals: Vec<String> = solution_strings(&a, &solutions);
assert!(
vals.iter().all(|v| v.contains("I")),
"roots should contain I: {vals:?}"
);
}
#[test]
fn solve_quadratic_irrational_roots() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let x_sq = a.pow(x, two);
let expr = a.sub(x_sq, two);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 2, "x²-2=0 should have 2 solutions");
let vals: Vec<String> = solution_strings(&a, &solutions);
let has_sqrt = vals.iter().any(|v| v.contains("sqrt"));
assert!(has_sqrt, "should contain sqrt(2): {vals:?}");
}
#[test]
fn solve_x_squared_minus_one() {
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 expr = a.sub(x_sq, one);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 2);
let vals: Vec<String> = solution_strings(&a, &solutions);
assert!(vals.contains(&"1".to_string()));
assert!(vals.contains(&"-1".to_string()));
}
#[test]
fn solve_cubic_all_rational() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let three = a.int(3);
let six = a.int(6);
let eleven = a.int(11);
let two = a.int(2);
let x3 = a.pow(x, three);
let x2 = a.pow(x, two);
let six_x2 = a.mul(&[six, x2]);
let eleven_x = a.mul(&[eleven, x]);
let neg_six_x2 = a.neg(six_x2);
let neg_six = a.neg(six);
let expr = a.add(&[x3, neg_six_x2, eleven_x, neg_six]);
let solutions = solve(&mut a, expr, x);
let vals: Vec<String> = solution_strings(&a, &solutions);
assert!(
vals.contains(&"1".to_string()),
"should have root 1: {vals:?}"
);
assert!(
vals.contains(&"2".to_string()),
"should have root 2: {vals:?}"
);
assert!(
vals.contains(&"3".to_string()),
"should have root 3: {vals:?}"
);
}
#[test]
fn solve_constant_nonzero_no_solutions() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let five = a.int(5);
let solutions = solve(&mut a, five, x);
assert!(solutions.is_empty());
assert!(matches!(
solve_classified(&mut a, five, x),
SolveOutcome::NoSolution(_)
));
}
#[test]
fn solve_zero_expression() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let zero = a.zero;
let solutions = solve(&mut a, zero, x);
assert!(solutions.is_empty());
assert!(matches!(
solve_classified(&mut a, zero, x),
SolveOutcome::Identity
));
}
#[test]
fn solve_classified_identity_after_eval() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let zero = a.zero;
let sin0 = a.sin(zero);
assert!(matches!(
solve_classified(&mut a, sin0, x),
SolveOutcome::Identity
));
}
#[test]
fn solve_classified_exp_eq_zero_no_solution() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let e = a.exp(x);
assert!(matches!(
solve_classified(&mut a, e, x),
SolveOutcome::NoSolution(_)
));
}
#[test]
fn solve_classified_polynomial_solutions() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let x2 = a.pow(x, two);
let four = a.int(4);
let expr = a.sub(x2, four);
match solve_classified(&mut a, expr, x) {
SolveOutcome::Solutions(s) => assert_eq!(s.len(), 2),
other => panic!("expected solutions, got {other:?}"),
}
}
#[test]
fn solve_general_sin_half_has_period_param() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let n = sym(&mut a, "n");
let half = a.rational(1, 2);
let sx = a.sin(x);
let expr = a.sub(sx, half);
let out = solve_general(&mut a, expr, x, n);
let sols = out.into_solutions();
assert_eq!(sols.len(), 2, "two families expected");
for s in &sols {
assert!(
expr_contains_var(&a, s.value, n),
"family should mention n: {}",
display(&a, s.value)
);
assert!(display(&a, s.value).contains("pi"));
}
}
#[test]
fn solve_general_tan_has_pi_n() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let n = sym(&mut a, "n");
let one = a.one;
let tx = a.tan(x);
let expr = a.sub(tx, one);
let sols = solve_general(&mut a, expr, x, n).into_solutions();
assert_eq!(sols.len(), 1);
let s = display(&a, sols[0].value);
assert!(s.contains("n") && s.contains("pi"), "got {s}");
}
#[test]
fn solve_general_polynomial_unchanged() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let n = sym(&mut a, "n");
let two = a.int(2);
let x2 = a.pow(x, two);
let one = a.one;
let expr = a.sub(x2, one);
let sols = solve_general(&mut a, expr, x, n).into_solutions();
assert_eq!(sols.len(), 2);
for s in &sols {
assert!(!expr_contains_var(&a, s.value, n));
}
}
#[test]
fn solve_sin_linear_argument() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let one = a.one;
let two_x = a.mul(&[two, x]);
let arg = a.add(&[two_x, one]);
let s = a.sin(arg);
let half = a.rational(1, 2);
let expr = a.sub(s, half);
let sols = solve(&mut a, expr, x);
assert_eq!(sols.len(), 2, "got {:?}", solution_strings(&a, &sols));
}
#[test]
fn solve_symbolic_quadratic_coefficients() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let k = sym(&mut a, "k");
let two = a.int(2);
let x2 = a.pow(x, two);
let expr = a.sub(x2, k);
let sols = solve(&mut a, expr, x);
assert_eq!(sols.len(), 2, "got {:?}", solution_strings(&a, &sols));
for s in &sols {
assert!(expr_contains_var(&a, s.value, k));
}
}
#[test]
fn solve_binomial_quintic_roots_of_unity() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let five = a.int(5);
let x5 = a.pow(x, five);
let two = a.int(2);
let expr = a.sub(x5, two);
let sols = solve(&mut a, expr, x);
assert_eq!(sols.len(), 5);
for s in &sols {
assert!(!matches!(a.node(s.value), ExprNode::RootOf(..)));
}
}
#[test]
fn solve_sin_x_eq_zero_via_change_of_variable() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let expr = a.sin(x);
let solutions = solve(&mut a, expr, x);
assert_eq!(
solutions.len(),
2,
"sin(x)=0 should have 2 solutions (two branches), got {}",
solutions.len()
);
let vals: Vec<String> = solutions.iter().map(|s| display(&a, s.value)).collect();
assert!(
vals.iter().any(|v| v == "0" || v.contains("asin(0)")),
"should have root 0 or asin(0): {vals:?}"
);
assert!(
vals.iter()
.any(|v| v == "pi" || v.contains("pi") || v.contains("asin")),
"should have root involving pi or asin: {vals:?}"
);
}
#[test]
fn solve_with_zero_root() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let x_sq = a.pow(x, two);
let expr = a.sub(x_sq, x);
let solutions = solve(&mut a, expr, x);
let vals: Vec<String> = solution_strings(&a, &solutions);
assert!(
vals.contains(&"0".to_string()),
"should have root 0: {vals:?}"
);
assert!(
vals.contains(&"1".to_string()),
"should have root 1: {vals:?}"
);
}
#[test]
fn solve_and_verify_quadratic() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let five = a.int(5);
let six = a.int(6);
let x_sq = a.pow(x, two);
let five_x = a.mul(&[five, x]);
let neg_five_x = a.neg(five_x);
let expr = a.add(&[x_sq, neg_five_x, six]);
let solutions = solve(&mut a, expr, x);
for sol in &solutions {
let val = crate::transforms::subs::subs(&mut a, expr, x, sol.value);
assert!(
a.is_zero_structural(val),
"substituting x={} should give 0, got {}",
display(&a, sol.value),
display(&a, val)
);
}
}
#[test]
fn solve_exp_x_eq_5() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let five = a.int(5);
let exp_x = a.exp(x);
let expr = a.sub(exp_x, five);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1, "exp(x)-5=0 should have 1 solution");
let val = display(&a, solutions[0].value);
assert!(
val.contains("ln") || val.contains("log"),
"solution should be ln(5): {val}"
);
}
#[test]
fn solve_ln_x_eq_2() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let ln_x = a.ln(x);
let expr = a.sub(ln_x, two);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1, "ln(x)-2=0 should have 1 solution");
let val = display(&a, solutions[0].value);
assert!(
val.contains("exp") || val.contains("e") || val.contains("E"),
"solution should be exp(2): {val}"
);
}
#[test]
fn solve_sin_x_eq_half() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let half = {
let nid = a.intern_num(Ratio::new(BigInt::from(1), BigInt::from(2)));
a.intern(ExprNode::Num(nid))
};
let sin_x = a.sin(x);
let expr = a.sub(sin_x, half);
let solutions = solve(&mut a, expr, x);
assert_eq!(
solutions.len(),
2,
"sin(x)-1/2=0 should have 2 solutions (two branches)"
);
let val0 = display(&a, solutions[0].value);
let val1 = display(&a, solutions[1].value);
assert!(
val0.contains("pi") || val0.contains("asin"),
"first solution should be pi/6 (or asin(1/2)): {val0}"
);
assert!(
val1.contains("pi") || val1.contains("asin"),
"second solution should be 5*pi/6 (or pi - asin(1/2)): {val1}"
);
assert_ne!(val0, val1);
}
#[test]
fn solve_sqrt_x_eq_3() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let three = a.int(3);
let sqrt_x = a.sqrt(x);
let expr = a.sub(sqrt_x, three);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1, "sqrt(x)-3=0 should have 1 solution");
let val = display(&a, solutions[0].value);
assert_eq!(val, "9", "solution should be 9: {val}");
}
#[test]
fn solve_mul_factors() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.one;
let two = a.int(2);
let x_minus_1 = a.sub(x, one);
let x_plus_2 = a.add(&[x, two]);
let expr = a.mul(&[x, x_minus_1, x_plus_2]);
let solutions = solve(&mut a, expr, x);
assert!(
solutions.len() >= 3,
"x*(x-1)*(x+2)=0 should have 3 roots, got {}",
solutions.len()
);
let vals: Vec<String> = solution_strings(&a, &solutions);
assert!(
vals.contains(&"0".to_string()),
"should have root 0: {vals:?}"
);
assert!(
vals.contains(&"1".to_string()),
"should have root 1: {vals:?}"
);
assert!(
vals.contains(&"-2".to_string()),
"should have root -2: {vals:?}"
);
}
#[test]
fn solve_2_exp_x_minus_6() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let six = a.int(6);
let exp_x = a.exp(x);
let two_exp_x = a.mul(&[two, exp_x]);
let expr = a.sub(two_exp_x, six);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1, "2*exp(x)-6=0 should have 1 solution");
let val = display(&a, solutions[0].value);
assert!(
val.contains("ln") || val.contains("log"),
"solution should be ln(3): {val}"
);
}
#[test]
fn solve_and_verify_cubic() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let three = a.int(3);
let six = a.int(6);
let eleven = a.int(11);
let two = a.int(2);
let x3 = a.pow(x, three);
let x2 = a.pow(x, two);
let six_x2 = a.mul(&[six, x2]);
let eleven_x = a.mul(&[eleven, x]);
let neg_six_x2 = a.neg(six_x2);
let neg_six = a.neg(six);
let expr = a.add(&[x3, neg_six_x2, eleven_x, neg_six]);
let solutions = solve(&mut a, expr, x);
for sol in &solutions {
let val = crate::transforms::subs::subs(&mut a, expr, x, sol.value);
assert!(
a.is_zero_structural(val),
"substituting x={} should give 0, got {}",
display(&a, sol.value),
display(&a, val)
);
}
}
#[test]
fn solve_exp_2x_minus_3_exp_x_plus_2() {
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 exp_2x = a.exp(two_x);
let exp_x = a.exp(x);
let three_exp_x = a.mul(&[three, exp_x]);
let neg_three_exp_x = a.neg(three_exp_x);
let expr = a.add(&[exp_2x, neg_three_exp_x, two]);
let solutions = solve(&mut a, expr, x);
assert!(
solutions.len() >= 2,
"exp(2x)-3*exp(x)+2=0 should have 2 solutions, got {}: {:?}",
solutions.len(),
solution_strings(&a, &solutions)
);
let vals: Vec<String> = solution_strings(&a, &solutions);
assert!(
vals.contains(&"0".to_string()) || vals.contains(&"ln(1)".to_string()),
"should have root 0 or ln(1) (from exp(x)=1): {vals:?}"
);
let has_ln2 = vals.iter().any(|v| v.contains("ln") || v.contains("log"));
assert!(has_ln2, "should have root ln(2): {vals:?}");
}
#[test]
fn solve_sin_squared_minus_sin() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let sin_x = a.sin(x);
let sin_x_sq = a.pow(sin_x, two);
let expr = a.sub(sin_x_sq, sin_x);
let solutions = solve(&mut a, expr, x);
assert!(
solutions.len() >= 2,
"sin(x)^2-sin(x)=0 should have ≥2 solutions, got {}: {:?}",
solutions.len(),
solution_strings(&a, &solutions)
);
let vals: Vec<String> = solution_strings(&a, &solutions);
let has_zero = vals.contains(&"0".to_string());
let has_asin = vals
.iter()
.any(|v| v.contains("asin") || v.contains("arcsin"));
assert!(
has_zero || has_asin,
"should have root 0 or asin(1): {vals:?}"
);
}
#[test]
fn solve_exp_quadratic_one_valid_root() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let five = a.int(5);
let six = a.int(6);
let two_x = a.mul(&[two, x]);
let exp_2x = a.exp(two_x);
let exp_x = a.exp(x);
let five_exp_x = a.mul(&[five, exp_x]);
let neg_five_exp_x = a.neg(five_exp_x);
let expr = a.add(&[exp_2x, neg_five_exp_x, six]);
let solutions = solve(&mut a, expr, x);
assert!(
solutions.len() >= 2,
"exp(2x)-5*exp(x)+6=0 should have 2 solutions, got {}: {:?}",
solutions.len(),
solution_strings(&a, &solutions)
);
let vals: Vec<String> = solution_strings(&a, &solutions);
let has_ln = vals.iter().all(|v| v.contains("ln") || v.contains("log"));
assert!(has_ln, "all roots should involve ln: {vals:?}");
}
#[test]
fn rational_sqrt_perfect() {
let r = Ratio::new(BigInt::from(9), BigInt::from(4));
let s = rational_sqrt(&r).unwrap();
assert_eq!(s, Ratio::new(BigInt::from(3), BigInt::from(2)));
}
#[test]
fn rational_sqrt_not_perfect() {
let r = Ratio::from_integer(BigInt::from(2));
assert!(rational_sqrt(&r).is_none());
}
#[test]
fn rational_sqrt_zero() {
let r = Ratio::zero();
let s = rational_sqrt(&r).unwrap();
assert!(s.is_zero());
}
#[test]
fn divisors_of_12() {
let d = divisors(&BigInt::from(12));
assert_eq!(
d,
vec![1, 2, 3, 4, 6, 12]
.into_iter()
.map(BigInt::from)
.collect::<Vec<_>>()
);
}
#[test]
fn divisors_of_1() {
let d = divisors(&BigInt::from(1));
assert_eq!(d, vec![BigInt::from(1)]);
}
#[test]
fn clear_denominators_works() {
let poly = Poly::from_coeffs(vec![
Ratio::new(BigInt::from(1), BigInt::from(2)),
Ratio::new(BigInt::from(1), BigInt::from(3)),
]);
let (int_poly, lcm) = clear_denominators(&poly);
assert_eq!(lcm, BigInt::from(6));
assert_eq!(int_poly.coeff(0), Ratio::from_integer(BigInt::from(3)));
assert_eq!(int_poly.coeff(1), Ratio::from_integer(BigInt::from(2)));
}
#[test]
fn solve_x2_plus_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 expr = a.add(&[x_sq, one]);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 2, "x²+1=0 should have 2 complex roots");
let vals: Vec<String> = solution_strings(&a, &solutions);
assert!(
vals.iter().all(|v| v.contains("I")),
"roots should contain I: {vals:?}"
);
}
#[test]
fn solve_x2_plus_2x_plus_5() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let five = a.int(5);
let x_sq = a.pow(x, two);
let two_x = a.mul(&[two, x]);
let expr = a.add(&[x_sq, two_x, five]);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 2, "x²+2x+5=0 should have 2 complex roots");
let vals: Vec<String> = solution_strings(&a, &solutions);
for v in &vals {
assert!(v.contains("I"), "root should contain I: {v}");
}
}
#[test]
fn solve_x2_plus_4() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let four = a.int(4);
let x_sq = a.pow(x, two);
let expr = a.add(&[x_sq, four]);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 2, "x²+4=0 should have 2 complex roots");
let vals: Vec<String> = solution_strings(&a, &solutions);
assert!(
vals.iter().all(|v| v.contains("I")),
"roots should contain I: {vals:?}"
);
}
#[test]
fn solve_even_power_peeling_both_roots() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.int(1);
let two = a.int(2);
let nine = a.int(9);
let two_x = a.mul(&[two, x]);
let inner = a.add(&[two_x, one]); let squared = a.pow(inner, two); let neg_nine = a.neg(nine);
let expr = a.add(&[squared, neg_nine]); let solutions = solve(&mut a, expr, x);
let mut vals: Vec<String> = solution_strings(&a, &solutions);
vals.sort();
assert_eq!(vals.len(), 2, "expected 2 solutions, got {vals:?}");
assert_eq!(vals, vec!["-2", "1"], "solutions: {vals:?}");
}
#[test]
fn solve_lambert_x_exp_x_eq_1() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.one;
let exp_x = a.exp(x);
let x_exp_x = a.mul(&[x, exp_x]);
let expr = a.sub(x_exp_x, one);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1, "x·exp(x)=1 should have 1 solution");
let val = display(&a, solutions[0].value);
assert!(val.contains("W("), "solution should be W(1): {val}");
}
#[test]
fn solve_lambert_x_exp_x_eq_5() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let five = a.int(5);
let exp_x = a.exp(x);
let x_exp_x = a.mul(&[x, exp_x]);
let expr = a.sub(x_exp_x, five);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1, "x·exp(x)=5 should have 1 solution");
let val = display(&a, solutions[0].value);
assert!(val.contains("W("), "solution should be W(5): {val}");
}
#[test]
fn solve_lambert_x_exp_x_eq_0() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let exp_x = a.exp(x);
let expr = a.mul(&[x, exp_x]);
let solutions = solve(&mut a, expr, x);
assert!(!solutions.is_empty(), "x·exp(x)=0 should have a solution");
let vals: Vec<String> = solution_strings(&a, &solutions);
assert!(
vals.contains(&"0".to_string()),
"should have root 0: {vals:?}"
);
}
#[test]
fn solve_lambert_2x_exp_x_eq_4() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let four = a.int(4);
let exp_x = a.exp(x);
let two_x_exp_x = a.mul(&[two, x, exp_x]);
let expr = a.sub(two_x_exp_x, four);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1, "2·x·exp(x)=4 should have 1 solution");
let val = display(&a, solutions[0].value);
assert!(val.contains("W("), "solution should involve W: {val}");
}
#[test]
fn solve_lambert_x_exp_2x_eq_3() {
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 exp_2x = a.exp(two_x);
let x_exp_2x = a.mul(&[x, exp_2x]);
let expr = a.sub(x_exp_2x, three);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1, "x·exp(2x)=3 should have 1 solution");
let val = display(&a, solutions[0].value);
assert!(val.contains("W("), "solution should involve W: {val}");
}
#[test]
fn solve_lambert_exp_x_plus_x_eq_2() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let exp_x = a.exp(x);
let sum = a.add(&[exp_x, x]);
let expr = a.sub(sum, two);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1, "exp(x)+x-2=0 should have 1 solution");
let val = display(&a, solutions[0].value);
assert!(val.contains("W("), "solution should involve W: {val}");
}
#[test]
fn solve_lambert_neg_exp_x_minus_x_plus_2() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let exp_x = a.exp(x);
let neg_exp_x = a.neg(exp_x);
let neg_x = a.neg(x);
let expr = a.add(&[neg_exp_x, neg_x, two]);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1, "-exp(x)-x+2=0 should have 1 solution");
let val = display(&a, solutions[0].value);
assert!(val.contains("W("), "solution should involve W: {val}");
}
#[test]
fn solve_lambert_x_squared_exp_x_not_lambert() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.one;
let two = a.int(2);
let x2 = a.pow(x, two);
let exp_x = a.exp(x);
let x2_exp_x = a.mul(&[x2, exp_x]);
let expr = a.sub(x2_exp_x, one);
let solutions = solve(&mut a, expr, x);
assert!(
solutions.is_empty(),
"x²·exp(x)=1 is not a simple LambertW pattern; got {} solutions",
solutions.len()
);
}
#[test]
fn solve_lambert_x_exp_x_eq_e_gives_1() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let e = a.e_const;
let exp_x = a.exp(x);
let x_exp_x = a.mul(&[x, exp_x]);
let expr = a.sub(x_exp_x, e);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1, "x·exp(x)=e should have 1 solution");
let val = display(&a, solutions[0].value);
assert_eq!(val, "1", "W(e) should evaluate to 1: {val}");
}
#[test]
fn solve_lambert_preserves_existing_polynomial() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.one;
let two = a.int(2);
let x2 = a.pow(x, two);
let expr = a.sub(x2, one);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 2, "x²-1 should still give 2 roots");
}
#[test]
fn solve_lambert_preserves_existing_transcendental() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let five = a.int(5);
let exp_x = a.exp(x);
let expr = a.sub(exp_x, five);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1, "exp(x)-5=0 should still give 1 root");
let val = display(&a, solutions[0].value);
assert!(
val.contains("ln"),
"solution should be ln(5), not lambertw: {val}"
);
}
#[test]
fn solve_symbolic_linear_kx_minus_f() {
let mut a = Arena::new();
let k = sym(&mut a, "k");
let x = sym(&mut a, "x");
let f = sym(&mut a, "F");
let kx = a.mul(&[k, x]);
let expr = a.sub(kx, f);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1);
let s = display(&a, solutions[0].value);
assert!(s.contains('F') && s.contains('k'), "Expected F/k, got: {s}");
}
#[test]
fn solve_symbolic_linear_bare_var() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let c = sym(&mut a, "c");
let expr = a.add(&[x, c]);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1);
let s = display(&a, solutions[0].value);
assert!(s.contains('c'), "Expected -c, got: {s}");
}
#[test]
fn solve_symbolic_linear_multiple_terms() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let p = sym(&mut a, "a");
let b = sym(&mut a, "b");
let c = sym(&mut a, "c");
let ax = a.mul(&[p, x]);
let bx = a.mul(&[b, x]);
let expr = a.add(&[ax, bx, c]);
let solutions = solve(&mut a, expr, x);
assert_eq!(solutions.len(), 1);
let s = display(&a, solutions[0].value);
assert!(s.contains('c'), "Expected -c/(a+b), got: {s}");
}
#[test]
fn solve_exp_x_eq_zero_no_solution() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let exp_x = a.exp(x);
let solutions = solve(&mut a, exp_x, x);
assert!(
solutions.is_empty(),
"exp(x)=0 should have no solutions, got: {:?}",
solution_strings(&a, &solutions)
);
}
#[test]
fn solve_exp_x_eq_negative_no_solution() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let three = a.int(3);
let exp_x = a.exp(x);
let expr = a.add(&[exp_x, three]);
let solutions = solve(&mut a, expr, x);
assert!(
solutions.is_empty(),
"exp(x)=-3 should have no solutions, got: {:?}",
solution_strings(&a, &solutions)
);
}
#[test]
fn solve_sin_x_eq_2_no_solution() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let sin_x = a.sin(x);
let expr = a.sub(sin_x, two);
let solutions = solve(&mut a, expr, x);
assert!(
solutions.is_empty(),
"sin(x)=2 should have no solutions, got: {:?}",
solution_strings(&a, &solutions)
);
}
#[test]
fn solve_ln_x_eq_zero() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let ln_x = a.ln(x);
let solutions = solve(&mut a, ln_x, x);
assert_eq!(solutions.len(), 1, "ln(x)=0 should have 1 solution");
let val = display(&a, solutions[0].value);
assert!(
val == "1" || val.contains("exp(0)"),
"solution should be 1 or exp(0), got: {val}"
);
}
#[test]
fn solve_abs_x_plus_1_no_solution() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let one = a.one;
let abs_x = a.abs(x);
let expr = a.add(&[abs_x, one]);
let solutions = solve(&mut a, expr, x);
assert!(
solutions.is_empty(),
"|x|+1=0 should have no solutions, got: {:?}",
solution_strings(&a, &solutions)
);
}
}