use crate::base::arena::Arena;
use crate::base::cast::integer_span;
use crate::base::interval::Interval;
use crate::base::interval::IntervalKind;
use crate::base::node::{ExprId, ExprNode, SymbolId};
use crate::base::walk;
use crate::transforms::eval;
use crate::transforms::evalf;
use crate::transforms::inequalities::Relation;
use crate::transforms::solve;
#[allow(dead_code)]
pub(crate) fn continuous_domain(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
_var_sym: SymbolId,
domain: ExprId,
) -> ExprId {
tracing::debug!("continuous_domain: starting domain analysis");
let post_order = walk::post_order_ids(arena, expr);
let mut valid = domain;
for &id in &post_order {
let node = arena.node(id).clone();
let constraint = compute_node_constraint(arena, &node, var);
if let Some(c) = constraint {
valid = arena.set_intersection(&[valid, c]);
}
}
valid
}
#[allow(dead_code)]
fn compute_node_constraint(arena: &mut Arena, node: &ExprNode, var: ExprId) -> Option<ExprId> {
match node {
ExprNode::Pow(base, exp) => {
let base = *base;
let exp = *exp;
if !walk::contains(arena, base, var) {
return None;
}
if let Some(r) = arena.as_num(exp).cloned() {
use num_traits::Signed;
if r.is_negative() {
return Some(domain_exclude_zeros(arena, base, var));
}
let half = num_rational::Ratio::new(
num_bigint::BigInt::from(1),
num_bigint::BigInt::from(2),
);
if r == half {
return solve_ge_zero(arena, base, var);
}
}
if let ExprNode::Neg(_) = arena.node(exp)
&& walk::contains(arena, base, var)
{
return Some(domain_exclude_zeros(arena, base, var));
}
None
}
ExprNode::Ln(inner) => {
let inner = *inner;
if !walk::contains(arena, inner, var) {
return None;
}
solve_gt_zero(arena, inner, var)
}
ExprNode::Tan(inner) => {
let inner = *inner;
if !walk::contains(arena, inner, var) {
return None;
}
let cos_inner = arena.cos(inner);
Some(domain_exclude_zeros(arena, cos_inner, var))
}
ExprNode::Asin(inner) | ExprNode::Acos(inner) => {
let inner = *inner;
if !walk::contains(arena, inner, var) {
return None;
}
let neg_one = arena.neg_one;
let one = arena.one;
let shifted_low = arena.sub(inner, neg_one); let set_low = solve_ge_zero(arena, shifted_low, var);
let shifted_high = arena.sub(one, inner);
let set_high = solve_ge_zero(arena, shifted_high, var);
match (set_low, set_high) {
(Some(a), Some(b)) => Some(arena.set_intersection(&[a, b])),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
}
}
ExprNode::Acosh(inner) => {
let inner = *inner;
if !walk::contains(arena, inner, var) {
return None;
}
let one = arena.one;
let shifted = arena.sub(inner, one); solve_ge_zero(arena, shifted, var)
}
ExprNode::Atanh(inner) => {
let inner = *inner;
if !walk::contains(arena, inner, var) {
return None;
}
let neg_one = arena.neg_one;
let one = arena.one;
let shifted_low = arena.sub(inner, neg_one);
let set_low = solve_gt_zero(arena, shifted_low, var);
let shifted_high = arena.sub(one, inner);
let set_high = solve_gt_zero(arena, shifted_high, var);
match (set_low, set_high) {
(Some(a), Some(b)) => Some(arena.set_intersection(&[a, b])),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
}
}
_ => None,
}
}
#[allow(dead_code)]
fn solve_gt_zero(arena: &mut Arena, expr: ExprId, var: ExprId) -> Option<ExprId> {
arena.solve_inequality_expr(expr, var, Relation::Gt).ok()
}
#[allow(dead_code)]
fn solve_ge_zero(arena: &mut Arena, expr: ExprId, var: ExprId) -> Option<ExprId> {
arena.solve_inequality_expr(expr, var, Relation::Ge).ok()
}
#[allow(dead_code)]
fn domain_exclude_zeros(arena: &mut Arena, expr: ExprId, var: ExprId) -> ExprId {
let solutions = solve::known_real_roots(arena, expr, var);
if solutions.is_empty() {
return arena.interval(arena.neg_infinity, arena.infinity, IntervalKind::Open);
}
let roots_set = arena.finite_set(&solutions);
let reals = arena.interval(arena.neg_infinity, arena.infinity, IntervalKind::Open);
arena.set_complement(reals, roots_set)
}
pub(crate) fn singularities(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
_var_sym: SymbolId,
range: Interval<f64>,
) -> Vec<f64> {
tracing::debug!(
range_lo = range.lower,
range_hi = range.upper,
"singularities: scanning for singularities"
);
let scan = scan_breakpoints(arena, expr, var, Some(range));
let mut sing_points: Vec<f64> = Vec::new();
for bp in scan.points {
if bp.kind != BreakKind::Singular {
continue;
}
if let Some(val) = bp.value
&& val.is_finite()
&& val >= range.lower
&& val <= range.upper
{
if !sing_points.iter().any(|&v| (v - val).abs() < 1e-12) {
sing_points.push(val);
}
}
}
sing_points.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
sing_points
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum BreakKind {
Singular,
Kink,
}
#[derive(Clone, Debug)]
pub(crate) struct Breakpoint {
pub point: ExprId,
pub value: Option<f64>,
pub kind: BreakKind,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct BreakScan {
pub points: Vec<Breakpoint>,
pub complete: bool,
pub opaque: bool,
}
pub(crate) fn scan_breakpoints(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
range: Option<Interval<f64>>,
) -> BreakScan {
let post_order = walk::post_order_ids(arena, expr);
let mut scan = BreakScan {
points: Vec::new(),
complete: true,
opaque: false,
};
for &id in &post_order {
let node = arena.node(id).clone();
breakpoints_of_node(arena, &node, var, range, &mut scan);
}
scan
}
fn push_zeros(
arena: &mut Arena,
g: ExprId,
var: ExprId,
kind: BreakKind,
range: Option<Interval<f64>>,
scan: &mut BreakScan,
) {
if !walk::contains(arena, g, var) {
return;
}
if let Some(done) = push_trig_zeros(arena, g, var, kind, range, scan) {
if !done {
scan.complete = false;
}
return;
}
let solutions = solve::known_real_roots(arena, g, var);
if solutions.is_empty() {
if crate::poly::polybridge::expr_to_poly(arena, g, var).is_none() {
scan.complete = false;
}
return;
}
for s in solutions {
push_point(arena, s, kind, scan);
}
}
fn push_point(arena: &mut Arena, point: ExprId, kind: BreakKind, scan: &mut BreakScan) {
if walk::free_symbols(arena, point).is_empty() {
match expr_to_f64(arena, point) {
Some(v) if v.is_finite() => scan.points.push(Breakpoint {
point,
value: Some(v),
kind,
}),
_ => {}
}
} else {
let mut cache = crate::base::assumptions::AssumptionCache::new();
if cache.query(arena, point, crate::base::assumptions::Props::REAL) == Some(false) {
return;
}
let declared_real = walk::free_symbols(arena, point).iter().all(|&s| {
matches!(*arena.node(s), ExprNode::Symbol(sid)
if crate::transforms::realness::symbol_declared_real(arena, sid))
});
let mut nonreal = false;
let mut stack = if declared_real {
vec![point]
} else {
Vec::new()
};
while let Some(id) = stack.pop() {
match arena.node(id).clone() {
ExprNode::ImaginaryUnit => {
nonreal = true;
break;
}
ExprNode::Pow(b, e) => {
if let Some(r) = arena.as_num(e)
&& !r.is_integer()
&& num_integer::Integer::is_even(r.denom())
&& cache.query(arena, b, crate::base::assumptions::Props::NEGATIVE)
== Some(true)
{
nonreal = true;
break;
}
stack.push(b);
stack.push(e);
}
node => node.for_each_child(|c| stack.push(c)),
}
}
if nonreal && cache.query(arena, point, crate::base::assumptions::Props::REAL) != Some(true)
{
return;
}
scan.points.push(Breakpoint {
point,
value: None,
kind,
});
}
}
fn push_trig_zeros(
arena: &mut Arena,
g: ExprId,
var: ExprId,
kind: BreakKind,
range: Option<Interval<f64>>,
scan: &mut BreakScan,
) -> Option<bool> {
let (inner, offset) = match arena.node(g).clone() {
ExprNode::Sin(i) | ExprNode::Tan(i) => (i, 0.0),
ExprNode::Cos(i) => (i, std::f64::consts::FRAC_PI_2),
_ => return None,
};
if !walk::contains(arena, inner, var) {
return None;
}
let LinearCoeffs {
slope: alpha,
intercept: beta,
} = match linear_coeffs_f64(arena, inner, var) {
Some(ab) => ab,
None => return Some(false),
};
let (lo, hi) = match range {
Some(r) => (r.lower, r.upper),
None => return Some(false),
};
if alpha == 0.0 || !lo.is_finite() || !hi.is_finite() {
return Some(false);
}
let t_lo = alpha * lo + beta;
let t_hi = alpha * hi + beta;
let (t_min, t_max) = if t_lo <= t_hi {
(t_lo, t_hi)
} else {
(t_hi, t_lo)
};
let k_min = (((t_min - offset) / std::f64::consts::PI).floor() as i64).saturating_sub(1);
let k_max = (((t_max - offset) / std::f64::consts::PI).ceil() as i64).saturating_add(1);
if k_max.saturating_sub(k_min) > 10_000 {
return Some(false);
}
let LinearCoeffs {
slope: alpha_ex,
intercept: beta_ex,
} = match linear_coeffs_exact(arena, inner, var) {
Some(ab) => ab,
None => return Some(false),
};
for k in k_min..=k_max {
let t = offset + (k as f64) * std::f64::consts::PI;
let xv = (t - beta) / alpha;
if xv < lo - 1e-12 || xv > hi + 1e-12 {
continue;
}
let k_ex = if offset == 0.0 {
arena.int(k)
} else {
arena.rational(2 * k + 1, 2)
};
let pi = arena.pi();
let k_pi = arena.mul(&[k_ex, pi]);
let num = arena.sub(k_pi, beta_ex);
let point = arena.div(num, alpha_ex);
scan.points.push(Breakpoint {
point,
value: Some(xv),
kind,
});
}
Some(true)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct LinearCoeffs<T> {
pub slope: T,
pub intercept: T,
}
fn linear_coeffs_f64(arena: &mut Arena, expr: ExprId, var: ExprId) -> Option<LinearCoeffs<f64>> {
let lin = linear_coeffs_exact(arena, expr, var)?;
let af = expr_to_f64(arena, lin.slope)?;
let bf = expr_to_f64(arena, lin.intercept)?;
if af.is_finite() && bf.is_finite() {
Some(LinearCoeffs {
slope: af,
intercept: bf,
})
} else {
None
}
}
pub(crate) fn linear_coeffs_exact(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
) -> Option<LinearCoeffs<ExprId>> {
let coeffs = poly_coeffs_symbolic(arena, expr, var)?;
if coeffs.len() != 2 {
return None;
}
Some(LinearCoeffs {
slope: coeffs[1],
intercept: coeffs[0],
})
}
pub(crate) fn poly_coeffs_symbolic(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
) -> Option<Vec<ExprId>> {
let expanded = crate::transforms::expand::expand(arena, expr);
let expanded = eval::eval(arena, expanded);
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 (k, coeff) = split_term_power(arena, term, var)?;
while buckets.len() <= k {
buckets.push(Vec::new());
}
buckets[k].push(coeff);
}
if buckets.is_empty() {
return None;
}
let mut result = Vec::with_capacity(buckets.len());
for bucket in buckets {
let c = if bucket.is_empty() {
arena.zero()
} else {
let s = arena.add(&bucket);
eval::eval(arena, s)
};
result.push(c);
}
while result.len() > 1 && arena.is_zero_structural(*result.last()?) {
result.pop();
}
if result.len() == 1 && arena.is_zero_structural(result[0]) {
return None;
}
Some(result)
}
fn split_term_power(arena: &mut Arena, term: ExprId, var: ExprId) -> Option<(usize, ExprId)> {
if !walk::contains(arena, term, var) {
return Some((0, term));
}
if term == var {
return Some((1, arena.one()));
}
match arena.node(term).clone() {
ExprNode::Pow(base, exp) if base == var => {
let r = arena.as_num(exp)?.clone();
if !r.is_integer() {
return None;
}
use num_traits::{Signed, ToPrimitive};
if r.is_negative() {
return None;
}
let k = r.to_integer().to_usize()?;
Some((k, arena.one()))
}
ExprNode::Mul(children) => {
let mut k_total = 0usize;
let mut coeff_parts: Vec<ExprId> = Vec::new();
for c in children.iter() {
if !walk::contains(arena, *c, var) {
coeff_parts.push(*c);
continue;
}
let (k, cc) = split_term_power(arena, *c, var)?;
if !arena.is_one_structural(cc) {
return None;
}
k_total += k;
}
let coeff = if coeff_parts.is_empty() {
arena.one()
} else {
arena.mul(&coeff_parts)
};
Some((k_total, coeff))
}
ExprNode::Neg(inner) => {
let (k, c) = split_term_power(arena, inner, var)?;
Some((k, arena.neg(c)))
}
_ => None,
}
}
fn breakpoints_of_node(
arena: &mut Arena,
node: &ExprNode,
var: ExprId,
range: Option<Interval<f64>>,
scan: &mut BreakScan,
) {
use BreakKind::{Kink, Singular};
match node {
ExprNode::Pow(base, exp) => {
let base = *base;
let exp = *exp;
if !walk::contains(arena, base, var) {
return;
}
if walk::contains(arena, exp, var) {
scan.complete = false;
return;
}
let matters = if let Some(r) = arena.as_num(exp).cloned() {
use num_traits::Signed;
r.is_negative() || !r.is_integer()
} else {
true
};
if matters {
push_zeros(arena, base, var, Singular, range, scan);
}
}
ExprNode::Ln(inner) => push_zeros(arena, *inner, var, Singular, range, scan),
ExprNode::Tan(inner) => {
let inner = *inner;
if !walk::contains(arena, inner, var) {
return;
}
let cos_inner = arena.cos(inner);
push_zeros(arena, cos_inner, var, Singular, range, scan);
}
ExprNode::Asin(inner) | ExprNode::Acos(inner) | ExprNode::Atanh(inner) => {
let inner = *inner;
if !walk::contains(arena, inner, var) {
return;
}
let one = arena.one();
let g_minus = arena.sub(inner, one);
let g_plus = arena.add(&[inner, one]);
push_zeros(arena, g_minus, var, Singular, range, scan);
push_zeros(arena, g_plus, var, Singular, range, scan);
}
ExprNode::Acosh(inner) => {
let inner = *inner;
if !walk::contains(arena, inner, var) {
return;
}
let one = arena.one();
let g_minus = arena.sub(inner, one);
push_zeros(arena, g_minus, var, Singular, range, scan);
}
ExprNode::Gamma(inner) | ExprNode::Digamma(inner) | ExprNode::LogGamma(inner) => {
let inner = *inner;
if !walk::contains(arena, inner, var) {
return;
}
push_integer_family(arena, inner, var, range, Singular, scan, |n| n <= 0);
}
ExprNode::Abs(inner)
| ExprNode::Sign(inner)
| ExprNode::Heaviside(inner)
| ExprNode::DiracDelta(inner) => push_zeros(arena, *inner, var, Kink, range, scan),
ExprNode::Floor(inner) | ExprNode::Ceiling(inner) => {
let inner = *inner;
if !walk::contains(arena, inner, var) {
return;
}
push_integer_family(arena, inner, var, range, Kink, scan, |_| true);
}
ExprNode::Gt(a, b) | ExprNode::Ge(a, b) | ExprNode::Eq_(a, b) | ExprNode::Ne(a, b) => {
let d = arena.sub(*a, *b);
push_zeros(arena, d, var, Kink, range, scan);
}
ExprNode::Apply(_, args) if args.iter().any(|&a| walk::contains(arena, a, var)) => {
scan.complete = false;
scan.opaque = true;
}
ExprNode::Integral(body, _)
| ExprNode::Derivative(body, _)
| ExprNode::Limit(body, _, _)
| ExprNode::Sum(body, _, _, _)
| ExprNode::Product_(body, _, _, _)
| ExprNode::Residue(body, _, _)
if walk::contains(arena, *body, var) =>
{
scan.complete = false;
scan.opaque = true;
}
ExprNode::RootOf(..)
| ExprNode::RootSum(..)
| ExprNode::LambertW(_)
| ExprNode::Subs(..)
if node
.children()
.iter()
.any(|&c| walk::contains(arena, c, var)) =>
{
scan.complete = false;
scan.opaque = true;
}
_ => {}
}
}
fn push_integer_family(
arena: &mut Arena,
inner: ExprId,
var: ExprId,
range: Option<Interval<f64>>,
kind: BreakKind,
scan: &mut BreakScan,
keep: impl Fn(i64) -> bool,
) {
let (Some(lin), Some(lin_ex), Some(r)) = (
linear_coeffs_f64(arena, inner, var),
linear_coeffs_exact(arena, inner, var),
range,
) else {
scan.complete = false;
return;
};
let (alpha, beta) = (lin.slope, lin.intercept);
let (alpha_ex, beta_ex) = (lin_ex.slope, lin_ex.intercept);
let (lo, hi) = (r.lower, r.upper);
if alpha == 0.0 || !lo.is_finite() || !hi.is_finite() {
scan.complete = false;
return;
}
let t_lo = alpha * lo + beta;
let t_hi = alpha * hi + beta;
let (t_min, t_max) = if t_lo <= t_hi {
(t_lo, t_hi)
} else {
(t_hi, t_lo)
};
let Ok(span) = integer_span(t_min, t_max, 1, 10_001) else {
scan.complete = false;
return;
};
for n in span {
if !keep(n) {
continue;
}
let xv = ((n as f64) - beta) / alpha;
if xv < lo - 1e-12 || xv > hi + 1e-12 {
continue;
}
let n_ex = arena.int(n);
let num = arena.sub(n_ex, beta_ex);
let point = arena.div(num, alpha_ex);
let point = eval::eval(arena, point);
scan.points.push(Breakpoint {
point,
value: Some(xv),
kind,
});
}
}
pub(crate) fn expr_to_f64(arena: &mut Arena, expr: ExprId) -> Option<f64> {
let evaled = eval::eval(arena, expr);
if let Some(r) = arena.as_num(evaled).cloned() {
use num_traits::ToPrimitive;
return r.to_f64();
}
let s = evalf::evalf(arena, evaled, 16).ok()?;
if s.contains('I') || s.contains('i') {
return None;
}
s.trim().parse::<f64>().ok()
}
pub(crate) fn estimate_frequency(
arena: &Arena,
expr: ExprId,
var: ExprId,
_var_sym: SymbolId,
) -> Option<f64> {
tracing::debug!("estimate_frequency: scanning for trig terms");
let post_order = walk::post_order_ids(arena, expr);
let mut max_omega: Option<f64> = None;
for &id in &post_order {
let node = arena.node(id);
let inner = match node {
ExprNode::Sin(i) => Some(*i),
ExprNode::Cos(i) => Some(*i),
ExprNode::Tan(i) => Some(*i),
_ => None,
};
if let Some(inner) = inner
&& let Some(omega) = extract_linear_coefficient(arena, inner, var)
{
let omega_abs = omega.abs();
match max_omega {
Some(cur) if cur >= omega_abs => {}
_ => max_omega = Some(omega_abs),
}
}
}
max_omega
}
fn extract_linear_coefficient(arena: &Arena, expr: ExprId, var: ExprId) -> Option<f64> {
if expr == var {
return Some(1.0);
}
let node = arena.node(expr);
match node {
ExprNode::Mul(args) => {
let mut has_var = false;
let mut coeff_ids: Vec<ExprId> = Vec::new();
for &arg in args.iter() {
if arg == var {
has_var = true;
} else if walk::contains(arena, arg, var) {
return None;
} else {
coeff_ids.push(arg);
}
}
if !has_var {
return None;
}
if coeff_ids.is_empty() {
return Some(1.0);
}
if coeff_ids.len() == 1 {
return num_value(arena, coeff_ids[0]);
}
None
}
ExprNode::Add(args) => {
let mut omega = None;
for &arg in args.iter() {
if walk::contains(arena, arg, var) {
omega = extract_linear_coefficient(arena, arg, var);
}
}
omega
}
ExprNode::Neg(inner) => extract_linear_coefficient(arena, *inner, var).map(|c| -c),
_ => None,
}
}
fn num_value(arena: &Arena, expr: ExprId) -> Option<f64> {
if let Some(r) = arena.as_num(expr) {
use num_traits::ToPrimitive;
r.to_f64()
} else {
match arena.node(expr) {
ExprNode::Neg(inner) => num_value(arena, *inner).map(|v| -v),
_ => None,
}
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct ContinuityScan {
pub singular: Vec<ExprId>,
pub kinks: Vec<ExprId>,
pub opaque: Option<&'static str>,
}
pub(crate) fn continuity_scan(arena: &mut Arena, expr: ExprId, var: ExprId) -> ContinuityScan {
let mut scan = ContinuityScan::default();
let mut assumptions = crate::base::assumptions::AssumptionCache::new();
for id in walk::post_order_ids(arena, expr) {
let node = arena.node(id).clone();
let depends = |arena: &Arena, child: ExprId| walk::contains(arena, child, var);
match node {
ExprNode::Pow(base, exp) => {
if !depends(arena, base) || depends(arena, exp) {
continue;
}
let negative = match arena.as_num(exp) {
Some(r) => {
use num_traits::Signed;
r.is_negative()
}
None => {
assumptions.query(arena, exp, crate::base::assumptions::Props::NEGATIVE)
== Some(true)
}
};
if negative {
push_unique(&mut scan.singular, base);
}
}
ExprNode::Ln(g) if depends(arena, g) => push_unique(&mut scan.singular, g),
ExprNode::Tan(g) if depends(arena, g) => {
let cos_g = arena.cos(g);
push_unique(&mut scan.singular, cos_g);
}
ExprNode::Atanh(g) if depends(arena, g) => {
let one = arena.one();
let minus = arena.sub(g, one);
let plus = arena.add(&[g, one]);
push_unique(&mut scan.singular, minus);
push_unique(&mut scan.singular, plus);
}
ExprNode::Abs(g) if depends(arena, g) => push_unique(&mut scan.kinks, g),
_ => {
if scan.opaque.is_none()
&& let Some(name) = opaque_node_name(&node)
&& node.children().iter().any(|&c| depends(arena, c))
{
scan.opaque = Some(name);
}
}
}
}
scan
}
fn push_unique(list: &mut Vec<ExprId>, id: ExprId) {
if !list.contains(&id) {
list.push(id);
}
}
fn opaque_node_name(node: &ExprNode) -> Option<&'static str> {
Some(match node {
ExprNode::Floor(_) => "floor",
ExprNode::Ceiling(_) => "ceiling",
ExprNode::Sign(_) => "sign",
ExprNode::Heaviside(_) => "Heaviside",
ExprNode::DiracDelta(_) => "DiracDelta",
ExprNode::Piecewise(_) => "Piecewise",
ExprNode::Min(_) => "min",
ExprNode::Max(_) => "max",
ExprNode::Re(_) => "re",
ExprNode::Im(_) => "im",
ExprNode::Conjugate(_) => "conjugate",
ExprNode::Arg(_) => "arg",
ExprNode::Atan2(..) => "atan2",
ExprNode::Gamma(_) => "Gamma",
ExprNode::LogGamma(_) => "loggamma",
ExprNode::Digamma(_) => "digamma",
ExprNode::Polygamma(..) => "polygamma",
ExprNode::Zeta(_) => "zeta",
ExprNode::Beta(..) => "Beta",
ExprNode::Factorial(_) => "factorial",
ExprNode::Binomial(..) => "binomial",
ExprNode::KroneckerDelta(..) => "KroneckerDelta",
ExprNode::LambertW(_) => "LambertW",
ExprNode::Apply(..) => "an unknown function",
ExprNode::Derivative(..) => "Derivative",
ExprNode::Integral(..) => "Integral",
ExprNode::DefiniteIntegral(..) => "DefiniteIntegral",
ExprNode::Sum(..) => "Sum",
ExprNode::Product_(..) => "Product",
ExprNode::Limit(..) => "Limit",
ExprNode::Series(..) => "Series",
ExprNode::LaplaceTransform(..) => "LaplaceTransform",
ExprNode::InverseLaplaceTransform(..) => "InverseLaplaceTransform",
ExprNode::Residue(..) => "Residue",
ExprNode::RootOf(..) => "RootOf",
ExprNode::RootSum(..) => "RootSum",
ExprNode::DSolve(..) => "DSolve",
ExprNode::Subs(..) => "Subs",
ExprNode::Gt(..)
| ExprNode::Ge(..)
| ExprNode::Eq_(..)
| ExprNode::Ne(..)
| ExprNode::And(_)
| ExprNode::Or(_)
| ExprNode::Not(_) => "a boolean",
ExprNode::ConditionSet(..)
| ExprNode::Interval(..)
| ExprNode::FiniteSet(_)
| ExprNode::SetUnion(_)
| ExprNode::SetIntersection(_)
| ExprNode::SetComplement(..)
| ExprNode::ImageSet(..) => "a set",
_ => return None,
})
}
pub(crate) fn sign_nodes_of(arena: &Arena, expr: ExprId, var: ExprId) -> Vec<(ExprId, ExprId)> {
let mut out: Vec<(ExprId, ExprId)> = Vec::new();
for id in walk::post_order_ids(arena, expr) {
if let ExprNode::Sign(h) = arena.node(id)
&& walk::contains(arena, *h, var)
&& !out.iter().any(|(s, _)| *s == id)
{
out.push((id, *h));
}
}
out
}
pub(crate) fn has_trig_of(arena: &Arena, expr: ExprId, var: ExprId) -> bool {
walk::post_order_ids(arena, expr)
.into_iter()
.any(|id| match arena.node(id) {
ExprNode::Sin(g) | ExprNode::Cos(g) | ExprNode::Tan(g) => {
walk::contains(arena, *g, var)
}
_ => false,
})
}
pub(crate) fn periodicity(arena: &mut Arena, expr: ExprId, var: ExprId) -> Option<ExprId> {
if !walk::contains(arena, expr, var) {
return Some(arena.zero());
}
let mut memo: rustc_hash::FxHashMap<ExprId, Option<ExprId>> = rustc_hash::FxHashMap::default();
for id in walk::post_order_ids(arena, expr) {
if memo.contains_key(&id) {
continue;
}
let period = if walk::contains(arena, id, var) {
node_period(arena, id, var, &memo)
} else {
Some(arena.zero())
};
memo.insert(id, period);
}
memo.get(&expr).copied().flatten()
}
fn node_period(
arena: &mut Arena,
id: ExprId,
var: ExprId,
memo: &rustc_hash::FxHashMap<ExprId, Option<ExprId>>,
) -> Option<ExprId> {
let node = arena.node(id).clone();
match &node {
ExprNode::Symbol(_) => None,
ExprNode::Sin(g) | ExprNode::Cos(g) | ExprNode::Tan(g) => {
if let Some(p) = trig_power_period(arena, id, var) {
return Some(p);
}
memo.get(g).copied().flatten()
}
ExprNode::Abs(g) => {
if let ExprNode::Sin(h) | ExprNode::Cos(h) = arena.node(*g).clone()
&& let Some(lin) = linear_coeffs_exact(arena, h, var)
{
return Some(pi_over_abs(arena, lin.slope, 1));
}
memo.get(g).copied().flatten()
}
ExprNode::Pow(_, exp) => {
if !walk::contains(arena, *exp, var)
&& let Some(p) = trig_power_period(arena, id, var)
{
return Some(p);
}
lcm_of_children(arena, &node, memo)
}
ExprNode::Mul(children) => {
let children = children.clone();
mul_period(arena, &children, var, memo)
}
ExprNode::Integral(..)
| ExprNode::DefiniteIntegral(..)
| ExprNode::Sum(..)
| ExprNode::Product_(..)
| ExprNode::Limit(..)
| ExprNode::Series(..)
| ExprNode::LaplaceTransform(..)
| ExprNode::InverseLaplaceTransform(..)
| ExprNode::Residue(..)
| ExprNode::RootOf(..)
| ExprNode::RootSum(..)
| ExprNode::DSolve(..)
| ExprNode::Piecewise(_)
| ExprNode::Gt(..)
| ExprNode::Ge(..)
| ExprNode::Eq_(..)
| ExprNode::Ne(..)
| ExprNode::And(_)
| ExprNode::Or(_)
| ExprNode::Not(_)
| ExprNode::ConditionSet(..)
| ExprNode::Interval(..)
| ExprNode::FiniteSet(_)
| ExprNode::SetUnion(_)
| ExprNode::SetIntersection(_)
| ExprNode::SetComplement(..)
| ExprNode::ImageSet(..) => None,
_ => lcm_of_children(arena, &node, memo),
}
}
fn lcm_of_children(
arena: &mut Arena,
node: &ExprNode,
memo: &rustc_hash::FxHashMap<ExprId, Option<ExprId>>,
) -> Option<ExprId> {
let mut acc = arena.zero();
for c in node.children() {
let p = memo.get(&c).copied().flatten()?;
acc = lcm_periods(arena, acc, p)?;
}
Some(acc)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum TrigKind {
SinCos,
Tan,
}
fn as_trig_power(arena: &Arena, factor: ExprId) -> Option<(ExprId, TrigKind, i64)> {
let (base, k) = match arena.node(factor) {
ExprNode::Pow(base, exp) => {
let r = arena.as_num(*exp)?;
if !r.is_integer() {
return None;
}
use num_traits::ToPrimitive;
(*base, r.to_integer().to_i64()?)
}
_ => (factor, 1),
};
match arena.node(base) {
ExprNode::Sin(g) | ExprNode::Cos(g) => Some((*g, TrigKind::SinCos, k)),
ExprNode::Tan(g) => Some((*g, TrigKind::Tan, k)),
_ => None,
}
}
fn trig_power_period(arena: &mut Arena, factor: ExprId, var: ExprId) -> Option<ExprId> {
let (g, kind, k) = as_trig_power(arena, factor)?;
let a = linear_coeffs_exact(arena, g, var)?.slope;
let halves = kind == TrigKind::Tan || k % 2 == 0;
Some(pi_over_abs(arena, a, if halves { 1 } else { 2 }))
}
fn mul_period(
arena: &mut Arena,
children: &[ExprId],
var: ExprId,
memo: &rustc_hash::FxHashMap<ExprId, Option<ExprId>>,
) -> Option<ExprId> {
let mut groups: Vec<(ExprId, ExprId, i64)> = Vec::new();
let mut acc = arena.zero();
for &c in children {
if !walk::contains(arena, c, var) {
continue;
}
if let Some((g, kind, k)) = as_trig_power(arena, c)
&& let Some(LinearCoeffs { slope: a, .. }) = linear_coeffs_exact(arena, g, var)
{
let weight = if kind == TrigKind::Tan { 0 } else { k };
match groups.iter_mut().find(|(gg, _, _)| *gg == g) {
Some(entry) => entry.2 += weight,
None => groups.push((g, a, weight)),
}
continue;
}
let p = memo.get(&c).copied().flatten()?;
acc = lcm_periods(arena, acc, p)?;
}
for (_, a, sum) in groups {
let p = pi_over_abs(arena, a, if sum % 2 == 0 { 1 } else { 2 });
acc = lcm_periods(arena, acc, p)?;
}
Some(acc)
}
fn pi_over_abs(arena: &mut Arena, a: ExprId, k: i64) -> ExprId {
let k = arena.int(k);
let pi = arena.pi();
let num = arena.mul(&[k, pi]);
let mut assumptions = crate::base::assumptions::AssumptionCache::new();
let den = if assumptions.query(arena, a, crate::base::assumptions::Props::POSITIVE)
== Some(true)
{
a
} else if assumptions.query(arena, a, crate::base::assumptions::Props::NEGATIVE) == Some(true) {
arena.neg(a)
} else {
arena.abs(a)
};
let q = arena.div(num, den);
eval::eval(arena, q)
}
fn lcm_periods(arena: &mut Arena, a: ExprId, b: ExprId) -> Option<ExprId> {
if arena.is_zero_structural(a) {
return Some(b);
}
if arena.is_zero_structural(b) || a == b {
return Some(a);
}
let ratio = arena.div(a, b);
let ratio = eval::eval(arena, ratio);
let r = arena.as_num(ratio)?.clone();
use num_traits::Signed;
if !r.is_positive() {
return None;
}
let n = arena.big_int(r.denom().clone());
let l = arena.mul(&[a, n]);
Some(eval::eval(arena, l))
}
const HIDDEN_ZERO_DIGITS: u32 = 30;
const MAX_EXACT_ZERO_SIZE: usize = 400;
const MAX_IDENTITY_SIZE: usize = 150;
pub(crate) fn hidden_zero(
arena: &mut Arena,
c: ExprId,
) -> crate::base::truth::Verdict<crate::base::truth::Truth> {
use crate::base::truth::{Evidence, Truth, Verdict};
if let Some(r) = arena.as_num(c) {
return Verdict::new(
Truth::from_bool(num_traits::Zero::is_zero(r)),
Evidence::Exact,
);
}
if !walk::free_symbols(arena, c).is_empty() || walk::has_unevaluated(arena, c) {
return Verdict::new(Truth::Unknown, Evidence::Approximate);
}
if has_dirac_delta(arena, c) {
let rewritten = exact_rewrites(arena, c);
if exactly_zero(arena, c, rewritten) {
return Verdict::new(Truth::True, Evidence::Exact);
}
return Verdict::new(Truth::Unknown, Evidence::Approximate);
}
if walk::post_order_ids(arena, c).into_iter().all(|id| {
let node = arena.node(id);
node.is_atom()
|| matches!(
node,
ExprNode::Mul(_) | ExprNode::Pow(..) | ExprNode::Neg(_)
)
}) {
return Verdict::new(Truth::False, Evidence::Exact);
}
let certified = |z: &crate::base::bigcomplex::Complex| {
Verdict::new(
Truth::from_bool(z.0.is_zero() && z.1.is_zero()),
Evidence::Certified,
)
};
let cap = evalf::evalf_verdict(arena, c, HIDDEN_ZERO_DIGITS, evalf::ZeroSearch::Cap);
match cap.map(crate::base::truth::Verdict::proven) {
Ok(Some(z)) => return certified(&z),
Ok(None) => {}
Err(_) => return Verdict::new(Truth::Unknown, Evidence::Approximate),
}
let rewritten = exact_rewrites(arena, c);
if exactly_zero(arena, c, rewritten) {
return Verdict::new(Truth::True, Evidence::Exact);
}
if let Some(z) = evalf::evalf_verdict(arena, c, HIDDEN_ZERO_DIGITS, evalf::ZeroSearch::Deep)
.ok()
.and_then(crate::base::truth::Verdict::proven)
{
return certified(&z);
}
if zero_by_identity(arena, c, rewritten) {
return Verdict::new(Truth::True, Evidence::Exact);
}
tracing::debug!(
c = %arena.display(c),
"hidden_zero: zero to the precision reached, not proved"
);
Verdict::new(Truth::True, Evidence::Approximate)
}
pub(crate) fn exactly_zero(arena: &mut Arena, c: ExprId, rewritten: ExprId) -> bool {
use crate::poly::algebraic::exact_is_zero;
if arena.is_zero_structural(rewritten) {
return true;
}
if walk::post_order_ids(arena, c).len() > MAX_EXACT_ZERO_SIZE {
return false;
}
let constant = walk::free_symbols(arena, c).is_empty();
if constant && exact_is_zero(arena, c) == Some(true) {
return true;
}
if crate::simplify::ratsimp::vanishes_as_rational_function(arena, c) {
return true;
}
let expanded = crate::transforms::expand::expand(arena, c);
if arena.is_zero_structural(expanded)
|| (constant && expanded != c && exact_is_zero(arena, expanded) == Some(true))
{
return true;
}
if rewritten == c {
return false;
}
(constant && exact_is_zero(arena, rewritten) == Some(true))
|| crate::simplify::ratsimp::vanishes_as_rational_function(arena, rewritten)
}
pub(crate) fn zero_by_identity(arena: &mut Arena, c: ExprId, rewritten: ExprId) -> bool {
if walk::post_order_ids(arena, c).len() > MAX_IDENTITY_SIZE {
return false;
}
crate::simplify::identically_zero::vanishes_by_identity(arena, c)
|| (rewritten != c
&& crate::simplify::identically_zero::vanishes_by_identity(arena, rewritten))
}
const MAX_LOG_PRIME: u64 = 1 << 20;
pub(crate) fn exact_rewrites(arena: &mut Arena, c: ExprId) -> ExprId {
if walk::post_order_ids(arena, c).len() > MAX_EXACT_ZERO_SIZE {
return c;
}
let r = compositions_pass(arena, c);
let r = exact_pass(arena, r);
let logs = crate::calculus::limit::inverse_functions_as_logs(arena, r);
let r = if logs == r {
r
} else {
exact_pass(arena, logs)
};
like_arguments_unified(arena, r)
}
const MAX_LIKE_NODES: usize = 12;
fn like_arguments_unified(arena: &mut Arena, c: ExprId) -> ExprId {
use rustc_hash::FxHashSet;
type Kind = (std::mem::Discriminant<ExprNode>, Option<ExprId>);
let mut like: Vec<(ExprId, ExprId, Kind)> = Vec::new();
for id in walk::post_order_ids(arena, c) {
let node = arena.node(id);
let kind = std::mem::discriminant(node);
let entry = match *node {
ExprNode::Pow(b, q)
if arena.as_num(q).is_some_and(|q| !q.is_integer())
&& !matches!(arena.node(b), ExprNode::Num(_)) =>
{
Some((b, (kind, Some(q))))
}
ExprNode::Ln(a)
| ExprNode::Exp(a)
| ExprNode::Sin(a)
| ExprNode::Cos(a)
| ExprNode::Tan(a)
| ExprNode::Sinh(a)
| ExprNode::Cosh(a)
| ExprNode::Tanh(a)
| ExprNode::Asin(a)
| ExprNode::Acos(a)
| ExprNode::Atan(a)
| ExprNode::Asinh(a)
| ExprNode::Acosh(a)
| ExprNode::Atanh(a) => Some((a, (kind, None))),
_ => None,
};
if let Some((arg, kind)) = entry {
like.push((id, arg, kind));
}
}
let mut out = c;
let mut replaced: FxHashSet<ExprId> = FxHashSet::default();
for i in 0..like.len() {
let (node_i, arg_i, kind_i) = like[i];
if replaced.contains(&node_i) {
continue;
}
let mut compared = 0;
for &(node_j, arg_j, kind_j) in &like[i + 1..] {
if kind_j != kind_i || replaced.contains(&node_j) || arg_i == arg_j {
continue;
}
compared += 1;
if compared > MAX_LIKE_NODES {
break;
}
let d = arena.sub(arg_i, arg_j);
let d = eval::eval(arena, d);
if arena.is_zero_structural(d)
|| crate::simplify::identically_zero::vanishes_by_identity(arena, d)
{
out = crate::transforms::subs::subs(arena, out, node_j, node_i);
replaced.insert(node_j);
}
}
}
if out == c { c } else { eval::eval(arena, out) }
}
fn compositions_pass(arena: &mut Arena, c: ExprId) -> ExprId {
let post = walk::post_order_ids(arena, c);
let mut cache: rustc_hash::FxHashMap<ExprId, ExprId> = rustc_hash::FxHashMap::default();
let mut changed = false;
for &id in &post {
let r = walk::rebuild_with_cache(arena, id, &cache);
let node = arena.node(r).clone();
let new = match &node {
ExprNode::Sin(a)
| ExprNode::Cos(a)
| ExprNode::Tan(a)
| ExprNode::Sinh(a)
| ExprNode::Cosh(a)
| ExprNode::Tanh(a) => function_of_inverse(arena, &node, *a).unwrap_or(r),
_ => r,
};
changed |= new != r;
cache.insert(id, new);
}
if !changed {
return c;
}
let r = cache.get(&c).copied().unwrap_or(c);
eval::eval(arena, r)
}
fn exact_pass(arena: &mut Arena, c: ExprId) -> ExprId {
use rustc_hash::{FxHashMap, FxHashSet};
let post = walk::post_order_ids(arena, c);
let mut arguments: FxHashSet<ExprId> = FxHashSet::default();
for &id in &post {
let node = arena.node(id);
if !node.is_atom()
&& !matches!(
node,
ExprNode::Add(_) | ExprNode::Mul(_) | ExprNode::Pow(..) | ExprNode::Neg(_)
)
{
node.for_each_child(|ch| {
arguments.insert(ch);
});
}
}
let mut cache: FxHashMap<ExprId, ExprId> = FxHashMap::default();
let mut changed = false;
for &id in &post {
let r = walk::rebuild_with_cache(arena, id, &cache);
let node = arena.node(r).clone();
let mut new = match &node {
ExprNode::Ln(a) => ln_of_rational_in_primes(arena, *a).unwrap_or(r),
ExprNode::Apply(f, args) if args.len() == 2 => {
special_function_rewrite(arena, *f, args[0], args[1]).unwrap_or(r)
}
ExprNode::Atan2(y, a) if arena.is_zero_structural(*y) => {
if real_constant_within(arena, *a, 0.0, f64::INFINITY) {
arena.zero
} else if real_constant_within(arena, *a, f64::NEG_INFINITY, 0.0) {
arena.pi()
} else {
r
}
}
ExprNode::DiracDelta(a) => dirac_delta_off_zero(arena, *a).unwrap_or(r),
ExprNode::Abs(a) => {
if real_constant_within(arena, *a, 0.0, f64::INFINITY) {
*a
} else if real_constant_within(arena, *a, f64::NEG_INFINITY, 0.0) {
arena.neg(*a)
} else {
r
}
}
ExprNode::Pow(m, q)
if matches!(arena.node(*m), ExprNode::Mul(_)) && arena.as_num(*q).is_some() =>
{
power_of_positive_product(arena, *m, *q).unwrap_or(r)
}
ExprNode::Binomial(..) => certified_zero_value(arena, r).unwrap_or(r),
ExprNode::Min(args) => extreme_constant(arena, args, true).unwrap_or(r),
ExprNode::Max(args) => extreme_constant(arena, args, false).unwrap_or(r),
ExprNode::Acosh(a) => inverse_of_value(arena, &node, *a)
.or_else(|| acosh_as_acos(arena, *a))
.unwrap_or(r),
ExprNode::Mul(fs) => gamma_reflection(arena, fs)
.or_else(|| exponentials_combined(arena, fs))
.unwrap_or(r),
ExprNode::Pow(b, e)
if matches!(arena.node(*b), ExprNode::Add(_))
&& arena.as_num(*e).is_some_and(|q| {
*q == crate::base::numeric::Q::new(1.into(), 2.into())
}) =>
{
sqrt_of_squares(arena, *b).unwrap_or(r)
}
ExprNode::Asin(a) => inverse_of_value(arena, &node, *a)
.or_else(|| asin_on_cut(arena, *a))
.unwrap_or(r),
ExprNode::Acos(a) | ExprNode::Atan(a) | ExprNode::Asinh(a) | ExprNode::Atanh(a) => {
inverse_of_value(arena, &node, *a).unwrap_or(r)
}
_ => r,
};
if arguments.contains(&id) && has_quotient(arena, new) {
new = crate::simplify::ratsimp::ratsimp(arena, new);
}
changed |= new != r;
cache.insert(id, new);
}
if !changed {
return c;
}
let r = cache.get(&c).copied().unwrap_or(c);
eval::eval(arena, r)
}
fn has_dirac_delta(arena: &Arena, c: ExprId) -> bool {
walk::post_order_ids(arena, c)
.into_iter()
.any(|id| matches!(arena.node(id), ExprNode::DiracDelta(_)))
}
fn dirac_delta_off_zero(arena: &mut Arena, a: ExprId) -> Option<ExprId> {
let nonzero = if walk::free_symbols(arena, a).is_empty() {
!has_dirac_delta(arena, a)
&& evalf::evalf_verdict(arena, a, HIDDEN_ZERO_DIGITS, evalf::ZeroSearch::Cap)
.ok()
.and_then(crate::base::truth::Verdict::proven)
.is_some_and(|z| !(z.0.is_zero() && z.1.is_zero()))
} else {
crate::simplify::identically_zero::nonzero_at_a_sample(arena, a)
};
nonzero.then_some(arena.zero)
}
fn certified_zero_value(arena: &mut Arena, v: ExprId) -> Option<ExprId> {
if !walk::free_symbols(arena, v).is_empty() || has_dirac_delta(arena, v) {
return None;
}
let z = evalf::evalf_verdict(arena, v, HIDDEN_ZERO_DIGITS, evalf::ZeroSearch::Cap)
.ok()?
.proven()?;
(z.0.is_zero() && z.1.is_zero()).then_some(arena.zero)
}
fn power_of_positive_product(arena: &mut Arena, m: ExprId, q: ExprId) -> Option<ExprId> {
let ExprNode::Mul(fs) = arena.node(m).clone() else {
return None;
};
let mut powers: Vec<ExprId> = Vec::with_capacity(fs.len());
for &f in &fs {
if !real_constant_within(arena, f, 0.0, f64::INFINITY) {
return None;
}
powers.push(arena.pow(f, q));
}
Some(arena.mul(&powers))
}
const MAX_SPECIAL_ORDER: i64 = 20;
fn special_function_rewrite(
arena: &mut Arena,
f: crate::base::node::SymbolId,
order: ExprId,
z: ExprId,
) -> Option<ExprId> {
use crate::base::libfn::LibFn;
use num_traits::ToPrimitive;
let lib = arena.lib_fn(f)?;
let n = arena
.as_num(order)
.filter(|q| q.is_integer())?
.to_integer()
.to_i64()?;
if n.abs() > MAX_SPECIAL_ORDER {
return None;
}
match lib {
LibFn::ExpInt if n <= 0 => {
let m = -n;
let mut terms: Vec<ExprId> = Vec::new();
let mut factorial = num_bigint::BigInt::from(1);
for k in 0..=m {
if k > 0 {
factorial *= k;
}
let inv =
arena.num_ratio(crate::base::numeric::Q::new(1.into(), factorial.clone()));
let k = arena.int(k);
let zk = arena.pow(z, k);
terms.push(arena.mul(&[inv, zk]));
}
let sum = arena.add(&terms);
let m_fact = arena.big_int(factorial);
let nz = arena.neg(z);
let e = arena.exp(nz);
let power = arena.int(-(m + 1));
let den = arena.pow(z, power);
Some(arena.mul(&[m_fact, e, sum, den]))
}
LibFn::BesselJ | LibFn::BesselI | LibFn::BesselY | LibFn::BesselK => {
let bessel = matches!(lib, LibFn::BesselJ | LibFn::BesselY);
let mut sign = if n < 0 && bessel && n % 2 != 0 { -1 } else { 1 };
let n = n.abs();
let (coeff, _) = arena.as_coeff_term(z);
let entire = matches!(lib, LibFn::BesselJ | LibFn::BesselI);
let z = if entire && num_traits::Signed::is_negative(&coeff) {
if n % 2 != 0 {
sign = -sign;
}
arena.neg(z)
} else {
z
};
let call = |arena: &mut Arena, k: i64| {
let k = arena.int(k);
arena.lib_apply(lib, &[k, z])
};
let (mut prev, mut cur) = (call(arena, 0), call(arena, 1));
if n == 0 {
cur = prev;
}
for k in 2..=n {
let c = arena.int(2 * (k - 1));
let m1 = arena.neg_one();
let zi = arena.pow(z, m1);
let t = arena.mul(&[c, zi, cur]);
let next = match lib {
LibFn::BesselJ | LibFn::BesselY => arena.sub(t, prev),
LibFn::BesselK => arena.add(&[prev, t]),
_ => arena.sub(prev, t),
};
(prev, cur) = (cur, next);
}
let s = arena.int(sign);
Some(arena.mul(&[s, cur]))
}
_ => None,
}
}
fn exponentials_combined(arena: &mut Arena, fs: &[ExprId]) -> Option<ExprId> {
let mut exponents: Vec<ExprId> = Vec::new();
let mut rest: Vec<ExprId> = Vec::new();
for &f in fs {
match *arena.node(f) {
ExprNode::Exp(a) => exponents.push(a),
ExprNode::E => exponents.push(arena.one()),
ExprNode::Pow(b, a) if b == arena.e_const => exponents.push(a),
_ => rest.push(f),
}
}
if exponents.len() < 2 {
return None;
}
let s = arena.add(&exponents);
let e = arena.exp(s);
rest.push(e);
Some(arena.mul(&rest))
}
fn gamma_reflection(arena: &mut Arena, fs: &[ExprId]) -> Option<ExprId> {
let gammas: Vec<(usize, ExprId)> = fs
.iter()
.enumerate()
.filter_map(|(i, &f)| match arena.node(f) {
ExprNode::Gamma(a) => Some((i, *a)),
_ => None,
})
.collect();
for (j, &(i1, p)) in gammas.iter().enumerate() {
for &(i2, q) in &gammas[j + 1..] {
let s = arena.add(&[p, q]);
if eval::eval(arena, s) != arena.one() {
continue;
}
let mut rest: Vec<ExprId> = fs
.iter()
.enumerate()
.filter(|&(i, _)| i != i1 && i != i2)
.map(|(_, &f)| f)
.collect();
let pi = arena.pi();
let pi_p = arena.mul(&[pi, p]);
let sine = arena.sin(pi_p);
let m1 = arena.neg_one();
let cosecant = arena.pow(sine, m1);
rest.push(pi);
rest.push(cosecant);
return Some(arena.mul(&rest));
}
}
None
}
fn sqrt_of_squares(arena: &mut Arena, b: ExprId) -> Option<ExprId> {
use crate::base::assumptions::{AssumptionCache, Props};
let (content, factors) = crate::simplify::factor::factor_list(arena, b, None);
if factors.iter().all(|&(_, m)| m < 2)
|| !arena
.as_num(content)
.is_some_and(num_traits::Signed::is_positive)
{
return None;
}
let mut cache = AssumptionCache::new();
let mut outside: Vec<ExprId> = Vec::new();
let mut inside: Vec<ExprId> = vec![content];
for (f, m) in factors {
if m >= 2 && cache.query(arena, f, Props::NONNEGATIVE) == Some(true) {
let half = arena.int(i64::from(m / 2));
let p = arena.pow(f, half);
outside.push(p);
if m % 2 == 1 {
inside.push(f);
}
} else {
let m = arena.int(i64::from(m));
let p = arena.pow(f, m);
inside.push(p);
}
}
if outside.is_empty() {
return None;
}
let rest = arena.mul(&inside);
let root = arena.sqrt(rest);
outside.push(root);
Some(arena.mul(&outside))
}
fn has_quotient(arena: &Arena, e: ExprId) -> bool {
walk::post_order_ids(arena, e)
.into_iter()
.any(|id| match arena.node(id) {
ExprNode::Pow(b, n) => {
!arena.node(*b).is_atom()
&& arena
.as_num(*n)
.is_some_and(num_traits::Signed::is_negative)
}
_ => false,
})
}
fn ln_of_rational_in_primes(arena: &mut Arena, a: ExprId) -> Option<ExprId> {
use num_traits::{One, Signed, ToPrimitive};
let q = arena.as_num(a)?.clone();
if !q.is_positive() || q.is_one() {
return None;
}
let mut factors: Vec<(u64, i64)> = Vec::new();
for (n, sign) in [(q.numer(), 1i64), (q.denom(), -1i64)] {
let mut n = n.to_u64()?;
let mut d = 2u64;
while d <= MAX_LOG_PRIME && d.saturating_mul(d) <= n {
let mut e = 0i64;
while n % d == 0 {
n /= d;
e += 1;
}
if e > 0 {
factors.push((d, sign * e));
}
d += 1;
}
if n > 1 {
factors.push((n, sign));
}
}
if factors.len() == 1 && factors[0].1 == 1 {
return None; }
let terms: Vec<ExprId> = factors
.into_iter()
.map(|(p, e)| {
let p = arena.big_int(num_bigint::BigInt::from(p));
let l = arena.ln(p);
let e = arena.int(e);
arena.mul(&[e, l])
})
.collect();
Some(arena.add(&terms))
}
fn inverse_of_value(arena: &mut Arena, outer: &ExprNode, inner: ExprId) -> Option<ExprId> {
use std::f64::consts::{FRAC_PI_2, PI};
let (a, lo, hi) = match (outer, arena.node(inner)) {
(ExprNode::Asin(_), ExprNode::Sin(a)) | (ExprNode::Atan(_), ExprNode::Tan(a)) => {
(*a, -FRAC_PI_2, FRAC_PI_2)
}
(ExprNode::Acos(_), ExprNode::Cos(a)) => (*a, 0.0, PI),
(ExprNode::Acosh(_), ExprNode::Cosh(a)) => (*a, 0.0, f64::INFINITY),
(ExprNode::Asinh(_), ExprNode::Sinh(a)) | (ExprNode::Atanh(_), ExprNode::Tanh(a)) => {
(*a, f64::NEG_INFINITY, f64::INFINITY)
}
_ => return None,
};
real_constant_within(arena, a, lo, hi).then_some(a)
}
fn acosh_as_acos(arena: &mut Arena, a: ExprId) -> Option<ExprId> {
if !real_constant_within(arena, a, -1.0, 1.0) {
return None;
}
let acos = arena.acos(a);
let i = arena.i_unit();
Some(arena.mul(&[i, acos]))
}
fn function_of_inverse(arena: &mut Arena, outer: &ExprNode, inner: ExprId) -> Option<ExprId> {
let one = arena.one();
let two = arena.int(2);
let half = arena.rational(1, 2);
let m_half = arena.rational(-1, 2);
let root = |arena: &mut Arena, z: ExprId, minus: bool, e: ExprId| {
let z2 = arena.pow(z, two);
let s = if minus {
arena.sub(one, z2)
} else {
arena.add(&[one, z2])
};
arena.pow(s, e)
};
Some(match (outer, arena.node(inner).clone()) {
(ExprNode::Sin(_), ExprNode::Asin(z))
| (ExprNode::Cos(_), ExprNode::Acos(z))
| (ExprNode::Tan(_), ExprNode::Atan(z))
| (ExprNode::Sinh(_), ExprNode::Asinh(z))
| (ExprNode::Cosh(_), ExprNode::Acosh(z))
| (ExprNode::Tanh(_), ExprNode::Atanh(z)) => z,
(ExprNode::Cos(_), ExprNode::Asin(z)) | (ExprNode::Sin(_), ExprNode::Acos(z)) => {
root(arena, z, true, half)
}
(ExprNode::Tan(_), ExprNode::Asin(z)) => {
let r = root(arena, z, true, m_half);
arena.mul(&[z, r])
}
(ExprNode::Tan(_), ExprNode::Acos(z)) => {
let r = root(arena, z, true, half);
let m1 = arena.neg_one();
let zi = arena.pow(z, m1);
arena.mul(&[r, zi])
}
(ExprNode::Sin(_), ExprNode::Atan(z)) => {
let r = root(arena, z, false, m_half);
arena.mul(&[z, r])
}
(ExprNode::Cos(_), ExprNode::Atan(z)) => root(arena, z, false, m_half),
(ExprNode::Cosh(_), ExprNode::Asinh(z)) => root(arena, z, false, half),
(ExprNode::Sinh(_), ExprNode::Acosh(z)) => {
let zm = arena.sub(z, one);
let zp = arena.add(&[z, one]);
let rm = arena.sqrt(zm);
let rp = arena.sqrt(zp);
arena.mul(&[rm, rp])
}
_ => return None,
})
}
fn asin_on_cut(arena: &mut Arena, a: ExprId) -> Option<ExprId> {
let (t, sign) = if real_constant_within(arena, a, 1.0, f64::INFINITY) {
(a, 1)
} else if real_constant_within(arena, a, f64::NEG_INFINITY, -1.0) {
(arena.neg(a), -1)
} else {
return None;
};
let half = arena.rational(1, 2);
let pi = arena.pi();
let half_pi = arena.mul(&[half, pi]);
let i = arena.i_unit();
let acosh = arena.acosh(t);
let i_acosh = arena.mul(&[i, acosh]);
let v = arena.sub(half_pi, i_acosh);
let s = arena.int(sign);
Some(arena.mul(&[s, v]))
}
fn real_constant_within(arena: &mut Arena, a: ExprId, lo: f64, hi: f64) -> bool {
certified_real_value(arena, a).is_some_and(|v| {
let margin = value_margin(v);
v > lo + margin && v < hi - margin
})
}
fn certified_real_value(arena: &mut Arena, a: ExprId) -> Option<f64> {
if !walk::free_symbols(arena, a).is_empty() {
return None;
}
let mut cache = crate::base::assumptions::AssumptionCache::new();
if cache.query(arena, a, crate::base::assumptions::Props::REAL) != Some(true) {
return None;
}
let z = evalf::evalf_verdict(arena, a, HIDDEN_ZERO_DIGITS, evalf::ZeroSearch::Cap)
.ok()?
.proven()?;
crate::base::bigfloat::to_f64(&z.0)
}
fn value_margin(v: f64) -> f64 {
1e-9 * v.abs().max(1.0)
}
fn extreme_constant(arena: &mut Arena, args: &[ExprId], min: bool) -> Option<ExprId> {
let mut vals: Vec<(f64, ExprId)> = Vec::with_capacity(args.len());
for &a in args {
let v = certified_real_value(arena, a)?;
vals.push(if min { (v, a) } else { (-v, a) });
}
let &(best, arg) = vals.iter().min_by(|p, q| p.0.total_cmp(&q.0))?;
let margin = value_margin(best);
vals.iter()
.all(|&(v, a)| a == arg || v > best + margin.max(value_margin(v)))
.then_some(arg)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ZeroTest {
Zero,
Nonzero(ExprId),
Undecided(ExprId),
}
impl ZeroTest {
pub(crate) fn is_zero(self) -> crate::base::truth::Truth {
use crate::base::truth::Truth;
match self {
ZeroTest::Zero => Truth::True,
ZeroTest::Nonzero(_) => Truth::False,
ZeroTest::Undecided(_) => Truth::Unknown,
}
}
pub(crate) fn value(self, arena: &Arena) -> ExprId {
match self {
ZeroTest::Zero => arena.zero,
ZeroTest::Nonzero(v) | ZeroTest::Undecided(v) => v,
}
}
}
pub(crate) fn settle_constant(arena: &mut Arena, c: ExprId) -> ZeroTest {
let v = eval::eval(arena, c);
if arena.is_zero_structural(v) {
return ZeroTest::Zero;
}
if !walk::free_symbols(arena, v).is_empty() {
return ZeroTest::Nonzero(v);
}
match zero_reading(hidden_zero(arena, v)) {
crate::base::truth::Truth::True => ZeroTest::Zero,
crate::base::truth::Truth::False => ZeroTest::Nonzero(v),
crate::base::truth::Truth::Unknown => ZeroTest::Undecided(v),
}
}
pub(crate) fn zero_reading(
verdict: crate::base::truth::Verdict<crate::base::truth::Truth>,
) -> crate::base::truth::Truth {
use crate::base::truth::Truth;
match (verdict.proven(), verdict.value) {
(Some(t), _) => t,
(None, Truth::True) => Truth::Unknown,
(None, Truth::False | Truth::Unknown) => Truth::False,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::arena::Arena;
use crate::base::node::ExprNode;
use std::f64::consts::PI;
fn sym_id(arena: &Arena, expr: ExprId) -> SymbolId {
match arena.node(expr) {
ExprNode::Symbol(sid) => *sid,
_ => panic!("expected Symbol node"),
}
}
fn set_display(arena: &Arena, set: ExprId) -> String {
use crate::output::display;
display::format_expr(arena, set)
}
#[test]
fn domain_sqrt_x() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let x_sym = sym_id(&arena, x);
let expr = arena.sqrt(x);
let reals = arena.interval(arena.neg_infinity, arena.infinity, IntervalKind::Open);
let result = continuous_domain(&mut arena, expr, x, x_sym, reals);
let s = set_display(&arena, result);
assert!(
s.contains("0") && (s.contains("oo") || s.contains("∞")),
"sqrt(x) domain should be [0, ∞), got: {s}"
);
assert!(
!s.contains("EmptySet"),
"sqrt(x) domain should not be empty: {s}"
);
}
#[test]
fn domain_1_over_x() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let x_sym = sym_id(&arena, x);
let expr = arena.div(arena.one, x);
let reals = arena.interval(arena.neg_infinity, arena.infinity, IntervalKind::Open);
let result = continuous_domain(&mut arena, expr, x, x_sym, reals);
let s = set_display(&arena, result);
assert!(
!s.contains("EmptySet"),
"1/x domain should not be empty: {s}"
);
assert!(
s.contains("0"),
"1/x domain should reference 0 as excluded point: {s}"
);
}
#[test]
fn domain_ln_x() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let x_sym = sym_id(&arena, x);
let expr = arena.ln(x);
let reals = arena.interval(arena.neg_infinity, arena.infinity, IntervalKind::Open);
let result = continuous_domain(&mut arena, expr, x, x_sym, reals);
let s = set_display(&arena, result);
assert!(
!s.contains("EmptySet"),
"ln(x) domain should not be empty: {s}"
);
assert!(
s.contains("0") && (s.contains("oo") || s.contains("∞")),
"ln(x) domain should be (0, ∞), got: {s}"
);
}
#[test]
fn domain_sqrt_x_minus_2() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let x_sym = sym_id(&arena, x);
let two = arena.int(2);
let inner = arena.sub(x, two);
let expr = arena.sqrt(inner);
let neg5 = arena.int(-5);
let five = arena.int(5);
let domain = arena.interval(neg5, five, IntervalKind::Closed);
let result = continuous_domain(&mut arena, expr, x, x_sym, domain);
let s = set_display(&arena, result);
assert!(
!s.contains("EmptySet"),
"sqrt(x-2) on [-5,5] should not be empty: {s}"
);
assert!(
s.contains("2") && s.contains("5"),
"sqrt(x-2) on [-5,5] should give [2, 5], got: {s}"
);
}
#[test]
fn singularities_tan_x() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let x_sym = sym_id(&arena, x);
let expr = arena.tan(x);
let sings = singularities(&mut arena, expr, x, x_sym, Interval::closed(0.0, 5.0));
assert!(
!sings.is_empty(),
"tan(x) should have singularities in [0, 5]"
);
let has_pi_half = sings.iter().any(|&v| (v - PI / 2.0).abs() < 0.1);
assert!(
has_pi_half,
"tan(x) singularities should include ≈π/2, got: {:?}",
sings
);
}
#[test]
fn singularities_1_over_x() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let x_sym = sym_id(&arena, x);
let expr = arena.div(arena.one, x);
let sings = singularities(&mut arena, expr, x, x_sym, Interval::closed(-2.0, 2.0));
assert_eq!(
sings.len(),
1,
"1/x should have one singularity: {:?}",
sings
);
assert!(
sings[0].abs() < 1e-10,
"1/x singularity should be at 0, got: {}",
sings[0]
);
}
#[test]
fn frequency_sin_100x() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let x_sym = sym_id(&arena, x);
let hundred = arena.int(100);
let inner = arena.mul(&[hundred, x]);
let expr = arena.sin(inner);
let freq = estimate_frequency(&arena, expr, x, x_sym);
assert!(freq.is_some(), "sin(100*x) should have a frequency");
let omega = freq.unwrap();
assert!(
(omega - 100.0).abs() < 1e-10,
"sin(100*x) angular frequency should be 100 rad/s, got: {}",
omega
);
}
#[test]
fn frequency_no_trig() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let x_sym = sym_id(&arena, x);
let two = arena.int(2);
let x2 = arena.pow(x, two);
let one = arena.one;
let expr = arena.add(&[x2, one]);
let freq = estimate_frequency(&arena, expr, x, x_sym);
assert!(
freq.is_none(),
"x^2 + 1 should have no frequency, got: {:?}",
freq
);
}
}