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 = 16;
#[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)
}
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;
}
}
is_literal_zero(pool, simplify_trig_normal_form(e, pool).value)
}
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) = probe_symbols(pool, e) else {
return false;
};
if symbols.len() > MAX_PROBE_SYMBOLS {
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>> {
let mut seen = HashSet::new();
let mut out = Vec::new();
collect_symbols(pool, e, &mut seen, &mut out)?;
out.sort_unstable();
Some(out)
}
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 + 11 * index as i64 + 101 * round as i64;
return ArbBall::from_integer(&rug::Integer::from(n), PROBE_PREC);
}
let numer = 733 + 269 * index as i64 + 1123 * round as i64;
let denom = 1021 + 7 * round as i64;
ArbBall::from_rational(&Rational::from((numer, denom)), PROBE_PREC)
}
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);
}
}