use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Signed, Zero};
use rustc_hash::FxHashMap;
use crate::base::arena::Arena;
use crate::base::errors::SymplexError;
use crate::base::interval::IntervalKind;
use crate::base::node::{ExprId, ExprNode};
use crate::base::numeric::Q;
use crate::base::truth::{SignSet, Truth};
use crate::base::walk;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Relation {
Gt,
Ge,
Lt,
Le,
}
impl Relation {
pub(crate) fn accepts(self, sign: std::cmp::Ordering) -> bool {
use std::cmp::Ordering::*;
matches!(
(self, sign),
(Relation::Gt, Greater)
| (Relation::Lt, Less)
| (Relation::Ge, Greater | Equal)
| (Relation::Le, Less | Equal)
)
}
}
const DECISION_DIGITS: u32 = 30;
const AMBIGUOUS_EXP: usize = 24;
const NONREAL_EXP: usize = 20;
const MERGE_REL: f64 = 1e-24;
const MAX_CRITICAL_POINTS: usize = 256;
const MAX_BRANCH_PASSES: usize = 8;
pub(crate) fn solve_inequality(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
rel: Relation,
) -> Result<ExprId, SymplexError> {
let set = solve_inequality_over_reals(arena, expr, var, rel)?;
crate::transforms::sets::restrict_real_set_to_domain(arena, var, set).ok_or_else(|| {
failed(
"the declared domain of the variable is not a union of intervals or of integers in one"
.into(),
)
})
}
pub(crate) fn solve_inequality_over_reals(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
rel: Relation,
) -> Result<ExprId, SymplexError> {
tracing::debug!("solve_inequality: rel={:?}", rel);
if let Some(set) = try_solve_abs_inequality(arena, expr, var, rel) {
return Ok(set);
}
if let Some(set) = try_solve_nonreal_inequality(arena, expr, var, rel) {
return set;
}
if let Some(poly) = crate::poly::polybridge::expr_to_poly(arena, expr, var) {
let chain = crate::poly::sturm::SturmChain::new(&poly);
if chain.has_no_real_roots() {
let ls = chain.leading_sign_of_original();
let sign = ls.cmp(&0);
return if rel.accepts(sign) {
Ok(arena.interval(arena.neg_infinity, arena.infinity, IntervalKind::Open))
} else {
Ok(arena.empty_set)
};
}
}
if let Some(set) = try_solve_periodic_inequality(arena, expr, var, rel) {
return set;
}
sign_chart(arena, expr, var, rel)
}
const MAX_PERIODIC_POINTS: usize = 128;
const MAX_PERIOD_DIVISOR: usize = 24;
fn trig_period_ratio(arena: &mut Arena, expr: ExprId, var: ExprId) -> Option<Q> {
let mut atoms: Vec<ExprId> = Vec::new();
let mut ratio: Option<Q> = None;
for id in walk::post_order_ids(arena, expr) {
let arg = match *arena.node(id) {
ExprNode::Sin(a) | ExprNode::Cos(a) | ExprNode::Tan(a) => a,
_ => continue,
};
if !walk::contains(arena, arg, var) {
continue;
}
let slope = crate::transforms::diff::diff(arena, arg, var);
let slope = crate::transforms::eval::eval(arena, slope);
let a = arena.as_num(slope)?.clone();
if a.is_zero() {
return None;
}
let r = Ratio::one() / a.abs();
ratio = Some(match ratio {
None => r,
Some(q) => {
let num = num_integer::Integer::lcm(q.numer(), r.numer());
let den = num_integer::Integer::gcd(q.denom(), r.denom());
Ratio::new(num, den)
}
});
if !atoms.contains(&id) {
atoms.push(id);
}
}
let ratio = ratio?;
let mut probe = expr;
for (k, &a) in atoms.iter().enumerate() {
let s = arena.symbol(&format!("__trig_atom_{k}"));
probe = arena.subs_structural(probe, a, s);
}
if walk::contains(arena, probe, var) {
return None;
}
for &a in &atoms {
let arg = match *arena.node(a) {
ExprNode::Sin(x) | ExprNode::Cos(x) | ExprNode::Tan(x) => x,
_ => return None,
};
let slope = crate::transforms::diff::diff(arena, arg, var);
let slope = crate::transforms::eval::eval(arena, slope);
let at0 = crate::transforms::subs::subs(arena, arg, var, arena.zero);
let at0 = crate::transforms::eval::eval(arena, at0);
let lin = arena.mul(&[slope, var]);
let lin = arena.add(&[lin, at0]);
let d = arena.sub(arg, lin);
let d = crate::transforms::expand::expand(arena, d);
let d = crate::transforms::eval::eval(arena, d);
if !arena.is_zero_structural(d) {
return None;
}
}
Some(ratio)
}
fn try_solve_periodic_inequality(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
rel: Relation,
) -> Option<Result<ExprId, SymplexError>> {
let ratio = trig_period_ratio(arena, expr, var)?;
Some(solve_periodic_inequality(arena, expr, var, rel, &ratio))
}
fn solve_periodic_inequality(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
rel: Relation,
ratio: &Q,
) -> Result<ExprId, SymplexError> {
let expr = crate::transforms::eval::eval(arena, expr);
let two_r = arena.num_ratio(ratio * Ratio::from_integer(BigInt::from(2)));
let period = arena.mul(&[two_r, arena.pi]);
let period = crate::transforms::eval::eval(arena, period);
let period_q =
approx_real(arena, period).ok_or_else(|| failed("the period cannot be located".into()))?;
let mut rewritten = expr;
for id in walk::post_order_ids(arena, expr) {
if let ExprNode::Tan(u) = *arena.node(id)
&& walk::contains(arena, u, var)
{
let s = arena.sin(u);
let c = arena.cos(u);
let q = arena.div(s, c);
rewritten = arena.subs_structural(rewritten, id, q);
}
}
let rewritten = crate::transforms::eval::eval(arena, rewritten);
let (numer, denom) = numer_denom(arena, rewritten);
let constraints = domain_constraints(arena, rewritten, var)?;
let mut critical: Vec<(ExprId, bool, bool)> = vec![(numer, true, false)];
if denom != arena.one {
critical.push((denom, false, true));
}
for c in &constraints {
critical.push((c.expr, false, c.kind != ConstraintKind::NonNegative));
}
let mut points: Vec<Critical> = Vec::new();
let mut identity = false;
for (e, numer_zero, undefined) in critical {
if !walk::contains(arena, e, var) {
continue;
}
let members = match periodic_zeros_in_period(arena, e, var, period)? {
Some(m) => m,
None => {
if numer_zero {
identity = true;
continue;
}
return Err(failed(format!("{} vanishes identically", arena.display(e))));
}
};
add_roots(arena, &mut points, &members, numer_zero, undefined)?;
}
sort_points(&mut points);
if points.len() > MAX_PERIODIC_POINTS {
return Err(failed(format!(
"too many critical points in one period ({})",
points.len()
)));
}
let branch = Branch {
resolved: expr,
constraints,
};
if identity {
if !rel.accepts(std::cmp::Ordering::Equal) {
return Ok(arena.empty_set);
}
}
let k = points.len();
if k == 0 {
let p = arena.zero;
let all = sample_satisfies(arena, &branch, var, p, rel)?;
let (ninf, pinf) = (arena.neg_infinity, arena.infinity);
return Ok(if all {
arena.interval(ninf, pinf, IntervalKind::Open)
} else {
arena.empty_set
});
}
let first_shifted = {
let id = arena.add(&[points[0].id, period]);
let id = crate::transforms::eval::eval(arena, id);
Critical {
id,
approx: &points[0].approx + &period_q,
exact: false,
numer_zero: points[0].numer_zero,
undefined: points[0].undefined,
}
};
let mut arc_in: Vec<bool> = Vec::with_capacity(k);
for i in 0..k {
let lo = &points[i];
let hi = if i + 1 < k {
&points[i + 1]
} else {
&first_shifted
};
let mut decision: Option<bool> = None;
for r in probe_ratios(Some(lo), Some(hi))? {
let p = arena.num_ratio(r);
let here = sample_satisfies(arena, &branch, var, p, rel)?;
match decision {
None => decision = Some(here),
Some(d) if d != here => {
return Err(failed(format!(
"the sign of {} changes between two consecutive critical points \
(a zero or a pole was not found)",
arena.display(expr)
)));
}
Some(_) => {}
}
}
arc_in.push(decision.unwrap_or(false));
}
let mut point_in: Vec<bool> = Vec::with_capacity(k);
for c in &points {
let inside = point_in_domain(arena, &branch, var, c)?;
point_in.push(inside && point_satisfies(arena, expr, var, c, rel)?);
}
let mut m_best = 1usize;
for m in (2..=MAX_PERIOD_DIVISOR.min(k)).rev() {
if !k.is_multiple_of(m) {
continue;
}
let step = k / m;
let shift = &period_q / Ratio::from_integer(BigInt::from(m as i64));
let repeats = (0..k).all(|i| {
let j = (i + step) % k;
let target = if i + step >= k {
&points[j].approx + &period_q
} else {
points[j].approx.clone()
};
approx_same(&(&points[i].approx + &shift), &target)
&& point_in[i] == point_in[j]
&& arc_in[i] == arc_in[j]
});
if repeats {
m_best = m;
break;
}
}
let (k, period, period_q) = if m_best > 1 {
let m_id = arena.int(m_best as i64);
let p = arena.div(period, m_id);
let p = crate::transforms::eval::eval(arena, p);
(
k / m_best,
p,
&period_q / Ratio::from_integer(BigInt::from(m_best as i64)),
)
} else {
(k, period, period_q)
};
let included = |e: usize| -> bool {
let e = e % (2 * k);
if e.is_multiple_of(2) {
point_in[e / 2]
} else {
arc_in[e / 2]
}
};
if (0..2 * k).all(included) {
let (ninf, pinf) = (arena.neg_infinity, arena.infinity);
return Ok(arena.interval(ninf, pinf, IntervalKind::Open));
}
if (0..k).all(|i| arc_in[i]) {
let (ninf, pinf) = (arena.neg_infinity, arena.infinity);
let line = arena.interval(ninf, pinf, IntervalKind::Open);
let mut holes: Vec<ExprId> = Vec::new();
for (i, c) in points.iter().take(k).enumerate() {
if !point_in[i] {
holes.push(point_family(arena, c.id, period));
}
}
let holes = arena.set_union(&holes);
let set = arena.set_complement(line, holes);
return Ok(crate::transforms::sets::simplify_set(arena, set));
}
let start = (0..2 * k).find(|&e| !included(e)).unwrap_or(0) + 1;
let mut parts: Vec<ExprId> = Vec::new();
let mut e = start;
while e < start + 2 * k {
if !included(e) {
e += 1;
continue;
}
let run_start = e;
while e < start + 2 * k && included(e) {
e += 1;
}
let run_end = e - 1;
let end_of = |idx: usize, upper: bool| -> (usize, i64) {
let pt = if idx.is_multiple_of(2) {
idx / 2
} else if upper {
idx / 2 + 1
} else {
idx / 2
};
(pt % k, (pt / k) as i64)
};
if run_start == run_end && run_start % 2 == 0 {
let (pt, wraps) = end_of(run_start, false);
let at = shifted(arena, points[pt].id, period, wraps);
parts.push(point_family(arena, at, period));
continue;
}
let (lo_pt, lo_wraps) = end_of(run_start, false);
let (hi_pt, hi_wraps) = end_of(run_end, true);
let lo_open = run_start % 2 == 1;
let hi_open = run_end % 2 == 1;
let mid = (&points[lo_pt].approx
+ &period_q * Ratio::from_integer(BigInt::from(lo_wraps))
+ &points[hi_pt].approx
+ &period_q * Ratio::from_integer(BigInt::from(hi_wraps)))
/ Ratio::from_integer(BigInt::from(2));
let half = &period_q / Ratio::from_integer(BigInt::from(2));
let turns = ((&mid + &half) / &period_q).ceil() - Ratio::one();
let turns = turns.to_integer();
let turns = num_traits::ToPrimitive::to_i64(&turns).unwrap_or(0);
let lo = shifted(arena, points[lo_pt].id, period, lo_wraps - turns);
let hi = shifted(arena, points[hi_pt].id, period, hi_wraps - turns);
let kind = IntervalKind::from_open_ends(lo_open, hi_open);
parts.push(crate::transforms::sets::periodic_interval_set(
arena, lo, hi, kind, period,
));
}
let union = arena.set_union(&parts);
Ok(crate::transforms::sets::simplify_set(arena, union))
}
fn shifted(arena: &mut Arena, p: ExprId, period: ExprId, k: i64) -> ExprId {
if k == 0 {
return p;
}
let kk = arena.int(k);
let s = arena.mul(&[kk, period]);
let sum = arena.add(&[p, s]);
crate::transforms::eval::eval(arena, sum)
}
fn point_family(arena: &mut Arena, p: ExprId, period: ExprId) -> ExprId {
let scope = arena.finite_set(&[p, period]);
let zero = arena.zero;
let n = crate::transforms::sets::image_param(arena, zero, scope);
let pn = arena.mul(&[period, n]);
let body = arena.add(&[p, pn]);
crate::transforms::sets::image_set(arena, n, body)
}
fn periodic_zeros_in_period(
arena: &mut Arena,
e: ExprId,
var: ExprId,
period: ExprId,
) -> Result<Option<Vec<ExprId>>, SymplexError> {
use crate::transforms::solve::{Reason, RootSet as Roots, Solutions};
let scope = arena.finite_set(&[e, var]);
let zero = arena.zero;
let n = crate::transforms::sets::image_param(arena, zero, scope);
let outcome = crate::transforms::solve::solve_general(arena, e, var, n);
let sols = match outcome {
Solutions::Complete(Roots::All) => return Ok(None),
Solutions::Complete(Roots::Empty(_)) => return Ok(Some(Vec::new())),
complete @ Solutions::Complete(_) => complete.claimed_points(),
Solutions::Refused(Reason::NotFound) | Solutions::Partial(..) => {
return Err(failed(format!(
"could not find the zeros of {}",
arena.display(e)
)));
}
Solutions::Refused(reason) => {
return Err(failed(format!(
"could not find the zeros of {}: {}",
arena.display(e),
reason.text()
)));
}
};
let window = crate::transforms::sets::Window::new(arena, zero, period)
.ok_or_else(|| failed("the period cannot be located".into()))?;
let mut out: Vec<ExprId> = Vec::new();
for s in sols {
let v = crate::transforms::eval::eval(arena, s);
let family = walk::contains(arena, v, n);
let zeros = if family {
let members = crate::transforms::sets::image_set_points_in(arena, n, v, window)
.ok_or_else(|| {
failed(format!(
"the family {} cannot be located on the real line",
arena.display(v)
))
})?;
if members.len() > 4 * MAX_PERIODIC_POINTS {
return Err(failed(format!(
"the family {} has too many members in one period",
arena.display(v)
)));
}
members
} else {
match crate::transforms::realness::imaginary_part_sign(arena, v) {
SignSet::ZERO => {}
s if s.is_nonzero().is_true() => continue,
_ => {
return Err(failed(format!(
"the zero {} cannot be located on the real line",
arena.display(v)
)));
}
}
vec![v]
};
for z in zeros {
let q = arena.div(z, period);
let turns = arena.floor(q);
let turns = crate::transforms::eval::eval(arena, turns);
let Some(t) = arena.as_num(turns).filter(|t| t.is_integer()).cloned() else {
return Err(failed(format!(
"the zero {} cannot be reduced into one period",
arena.display(z)
)));
};
if t.is_zero() {
out.push(z);
} else if !family {
let back = arena.mul(&[turns, period]);
let back = arena.neg(back);
let sum = arena.add(&[z, back]);
out.push(crate::transforms::eval::eval(arena, sum));
}
}
}
Ok(Some(out))
}
pub(crate) fn solveset(arena: &mut Arena, expr: ExprId, var: ExprId) -> ExprId {
let set = solveset_over_all_values(arena, expr, var);
if walk::has_unevaluated(arena, set) {
return set;
}
crate::transforms::sets::restrict_set_to_domain(arena, var, set).unwrap_or_else(|| {
let zero = arena.zero;
let cond = arena.eq_(expr, zero);
arena.intern(ExprNode::ConditionSet(var, cond))
})
}
fn solveset_over_all_values(arena: &mut Arena, expr: ExprId, var: ExprId) -> ExprId {
let unknown = |arena: &mut Arena| {
let zero = arena.zero;
let cond = arena.eq_(expr, zero);
arena.intern(ExprNode::ConditionSet(var, cond))
};
if !is_algebraic_in(arena, expr, var) && walk::has_free_var(arena, expr, var) {
return general_solution_set(arena, expr, var, true).unwrap_or_else(|| unknown(arena));
}
crate::transforms::solve::solve_classified(arena, expr, var).into_set(arena, var, expr)
}
fn try_solve_nonreal_inequality(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
rel: Relation,
) -> Option<Result<ExprId, SymplexError>> {
let has_i = walk::post_order_ids(arena, expr)
.into_iter()
.any(|id| matches!(arena.node(id), ExprNode::ImaginaryUnit));
if !has_i || !walk::contains(arena, expr, var) {
return None;
}
let t = crate::transforms::subs::fresh_symbol(arena, "__ineq_real", &[expr]);
if let ExprNode::Symbol(sid) = *arena.node(t) {
let mut real = crate::base::assumptions::Assumptions::default();
real.assert_true(crate::base::assumptions::Props::REAL);
arena.set_symbol_assumptions(sid, real);
}
let e_t = crate::transforms::subs::subs(arena, expr, var, t);
let expanded = crate::transforms::expand::expand(arena, e_t);
let (_, im) = crate::base::complex::as_real_imag(arena, expanded);
let im = crate::transforms::eval::eval(arena, im);
let opaque = walk::post_order_ids(arena, im).into_iter().any(|id| {
matches!(
arena.node(id),
ExprNode::Re(_) | ExprNode::Im(_) | ExprNode::Arg(_) | ExprNode::Conjugate(_)
)
});
if opaque || arena.is_zero_structural(im) {
return None;
}
let (im_numer, _) = numer_denom(arena, im);
crate::poly::polybridge::expr_to_poly(arena, im_numer, t)?;
let roots = match real_roots(arena, im_numer, t) {
Ok(RootSet::Finite(r)) => r,
Ok(RootSet::Everywhere) => return None,
Err(e) => return Some(Err(e)),
};
let mut kept: Vec<ExprId> = Vec::new();
for r in roots {
let at = crate::transforms::subs::subs(arena, expr, var, r);
let at = crate::transforms::eval::eval(arena, at);
let value = value_sign(arena, at);
match value.ordering() {
Some(o) => {
if rel.accepts(o) {
kept.push(r);
}
}
None if value.takes_no_real_value() => {}
None => {
return Some(Err(failed(format!(
"the sign of {} at {} is not decided",
arena.display(expr),
arena.display(r)
))));
}
}
}
Some(Ok(arena.finite_set(&kept)))
}
pub(crate) fn lost_domain(
arena: &mut Arena,
sides: &[ExprId],
combined: ExprId,
var: ExprId,
) -> Result<Option<ExprId>, SymplexError> {
if !kink_expressions(arena, combined, var).is_empty() {
return Ok(None);
}
let have = domain_constraints(arena, combined, var)?;
let mut lost: Vec<Constraint> = Vec::new();
for &s in sides {
if !walk::contains(arena, s, var) {
continue;
}
if !kink_expressions(arena, s, var).is_empty() {
return Ok(None);
}
for c in domain_constraints(arena, s, var)? {
let known = have
.iter()
.chain(lost.iter())
.any(|h| h.expr == c.expr && h.kind == c.kind);
if !known {
lost.push(c);
}
}
}
if lost.is_empty() {
return Ok(None);
}
let mut parts: Vec<ExprId> = Vec::with_capacity(lost.len());
for c in lost {
let set = match c.kind {
ConstraintKind::Positive => {
solve_inequality_over_reals(arena, c.expr, var, Relation::Gt)?
}
ConstraintKind::NonNegative => {
solve_inequality_over_reals(arena, c.expr, var, Relation::Ge)?
}
ConstraintKind::NonZero => {
let above = solve_inequality_over_reals(arena, c.expr, var, Relation::Gt)?;
let below = solve_inequality_over_reals(arena, c.expr, var, Relation::Lt)?;
arena.set_union(&[above, below])
}
};
parts.push(set);
}
let meet = arena.set_intersection(&parts);
Ok(Some(crate::transforms::sets::simplify_set(arena, meet)))
}
fn is_algebraic_in(arena: &Arena, expr: ExprId, var: ExprId) -> bool {
let mut stack = vec![expr];
let mut seen: rustc_hash::FxHashSet<ExprId> = rustc_hash::FxHashSet::default();
while let Some(id) = stack.pop() {
if !seen.insert(id) || !walk::has_free_var(arena, id, var) {
continue;
}
match arena.node(id) {
ExprNode::Symbol(_) => {}
ExprNode::Add(ch) | ExprNode::Mul(ch) => stack.extend(ch.iter().copied()),
ExprNode::Neg(a) => stack.push(*a),
ExprNode::Pow(b, e) if arena.as_num(*e).is_some() => stack.push(*b),
_ => return false,
}
}
true
}
pub(crate) fn general_solution_set(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
over_c: bool,
) -> Option<ExprId> {
match general_solve_set(arena, expr, var, over_c) {
Some(GeneralSet::Set(s)) => Some(s),
Some(GeneralSet::Refused) => {
let (g, denominators) = split_polynomial_denominators(arena, expr, var)?;
let Some(GeneralSet::Set(g_set)) = general_solve_set(arena, g, var, over_c) else {
return None;
};
let mut poles: Vec<ExprId> = Vec::new();
for q in denominators {
poles.extend(
crate::transforms::solve::solve_classified(arena, q, var)
.points_if_complete()?,
);
}
let mut excluded = Vec::new();
for p in poles {
match crate::transforms::sets::set_contains(arena, g_set, p) {
Some(true) => excluded
.push(crate::transforms::sets::member_form(arena, g_set, p).unwrap_or(p)),
Some(false) => {}
None => return None,
}
}
if excluded.is_empty() {
return Some(g_set);
}
let excluded = arena.finite_set(&excluded);
let set = arena.set_complement(g_set, excluded);
Some(crate::transforms::sets::simplify_set(arena, set))
}
None => None,
}
}
enum GeneralSet {
Set(ExprId),
Refused,
}
fn general_solve_set(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
over_c: bool,
) -> Option<GeneralSet> {
use crate::transforms::solve::{Reason, Solutions};
let scope = arena.finite_set(&[expr, var]);
let zero = arena.zero;
let n = crate::transforms::sets::image_param(arena, zero, scope);
let outcome = if over_c {
crate::transforms::solve::solve_general_over_c(arena, expr, var, n)
} else {
crate::transforms::solve::solve_general(arena, expr, var, n)
};
match outcome {
Solutions::Refused(Reason::NotFound) => None,
conditional if conditional.is_conditional() => {
Some(GeneralSet::Set(conditional.into_set(arena, var, expr)))
}
Solutions::Refused(_) => Some(GeneralSet::Refused),
partial @ Solutions::Partial(..) => {
Some(GeneralSet::Set(partial.into_set(arena, var, expr)))
}
complete @ Solutions::Complete(_) => {
Some(GeneralSet::Set(complete.into_set(arena, var, expr)))
}
}
}
fn is_polynomial_in(arena: &mut Arena, e: ExprId, var: ExprId) -> bool {
if crate::poly::polybridge::expr_to_poly(arena, e, var).is_some() {
return true;
}
crate::transforms::solve::symbolic_poly_coeffs(arena, e, var)
.is_some_and(|cs| cs.iter().all(|&c| !walk::has_free_var(arena, c, var)))
}
fn split_polynomial_denominators(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
) -> Option<(ExprId, Vec<ExprId>)> {
let ExprNode::Mul(factors) = arena.node(expr).clone() else {
return None;
};
let mut keep: Vec<ExprId> = Vec::new();
let mut denominators: Vec<ExprId> = Vec::new();
for f in factors {
if let ExprNode::Pow(base, e) = *arena.node(f)
&& arena.as_num(e).is_some_and(|r| r.is_negative())
&& walk::has_free_var(arena, base, var)
&& is_polynomial_in(arena, base, var)
{
denominators.push(base);
} else {
keep.push(f);
}
}
if denominators.is_empty() || keep.is_empty() {
return None;
}
let g = arena.mul(&keep);
Some((g, denominators))
}
#[derive(Clone, Debug)]
struct Critical {
id: ExprId,
approx: Q,
exact: bool,
numer_zero: bool,
undefined: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ConstraintKind {
NonZero,
NonNegative,
Positive,
}
impl ConstraintKind {
fn allowed(self) -> SignSet {
match self {
ConstraintKind::NonZero => SignSet::NONZERO_REAL,
ConstraintKind::NonNegative => SignSet::NONNEGATIVE,
ConstraintKind::Positive => SignSet::POSITIVE,
}
}
}
#[derive(Clone, Debug)]
struct Constraint {
expr: ExprId,
kind: ConstraintKind,
}
#[derive(Clone, Debug)]
struct Branch {
resolved: ExprId,
constraints: Vec<Constraint>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ValueSign {
Defined(SignSet),
Undefined,
}
impl ValueSign {
const UNDECIDED: ValueSign = ValueSign::Defined(SignSet::UNKNOWN);
fn ordering(self) -> Option<std::cmp::Ordering> {
match self {
ValueSign::Defined(s) => s.to_ordering(),
ValueSign::Undefined => None,
}
}
fn is_in(self, property: SignSet) -> Truth {
match self {
ValueSign::Defined(s) => s.is_in(property),
ValueSign::Undefined => Truth::False,
}
}
fn is_nonreal(self) -> Truth {
self.is_in(SignSet::NONREAL)
}
fn takes_no_real_value(self) -> bool {
self == ValueSign::Undefined || self.is_nonreal().is_true()
}
fn undecided(self) -> bool {
matches!(self, ValueSign::Defined(s) if !s.is_single())
}
}
fn failed(reason: String) -> SymplexError {
SymplexError::computation_failed("solve_inequality", reason)
}
fn sign_chart(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
rel: Relation,
) -> Result<ExprId, SymplexError> {
check_supported(arena, expr, var)?;
let expr = crate::transforms::eval::eval(arena, expr);
let kinks = kink_expressions(arena, expr, var);
let mut points: Vec<Critical> = Vec::new();
for &k in &kinks {
let (kn, kd) = numer_denom(arena, k);
if let RootSet::Finite(roots) = real_roots(arena, kn, var)? {
add_roots(arena, &mut points, &roots, false, false)?;
}
if kd != arena.one
&& let RootSet::Finite(roots) = real_roots(arena, kd, var)?
{
add_roots(arena, &mut points, &roots, false, false)?;
}
}
sort_points(&mut points);
let kink_points: Vec<Critical> = points.clone();
let has_piecewise = !kinks.is_empty();
let mut branches: Vec<Branch> = Vec::with_capacity(kink_points.len() + 1);
for i in 0..=kink_points.len() {
let lo = if i == 0 {
None
} else {
Some(&kink_points[i - 1])
};
let hi = kink_points.get(i);
let resolved = if has_piecewise {
let p = sample_between(arena, lo, hi)?;
resolve_branches(arena, expr, var, p)?
} else {
expr
};
let constraints = domain_constraints(arena, resolved, var)?;
let (numer, denom) = numer_denom(arena, resolved);
let mut new_points: Vec<Critical> = Vec::new();
match real_roots(arena, numer, var)? {
RootSet::Everywhere => {}
RootSet::Finite(roots) => add_roots(arena, &mut new_points, &roots, true, false)?,
}
if denom != arena.one {
match real_roots(arena, denom, var)? {
RootSet::Everywhere => {
return Err(failed("denominator is identically zero".into()));
}
RootSet::Finite(roots) => {
add_roots(arena, &mut new_points, &roots, false, true)?;
}
}
}
for c in &constraints {
if let RootSet::Finite(roots) = real_roots(arena, c.expr, var)? {
let undefined = c.kind != ConstraintKind::NonNegative;
add_roots(arena, &mut new_points, &roots, false, undefined)?;
}
}
for np in new_points {
let inside_lo = lo.is_none_or(|l| approx_le(&l.approx, &np.approx));
let inside_hi = hi.is_none_or(|h| approx_le(&np.approx, &h.approx));
if inside_lo && inside_hi {
merge_point(arena, &mut points, np)?;
}
}
branches.push(Branch {
resolved,
constraints,
});
}
sort_points(&mut points);
if points.len() > MAX_CRITICAL_POINTS {
return Err(failed(format!(
"too many critical points ({}); limit is {MAX_CRITICAL_POINTS}",
points.len()
)));
}
let n = points.len();
let mut interval_in: Vec<bool> = Vec::with_capacity(n + 1);
for i in 0..=n {
let lo = if i == 0 { None } else { Some(&points[i - 1]) };
let hi = points.get(i);
let probes = probe_ratios(lo, hi)?;
let mut decision: Option<bool> = None;
for p_ratio in probes {
let p = arena.num_ratio(p_ratio.clone());
let branch = branch_for(&branches, &kink_points, &p_ratio);
let here = sample_satisfies(arena, branch, var, p, rel)?;
match decision {
None => decision = Some(here),
Some(d) if d != here => {
return Err(failed(format!(
"the sign of {} changes between two consecutive critical points \
(a zero or a pole was not found)",
arena.display(expr)
)));
}
Some(_) => {}
}
}
interval_in.push(decision.unwrap_or(false));
}
let mut point_in: Vec<bool> = Vec::with_capacity(n);
for c in &points {
let branch = branch_for(&branches, &kink_points, &c.approx);
let inside = point_in_domain(arena, branch, var, c)?;
point_in.push(inside && point_satisfies(arena, expr, var, c, rel)?);
}
let mut pieces: Vec<ExprId> = Vec::new();
let mut isolated: Vec<ExprId> = Vec::new();
for i in 0..=n {
if !interval_in[i] {
continue;
}
let (lo, lo_closed) = if i == 0 {
(arena.neg_infinity, false)
} else {
(points[i - 1].id, point_in[i - 1])
};
let (hi, hi_closed) = if i == n {
(arena.infinity, false)
} else {
(points[i].id, point_in[i])
};
let kind = IntervalKind::from_open_ends(!lo_closed, !hi_closed);
pieces.push(arena.interval(lo, hi, kind));
}
for (i, c) in points.iter().enumerate() {
if point_in[i] && !interval_in[i] && !interval_in[i + 1] {
isolated.push(c.id);
}
}
if !isolated.is_empty() {
pieces.push(arena.finite_set(&isolated));
}
Ok(match pieces.len() {
0 => arena.empty_set,
1 => pieces[0],
_ => {
let u = arena.set_union(&pieces);
crate::transforms::sets::simplify_set(arena, u)
}
})
}
fn check_supported(arena: &Arena, expr: ExprId, var: ExprId) -> Result<(), SymplexError> {
for id in walk::post_order_ids(arena, expr) {
if !walk::contains(arena, id, var) {
continue;
}
let ok = matches!(
arena.node(id),
ExprNode::Symbol(_)
| ExprNode::Add(_)
| ExprNode::Mul(_)
| ExprNode::Pow(..)
| ExprNode::Neg(_)
| ExprNode::Exp(_)
| ExprNode::Ln(_)
| ExprNode::Abs(_)
| ExprNode::Sign(_)
| ExprNode::Heaviside(_)
| ExprNode::Sinh(_)
| ExprNode::Cosh(_)
| ExprNode::Tanh(_)
| ExprNode::Asinh(_)
| ExprNode::Acosh(_)
| ExprNode::Atanh(_)
| ExprNode::Asin(_)
| ExprNode::Acos(_)
| ExprNode::Atan(_)
| ExprNode::Erf(_)
| ExprNode::Erfc(_)
| ExprNode::Min(_)
| ExprNode::Max(_)
| ExprNode::Piecewise(_)
| ExprNode::Gt(..)
| ExprNode::Ge(..)
| ExprNode::Eq_(..)
| ExprNode::Ne(..)
| ExprNode::And(_)
| ExprNode::Or(_)
| ExprNode::Not(_)
| ExprNode::LambertW(_)
);
if !ok {
return Err(failed(format!(
"cannot build a sign chart for {}",
arena.display(id)
)));
}
}
Ok(())
}
enum RootSet {
Everywhere,
Finite(Vec<ExprId>),
}
fn numer_denom(arena: &mut Arena, e: ExprId) -> (ExprId, ExprId) {
let combined = crate::poly::polybridge::together(arena, e);
crate::poly::polybridge::as_numer_denom(arena, combined)
}
fn real_roots(arena: &mut Arena, e: ExprId, var: ExprId) -> Result<RootSet, SymplexError> {
use crate::transforms::solve::{Domain, Missing, Reason, Solutions, SolveCx};
if !walk::contains(arena, e, var) {
let ev = crate::transforms::eval::eval(arena, e);
return Ok(if arena.is_zero_structural(ev) {
RootSet::Everywhere
} else {
RootSet::Finite(Vec::new())
});
}
if crate::poly::polybridge::expr_to_poly(arena, e, var).is_none() {
let (n, d) = numer_denom(arena, e);
if d != arena.one
&& n != e
&& crate::poly::polybridge::expr_to_poly(arena, n, var).is_some()
{
let numer_roots = match real_roots(arena, n, var)? {
RootSet::Everywhere => return Ok(RootSet::Everywhere),
RootSet::Finite(r) => r,
};
let mut out = Vec::new();
for r in numer_roots {
let at = crate::transforms::subs::subs(arena, d, var, r);
if !value_sign(arena, at).is_in(SignSet::ZERO).is_true() {
out.push(r);
}
}
return Ok(RootSet::Finite(out));
}
}
if let Some(poly) = crate::poly::polybridge::expr_to_poly(arena, e, var) {
if poly.is_zero() {
return Ok(RootSet::Everywhere);
}
if poly.is_constant() {
return Ok(RootSet::Finite(Vec::new()));
}
let sqf = poly.square_free_part();
let n_real = crate::poly::sturm::SturmChain::new(&sqf).count_real_roots();
if n_real == 0 {
return Ok(RootSet::Finite(Vec::new()));
}
let candidates = crate::transforms::solve::known_real_roots(arena, e, var);
let mut reals: Vec<(ExprId, Q)> = Vec::new();
for s in candidates {
if value_sign(arena, s).takes_no_real_value() {
continue;
}
let Some(a) = approx_real(arena, s) else {
return Err(failed(format!(
"root {} cannot be evaluated numerically",
arena.display(s)
)));
};
if !reals.iter().any(|(_, b)| approx_same(&a, b)) {
reals.push((s, a));
}
}
if reals.len() != n_real {
return Err(failed(format!(
"found {} of the {n_real} real roots of {}",
reals.len(),
arena.display(e)
)));
}
return Ok(RootSet::Finite(
reals.into_iter().map(|(id, _)| id).collect(),
));
}
let because = |reason: &Reason| match reason {
Reason::NotFound => String::new(),
other => format!(": {}", other.text()),
};
match crate::transforms::solve::solutions(arena, e, var, &SolveCx::principal(Domain::Real)) {
Solutions::Complete(crate::transforms::solve::RootSet::All) => Ok(RootSet::Everywhere),
Solutions::Complete(crate::transforms::solve::RootSet::Empty(_)) => {
Ok(RootSet::Finite(Vec::new()))
}
Solutions::Refused(reason) => Err(failed(format!(
"could not find the zeros of {}{}",
arena.display(e),
because(&reason)
))),
Solutions::Partial(_, Missing::Alternative(reason)) => Err(failed(format!(
"could not find all the zeros of {}{}",
arena.display(e),
because(&reason)
))),
complete @ Solutions::Complete(_) => {
let mut out = Vec::new();
for sol in complete.claimed_points() {
let value = value_sign(arena, sol);
if value.takes_no_real_value() {
continue;
}
if value.undecided() && approx_real(arena, sol).is_none() {
return Err(failed(format!(
"zero {} of {} cannot be located on the real line",
arena.display(sol),
arena.display(e)
)));
}
out.push(sol);
}
Ok(RootSet::Finite(out))
}
}
}
fn add_roots(
arena: &mut Arena,
points: &mut Vec<Critical>,
roots: &[ExprId],
numer_zero: bool,
undefined: bool,
) -> Result<(), SymplexError> {
for &r in roots {
let ev = crate::transforms::eval::eval(arena, r);
let exact = arena.as_num(ev).is_some();
let Some(approx) = approx_real(arena, ev) else {
return Err(failed(format!(
"endpoint {} cannot be ordered on the real line",
arena.display(ev)
)));
};
merge_point(
arena,
points,
Critical {
id: ev,
approx,
exact,
numer_zero,
undefined,
},
)?;
}
Ok(())
}
fn merge_point(
arena: &mut Arena,
points: &mut Vec<Critical>,
np: Critical,
) -> Result<(), SymplexError> {
let mut same: Option<usize> = None;
for (i, c) in points.iter().enumerate() {
if !approx_same(&c.approx, &np.approx) {
continue;
}
if c.id == np.id {
same = Some(i);
break;
}
let d = arena.sub(c.id, np.id);
match crate::transforms::eval::constant_zero_decision(arena, d) {
Some(true) => {
same = Some(i);
break;
}
Some(false) => {}
None => {
return Err(failed(format!(
"cannot decide whether the critical points {} and {} are equal",
arena.display(c.id),
arena.display(np.id)
)));
}
}
}
if let Some(existing) = same.and_then(|i| points.get_mut(i)) {
existing.numer_zero |= np.numer_zero;
existing.undefined |= np.undefined;
if np.exact && !existing.exact {
existing.id = np.id;
existing.approx = np.approx;
existing.exact = true;
}
return Ok(());
}
points.push(np);
Ok(())
}
fn sort_points(points: &mut [Critical]) {
points.sort_by(|a, b| a.approx.cmp(&b.approx));
}
fn approx_same(a: &Q, b: &Q) -> bool {
let scale = ratio_abs_f64(a).max(ratio_abs_f64(b)).max(1.0);
let Some(tol) = crate::base::numeric::f64_to_ratio_exact(MERGE_REL * scale) else {
return a == b;
};
(a - b).abs() <= tol
}
fn approx_le(a: &Q, b: &Q) -> bool {
a <= b || approx_same(a, b)
}
fn ratio_abs_f64(r: &Q) -> f64 {
crate::base::numeric::ratio_to_f64(r)
.map(f64::abs)
.unwrap_or(f64::INFINITY)
}
fn domain_constraints(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
) -> Result<Vec<Constraint>, SymplexError> {
let mut out: Vec<Constraint> = Vec::new();
let mut push = |e: ExprId, kind: ConstraintKind| {
if !out.iter().any(|c| c.expr == e && c.kind == kind) {
out.push(Constraint { expr: e, kind });
}
};
for id in walk::post_order_ids(arena, expr) {
if !walk::contains(arena, id, var) {
continue;
}
match arena.node(id).clone() {
ExprNode::Pow(base, exp) => {
let base_has_var = walk::contains(arena, base, var);
if let Some(r) = arena.as_num(exp).cloned() {
if !base_has_var {
continue;
}
if r.is_negative() {
push(base, ConstraintKind::NonZero);
}
if !r.is_integer() {
push(
base,
if r.is_negative() {
ConstraintKind::Positive
} else {
ConstraintKind::NonNegative
},
);
}
} else if walk::contains(arena, exp, var) {
if base_has_var {
push(base, ConstraintKind::Positive);
} else {
let positive_const = base == arena.e_const
|| arena.as_num(base).is_some_and(|b| b.is_positive());
if !positive_const {
return Err(failed(format!(
"variable exponent with base {}",
arena.display(base)
)));
}
}
} else {
match sign_of(arena, exp) {
Some(1) => push(base, ConstraintKind::NonNegative),
Some(_) => push(base, ConstraintKind::Positive),
None => {
return Err(failed(format!(
"exponent {} has unknown sign",
arena.display(exp)
)));
}
}
}
}
ExprNode::Ln(a) => push(a, ConstraintKind::Positive),
ExprNode::Asin(a) | ExprNode::Acos(a) => {
let one = arena.one;
let lo = arena.add(&[a, one]);
let hi = arena.sub(one, a);
push(lo, ConstraintKind::NonNegative);
push(hi, ConstraintKind::NonNegative);
}
ExprNode::Acosh(a) => {
let one = arena.one;
let shifted = arena.sub(a, one);
push(shifted, ConstraintKind::NonNegative);
}
ExprNode::Atanh(a) => {
let one = arena.one;
let lo = arena.add(&[a, one]);
let hi = arena.sub(one, a);
push(lo, ConstraintKind::Positive);
push(hi, ConstraintKind::Positive);
}
ExprNode::LambertW(a) => {
let inv_e = arena.pow(arena.e_const, arena.neg_one);
let shifted = arena.add(&[a, inv_e]);
push(shifted, ConstraintKind::NonNegative);
}
_ => {}
}
}
Ok(out)
}
fn kink_expressions(arena: &mut Arena, expr: ExprId, var: ExprId) -> Vec<ExprId> {
let mut out: Vec<ExprId> = Vec::new();
for id in walk::post_order_ids(arena, expr) {
if !walk::contains(arena, id, var) {
continue;
}
match arena.node(id).clone() {
ExprNode::Abs(a) | ExprNode::Sign(a) | ExprNode::Heaviside(a) => out.push(a),
ExprNode::Min(args) | ExprNode::Max(args) => {
for i in 0..args.len() {
for j in (i + 1)..args.len() {
let d = arena.sub(args[i], args[j]);
out.push(d);
}
}
}
ExprNode::Piecewise(pairs) => {
for (_, cond) in pairs.iter() {
for cid in walk::post_order_ids(arena, *cond) {
if let ExprNode::Gt(a, b)
| ExprNode::Ge(a, b)
| ExprNode::Eq_(a, b)
| ExprNode::Ne(a, b) = arena.node(cid).clone()
{
let d = arena.sub(a, b);
out.push(d);
}
}
}
}
_ => {}
}
}
out.retain(|&e| walk::contains(arena, e, var));
out.sort_by(|&a, &b| arena.canonical_cmp(a, b));
out.dedup();
out
}
fn resolve_branches(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
p: ExprId,
) -> Result<ExprId, SymplexError> {
let mut current = expr;
for _ in 0..MAX_BRANCH_PASSES {
let mut map: FxHashMap<ExprId, ExprId> = FxHashMap::default();
for id in walk::post_order_ids(arena, current) {
if !walk::contains(arena, id, var) {
continue;
}
let replacement = match arena.node(id).clone() {
ExprNode::Abs(a) => {
let at = crate::transforms::subs::subs(arena, a, var, p);
if value_sign(arena, at).is_nonreal().is_true() {
continue;
}
match sign_at(arena, a, var, p)? {
std::cmp::Ordering::Greater => a,
std::cmp::Ordering::Less => arena.neg(a),
std::cmp::Ordering::Equal => return Err(kink_at_sample(arena, id)),
}
}
ExprNode::Sign(a) => match sign_at(arena, a, var, p)? {
std::cmp::Ordering::Greater => arena.one,
std::cmp::Ordering::Less => arena.neg_one,
std::cmp::Ordering::Equal => return Err(kink_at_sample(arena, id)),
},
ExprNode::Heaviside(a) => match sign_at(arena, a, var, p)? {
std::cmp::Ordering::Greater => arena.one,
std::cmp::Ordering::Less => arena.zero,
std::cmp::Ordering::Equal => return Err(kink_at_sample(arena, id)),
},
ExprNode::Min(args) | ExprNode::Max(args) => {
let want_max = matches!(arena.node(id), ExprNode::Max(_));
let mut best: Option<(ExprId, Q)> = None;
for &a in args.iter() {
let at = crate::transforms::subs::subs(arena, a, var, p);
let Some(v) = approx_real(arena, at) else {
return Err(failed(format!(
"cannot evaluate {} at a sample point",
arena.display(a)
)));
};
best = Some(match best {
None => (a, v),
Some((b, bv)) => {
if approx_same(&v, &bv) {
return Err(kink_at_sample(arena, id));
}
if (v > bv) == want_max {
(a, v)
} else {
(b, bv)
}
}
});
}
best.map(|(a, _)| a).unwrap_or(id)
}
ExprNode::Piecewise(pairs) => {
let mut chosen: Option<ExprId> = None;
for (val, cond) in pairs.iter() {
let c = crate::transforms::subs::subs(arena, *cond, var, p);
let c = crate::transforms::eval::eval(arena, c);
match arena.node(c) {
ExprNode::BoolTrue => {
chosen = Some(*val);
break;
}
ExprNode::BoolFalse => continue,
_ => {
return Err(failed(format!(
"piecewise condition {} does not evaluate at a sample point",
arena.display(*cond)
)));
}
}
}
chosen
.ok_or_else(|| failed("piecewise function has no active branch".into()))?
}
_ => continue,
};
map.insert(id, replacement);
}
if map.is_empty() {
return Ok(current);
}
let next = walk::walk_and_rebuild(arena, current, &|_, id| map.get(&id).copied());
current = crate::transforms::eval::eval(arena, next);
}
Err(failed("piecewise nesting too deep".into()))
}
fn kink_at_sample(arena: &Arena, id: ExprId) -> SymplexError {
failed(format!(
"switching point of {} coincides with a sample point",
arena.display(id)
))
}
fn sign_at(
arena: &mut Arena,
e: ExprId,
var: ExprId,
p: ExprId,
) -> Result<std::cmp::Ordering, SymplexError> {
let at = crate::transforms::subs::subs(arena, e, var, p);
value_sign(arena, at).ordering().ok_or_else(|| {
failed(format!(
"cannot decide the sign of {} at a sample point",
arena.display(e)
))
})
}
fn branch_for<'a>(branches: &'a [Branch], kinks: &[Critical], p: &Q) -> &'a Branch {
let idx = kinks.iter().take_while(|k| k.approx < *p).count();
&branches[idx.min(branches.len() - 1)]
}
fn sample_between(
arena: &mut Arena,
lo: Option<&Critical>,
hi: Option<&Critical>,
) -> Result<ExprId, SymplexError> {
let r = probe_ratios(lo, hi)?.swap_remove(0);
Ok(arena.num_ratio(r.clone()))
}
fn probe_ratios(lo: Option<&Critical>, hi: Option<&Critical>) -> Result<Vec<Q>, SymplexError> {
let int = |n: i64| Ratio::from_integer(BigInt::from(n));
match (lo, hi) {
(None, None) => Ok(vec![
Ratio::zero(),
int(10),
int(-10),
int(1000),
int(-1000),
]),
(None, Some(h)) => {
let base = Ratio::from_integer(h.approx.floor().to_integer());
Ok(vec![&base - int(1), &base - int(10), &base - int(1000)])
}
(Some(l), None) => {
let base = Ratio::from_integer(l.approx.ceil().to_integer());
Ok(vec![&base + int(1), &base + int(10), &base + int(1000)])
}
(Some(l), Some(h)) => {
let gap = &h.approx - &l.approx;
if !gap.is_positive() {
return Err(failed("critical points too close to separate".into()));
}
let quarter = &gap / int(4);
let mid = &l.approx + &gap / int(2);
let targets = [mid.clone(), &l.approx + &quarter, &h.approx - &quarter];
if l.exact && h.exact {
return Ok(targets.to_vec());
}
let scale = ratio_abs_f64(&l.approx)
.max(ratio_abs_f64(&h.approx))
.max(1.0);
let Some(min_gap) = crate::base::numeric::f64_to_ratio_exact(MERGE_REL * scale) else {
return Err(failed("critical points too close to separate".into()));
};
if gap <= min_gap {
return Err(failed("critical points too close to separate".into()));
}
let max_denom = (int(8) / &gap).ceil().to_integer().max(BigInt::one());
Ok(targets
.iter()
.map(|t| crate::base::numeric::best_rational_approx(t, &max_denom))
.collect())
}
}
}
fn sample_satisfies(
arena: &mut Arena,
branch: &Branch,
var: ExprId,
p: ExprId,
rel: Relation,
) -> Result<bool, SymplexError> {
for c in &branch.constraints {
let at = crate::transforms::subs::subs(arena, c.expr, var, p);
let s = value_sign(arena, at);
if s.undecided() {
return Err(failed(format!(
"cannot decide the domain constraint {} at a sample point",
arena.display(c.expr)
)));
}
if !s.is_in(c.kind.allowed()).is_true() {
return Ok(false);
}
}
let at = crate::transforms::subs::subs(arena, branch.resolved, var, p);
let value = value_sign(arena, at);
if value.takes_no_real_value() {
return Ok(false);
}
match value.ordering() {
Some(o) => Ok(rel.accepts(o)),
None => Err(failed(format!(
"cannot decide the sign of {} at a sample point",
arena.display(branch.resolved)
))),
}
}
fn point_in_domain(
arena: &mut Arena,
branch: &Branch,
var: ExprId,
c: &Critical,
) -> Result<bool, SymplexError> {
if c.undefined {
return Ok(false);
}
for k in &branch.constraints {
let at = crate::transforms::subs::subs(arena, k.expr, var, c.id);
let s = value_sign(arena, at);
if s.takes_no_real_value() {
return Ok(false);
}
if s.undecided() {
return Err(failed(format!(
"cannot decide the domain constraint {} at {}",
arena.display(k.expr),
arena.display(c.id)
)));
}
if !s.is_in(k.kind.allowed()).is_true() {
return Ok(false);
}
}
Ok(true)
}
fn point_satisfies(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
c: &Critical,
rel: Relation,
) -> Result<bool, SymplexError> {
if c.undefined {
return Ok(false);
}
let at = crate::transforms::subs::subs(arena, expr, var, c.id);
let value = value_sign(arena, at);
if value.takes_no_real_value() {
return Ok(false);
}
match value.ordering() {
Some(o) => Ok(rel.accepts(o)),
None if c.numer_zero => Ok(rel.accepts(std::cmp::Ordering::Equal)),
None => Err(failed(format!(
"cannot decide the sign of the expression at {}",
arena.display(c.id)
))),
}
}
fn value_sign(arena: &mut Arena, e: ExprId) -> ValueSign {
let real_sign = |q: &Q| ValueSign::Defined(SignSet::from_ordering(q.cmp(&Q::zero())));
let ev = crate::transforms::eval::eval(arena, e);
if let Some(r) = arena.as_num(ev) {
return real_sign(r);
}
match arena.node(ev) {
ExprNode::Infinity
| ExprNode::NegInfinity
| ExprNode::ComplexInfinity
| ExprNode::NaN
| ExprNode::BoolTrue
| ExprNode::BoolFalse => return ValueSign::Undefined,
_ => {}
}
if !walk::free_symbols(arena, ev).is_empty() || walk::has_unevaluated(arena, ev) {
return ValueSign::UNDECIDED;
}
let Ok(text) = crate::transforms::evalf::evalf(arena, ev, DECISION_DIGITS) else {
return ValueSign::UNDECIDED;
};
let Some(parsed) = parse_evalf_complex(&text) else {
return ValueSign::UNDECIDED;
};
if !is_negligible_imaginary(&parsed) {
return ValueSign::Defined(SignSet::NONREAL);
}
let first = parsed.re;
if first.is_zero() || first.abs() >= pow10_inv(AMBIGUOUS_EXP) {
return real_sign(&first);
}
let Ok(text2) = crate::transforms::evalf::evalf(arena, ev, 2 * DECISION_DIGITS) else {
return ValueSign::UNDECIDED;
};
let Some(second) = parse_evalf_complex(&text2).map(|p| p.re) else {
return ValueSign::UNDECIDED;
};
if second.is_zero() || first.is_positive() != second.is_positive() {
return ValueSign::UNDECIDED;
}
let diff = (&first - &second).abs();
let tol = first.abs() / Ratio::from_integer(BigInt::from(1_000_000));
if diff > tol {
return ValueSign::UNDECIDED;
}
real_sign(&first)
}
fn pow10_inv(k: usize) -> Q {
Ratio::new(BigInt::one(), num_traits::pow(BigInt::from(10), k))
}
fn is_negligible_imaginary(p: &ParsedComplex) -> bool {
if p.im.is_zero() {
return true;
}
let scale = p.re.abs().max(Ratio::one());
p.im.abs() <= scale * pow10_inv(NONREAL_EXP)
}
fn approx_real(arena: &mut Arena, e: ExprId) -> Option<Q> {
let ev = crate::transforms::eval::eval(arena, e);
if let Some(r) = arena.as_num(ev) {
return Some(r.clone());
}
if !walk::free_symbols(arena, ev).is_empty() || walk::has_unevaluated(arena, ev) {
return None;
}
let text = crate::transforms::evalf::evalf(arena, ev, DECISION_DIGITS).ok()?;
let parsed = parse_evalf_complex(&text)?;
if !is_negligible_imaginary(&parsed) {
return None;
}
Some(parsed.re)
}
struct ParsedComplex {
re: Q,
im: Q,
}
fn parse_evalf_complex(s: &str) -> Option<ParsedComplex> {
let s = s.trim();
if let Some(re) = decimal_to_ratio(s) {
return Some(ParsedComplex {
re,
im: Ratio::zero(),
});
}
let has_i = s.ends_with('i') || s.ends_with('I');
if !has_i {
return None;
}
let body = s[..s.len() - 1].trim_end_matches('*').trim();
let pure_im = match body {
"" | "+" => Some(Ratio::one()),
"-" => Some(-Ratio::<BigInt>::one()),
t => decimal_to_ratio(t),
};
if let Some(im) = pure_im {
return Some(ParsedComplex {
re: Ratio::zero(),
im,
});
}
let bytes = body.as_bytes();
let mut split = None;
for i in (1..bytes.len()).rev() {
if (bytes[i] == b'+' || bytes[i] == b'-') && !matches!(bytes[i - 1], b'e' | b'E') {
split = Some(i);
break;
}
}
let idx = split?;
let re = decimal_to_ratio(body[..idx].trim())?;
let im_text: String = body[idx..].chars().filter(|c| !c.is_whitespace()).collect();
let im = match im_text.as_str() {
"+" => Ratio::one(),
"-" => -Ratio::<BigInt>::one(),
t => decimal_to_ratio(t)?,
};
Some(ParsedComplex { re, im })
}
fn decimal_to_ratio(text: &str) -> Option<Q> {
let t = text.trim();
let (negative, t) = match t.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, t.strip_prefix('+').unwrap_or(t)),
};
let (mantissa, exp) = match t.find(['e', 'E']) {
Some(pos) => (&t[..pos], t[pos + 1..].parse::<i64>().ok()?),
None => (t, 0i64),
};
let (int_part, frac_part) = match mantissa.find('.') {
Some(pos) => (&mantissa[..pos], &mantissa[pos + 1..]),
None => (mantissa, ""),
};
if int_part.is_empty() && frac_part.is_empty() {
return None;
}
if !int_part.chars().all(|c| c.is_ascii_digit())
|| !frac_part.chars().all(|c| c.is_ascii_digit())
{
return None;
}
let digits = format!("{int_part}{frac_part}");
let digits = if digits.is_empty() { "0" } else { &digits };
let n: BigInt = digits.parse().ok()?;
let scale = exp - frac_part.len() as i64;
let ten = BigInt::from(10);
let mut r = if scale >= 0 {
Ratio::from_integer(n * num_traits::pow(ten, scale.unsigned_abs() as usize))
} else {
Ratio::new(n, num_traits::pow(ten, scale.unsigned_abs() as usize))
};
if negative {
r = -r;
}
Some(r)
}
fn real_taking_symbols_real(arena: &Arena, e: ExprId) -> bool {
use crate::base::assumptions::{AssumptionCache, Assumptions, Props};
let mut cache = AssumptionCache::new();
for s in walk::free_symbols(arena, e) {
if let ExprNode::Symbol(sid) = *arena.node(s)
&& arena.symbol_assumptions(sid) == Assumptions::default()
{
let mut real = Assumptions::default();
real.known_true |= Props::REAL;
cache.set_symbol_assumptions(s, real);
}
}
cache.query(arena, e, Props::REAL) == Some(true)
}
fn try_solve_abs_inequality(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
rel: Relation,
) -> Option<ExprId> {
let terms: Vec<ExprId> = match arena.node(expr).clone() {
ExprNode::Add(children) => children.to_vec(),
_ => vec![expr],
};
let mut abs_inner: Option<ExprId> = None;
let mut abs_coeff: Option<ExprId> = None;
let mut rest: Vec<ExprId> = Vec::new();
for &t in &terms {
if !walk::contains(arena, t, var) {
rest.push(t);
continue;
}
let (inner, coeff) = match arena.node(t).clone() {
ExprNode::Abs(inner) => (inner, arena.one),
ExprNode::Neg(n) => match arena.node(n).clone() {
ExprNode::Abs(inner) => (inner, arena.neg_one),
_ => return None,
},
ExprNode::Mul(children) => {
let mut inner = None;
let mut consts = Vec::new();
for &c in &children {
if !walk::contains(arena, c, var) {
consts.push(c);
} else if let ExprNode::Abs(i) = arena.node(c).clone()
&& inner.is_none()
{
inner = Some(i);
} else {
return None;
}
}
let coeff = match consts.len() {
0 => arena.one,
1 => consts[0],
_ => arena.mul(&consts),
};
(inner?, coeff)
}
_ => return None,
};
if abs_inner.is_some() {
return None; }
abs_inner = Some(inner);
abs_coeff = Some(coeff);
}
let inner = abs_inner?;
let coeff = abs_coeff?;
let coeffs = crate::transforms::solve::symbolic_poly_coeffs(arena, inner, var)?;
if coeffs.len() != 2 {
return None;
}
let a = coeffs[1];
let b = coeffs[0];
if !real_taking_symbols_real(arena, a) || !real_taking_symbols_real(arena, b) {
return None;
}
let d = match rest.len() {
0 => arena.zero,
1 => rest[0],
_ => arena.add(&rest),
};
let c_sign = sign_of(arena, coeff)?;
let neg_d = arena.neg(d);
let k = arena.div(neg_d, coeff);
let k = crate::transforms::eval::eval(arena, k);
let rel = if c_sign < 0 { flip(rel) } else { rel };
if let Some(kv) = arena.as_num(k).cloned()
&& kv.is_negative()
{
return Some(match rel {
Relation::Lt | Relation::Le => arena.empty_set,
Relation::Gt | Relation::Ge => {
arena.interval(arena.neg_infinity, arena.infinity, IntervalKind::Open)
}
});
}
let a_sign = sign_of(arena, a)?;
let k_minus_b = arena.sub(k, b);
let neg_k = arena.neg(k);
let neg_k_minus_b = arena.sub(neg_k, b);
let x_hi = arena.div(k_minus_b, a);
let x_lo = arena.div(neg_k_minus_b, a);
let x_hi = crate::transforms::eval::eval(arena, x_hi);
let x_lo = crate::transforms::eval::eval(arena, x_lo);
let (lo, hi) = if a_sign > 0 {
(x_lo, x_hi)
} else {
(x_hi, x_lo)
};
Some(match rel {
Relation::Lt => arena.interval(lo, hi, IntervalKind::Open),
Relation::Le => arena.interval(lo, hi, IntervalKind::Closed),
Relation::Gt => {
let left = arena.interval(arena.neg_infinity, lo, IntervalKind::Open);
let right = arena.interval(hi, arena.infinity, IntervalKind::Open);
arena.set_union(&[left, right])
}
Relation::Ge => {
let left = arena.interval(arena.neg_infinity, lo, IntervalKind::LeftOpen);
let right = arena.interval(hi, arena.infinity, IntervalKind::RightOpen);
arena.set_union(&[left, right])
}
})
}
fn flip(rel: Relation) -> Relation {
match rel {
Relation::Gt => Relation::Lt,
Relation::Ge => Relation::Le,
Relation::Lt => Relation::Gt,
Relation::Le => Relation::Ge,
}
}
fn sign_of(arena: &mut Arena, e: ExprId) -> Option<i8> {
let ev = crate::transforms::eval::eval(arena, e);
if let Some(r) = arena.as_num(ev) {
return if r.is_positive() {
Some(1)
} else if r.is_negative() {
Some(-1)
} else {
None
};
}
match value_sign(arena, ev) {
ValueSign::Defined(SignSet::POSITIVE) => return Some(1),
ValueSign::Defined(SignSet::NEGATIVE) => return Some(-1),
undecided if undecided.undecided() => {}
_ => return None,
}
if let ExprNode::Symbol(sid) = arena.node(ev) {
let a = arena.symbol_assumptions(*sid);
if a.known_true
.contains(crate::base::assumptions::Props::POSITIVE)
{
return Some(1);
}
if a.known_true
.contains(crate::base::assumptions::Props::NEGATIVE)
{
return Some(-1);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::arena::Arena;
fn display(arena: &Arena, id: ExprId) -> String {
arena.display(id).to_string()
}
#[test]
fn sign_chart_x2_minus_4_gt() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let two = arena.int(2);
let x2 = arena.pow(x, two);
let four = arena.int(4);
let expr = arena.sub(x2, four);
let result = solve_inequality(&mut arena, expr, x, Relation::Gt).unwrap();
assert_eq!(display(&arena, result), "(-oo, -2) ∪ (2, oo)");
}
#[test]
fn positive_constant_gt_zero() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let five = arena.int(5);
let result = solve_inequality(&mut arena, five, x, Relation::Gt).unwrap();
assert_ne!(result, arena.empty_set, "5 > 0 should not be EmptySet");
}
#[test]
fn negative_constant_gt_zero() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let neg3 = arena.int(-3);
let result = solve_inequality(&mut arena, neg3, x, Relation::Gt).unwrap();
assert_eq!(result, arena.empty_set, "-3 > 0 should be EmptySet");
}
#[test]
fn rational_inequality_excludes_pole_and_closes_root() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let one = arena.one;
let num = arena.sub(x, one);
let den = arena.add(&[x, one]);
let expr = arena.div(num, den);
let result = solve_inequality(&mut arena, expr, x, Relation::Ge).unwrap();
assert_eq!(display(&arena, result), "(-oo, -1) ∪ [1, oo)");
}
#[test]
fn sqrt_inequality_respects_domain() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let s = arena.sqrt(x);
let two = arena.int(2);
let expr = arena.sub(s, two);
let result = solve_inequality(&mut arena, expr, x, Relation::Lt).unwrap();
assert_eq!(display(&arena, result), "[0, 4)");
}
#[test]
fn solveset_quadratic_arena() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let two = arena.int(2);
let x2 = arena.pow(x, two);
let five = arena.int(5);
let five_x = arena.mul(&[five, x]);
let neg_five_x = arena.neg(five_x);
let six = arena.int(6);
let expr = arena.add(&[x2, neg_five_x, six]);
let result = solveset(&mut arena, expr, x);
assert_ne!(result, arena.empty_set, "should find roots of x²-5x+6");
}
#[test]
fn parse_evalf_strings() {
let r = |p: i64, q: i64| Ratio::new(BigInt::from(p), BigInt::from(q));
let p = parse_evalf_complex("-1.5e-3").unwrap();
assert_eq!((p.re, p.im), (r(-3, 2000), r(0, 1)));
let p = parse_evalf_complex("1.25 - 2.5*i").unwrap();
assert_eq!((p.re, p.im), (r(5, 4), r(-5, 2)));
let p = parse_evalf_complex("-i").unwrap();
assert_eq!((p.re, p.im), (r(0, 1), r(-1, 1)));
let p = parse_evalf_complex("3*i").unwrap();
assert_eq!((p.re, p.im), (r(0, 1), r(3, 1)));
let p = parse_evalf_complex("2e-5 + i").unwrap();
assert_eq!((p.re, p.im), (r(1, 50000), r(1, 1)));
let p = parse_evalf_complex("1.0e500").unwrap();
assert!(p.re > r(1, 1));
let p = parse_evalf_complex("-2.5e-400").unwrap();
assert!(p.re.is_negative() && p.re > r(-1, 1));
}
#[test]
fn decimal_literals_to_ratio() {
let r = |p: i64, q: i64| Ratio::new(BigInt::from(p), BigInt::from(q));
assert_eq!(decimal_to_ratio("1.25"), Some(r(5, 4)));
assert_eq!(decimal_to_ratio("-0.5"), Some(r(-1, 2)));
assert_eq!(decimal_to_ratio("3"), Some(r(3, 1)));
assert_eq!(decimal_to_ratio("1.5e2"), Some(r(150, 1)));
assert_eq!(decimal_to_ratio("-12.5e-3"), Some(r(-1, 80)));
assert_eq!(decimal_to_ratio("abc"), None);
}
}