use num_bigint::BigInt;
use num_integer::Integer;
use num_rational::Ratio;
use num_traits::{One, Signed, Zero};
use crate::base::arena::Arena;
use crate::base::node::{ExprId, ExprNode, SymbolId};
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),
}
impl SolveOutcome {
pub(crate) fn into_solutions(self) -> Vec<Solution> {
match self {
SolveOutcome::Solutions(s) => s,
SolveOutcome::Identity | SolveOutcome::NoSolution(_) => 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) => finalize_solutions(arena, s),
other => other,
}
}
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);
}
if let ExprNode::Mul(ref children) = arena.node(expr).clone() {
let mut solutions: Vec<Solution> = Vec::new();
let mut any_identity = false;
for &child in children {
if !expr_contains_var(arena, child, var) {
continue;
}
match solve_raw(arena, child, var, period) {
SolveOutcome::Solutions(child_solutions) => {
for sol in child_solutions {
if !solutions.iter().any(|s| s.value == sol.value) {
solutions.push(sol);
}
}
}
SolveOutcome::Identity => any_identity = true,
SolveOutcome::NoSolution(_) => {}
}
}
if any_identity {
return SolveOutcome::Identity;
}
if !solutions.is_empty() {
return SolveOutcome::Solutions(solutions);
}
}
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);
}
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);
}
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()
&& let Ok(s) = crate::transforms::evalf::evalf(arena, expanded, 20)
{
let mag = parse_evalf_magnitude(&s);
if let Some(m) = mag {
if m > 1e-9 {
let shown = arena.display(expr).to_string();
return SolveOutcome::NoSolution(format!(
"equation reduces to the nonzero constant {shown} = 0"
));
}
if m < 1e-15 {
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"
))
}
pub(crate) fn parse_evalf_magnitude(s: &str) -> Option<f64> {
let s = s.trim();
if let Ok(v) = s.parse::<f64>() {
return Some(v.abs());
}
let body = s.replace('*', "");
let body = body.trim_end_matches(['I', 'i']).trim();
match body {
"" | "+" => return Some(1.0),
"-" => return Some(1.0),
_ => {}
}
if let Ok(v) = body.parse::<f64>() {
return Some(v.abs());
}
let mut split_at = None;
for (i, ch) in body.char_indices().skip(1) {
if (ch == '+' || ch == '-') && !body[..i].ends_with('e') && !body[..i].ends_with('E') {
split_at = Some(i);
}
}
let idx = split_at?;
let re: f64 = body[..idx].trim().parse().ok()?;
let im_text: String = body[idx..].chars().filter(|c| !c.is_whitespace()).collect();
let im: f64 = match im_text.as_str() {
"+" => 1.0,
"-" => -1.0,
t => t.parse().ok()?,
};
Some(re.hypot(im))
}
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);
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 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 = rational_to_expr(arena, &ratio);
Some(binomial_roots(arena, c, 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 new_rhs = arena.div(rhs, coeff);
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![]);
}
if let Some(c) = arena.as_num(rhs)
&& !c.is_positive()
{
tracing::debug!("solve_by_peeling: exp domain error, rhs <= 0");
return Some(vec![]);
}
let new_rhs = arena.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()
{
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()
{
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 is_even_positive =
n.is_integer() && n.is_positive() && n.to_integer().is_even();
let inv_n = Ratio::one() / n;
let inv_n_id = {
let nid = arena.intern_num(inv_n);
arena.intern(ExprNode::Num(nid))
};
let pos_rhs = arena.pow(rhs, inv_n_id);
if is_even_positive {
let neg_rhs = arena.neg(pos_rhs);
return peel_two_branches(arena, inner_base, pos_rhs, neg_rhs, var, period);
} else {
return solve_by_peeling(arena, inner_base, pos_rhs, 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(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 = arena.ln(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 new_rhs = arena.asinh(rhs);
solve_by_peeling(arena, inner, new_rhs, var, period)
}
ExprNode::Cosh(inner) => {
if 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);
peel_two_branches(arena, inner, acosh_rhs, neg_acosh, var, period)
}
ExprNode::Tanh(inner) => {
let new_rhs = arena.atanh(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![]);
}
let neg_rhs = arena.neg(rhs);
peel_two_branches(arena, inner, rhs, neg_rhs, var, period)
}
_ => None,
}
}
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 = rational_to_expr(arena, &value);
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 = rational_to_expr(arena, &value);
return vec![Solution { value: value_id }];
}
if discriminant.is_negative() {
let abs_disc = -discriminant;
let neg_b = rational_to_expr(arena, &(-&b));
let abs_disc_id = rational_to_expr(arena, &abs_disc);
let sqrt_abs_disc = if let Some(s) = rational_sqrt(&abs_disc) {
rational_to_expr(arena, &s)
} 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 = rational_to_expr(arena, &two_a_val);
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 = rational_to_expr(arena, &x1);
let x2_id = rational_to_expr(arena, &x2);
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;
rational_to_expr(arena, &neg_b_val)
};
let sqrt_disc = {
let disc_id = rational_to_expr(arena, &discriminant);
arena.sqrt(disc_id)
};
let two_a_val = Ratio::from_integer(BigInt::from(2)) * &a;
let two_a_id = rational_to_expr(arena, &two_a_val);
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 = rational_to_expr(arena, &a);
let b_id = rational_to_expr(arena, &b);
let c_id = rational_to_expr(arena, &c);
let d_id = rational_to_expr(arena, &d);
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: &Ratio<BigInt>, q: &Ratio<BigInt>, disc: &Ratio<BigInt>) -> (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 = rational_to_expr(arena, &shift_rat);
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 = rational_to_expr(arena, &a);
let b_id = rational_to_expr(arena, &b);
let c_id = rational_to_expr(arena, &c);
let d_id = rational_to_expr(arena, &d);
let e_id = rational_to_expr(arena, &e);
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) {
rational_to_expr(arena, &m_rat)
} 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: &Ratio<BigInt>) -> Option<Ratio<BigInt>> {
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<Ratio<BigInt>> = 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<Ratio<BigInt>> =
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 (int_poly, _scale) = clear_denominators(poly);
let degree = match int_poly.degree() {
Some(d) => d,
None => return Vec::new(),
};
if degree > 4
&& let Some(roots) = try_solve_binomial_rational(arena, poly)
{
return roots;
}
if degree > 20 {
let poly_expr = polybridge::poly_to_expr(arena, poly, var);
let mut roots = Vec::new();
for i in 0..degree {
let idx = arena.int(i as i64);
roots.push(Solution {
value: arena.intern(ExprNode::RootOf(poly_expr, idx)),
});
}
return roots;
}
let a0 = int_poly.coeff(0).to_integer(); let an = int_poly.leading_coeff().unwrap().to_integer();
if a0.is_zero() {
let mut roots = vec![Solution { value: arena.zero }];
let reduced_coeffs: Vec<Ratio<BigInt>> = poly.coeffs().iter().skip(1).cloned().collect();
let reduced = Poly::from_coeffs(reduced_coeffs);
if !reduced.is_zero() && !reduced.is_constant() {
let more = solve_rational_roots(arena, var, &reduced);
roots.extend(more);
}
return roots;
}
let divisors_a0 = divisors(&a0.abs());
let divisors_an = divisors(&an.abs());
let mut roots = Vec::new();
let mut remaining = poly.clone();
for p in &divisors_a0 {
for q in &divisors_an {
if remaining.is_constant() {
break;
}
for sign in &[1i64, -1i64] {
let candidate = Ratio::new(p * BigInt::from(*sign), q.clone());
if remaining.eval(&candidate).is_zero() {
let value_id = rational_to_expr(arena, &candidate);
roots.push(Solution { value: value_id });
let factor = Poly::from_coeffs(vec![-candidate.clone(), Ratio::one()]);
let (quotient, _rem) = remaining.div_rem(&factor);
remaining = quotient;
}
}
}
}
if let Some(d) = remaining.degree() {
match d {
1 => roots.extend(solve_linear(arena, &remaining)),
2 => roots.extend(solve_quadratic(arena, &remaining)),
3 => {
roots.extend(solve_cubic_cardano(arena, &remaining));
}
4 => {
roots.extend(solve_quartic_ferrari(arena, &remaining));
}
_ => {
if let Some(more) = try_solve_binomial_rational(arena, &remaining) {
roots.extend(more);
return roots;
}
let poly_expr = polybridge::poly_to_expr(arena, &remaining, var);
for i in 0..d {
let idx = arena.int(i as i64);
roots.push(Solution {
value: arena.intern(ExprNode::RootOf(poly_expr, idx)),
});
}
}
}
}
tracing::debug!(
rational_roots = roots.len(),
"rational roots found via theorem"
);
roots
}
fn rational_to_expr(arena: &mut Arena, r: &Ratio<BigInt>) -> ExprId {
let nid = arena.intern_num(r.clone());
arena.intern(ExprNode::Num(nid))
}
fn rational_sqrt(r: &Ratio<BigInt>) -> Option<Ratio<BigInt>> {
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 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<(ExprId, ExprId)> = Vec::new(); let mut var_exp_terms: Vec<(ExprId, 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(coeff, exp_coeff)) => {
exp_terms.push((coeff, exp_coeff));
}
Some(LambertTermClass::VarExpVar(coeff, exp_coeff)) => {
var_exp_terms.push((coeff, exp_coeff));
}
None => return None,
}
}
let d = match constant_terms.len() {
0 => arena.zero,
1 => constant_terms[0],
_ => arena.add(&constant_terms),
};
if var_exp_terms.len() == 1 && exp_terms.is_empty() && linear_coeffs.is_empty() {
let (a, 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]);
let w = arena.lambertw(b_arg);
let solution = arena.div(w, b);
let solution = crate::transforms::eval::eval(arena, solution);
return Some(vec![Solution { value: solution }]);
}
if exp_terms.len() == 1 && var_exp_terms.is_empty() && !linear_coeffs.is_empty() {
let (a_exp, b) = exp_terms[0];
let c = match linear_coeffs.len() {
1 => linear_coeffs[0],
_ => arena.add(&linear_coeffs),
};
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 w = arena.lambertw(w_arg);
let neg_w = arena.neg(w);
let neg_w_over_b = arena.div(neg_w, b);
let d_over_c = arena.div(d, c);
let solution = arena.sub(neg_w_over_b, d_over_c);
let solution = crate::transforms::eval::eval(arena, solution);
return Some(vec![Solution { value: solution }]);
}
None
}
enum LambertTermClass {
Linear(ExprId),
ExpVar(ExprId, ExprId),
VarExpVar(ExprId, ExprId),
}
fn classify_lambert_term(arena: &mut Arena, term: ExprId, var: ExprId) -> Option<LambertTermClass> {
if term == var {
return Some(LambertTermClass::Linear(arena.one));
}
if let ExprNode::Exp(inner) = arena.node(term).clone() {
if let Some(b) = extract_var_coeff_in_product(arena, inner, var) {
return Some(LambertTermClass::ExpVar(arena.one, b));
}
return None;
}
if let ExprNode::Neg(inner) = arena.node(term).clone() {
match classify_lambert_term(arena, inner, var)? {
LambertTermClass::Linear(c) => {
let neg_c = arena.neg(c);
return Some(LambertTermClass::Linear(neg_c));
}
LambertTermClass::ExpVar(c, b) => {
let neg_c = arena.neg(c);
return Some(LambertTermClass::ExpVar(neg_c, b));
}
LambertTermClass::VarExpVar(c, b) => {
let neg_c = arena.neg(c);
return Some(LambertTermClass::VarExpVar(neg_c, b));
}
}
}
if let ExprNode::Mul(children) = arena.node(term).clone() {
let mut const_factors: Vec<ExprId> = Vec::new();
let mut has_var = false;
let mut exp_inner: Option<ExprId> = None;
for &child in &children {
if !expr_contains_var(arena, child, var) {
const_factors.push(child);
} else if child == var {
if has_var {
return None;
} has_var = true;
} else {
match arena.node(child).clone() {
ExprNode::Exp(inner) if expr_contains_var(arena, inner, var) => {
if exp_inner.is_some() {
return None;
} exp_inner = Some(inner);
}
_ => return None, }
}
}
let coeff = match const_factors.len() {
0 => arena.one,
1 => const_factors[0],
_ => arena.mul(&const_factors),
};
match (has_var, exp_inner) {
(true, Some(inner)) => {
let b = extract_var_coeff_in_product(arena, inner, var)?;
Some(LambertTermClass::VarExpVar(coeff, b))
}
(true, None) => Some(LambertTermClass::Linear(coeff)),
(false, Some(inner)) => {
let b = extract_var_coeff_in_product(arena, inner, var)?;
Some(LambertTermClass::ExpVar(coeff, b))
}
(false, None) => None, }
} else {
None
}
}
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);
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)
);
}
}