use ocas_atom::normalize::normalize;
use ocas_atom::walk::collect_funs;
use ocas_atom::{Atom, AtomArena, AtomNode, Symbol};
use ocas_rewrite::rules::default_rules;
use ocas_rewrite::simplify::simplify;
use super::trig::trig_to_exp;
use super::{contains_integral, node_count};
use crate::derivative::diff;
use crate::rules::calculus_rules;
use crate::tower::build::{GenInfo, GenKind};
use crate::tower::convert::atom_to_rational_extended;
use crate::tower::elem::KElem;
use crate::tower::merge;
const MAX_CERT_NODES: usize = 20_000;
const MAX_CERT_GENS: usize = 16;
const MAX_CERT_EXPANSION: u64 = 4_096;
const MAX_CERT_EXPONENT: u64 = 64;
const MAX_CERT_FIELD_WORK: u64 = 400;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum CertMethod {
Structural,
ElementaryField,
RadicalField,
}
impl CertMethod {
pub fn as_str(self) -> &'static str {
match self {
CertMethod::Structural => "structural",
CertMethod::ElementaryField => "field",
CertMethod::RadicalField => "radical",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Certificate<'a> {
pub method: CertMethod,
pub difference: Atom<'a>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum CertDecline {
NotInField,
Budget,
NonZero,
}
impl CertDecline {
pub fn as_str(self) -> &'static str {
match self {
CertDecline::NotInField => "notinfield",
CertDecline::Budget => "budget",
CertDecline::NonZero => "nonzero",
}
}
}
pub fn certify<'a>(
ctx: &'a AtomArena<'a>,
integrand: Atom<'a>,
antiderivative: Atom<'a>,
var: Symbol,
) -> Result<Certificate<'a>, CertDecline> {
certify_with_limits(
ctx,
integrand,
antiderivative,
var,
MAX_CERT_NODES,
MAX_CERT_GENS,
)
}
pub(crate) fn certify_with_limits<'a>(
ctx: &'a AtomArena<'a>,
integrand: Atom<'a>,
antiderivative: Atom<'a>,
var: Symbol,
max_nodes: usize,
max_gens: usize,
) -> Result<Certificate<'a>, CertDecline> {
if contains_integral(antiderivative) {
return Err(CertDecline::NotInField);
}
let derived = diff(ctx, antiderivative, var);
let minus_one = ctx.num(-1);
let reduced = ctx.add(&[derived, ctx.mul(&[minus_one, integrand])]);
let reduced = normalize(ctx, reduced);
let default_rules = default_rules(ctx, &crate::pattern_alloc::VecAlloc);
let calc_rules = calculus_rules(ctx, &crate::pattern_alloc::VecAlloc);
let reduced = simplify(ctx, reduced, &default_rules, 20);
let reduced = simplify(ctx, reduced, &calc_rules, 10);
let reduced = normalize(ctx, reduced);
if std::env::var_os("OCAS_CERT_TIMING").is_some() {
eprintln!(
"[cert] difference nodes={} (integrand {} / antiderivative {})",
node_count(reduced),
node_count(integrand),
node_count(antiderivative)
);
}
if node_count(reduced) > max_nodes {
return Err(CertDecline::Budget);
}
let zero = ctx.num(0);
if is_zero(reduced) {
return Ok(Certificate {
method: CertMethod::Structural,
difference: zero,
});
}
let expansion = expansion_size(reduced);
if expansion > MAX_CERT_EXPANSION {
return Err(CertDecline::Budget);
}
let collected = crate::ode::util::collect_terms(ctx, reduced);
if node_count(collected) > max_nodes {
return Err(CertDecline::Budget);
}
if is_zero(collected) {
return Ok(Certificate {
method: CertMethod::Structural,
difference: zero,
});
}
let rewritten = trig_to_exp(ctx, reduced).unwrap_or(reduced);
let rewritten = reduce_imaginary_unit(ctx, rewritten);
let rewritten = merge_dependent_atoms(ctx, rewritten, var);
let rewritten = normalize(ctx, rewritten);
let mut gens: Vec<Atom<'a>> = Vec::with_capacity(8);
gens.push(ctx.var(var.as_str()));
let mut overflow = false;
let complete = collect_generators(rewritten, &mut gens, max_gens, &mut overflow);
if overflow {
return Err(CertDecline::Budget);
}
if !complete {
return Err(CertDecline::NotInField);
}
if std::env::var_os("OCAS_CERT_TIMING").is_some() {
eprintln!(
"[cert] field input nodes={} expansion={} gens={}",
node_count(rewritten),
expansion,
gens.len()
);
}
if expansion.saturating_mul(gens.len() as u64) > MAX_CERT_FIELD_WORK {
return Err(CertDecline::Budget);
}
let field = atom_to_rational_extended(rewritten, &gens, gens.len());
match field {
Some(rf) if rf.numerator.n_terms() == 0 => Ok(Certificate {
method: CertMethod::ElementaryField,
difference: zero,
}),
Some(_) => Err(CertDecline::NonZero),
None => Err(CertDecline::NotInField),
}
}
fn is_zero(atom: Atom<'_>) -> bool {
matches!(atom.node(), AtomNode::Num(0))
}
fn reduce_imaginary_unit<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>) -> Atom<'a> {
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => expr,
AtomNode::Fun(name, args) => {
let rebuilt: Vec<Atom<'a>> = args
.iter()
.map(|a| reduce_imaginary_unit(ctx, *a))
.collect();
let rebuilt = ctx.fun(name.as_str(), &rebuilt);
if rebuilt == expr { expr } else { rebuilt }
}
AtomNode::Add(args) => {
let rebuilt: Vec<Atom<'a>> = args
.iter()
.map(|a| reduce_imaginary_unit(ctx, *a))
.collect();
let rebuilt = ctx.add(&rebuilt);
if rebuilt == expr { expr } else { rebuilt }
}
AtomNode::Mul(args) => {
let mut power: i64 = 0;
let mut rest: Vec<Atom<'a>> = Vec::with_capacity(args.len());
let mut changed = false;
for a in args.iter() {
let a = reduce_imaginary_unit(ctx, *a);
if let Some(n) = imaginary_exponent(a) {
power += n;
changed = true;
} else {
rest.push(a);
}
}
if !changed {
return expr;
}
match power.rem_euclid(4) {
0 => {}
1 => rest.push(ctx.var("I")),
2 => rest.push(ctx.num(-1)),
_ => {
rest.push(ctx.num(-1));
rest.push(ctx.var("I"));
}
}
ctx.mul(&rest)
}
AtomNode::Pow(base, exp) => {
let base = reduce_imaginary_unit(ctx, *base);
let exp = reduce_imaginary_unit(ctx, *exp);
if let AtomNode::Num(n) = exp.node()
&& is_imaginary_unit(base)
{
return imaginary_power(ctx, *n);
}
let rebuilt = ctx.pow(base, exp);
if rebuilt == expr { expr } else { rebuilt }
}
}
}
fn is_imaginary_unit(atom: Atom<'_>) -> bool {
matches!(atom.node(), AtomNode::Var(s) if s.as_str() == "I")
}
fn imaginary_exponent(atom: Atom<'_>) -> Option<i64> {
if is_imaginary_unit(atom) {
return Some(1);
}
match atom.node() {
AtomNode::Pow(base, exp) if is_imaginary_unit(*base) => match exp.node() {
AtomNode::Num(n) => Some(*n),
_ => None,
},
_ => None,
}
}
fn imaginary_power<'a>(ctx: &'a AtomArena<'a>, n: i64) -> Atom<'a> {
match n.rem_euclid(4) {
0 => ctx.num(1),
1 => ctx.var("I"),
2 => ctx.num(-1),
_ => ctx.mul(&[ctx.num(-1), ctx.var("I")]),
}
}
const MAX_CERT_MERGE_ROUNDS: usize = 8;
fn merge_dependent_atoms<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Atom<'a> {
let mut current = expr;
for _ in 0..MAX_CERT_MERGE_ROUNDS {
let mut gens: Vec<GenInfo<'a>> = Vec::new();
let mut changed = false;
for (name, app) in collect_funs(current) {
if gens.iter().any(|g| g.atom == app) {
continue;
}
let kind = match name.as_str() {
"log" => GenKind::Log,
"exp" => GenKind::Exp,
_ => continue,
};
let args = app.children();
if args.len() != 1 {
continue;
}
if let Some(merge) = merge::merge_candidate(ctx, kind, args[0], app, &gens, var) {
current = merge::substitute_atom(ctx, current, app, merge.replacement);
changed = true;
break;
}
gens.push(GenInfo {
kind,
atom: app,
arg: args[0],
dt: KElem::zero(0),
});
}
if !changed {
break;
}
}
current
}
fn expansion_size(expr: Atom<'_>) -> u64 {
let cap = MAX_CERT_EXPANSION.saturating_add(1);
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) | AtomNode::Fun(_, _) => 1,
AtomNode::Add(args) => args
.iter()
.fold(0u64, |acc, a| acc.saturating_add(expansion_size(*a)))
.min(cap),
AtomNode::Mul(args) => args
.iter()
.fold(1u64, |acc, a| acc.saturating_mul(expansion_size(*a)))
.min(cap),
AtomNode::Pow(base, exp) => {
let AtomNode::Num(n) = exp.node() else {
return cap;
};
let power = n.unsigned_abs();
if power > MAX_CERT_EXPONENT {
return cap;
}
let base_size = expansion_size(*base);
let mut acc = 1u64;
for _ in 0..power {
acc = acc.saturating_mul(base_size).min(cap);
}
acc
}
}
}
fn collect_generators<'a>(
expr: Atom<'a>,
gens: &mut Vec<Atom<'a>>,
max_gens: usize,
overflow: &mut bool,
) -> bool {
match expr.node() {
AtomNode::Num(_) => true,
AtomNode::Var(_) => push_generator(gens, expr, max_gens, overflow),
AtomNode::Fun(name, args) => {
let n = name.as_str();
if n == "Integral" || n == "Derivative" {
return false;
}
if !push_generator(gens, expr, max_gens, overflow) {
return false;
}
args.iter()
.all(|a| collect_generators(*a, gens, max_gens, overflow))
}
AtomNode::Add(args) | AtomNode::Mul(args) => args
.iter()
.all(|a| collect_generators(*a, gens, max_gens, overflow)),
AtomNode::Pow(base, exp) => {
if let AtomNode::Num(_) = exp.node() {
collect_generators(*base, gens, max_gens, overflow)
&& collect_generators(*exp, gens, max_gens, overflow)
} else if is_rational_constant(*base) {
push_generator(gens, expr, max_gens, overflow)
} else {
false
}
}
}
}
fn push_generator<'a>(
gens: &mut Vec<Atom<'a>>,
atom: Atom<'a>,
max_gens: usize,
overflow: &mut bool,
) -> bool {
if gens.contains(&atom) {
return true;
}
if gens.len() >= max_gens {
*overflow = true;
return false;
}
gens.push(atom);
true
}
fn is_rational_constant(atom: Atom<'_>) -> bool {
match atom.node() {
AtomNode::Num(_) => true,
AtomNode::Var(_) | AtomNode::Fun(_, _) => false,
AtomNode::Add(args) | AtomNode::Mul(args) => args.iter().all(|a| is_rational_constant(*a)),
AtomNode::Pow(b, e) => is_rational_constant(*b) && is_rational_constant(*e),
}
}
#[cfg(test)]
mod tests {
use ocas_core::arena::Arena;
use ocas_parse::parse;
use super::*;
fn certify_str<'a>(
ctx: &'a AtomArena<'a>,
f: &str,
big_f: &str,
) -> Result<Certificate<'a>, CertDecline> {
let integrand = normalize(ctx, parse(ctx, f).expect("parse integrand"));
let antiderivative = parse(ctx, big_f).expect("parse antiderivative");
certify(ctx, integrand, antiderivative, Symbol::new("x"))
}
#[test]
fn structural_layer_certifies_polynomial_pairs() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let cert = certify_str(&ctx, "2*x", "x^2").expect("certificate");
assert_eq!(cert.method, CertMethod::Structural);
assert_eq!(cert.difference.to_string(), "0");
}
#[test]
fn structural_layer_collects_like_terms() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let cert = certify_str(&ctx, "x", "(1/2)*x^2").expect("certificate");
assert_eq!(cert.method, CertMethod::Structural);
}
#[test]
fn field_layer_certifies_a_sum_of_logs() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let cert = certify_str(&ctx, "1/(x*(x+1))", "log(x)-log(x+1)").expect("certificate");
assert_eq!(cert.method, CertMethod::ElementaryField);
}
#[test]
fn trig_heads_are_rewritten_before_the_field_layer() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let cert = certify_str(&ctx, "cos(x)", "sin(x)").expect("certificate");
assert!(matches!(
cert.method,
CertMethod::Structural | CertMethod::ElementaryField
));
}
#[test]
fn wrong_antiderivative_is_declined() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert_eq!(
certify_str(&ctx, "x", "x^2").unwrap_err(),
CertDecline::NonZero
);
assert!(certify_str(&ctx, "2*sin(x)", "sin(x)").is_err());
}
#[test]
fn residue_carrying_answers_are_not_certifiable() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert_eq!(
certify_str(&ctx, "1/(x^2+5)", "Integral(1/(x^2+5), x)").unwrap_err(),
CertDecline::NotInField
);
}
#[test]
fn budget_is_reported_separately() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let integrand = normalize(&ctx, parse(&ctx, "x").unwrap());
let antiderivative = parse(&ctx, "log(x)-log(x+1)").unwrap();
assert_eq!(
certify_with_limits(&ctx, integrand, antiderivative, Symbol::new("x"), 1, 16)
.unwrap_err(),
CertDecline::Budget
);
}
#[test]
fn generator_budget_is_reported() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let integrand = normalize(&ctx, parse(&ctx, "log(x)").unwrap());
let antiderivative = parse(&ctx, "x").unwrap();
assert_eq!(
certify_with_limits(&ctx, integrand, antiderivative, Symbol::new("x"), 20_000, 1)
.unwrap_err(),
CertDecline::Budget
);
}
#[test]
fn radical_of_a_non_constant_base_declines_for_now() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let verdict = certify_str(&ctx, "1/sqrt(1-x^2)", "asin(x)");
assert!(
matches!(
verdict,
Ok(_) | Err(CertDecline::NotInField | CertDecline::NonZero | CertDecline::Budget)
),
"unexpected verdict: {verdict:?}"
);
}
}