use crate::calculus::limits::{limit, LimitDirection};
use crate::kernel::{subs, Domain, ExprData, ExprId, ExprPool};
use crate::poly::RationalFunction;
use crate::simplify::simplify;
use std::collections::HashMap;
use std::fmt;
#[derive(Clone, Debug)]
pub struct PathWitness {
pub description: String,
pub value: ExprId,
pub value_numeric: f64,
}
#[derive(Clone, Debug)]
pub enum MultiLimit {
Value(ExprId),
DoesNotExist {
path_a: PathWitness,
path_b: PathWitness,
},
Undecided,
}
impl fmt::Display for MultiLimit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MultiLimit::Value(_) => write!(f, "Value"),
MultiLimit::DoesNotExist { path_a, path_b } => write!(
f,
"DoesNotExist (along {} → {}; along {} → {})",
path_a.description, path_a.value_numeric, path_b.description, path_b.value_numeric
),
MultiLimit::Undecided => write!(f, "Undecided"),
}
}
}
const PROBE_SEPARATION: f64 = 1e-4;
const NUMERIC_TOL: f64 = 1e-6;
pub fn multilimit(
f: ExprId,
x: ExprId,
y: ExprId,
a: ExprId,
b: ExprId,
pool: &ExprPool,
) -> MultiLimit {
if x == y {
return MultiLimit::Undecided;
}
let u = pool.symbol("__ml_u", Domain::Real);
let v = pool.symbol("__ml_v", Domain::Real);
let xa = simplify(pool.add(vec![a, u]), pool).value;
let yb = simplify(pool.add(vec![b, v]), pool).value;
let mut t = HashMap::new();
t.insert(x, xa);
t.insert(y, yb);
let g = simplify(subs(f, &t, pool), pool).value;
if let Some(val) = continuity_value(g, u, v, pool) {
return MultiLimit::Value(val);
}
if let Some(dne) = probe_nonexistence(g, u, v, pool) {
return dne;
}
if let Some(l) = probes_agree_on(g, u, v, pool) {
if rational_polar_bound_to_zero(g, u, v, l, pool) {
return MultiLimit::Value(l);
}
}
MultiLimit::Undecided
}
fn continuity_value(g: ExprId, u: ExprId, v: ExprId, pool: &ExprPool) -> Option<ExprId> {
let (_num, den) = numer_denom(g, pool);
if let Some(dv) = eval2(den, u, v, 0.0, 0.0, pool) {
if dv.abs() < 1e-12 {
return None;
}
} else {
return None;
}
let mut m = HashMap::new();
m.insert(u, pool.integer(0_i32));
m.insert(v, pool.integer(0_i32));
let at0 = simplify(subs(g, &m, pool), pool).value;
if contains_var(at0, u, pool) || contains_var(at0, v, pool) {
return None;
}
let lv = eval2(at0, u, v, 0.0, 0.0, pool)?;
if !lv.is_finite() {
return None;
}
let dirs = [
(0.6, 0.8),
(-0.8, 0.6),
(0.5, -0.866),
(-0.707, -0.707),
(1.0, 0.1),
];
let radii = [0.1_f64, 0.01, 0.001];
let mut prev_dev = f64::INFINITY;
for &r in &radii {
let mut dev = 0.0_f64;
for (cu, cv) in dirs {
let gv = eval2(g, u, v, r * cu, r * cv, pool)?;
dev = dev.max((gv - lv).abs());
}
if dev > prev_dev + 1e-12 {
return None;
}
prev_dev = dev;
}
if prev_dev > NUMERIC_TOL * (1.0 + lv.abs()) {
return None;
}
Some(at0)
}
type PathFn = fn(f64) -> (f64, f64);
struct Probe {
label: String,
expr_of_t: ExprId,
numeric: PathFn,
}
fn probe_nonexistence(g: ExprId, u: ExprId, v: ExprId, pool: &ExprPool) -> Option<MultiLimit> {
let probes = build_probes(g, u, v, pool);
let mut witnesses: Vec<PathWitness> = Vec::new();
for p in &probes {
if let Some(w) = verify_probe(p, u, v, pool) {
for prev in &witnesses {
if (prev.value_numeric - w.value_numeric).abs() > PROBE_SEPARATION {
return Some(MultiLimit::DoesNotExist {
path_a: prev.clone(),
path_b: w.clone(),
});
}
}
witnesses.push(w);
}
}
None
}
fn build_probes(g: ExprId, u: ExprId, v: ExprId, pool: &ExprPool) -> Vec<Probe> {
let mut probes = Vec::new();
let sub_v = |rhs: ExprId| -> ExprId {
let mut m = HashMap::new();
m.insert(v, rhs);
simplify(subs(g, &m, pool), pool).value
};
let sub_u = |rhs: ExprId| -> ExprId {
let mut m = HashMap::new();
m.insert(u, rhs);
simplify(subs(g, &m, pool), pool).value
};
probes.push(Probe {
label: "v = 0 (the u-axis)".to_string(),
expr_of_t: sub_v(pool.integer(0_i32)),
numeric: |t| (t, 0.0),
});
probes.push(Probe {
label: "u = 0 (the v-axis)".to_string(),
expr_of_t: sub_u(pool.integer(0_i32)),
numeric: |t| (0.0, t),
});
let slopes: [(i64, &str, PathFn); 4] = [
(1, "v = u", |t| (t, t)),
(-1, "v = -u", |t| (t, -t)),
(2, "v = 2·u", |t| (t, 2.0 * t)),
(3, "v = 3·u", |t| (t, 3.0 * t)),
];
for (m_int, lbl, num) in slopes {
let rhs = simplify(pool.mul(vec![pool.integer(m_int), u]), pool).value;
probes.push(Probe {
label: lbl.to_string(),
expr_of_t: sub_v(rhs),
numeric: num,
});
}
let pow_v: [(i32, &str, PathFn); 2] = [
(2, "v = u²", |t| (t, t * t)),
(3, "v = u³", |t| (t, t * t * t)),
];
for (k, lbl, num) in pow_v {
let rhs = simplify(pool.pow(u, pool.integer(k)), pool).value;
probes.push(Probe {
label: lbl.to_string(),
expr_of_t: sub_v(rhs),
numeric: num,
});
}
let pow_u: [(i32, &str, PathFn); 2] = [
(2, "u = v²", |t| (t * t, t)),
(3, "u = v³", |t| (t * t * t, t)),
];
for (k, lbl, num) in pow_u {
let rhs = simplify(pool.pow(v, pool.integer(k)), pool).value;
probes.push(Probe {
label: lbl.to_string(),
expr_of_t: sub_u(rhs),
numeric: num,
});
}
probes
}
fn verify_probe(p: &Probe, u: ExprId, v: ExprId, pool: &ExprPool) -> Option<PathWitness> {
let uses_u = contains_var(p.expr_of_t, u, pool);
let uses_v = contains_var(p.expr_of_t, v, pool);
let var = if uses_u && !uses_v {
u
} else if uses_v && !uses_u {
v
} else if !uses_u && !uses_v {
let lv = eval2(p.expr_of_t, u, v, 0.0, 0.0, pool)?;
if !lv.is_finite() {
return None;
}
return Some(PathWitness {
description: p.label.clone(),
value: p.expr_of_t,
value_numeric: lv,
});
} else {
return None;
};
let lim_sym = limit(
p.expr_of_t,
var,
pool.integer(0_i32),
LimitDirection::Plus,
pool,
)
.ok()?;
let lim_num = eval2(lim_sym, u, v, 0.0, 0.0, pool)?;
if !lim_num.is_finite() {
return None;
}
let mut sampled = None;
for &t in &[1e-2, 1e-3, 1e-4] {
let (du, dv) = (p.numeric)(t);
if let Some(val) = eval2(p.expr_of_t, u, v, du, dv, pool) {
sampled = Some(val);
}
}
let sampled = sampled?;
if (sampled - lim_num).abs() > 1e-2 * (1.0 + lim_num.abs()) {
return None;
}
Some(PathWitness {
description: p.label.clone(),
value: lim_sym,
value_numeric: lim_num,
})
}
fn probes_agree_on(g: ExprId, u: ExprId, v: ExprId, pool: &ExprPool) -> Option<ExprId> {
let probes = build_probes(g, u, v, pool);
let mut value_sym: Option<ExprId> = None;
let mut value_num: Option<f64> = None;
let mut count = 0;
for p in &probes {
if let Some(w) = verify_probe(p, u, v, pool) {
match value_num {
None => {
value_num = Some(w.value_numeric);
value_sym = Some(w.value);
}
Some(prev) => {
if (prev - w.value_numeric).abs() > PROBE_SEPARATION {
return None;
}
}
}
count += 1;
}
}
if count >= 3 {
value_sym
} else {
None
}
}
fn rational_polar_bound_to_zero(
g: ExprId,
u: ExprId,
v: ExprId,
l: ExprId,
pool: &ExprPool,
) -> bool {
if !is_rational_in(g, u, v, pool) {
return false;
}
let neg_l = simplify(pool.mul(vec![pool.integer(-1_i32), l]), pool).value;
let h = simplify(pool.add(vec![g, neg_l]), pool).value;
let (_hn, hd) = numer_denom(h, pool);
let n_theta = 720;
let thetas: Vec<f64> = (0..n_theta)
.map(|i| 2.0 * std::f64::consts::PI * (i as f64) / (n_theta as f64))
.collect();
let radii = [
0.5_f64,
0.125,
0.03125,
7.8125e-3,
1.953_125e-3,
4.882_812_5e-4,
1.220_703_125e-4,
];
let mut prev_env: Option<f64> = None;
let mut first_env: Option<f64> = None;
let mut max_overall = 0.0_f64;
for &r in &radii {
let mut env = 0.0_f64;
for &th in &thetas {
let du = r * th.cos();
let dv = r * th.sin();
match eval2(hd, u, v, du, dv, pool) {
Some(d) if d.abs() > 1e-9 => {}
_ => return false, }
let hv = match eval2(h, u, v, du, dv, pool) {
Some(x) => x.abs(),
_ => return false,
};
if hv > env {
env = hv;
}
}
max_overall = max_overall.max(env);
if first_env.is_none() {
first_env = Some(env);
}
if let Some(pe) = prev_env {
if env > pe * 1.05 + 1e-12 {
return false;
}
}
prev_env = Some(env);
}
let final_env = prev_env.unwrap_or(f64::INFINITY);
let first = first_env.unwrap_or(f64::INFINITY);
final_env < 1e-2 && final_env < first * 0.05 + 1e-12 && final_env < max_overall * 0.5 + 1e-12
}
fn numer_denom(expr: ExprId, pool: &ExprPool) -> (ExprId, ExprId) {
let factors = match pool.get(expr) {
ExprData::Mul(xs) => xs,
_ => vec![expr],
};
let mut nums = Vec::new();
let mut dens = Vec::new();
for f in factors {
match pool.get(f) {
ExprData::Pow { base, exp } => {
if let ExprData::Integer(n) = pool.get(exp) {
if n.0 < 0 {
let neg_exp =
simplify(pool.mul(vec![pool.integer(-1_i32), exp]), pool).value;
dens.push(pool.pow(base, neg_exp));
continue;
}
}
nums.push(f);
}
_ => nums.push(f),
}
}
let num = if nums.is_empty() {
pool.integer(1_i32)
} else {
simplify(pool.mul(nums), pool).value
};
let den = if dens.is_empty() {
pool.integer(1_i32)
} else {
simplify(pool.mul(dens), pool).value
};
(num, den)
}
fn is_rational_in(expr: ExprId, u: ExprId, v: ExprId, pool: &ExprPool) -> bool {
let (num, den) = numer_denom(expr, pool);
RationalFunction::from_symbolic(num, den, vec![u, v], pool).is_ok()
}
fn contains_var(expr: ExprId, var: ExprId, pool: &ExprPool) -> bool {
if expr == var {
return true;
}
match pool.get(expr) {
ExprData::Add(xs) | ExprData::Mul(xs) => xs.iter().any(|x| contains_var(*x, var, pool)),
ExprData::Pow { base, exp } => {
contains_var(base, var, pool) || contains_var(exp, var, pool)
}
ExprData::Func { args, .. } => args.iter().any(|a| contains_var(*a, var, pool)),
_ => false,
}
}
fn eval2(expr: ExprId, u: ExprId, v: ExprId, du: f64, dv: f64, pool: &ExprPool) -> Option<f64> {
let r = eval2_inner(expr, u, v, du, dv, pool)?;
if r.is_finite() {
Some(r)
} else {
None
}
}
fn eval2_inner(
expr: ExprId,
u: ExprId,
v: ExprId,
du: f64,
dv: f64,
pool: &ExprPool,
) -> Option<f64> {
if expr == u {
return Some(du);
}
if expr == v {
return Some(dv);
}
match pool.get(expr) {
ExprData::Integer(n) => Some(n.0.to_f64()),
ExprData::Rational(r) => {
let (num, den) = r.0.clone().into_numer_denom();
Some(num.to_f64() / den.to_f64())
}
ExprData::Float(f) => Some(f.inner.to_f64()),
ExprData::Symbol { name, .. } => match name.as_str() {
"pi" => Some(std::f64::consts::PI),
"e" => Some(std::f64::consts::E),
_ => None,
},
ExprData::Add(xs) => {
let mut s = 0.0;
for x in xs {
s += eval2_inner(x, u, v, du, dv, pool)?;
}
Some(s)
}
ExprData::Mul(xs) => {
let mut p = 1.0;
for x in xs {
p *= eval2_inner(x, u, v, du, dv, pool)?;
}
Some(p)
}
ExprData::Pow { base, exp } => {
let b = eval2_inner(base, u, v, du, dv, pool)?;
let e = eval2_inner(exp, u, v, du, dv, pool)?;
Some(b.powf(e))
}
ExprData::Func { name, args } if args.len() == 1 => {
let a = eval2_inner(args[0], u, v, du, dv, pool)?;
Some(match name.as_str() {
"sin" => a.sin(),
"cos" => a.cos(),
"tan" => a.tan(),
"exp" => a.exp(),
"log" => a.ln(),
"sqrt" => a.sqrt(),
"sinh" => a.sinh(),
"cosh" => a.cosh(),
"tanh" => a.tanh(),
"asin" => a.asin(),
"acos" => a.acos(),
"atan" => a.atan(),
"abs" => a.abs(),
_ => return None,
})
}
ExprData::Func { name, args } if args.len() == 2 && name == "atan2" => {
let y = eval2_inner(args[0], u, v, du, dv, pool)?;
let x = eval2_inner(args[1], u, v, du, dv, pool)?;
Some(y.atan2(x))
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Domain;
fn setup() -> (ExprPool, ExprId, ExprId) {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let y = p.symbol("y", Domain::Real);
(p, x, y)
}
#[test]
fn continuity_sum_of_squares() {
let (p, x, y) = setup();
let f = simplify(
p.add(vec![p.pow(x, p.integer(2)), p.pow(y, p.integer(2))]),
&p,
)
.value;
let r = multilimit(f, x, y, p.integer(1), p.integer(2), &p);
match r {
MultiLimit::Value(val) => {
assert_eq!(val, p.integer(5), "got {}", p.display(val));
}
other => panic!("expected Value(5), got {other}"),
}
}
#[test]
fn dne_xy_over_x2_plus_y2() {
let (p, x, y) = setup();
let num = p.mul(vec![x, y]);
let den = p.add(vec![p.pow(x, p.integer(2)), p.pow(y, p.integer(2))]);
let f = simplify(p.mul(vec![num, p.pow(den, p.integer(-1))]), &p).value;
let r = multilimit(f, x, y, p.integer(0), p.integer(0), &p);
match r {
MultiLimit::DoesNotExist { path_a, path_b } => {
assert!(
(path_a.value_numeric - path_b.value_numeric).abs() > PROBE_SEPARATION,
"witness values not separated: {} vs {}",
path_a.value_numeric,
path_b.value_numeric
);
}
other => panic!("expected DNE, got {other}"),
}
}
#[test]
fn dne_x2y_over_x4_plus_y2() {
let (p, x, y) = setup();
let num = p.mul(vec![p.pow(x, p.integer(2)), y]);
let den = p.add(vec![p.pow(x, p.integer(4)), p.pow(y, p.integer(2))]);
let f = simplify(p.mul(vec![num, p.pow(den, p.integer(-1))]), &p).value;
let r = multilimit(f, x, y, p.integer(0), p.integer(0), &p);
match r {
MultiLimit::DoesNotExist { path_a, path_b } => {
assert!(
(path_a.value_numeric - path_b.value_numeric).abs() > PROBE_SEPARATION,
"witnesses: {} vs {}",
path_a.value_numeric,
path_b.value_numeric
);
}
other => panic!("expected DNE via parabola, got {other}"),
}
}
#[test]
fn value_x2y2_over_x2_plus_y2() {
let (p, x, y) = setup();
let num = p.mul(vec![p.pow(x, p.integer(2)), p.pow(y, p.integer(2))]);
let den = p.add(vec![p.pow(x, p.integer(2)), p.pow(y, p.integer(2))]);
let f = simplify(p.mul(vec![num, p.pow(den, p.integer(-1))]), &p).value;
let r = multilimit(f, x, y, p.integer(0), p.integer(0), &p);
match r {
MultiLimit::Value(val) => {
let n = eval2(val, x, y, 0.0, 0.0, &p).unwrap();
assert!(n.abs() < 1e-9, "expected 0, got {n}");
}
other => panic!("expected Value(0), got {other}"),
}
}
#[test]
fn value_x3_plus_y3_over_x2_plus_y2() {
let (p, x, y) = setup();
let num = p.add(vec![p.pow(x, p.integer(3)), p.pow(y, p.integer(3))]);
let den = p.add(vec![p.pow(x, p.integer(2)), p.pow(y, p.integer(2))]);
let f = simplify(p.mul(vec![num, p.pow(den, p.integer(-1))]), &p).value;
let r = multilimit(f, x, y, p.integer(0), p.integer(0), &p);
match r {
MultiLimit::Value(val) => {
let n = eval2(val, x, y, 0.0, 0.0, &p).unwrap();
assert!(n.abs() < 1e-9, "expected 0, got {n}");
}
other => panic!("expected Value(0), got {other}"),
}
}
#[test]
fn xy_over_x_plus_y_never_value() {
let (p, x, y) = setup();
let num = p.mul(vec![x, y]);
let den = p.add(vec![x, y]);
let f = simplify(p.mul(vec![num, p.pow(den, p.integer(-1))]), &p).value;
let r = multilimit(f, x, y, p.integer(0), p.integer(0), &p);
assert!(
!matches!(r, MultiLimit::Value(_)),
"must not certify a value for xy/(x+y); got {r}"
);
}
#[test]
fn sin_xy_over_xy_one_or_undecided() {
let (p, x, y) = setup();
let xy = p.mul(vec![x, y]);
let f = simplify(
p.mul(vec![p.func("sin", vec![xy]), p.pow(xy, p.integer(-1))]),
&p,
)
.value;
let r = multilimit(f, x, y, p.integer(0), p.integer(0), &p);
match r {
MultiLimit::Value(val) => {
let n = eval2(val, x, y, 0.0, 0.0, &p).unwrap();
assert!((n - 1.0).abs() < 1e-6, "expected 1, got {n}");
}
MultiLimit::Undecided => {}
MultiLimit::DoesNotExist { .. } => panic!("sin(xy)/(xy) exists; must not be DNE"),
}
}
}