use crate::kernel::{Domain, ExprData, ExprId, ExprPool};
use crate::validated::taylor::taylor_range;
use crate::validated::ValidatedError;
use rug::Float;
use std::collections::{HashMap, HashSet};
use std::sync::{OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard};
const PROBE_LO: f64 = 0.25;
const PROBE_HI: f64 = 0.5;
const PROBE_ORDER: usize = 1;
const PROBE_PREC: u32 = 64;
const MAX_PROBE_ARITY: usize = 3;
pub fn taylor_model_refusal(expr: ExprId, pool: &ExprPool) -> Option<String> {
let (symbols, _) = walk_expr(expr, pool);
let mut probe_box: Vec<(ExprId, Float, Float)> = symbols
.into_iter()
.map(|s| {
(
s,
Float::with_val(PROBE_PREC, PROBE_LO),
Float::with_val(PROBE_PREC, PROBE_HI),
)
})
.collect();
if probe_box.is_empty() {
probe_box.push((
expr,
Float::with_val(PROBE_PREC, PROBE_LO),
Float::with_val(PROBE_PREC, PROBE_HI),
));
}
match taylor_range(expr, pool, &probe_box, PROBE_ORDER, PROBE_PREC) {
Err(ValidatedError::Unsupported { what }) => Some(what),
_ => None,
}
}
pub fn taylor_model_blockers(expr: ExprId, pool: &ExprPool) -> Vec<String> {
let (_, calls) = walk_expr(expr, pool);
let mut out: Vec<String> = calls
.into_iter()
.filter(|(name, arity)| !taylor_model_supports_call(name, *arity))
.map(|(name, _)| name)
.collect();
out.sort();
out.dedup();
out
}
pub fn taylor_model_supports(name: &str) -> bool {
(1..=MAX_PROBE_ARITY).any(|arity| taylor_model_supports_call(name, arity))
}
pub fn taylor_model_supports_call(name: &str, arity: usize) -> bool {
if arity == 0 || arity > MAX_PROBE_ARITY {
return false;
}
let cache = &cache()[arity - 1];
if let Some(&hit) = read_lock(cache).get(name) {
return hit;
}
let answer = probe_call(name, arity);
write_lock(cache).insert(name.to_string(), answer);
answer
}
fn probe_call(name: &str, arity: usize) -> bool {
let pool = ExprPool::new();
let args: Vec<ExprId> = (0..arity)
.map(|i| pool.symbol(format!("__taylor_probe_{i}"), Domain::Real))
.collect();
let call = pool.func(name, args);
taylor_model_refusal(call, &pool).is_none()
}
fn walk_expr(expr: ExprId, pool: &ExprPool) -> (Vec<ExprId>, Vec<(String, usize)>) {
let mut seen: HashSet<ExprId> = HashSet::new();
let mut symbols: Vec<ExprId> = Vec::new();
let mut calls: Vec<(String, usize)> = Vec::new();
let mut stack = vec![expr];
while let Some(id) = stack.pop() {
if !seen.insert(id) {
continue;
}
let children: Vec<ExprId> = pool.with(id, |data| match data {
ExprData::Symbol { .. } => {
symbols.push(id);
vec![]
}
ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_) => vec![],
ExprData::Add(args) | ExprData::Mul(args) => args.clone(),
ExprData::Pow { base, exp } => vec![*base, *exp],
ExprData::Func { name, args } => {
calls.push((name.clone(), args.len()));
args.clone()
}
ExprData::Piecewise { branches, default } => {
let mut ids: Vec<ExprId> = branches.iter().flat_map(|(c, v)| [*c, *v]).collect();
ids.push(*default);
ids
}
ExprData::Predicate { args, .. } => args.clone(),
ExprData::Forall { var, body } | ExprData::Exists { var, body } => vec![*var, *body],
ExprData::BigO(arg) => vec![*arg],
ExprData::RootSum { poly, var, body } => vec![*poly, *var, *body],
});
stack.extend(children);
}
symbols.sort_unstable();
symbols.dedup();
(symbols, calls)
}
type ProbeCache = [RwLock<HashMap<String, bool>>; MAX_PROBE_ARITY];
fn cache() -> &'static ProbeCache {
static CACHE: OnceLock<ProbeCache> = OnceLock::new();
CACHE.get_or_init(|| std::array::from_fn(|_| RwLock::new(HashMap::new())))
}
fn read_lock(c: &RwLock<HashMap<String, bool>>) -> RwLockReadGuard<'_, HashMap<String, bool>> {
c.read().unwrap_or_else(|e| e.into_inner())
}
fn write_lock(c: &RwLock<HashMap<String, bool>>) -> RwLockWriteGuard<'_, HashMap<String, bool>> {
c.write().unwrap_or_else(|e| e.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validated::bounds::{bound_on_box, BoundOptions};
#[test]
fn flag_matches_bound_on_box_for_every_primitive() {
let reg = crate::primitive::PrimitiveRegistry::default_registry();
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let opts = BoundOptions {
order: 2,
prec: 64,
tol: 1e-3,
max_subdivisions: 8,
};
for (name, caps) in reg.iter() {
let flag = caps.contains(crate::primitive::Capabilities::TAYLOR_MODEL);
let call = pool.func(name, vec![x]);
let refused_as_unsupported = matches!(
bound_on_box(call, &pool, &[(x, 0.25, 0.5)], &opts),
Err(ValidatedError::Unsupported { .. })
);
assert_eq!(
flag, !refused_as_unsupported,
"`{name}`: taylor_model flag = {flag} but bound_on_box \
unsupported = {refused_as_unsupported}"
);
}
}
#[test]
fn numeric_ball_is_accurate_where_it_differs_from_taylor_model() {
use crate::ball::ArbBall;
let reg = crate::primitive::PrimitiveRegistry::default_registry();
let mut differing = 0usize;
for (name, caps) in reg.iter() {
if !caps.contains(crate::primitive::Capabilities::NUMERIC_BALL)
|| caps.contains(crate::primitive::Capabilities::TAYLOR_MODEL)
{
continue;
}
differing += 1;
assert!(
[1.0_f64, 0.5].into_iter().any(|v| reg
.numeric_ball(name, &[ArbBall::from_f64(v, 128)])
.is_some()),
"`{name}` advertises numeric_ball but has none"
);
}
assert!(
differing > 0,
"the two flags have become the same question — this test is now vacuous"
);
}
#[test]
fn refusal_names_the_blocking_function() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = pool.mul(vec![x, pool.func("floor", vec![x])]);
let what = taylor_model_refusal(e, &pool).expect("floor has no Taylor rule");
assert!(what.contains("floor"), "{what}");
assert_eq!(taylor_model_blockers(e, &pool), vec!["floor".to_string()]);
}
#[test]
fn the_special_function_rules_are_reachable() {
for name in [
"asinh",
"acosh",
"atanh",
"erf",
"erfc",
"bessel_j0",
"bessel_j1",
"digamma",
"gamma",
"lambert_w",
"Ei",
"li",
"Si",
"Ci",
"Shi",
"Chi",
"fresnels",
"fresnelc",
"dilog",
"trigamma",
] {
assert!(taylor_model_supports(name), "`{name}` lost its rule");
}
for name in ["floor", "ceil"] {
assert!(
!taylor_model_supports(name),
"`{name}` is not differentiable; a Taylor rule for it would \
advertise coverage it cannot deliver"
);
}
}
#[test]
fn supported_expression_has_no_refusal_and_no_blockers() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = pool.add(vec![pool.func("sin", vec![x]), pool.func("exp", vec![x])]);
assert!(taylor_model_refusal(e, &pool).is_none());
assert!(taylor_model_blockers(e, &pool).is_empty());
}
#[test]
fn domain_violation_is_not_unsupported() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = pool.func("log", vec![x]);
assert!(taylor_model_refusal(e, &pool).is_none());
let opts = BoundOptions::default();
assert!(bound_on_box(e, &pool, &[(x, -2.0, -1.0)], &opts).is_err());
}
#[test]
fn constant_expression_is_classified() {
let pool = ExprPool::new();
let two = pool.integer(2_i32);
assert!(taylor_model_refusal(pool.func("sin", vec![two]), &pool).is_none());
assert!(taylor_model_refusal(pool.func("floor", vec![two]), &pool).is_some());
}
#[test]
fn arity_is_part_of_the_question() {
assert!(taylor_model_supports_call("sin", 1));
assert!(!taylor_model_supports_call("sin", 2));
assert!(!taylor_model_supports_call("sin", 0));
assert!(!taylor_model_supports("atan2"));
}
#[test]
fn cache_is_transparent() {
for name in ["sin", "erf", "sqrt", "digamma", "bessel_j0", "floor"] {
let cached = taylor_model_supports_call(name, 1);
assert_eq!(cached, probe_call(name, 1), "{name}");
assert_eq!(cached, taylor_model_supports_call(name, 1), "{name}");
}
}
}