use crate::ball::{ArbBall, IntervalEval};
use crate::kernel::{Domain, ExprData, ExprId, ExprPool};
use crate::simplify::engine::{
simplify, simplify_expanded, simplify_log_exp, simplify_trig_normal_form,
};
use rug::{Float, Rational};
use std::cell::RefCell;
use std::collections::HashSet;
use std::fmt;
const PROBE_PREC: u32 = 128;
const PROBE_ROUNDS: usize = 3;
const MAX_PROBE_SYMBOLS: usize = 144;
const MAX_PROBE_NODES: usize = 250_000;
const CANCEL_MAX_SYMBOLS: usize = 12;
const CANCEL_MAX_NODES: usize = 4_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ZeroStatus {
Zero,
NonZero,
Unknown,
}
impl ZeroStatus {
pub(crate) fn is_proven_zero(self) -> bool {
matches!(self, ZeroStatus::Zero)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RefusalSite {
Pivot,
Determinant,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ZeroTestRefusal {
entry: String,
site: RefusalSite,
}
impl ZeroTestRefusal {
pub fn entry(&self) -> &str {
&self.entry
}
}
impl fmt::Display for ZeroTestRefusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.site {
RefusalSite::Pivot => write!(
f,
"cannot decide whether the entry `{}` is zero; refusing rather \
than report a rank, a factorisation or a minimal polynomial \
that silently assumes an answer",
self.entry
),
RefusalSite::Determinant => write!(
f,
"cannot decide whether the determinant `{}` is zero; refusing to \
report an inverse that assumes it is not",
self.entry
),
}
}
}
impl std::error::Error for ZeroTestRefusal {}
impl crate::errors::AlkahestError for ZeroTestRefusal {
fn code(&self) -> &'static str {
match self.site {
RefusalSite::Pivot => "E-LINALG-010",
RefusalSite::Determinant => "E-MAT-004",
}
}
fn remediation(&self) -> Option<&'static str> {
match self.site {
RefusalSite::Pivot => Some(
"rewrite the entry into a form whose vanishing is decidable, or \
substitute concrete values for the parameters",
),
RefusalSite::Determinant => Some(
"rewrite the entries into a form whose determinant's vanishing is \
decidable, or substitute concrete values",
),
}
}
}
thread_local! {
static LAST_REFUSAL: RefCell<Option<ZeroTestRefusal>> = const { RefCell::new(None) };
}
pub(crate) fn record_refusal(pool: &ExprPool, e: ExprId, site: RefusalSite) {
let refusal = ZeroTestRefusal {
entry: pool.display(e).to_string(),
site,
};
LAST_REFUSAL.with(|c| *c.borrow_mut() = Some(refusal));
}
pub(crate) fn forget_refusal() {
LAST_REFUSAL.with(|c| *c.borrow_mut() = None);
}
pub fn take_zero_test_refusal() -> Option<ZeroTestRefusal> {
LAST_REFUSAL.with(|c| c.borrow_mut().take())
}
const MAX_STRUCTURAL_DEPTH: u32 = 8;
pub(crate) fn zero_status(pool: &ExprPool, e: ExprId) -> ZeroStatus {
status_of(pool, simplify(e, pool).value, 0)
}
pub(crate) fn settled_nonzero(e: ExprId, pool: &ExprPool) -> bool {
if matches!(crate::kernel::eval_const::try_expr_f64(e, pool), Some(v) if v != 0.0) {
return true;
}
if has_free_symbol(e, pool) {
return false;
}
matches!(zero_status(pool, e), ZeroStatus::NonZero)
}
fn has_free_symbol(expr: ExprId, pool: &ExprPool) -> bool {
pool.with(expr, |d| match d {
ExprData::Symbol { .. } => true,
ExprData::Add(args) | ExprData::Mul(args) | ExprData::Func { args, .. } => {
args.iter().any(|&a| has_free_symbol(a, pool))
}
ExprData::Pow { base, exp } => has_free_symbol(*base, pool) || has_free_symbol(*exp, pool),
_ => false,
})
}
fn status_of(pool: &ExprPool, e: ExprId, depth: u32) -> ZeroStatus {
if let Some(status) = literal_status(pool, e) {
return status;
}
if depth < MAX_STRUCTURAL_DEPTH {
if let Some(status) = structural_status(pool, e, depth) {
return status;
}
}
if probe_nonzero(pool, e) {
return ZeroStatus::NonZero;
}
if normalises_to_zero(pool, e) {
return ZeroStatus::Zero;
}
ZeroStatus::Unknown
}
fn structural_status(pool: &ExprPool, e: ExprId, depth: u32) -> Option<ZeroStatus> {
let next = depth + 1;
match pool.get(e) {
ExprData::Mul(args) => {
let statuses: Vec<ZeroStatus> = args
.iter()
.map(|&a| status_of(pool, a, next))
.collect::<Vec<_>>();
if statuses.contains(&ZeroStatus::Zero) {
Some(ZeroStatus::Zero)
} else if statuses.iter().all(|s| *s == ZeroStatus::NonZero) {
Some(ZeroStatus::NonZero)
} else {
None
}
}
ExprData::Pow { base, exp } => match integer_exponent(pool, exp) {
Some(n) if n > 0 => decisive(status_of(pool, base, next)),
Some(n) if n < 0 && status_of(pool, base, next) == ZeroStatus::NonZero => {
Some(ZeroStatus::NonZero)
}
_ => None,
},
ExprData::Func { ref name, ref args } if args.len() == 1 => match name.as_str() {
"sqrt" => decisive(status_of(pool, args[0], next)),
"exp" if !has_opaque_constant(pool, args[0], 0) => Some(ZeroStatus::NonZero),
_ => None,
},
_ => None,
}
}
fn decisive(status: ZeroStatus) -> Option<ZeroStatus> {
(status != ZeroStatus::Unknown).then_some(status)
}
fn integer_exponent(pool: &ExprPool, e: ExprId) -> Option<i64> {
pool.with(e, |data| match data {
ExprData::Integer(n) => n.0.to_i64(),
_ => None,
})
}
fn has_opaque_constant(pool: &ExprPool, e: ExprId, depth: u32) -> bool {
if depth >= MAX_STRUCTURAL_DEPTH {
return true;
}
let next = depth + 1;
match pool.get(e) {
ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_) => false,
ExprData::Symbol { ref name, .. } => OPAQUE_CONSTANTS.contains(&name.as_str()),
ExprData::Add(args) | ExprData::Mul(args) => {
args.iter().any(|&a| has_opaque_constant(pool, a, next))
}
ExprData::Pow { base, exp } => {
has_opaque_constant(pool, base, next) || has_opaque_constant(pool, exp, next)
}
ExprData::Func { args, .. } => args.iter().any(|&a| has_opaque_constant(pool, a, next)),
_ => true,
}
}
fn literal_status(pool: &ExprPool, e: ExprId) -> Option<ZeroStatus> {
pool.with(e, |data| match data {
ExprData::Integer(n) => Some(if n.0 == 0 {
ZeroStatus::Zero
} else {
ZeroStatus::NonZero
}),
ExprData::Rational(r) => Some(if r.0 == 0 {
ZeroStatus::Zero
} else {
ZeroStatus::NonZero
}),
ExprData::Float(f) => Some(if f.inner.is_zero() {
ZeroStatus::Zero
} else {
ZeroStatus::NonZero
}),
_ => None,
})
}
fn normalises_to_zero(pool: &ExprPool, e: ExprId) -> bool {
let expanded = simplify_expanded(e, pool).value;
if is_literal_zero(pool, expanded) {
return true;
}
for start in [e, expanded] {
if is_literal_zero(pool, simplify_log_exp(start, pool, &[]).value) {
return true;
}
}
if is_literal_zero(pool, simplify_trig_normal_form(e, pool).value) {
return true;
}
let deradicalised = fold_rational_radicals(pool, e);
if deradicalised != e {
if is_literal_zero(pool, simplify_expanded(deradicalised, pool).value) {
return true;
}
if cancels_to_zero(pool, deradicalised) {
return true;
}
}
cancels_to_zero(pool, e)
}
pub(crate) fn fold_rational_radicals(pool: &ExprPool, e: ExprId) -> ExprId {
fold_rational_radicals_at(pool, e, 0)
}
fn fold_rational_radicals_at(pool: &ExprPool, e: ExprId, depth: u32) -> ExprId {
if depth > MAX_STRUCTURAL_DEPTH {
return e;
}
let d = depth + 1;
match pool.get(e) {
ExprData::Add(args) => {
let next: Vec<ExprId> = args
.iter()
.map(|&a| fold_rational_radicals_at(pool, a, d))
.collect();
pool.add(next)
}
ExprData::Mul(args) => {
let next: Vec<ExprId> = args
.iter()
.map(|&a| fold_rational_radicals_at(pool, a, d))
.collect();
pool.mul(next)
}
ExprData::Func { name, args } => {
let next: Vec<ExprId> = args
.iter()
.map(|&a| fold_rational_radicals_at(pool, a, d))
.collect();
pool.func(name.as_str(), next)
}
ExprData::Pow { base, exp } => {
let (base, exp) = (base, exp);
let folded_base = fold_rational_radicals_at(pool, base, d);
let folded_exp = fold_rational_radicals_at(pool, exp, d);
if let (Some(radicand), Some(k)) = (
sqrt_of_nonneg_rational(pool, folded_base),
integer_of(pool, folded_exp),
) {
let half = Rational::from((k, rug::Integer::from(2)));
let new_exp = if *half.denom() == 1 {
pool.integer(half.numer().clone())
} else {
pool.rational(half.numer().clone(), half.denom().clone())
};
let rad = pool.rational(radicand.numer().clone(), radicand.denom().clone());
return pool.pow(rad, new_exp);
}
pool.pow(folded_base, folded_exp)
}
_ => e,
}
}
fn sqrt_of_nonneg_rational(pool: &ExprPool, e: ExprId) -> Option<Rational> {
let inner = pool.with(e, |d| match d {
ExprData::Func { name, args } if name.as_str() == "sqrt" && args.len() == 1 => {
Some(args[0])
}
ExprData::Pow { base, exp } => {
let half = pool.with(*exp, |x| match x {
ExprData::Rational(r) => *r.0.numer() == 1 && *r.0.denom() == 2,
_ => false,
});
if half {
Some(*base)
} else {
None
}
}
_ => None,
})?;
let r = rational_of(pool, inner)?;
if r < 0 {
return None;
}
Some(r)
}
fn rational_of(pool: &ExprPool, e: ExprId) -> Option<Rational> {
pool.with(e, |d| match d {
ExprData::Integer(n) => Some(Rational::from((n.0.clone(), rug::Integer::from(1)))),
ExprData::Rational(r) => Some(r.0.clone()),
_ => None,
})
}
fn integer_of(pool: &ExprPool, e: ExprId) -> Option<rug::Integer> {
pool.with(e, |d| match d {
ExprData::Integer(n) => Some(n.0.clone()),
_ => None,
})
}
fn cancels_to_zero(pool: &ExprPool, e: ExprId) -> bool {
let Some((symbols, nodes)) = probe_symbols(pool, e) else {
return false;
};
if symbols.len() > CANCEL_MAX_SYMBOLS || nodes > CANCEL_MAX_NODES {
return false;
}
match crate::poly::cancel::cancel(e, Vec::new(), pool) {
Ok(c) => is_literal_zero(pool, simplify(c, pool).value),
Err(_) => false,
}
}
fn is_literal_zero(pool: &ExprPool, e: ExprId) -> bool {
literal_status(pool, e) == Some(ZeroStatus::Zero)
}
fn probe_nonzero(pool: &ExprPool, e: ExprId) -> bool {
let Some((symbols, nodes)) = probe_symbols(pool, e) else {
return false;
};
if symbols.len() > MAX_PROBE_SYMBOLS || nodes > MAX_PROBE_NODES {
return false;
}
for round in 0..PROBE_ROUNDS {
let mut eval = IntervalEval::new(PROBE_PREC);
for (index, &sym) in symbols.iter().enumerate() {
eval.bind(sym, sample_ball(pool, sym, index, round));
}
if let Some(ball) = eval.eval(e, pool) {
if ball_excludes_zero(&ball) {
return true;
}
}
}
false
}
fn ball_excludes_zero(ball: &ArbBall) -> bool {
if !ball.rad.is_finite() || !ball.mid.is_finite() {
return false;
}
let lo = ball.lo();
let hi = ball.hi();
if lo.is_nan() || hi.is_nan() {
return false;
}
lo > 0.0 || hi < 0.0
}
fn probe_symbols(pool: &ExprPool, e: ExprId) -> Option<(Vec<ExprId>, usize)> {
let mut seen = HashSet::new();
let mut out = Vec::new();
collect_symbols(pool, e, &mut seen, &mut out)?;
out.sort_unstable();
Some((out, seen.len()))
}
const OPAQUE_CONSTANTS: &[&str] = &["oo", "inf", "infinity", "zoo", "nan", "NaN"];
fn collect_symbols(
pool: &ExprPool,
e: ExprId,
seen: &mut HashSet<ExprId>,
out: &mut Vec<ExprId>,
) -> Option<()> {
if !seen.insert(e) {
return Some(());
}
match pool.get(e) {
ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_) => Some(()),
ExprData::Symbol { ref name, .. } => {
if pool.is_imaginary_unit(e) || OPAQUE_CONSTANTS.contains(&name.as_str()) {
return None;
}
out.push(e);
Some(())
}
ExprData::Add(args) | ExprData::Mul(args) => {
for a in args {
collect_symbols(pool, a, seen, out)?;
}
Some(())
}
ExprData::Pow { base, exp } => {
collect_symbols(pool, base, seen, out)?;
collect_symbols(pool, exp, seen, out)
}
ExprData::Func { args, .. } => {
for a in args {
collect_symbols(pool, a, seen, out)?;
}
Some(())
}
_ => None,
}
}
fn sample_ball(pool: &ExprPool, sym: ExprId, index: usize, round: usize) -> ArbBall {
if pool.with(
sym,
|data| matches!(data, ExprData::Symbol { name, .. } if name.as_str() == "pi"),
) {
return pi_ball();
}
let integral = pool.with(sym, |data| {
matches!(
data,
ExprData::Symbol {
domain: Domain::Integer,
..
}
)
});
if integral {
let n = 7 + scrambled(index, round) % 977;
return ArbBall::from_integer(&rug::Integer::from(n), PROBE_PREC);
}
let numer = 733 + scrambled(index, round);
let denom = 1021 + 7 * round as i64;
ArbBall::from_rational(&Rational::from((numer, denom)), PROBE_PREC)
}
fn scrambled(index: usize, round: usize) -> i64 {
let mut x = (index as u64)
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
.wrapping_add((round as u64).wrapping_mul(0xBF58_476D_1CE4_E5B9))
.wrapping_add(0x2545_F491_4F6C_DD1D);
x ^= x >> 30;
x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9);
x ^= x >> 27;
x = x.wrapping_mul(0x94D0_49BB_1331_11EB);
x ^= x >> 31;
(x % 7919) as i64
}
fn pi_ball() -> ArbBall {
let mid = Float::with_val(PROBE_PREC, rug::float::Constant::Pi);
let accurate = Float::with_val(PROBE_PREC * 2, rug::float::Constant::Pi);
let rad = Float::with_val(PROBE_PREC, &accurate - &mid).abs();
ArbBall {
mid,
rad,
prec: PROBE_PREC,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::{Domain, ExprPool};
fn p() -> ExprPool {
ExprPool::new()
}
#[test]
fn literal_zero_is_zero() {
let pool = p();
assert_eq!(
zero_status(&pool, pool.integer(0_i32)),
ZeroStatus::Zero,
"the literal 0"
);
assert_eq!(zero_status(&pool, pool.integer(3_i32)), ZeroStatus::NonZero);
}
#[test]
fn symbol_is_generically_nonzero() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
assert_eq!(zero_status(&pool, x), ZeroStatus::NonZero);
}
#[test]
fn exp_square_minus_exp_double_is_zero() {
let pool = p();
let a = pool.symbol("a", Domain::Real);
let ea = pool.func("exp", vec![a]);
let lhs = pool.mul(vec![ea, ea]);
let rhs = pool.func("exp", vec![pool.add(vec![a, a])]);
let diff = pool.add(vec![lhs, pool.mul(vec![pool.integer(-1_i32), rhs])]);
assert_eq!(zero_status(&pool, diff), ZeroStatus::Zero);
}
#[test]
fn non_identity_difference_of_exps_is_nonzero() {
let pool = p();
let a = pool.symbol("a", Domain::Real);
let ea = pool.func("exp", vec![a]);
let lhs = pool.mul(vec![ea, ea]);
let diff = pool.add(vec![lhs, pool.mul(vec![pool.integer(-1_i32), ea])]);
assert_eq!(zero_status(&pool, diff), ZeroStatus::NonZero);
}
#[test]
fn sin_of_pi_is_not_certified_nonzero() {
let pool = p();
let pi = pool.symbol("pi", Domain::Real);
let e = pool.func("sin", vec![pi]);
assert_ne!(zero_status(&pool, e), ZeroStatus::NonZero);
}
#[test]
fn imaginary_unit_is_not_probed() {
let pool = p();
let i = pool.imaginary_unit();
let e = pool.add(vec![pool.mul(vec![i, i]), pool.integer(1_i32)]);
assert_ne!(zero_status(&pool, e), ZeroStatus::NonZero);
}
#[test]
fn unknown_function_yields_unknown() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let f = pool.func("mystery", vec![x]);
let g = pool.func("mystery", vec![pool.mul(vec![pool.integer(1_i32), x])]);
let diff = pool.add(vec![f, pool.mul(vec![pool.integer(-1_i32), g])]);
assert_ne!(zero_status(&pool, diff), ZeroStatus::NonZero);
}
#[test]
fn dense_symbolic_determinant_is_certified_non_vanishing() {
for n in 1..=6usize {
let pool = p();
let grid: Vec<Vec<crate::kernel::ExprId>> = (0..n)
.map(|i| {
(0..n)
.map(|j| pool.symbol(format!("s_{i}_{j}"), Domain::Complex))
.collect()
})
.collect();
let m = crate::matrix::Matrix::new(grid).unwrap();
let det =
crate::simplify::engine::simplify_expanded(m.det(&pool).unwrap(), &pool).value;
assert_eq!(
zero_status(&pool, det),
ZeroStatus::NonZero,
"det of a dense symbolic {n}×{n} is a sum of {n}! distinct monomials \
and cannot be the zero function"
);
}
}
#[test]
fn probe_samples_are_not_collinear_in_the_index() {
for round in 0..PROBE_ROUNDS {
let vals: Vec<i64> = (0..40).map(|i| scrambled(i, round)).collect();
let mut collinear = 0usize;
for w in vals.windows(3) {
if w[2] - 2 * w[1] + w[0] == 0 {
collinear += 1;
}
}
assert!(
collinear < 3,
"round {round}: {collinear} of 38 consecutive triples are collinear; \
an arithmetic progression would give 38"
);
let mut sorted = vals.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), vals.len(), "round {round}: duplicate samples");
}
}
#[test]
fn a_vanishing_rational_function_is_proven_zero() {
let pool = p();
let a = pool.symbol("a", Domain::Real);
let b = pool.symbol("b", Domain::Real);
let sum = pool.add(vec![a, b]);
let inv = pool.pow(sum, pool.integer(-1_i32));
let e = pool.add(vec![
pool.mul(vec![a, inv]),
pool.mul(vec![b, inv]),
pool.integer(-1_i32),
]);
assert_eq!(zero_status(&pool, e), ZeroStatus::Zero);
}
#[test]
fn a_squared_rational_radical_is_proven_zero() {
let pool = p();
for c in [2_i32, 3, 10, 12] {
let lit = pool.integer(c);
for root in [
pool.func("sqrt", vec![lit]),
pool.pow(lit, pool.rational(1, 2)),
] {
let e = pool.add(vec![
pool.mul(vec![root, root]),
pool.mul(vec![pool.integer(-1_i32), lit]),
]);
assert_eq!(
zero_status(&pool, e),
ZeroStatus::Zero,
"√{c}·√{c} − {c} = 0"
);
}
}
}
#[test]
fn the_radical_fold_does_not_touch_a_symbolic_base() {
let pool = p();
let x = pool.symbol("x", Domain::Complex);
let root = pool.func("sqrt", vec![x]);
let e = pool.add(vec![
pool.mul(vec![root, root]),
pool.mul(vec![pool.integer(-1_i32), x]),
]);
let folded = fold_rational_radicals(&pool, e);
assert_eq!(folded, e, "a symbolic radicand must not be folded");
}
#[test]
fn the_radical_fold_does_not_touch_a_negative_radicand() {
let pool = p();
let neg = pool.integer(-4_i32);
let root = pool.func("sqrt", vec![neg]);
let e = pool.pow(root, pool.integer(2_i32));
assert_eq!(fold_rational_radicals(&pool, e), e);
}
}