use ocas_atom::normalize::normalize;
use ocas_atom::walk::collect_funs;
use ocas_atom::{Atom, AtomArena, AtomNode, Symbol};
use ocas_domain::{Domain, RationalDomain};
use ocas_poly::{Lex, SparseMultivariatePolynomial};
use ocas_rewrite::rules::default_rules;
use ocas_rewrite::simplify::simplify;
use super::trig::trig_to_exp;
use super::{contains_integral, fraction_exponent, 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;
type Sparse = SparseMultivariatePolynomial<RationalDomain, Lex>;
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 {
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 std::env::var_os("OCAS_CERT_TIMING").is_some() {
eprintln!(
"[cert] field input nodes={} expansion={} gens={}",
node_count(rewritten),
expansion,
gens.len()
);
}
let layer2 = if !complete {
Err(CertDecline::NotInField)
} else if expansion.saturating_mul(gens.len() as u64) > MAX_CERT_FIELD_WORK {
Err(CertDecline::Budget)
} else {
match atom_to_rational_extended(rewritten, &gens, gens.len()) {
Some(rf) if rf.numerator.n_terms() == 0 => Ok(Certificate {
method: CertMethod::ElementaryField,
difference: zero,
}),
Some(_) => Err(CertDecline::NonZero),
None => Err(CertDecline::NotInField),
}
};
match layer2 {
Ok(_) => layer2,
Err(e) => radical_layer(ctx, rewritten, var, max_gens).or(Err(e)),
}
}
const MAX_CERT_RADICAL_Q: i64 = 8;
const MAX_CERT_RADICAL_P: i64 = 16;
const MAX_CERT_RADICALS: usize = 8;
const MAX_CERT_REDUCTION_TERMS: usize = 8192;
const MAX_CERT_RADICAL_NODES: usize = 2_000;
const MAX_CERT_RADICAL_POLY_TERMS: usize = 2_048;
struct RadicalRelation {
var_index: usize,
q: u32,
base: Sparse,
}
fn rewrite_radicals<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
groups: &mut Vec<(Atom<'a>, i64, Atom<'a>)>,
) -> Option<Atom<'a>> {
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => Some(expr),
AtomNode::Add(args) => {
let rebuilt: Vec<Atom> = args
.iter()
.map(|a| rewrite_radicals(ctx, *a, groups))
.collect::<Option<_>>()?;
Some(ctx.add(&rebuilt))
}
AtomNode::Mul(args) => {
let rebuilt: Vec<Atom> = args
.iter()
.map(|a| rewrite_radicals(ctx, *a, groups))
.collect::<Option<_>>()?;
Some(ctx.mul(&rebuilt))
}
AtomNode::Fun(name, args) => {
if name.as_str() == "sqrt" && args.len() == 1 {
let base = rewrite_radicals(ctx, args[0], groups)?;
let (r, _) = radical_group(ctx, base, 2, groups)?;
return Some(r);
}
let rebuilt: Vec<Atom> = args
.iter()
.map(|a| rewrite_radicals(ctx, *a, groups))
.collect::<Option<_>>()?;
Some(ctx.fun(name.as_str(), &rebuilt))
}
AtomNode::Pow(base, exp) => {
if let Some((p, q)) = fraction_exponent(*exp) {
if !(2..=MAX_CERT_RADICAL_Q).contains(&q)
|| p.unsigned_abs() > MAX_CERT_RADICAL_P as u64
{
return None;
}
let b = rewrite_radicals(ctx, *base, groups)?;
let (r, _) = radical_group(ctx, b, q, groups)?;
return Some(match p {
1 => r,
_ => ctx.pow(r, ctx.num(p)),
});
}
let b = rewrite_radicals(ctx, *base, groups)?;
let e = rewrite_radicals(ctx, *exp, groups)?;
Some(ctx.pow(b, e))
}
}
}
fn radical_group<'a>(
ctx: &'a AtomArena<'a>,
base: Atom<'a>,
q: i64,
groups: &mut Vec<(Atom<'a>, i64, Atom<'a>)>,
) -> Option<(Atom<'a>, usize)> {
if let Some(idx) = groups.iter().position(|(b, gq, _)| *b == base && *gq == q) {
return Some((groups[idx].2, idx));
}
if groups.len() >= MAX_CERT_RADICALS {
return None;
}
let r = ctx.var(&format!("$cert_r{}", groups.len()));
groups.push((base, q, r));
Some((r, groups.len() - 1))
}
fn reduce_mod_radicals(p: &Sparse, relations: &[RadicalRelation]) -> Option<Sparse> {
let mut cur = p.clone();
for rel in relations.iter().rev() {
let q = rel.q as usize;
let mut out = cur.zero();
for (exp, coeff) in cur.terms_ref() {
let e = exp[rel.var_index];
if e < q {
out = out.add(&Sparse::from_terms(
RationalDomain,
cur.n_vars(),
vec![(exp.to_vec(), coeff.clone())],
));
continue;
}
let (a, b) = (e / q, e % q);
let mut exp_red = exp.to_vec();
exp_red[rel.var_index] = b;
let mut term =
Sparse::from_terms(RationalDomain, cur.n_vars(), vec![(exp_red, coeff.clone())]);
for _ in 0..a {
term = term.mul(&rel.base);
if term.n_terms() > MAX_CERT_REDUCTION_TERMS {
return None;
}
}
out = out.add(&term);
if out.n_terms() > MAX_CERT_REDUCTION_TERMS {
return None;
}
}
cur = out;
}
Some(cur)
}
fn radical_layer<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
max_gens: usize,
) -> Result<Certificate<'a>, CertDecline> {
if node_count(expr) > MAX_CERT_RADICAL_NODES {
return Err(CertDecline::Budget);
}
let mut groups: Vec<(Atom<'a>, i64, Atom<'a>)> = Vec::new();
let Some(rewritten) = rewrite_radicals(ctx, expr, &mut groups) else {
if std::env::var_os("OCAS_CERT_TIMING").is_some() {
eprintln!("[cert] radical rewrite declined: {expr}");
}
return Err(CertDecline::Budget);
};
if std::env::var_os("OCAS_CERT_TIMING").is_some() {
eprintln!(
"[cert] radical layer input: {rewritten} (groups={})",
groups.len()
);
}
if groups.is_empty() {
return Err(CertDecline::NotInField);
}
let mut gens: Vec<Atom<'a>> = Vec::with_capacity(8);
gens.push(ctx.var(var.as_str()));
let mut overflow = false;
for (_, _, r) in &groups {
if !push_generator(&mut gens, *r, max_gens, &mut overflow) {
return Err(CertDecline::Budget);
}
}
if !collect_generators(rewritten, &mut gens, max_gens, &mut overflow) || overflow {
return Err(if overflow {
CertDecline::Budget
} else {
CertDecline::NotInField
});
}
let mut relations: Vec<RadicalRelation> = Vec::with_capacity(groups.len());
for (base, q, r) in &groups {
let rf =
atom_to_rational_extended(*base, &gens, gens.len()).ok_or(CertDecline::NotInField)?;
if rf.denominator.n_terms() != 1
|| !RationalDomain.is_one(&rf.denominator.coeff(&vec![0; gens.len()]))
{
return Err(CertDecline::NotInField);
}
let var_index = gens
.iter()
.position(|g| g == r)
.expect("generator registered");
relations.push(RadicalRelation {
var_index,
q: *q as u32,
base: rf.numerator,
});
}
let rf =
atom_to_rational_extended(rewritten, &gens, gens.len()).ok_or(CertDecline::NotInField)?;
if rf.numerator.n_terms() > MAX_CERT_RADICAL_POLY_TERMS
|| rf.denominator.n_terms() > MAX_CERT_RADICAL_POLY_TERMS
{
return Err(CertDecline::Budget);
}
let num = reduce_mod_radicals(&rf.numerator, &relations).ok_or(CertDecline::Budget)?;
if std::env::var_os("OCAS_CERT_TIMING").is_some() {
eprintln!(
"[cert] radical layer: gens={} relations={} num_terms={} den_terms={}",
gens.len(),
relations.len(),
num.n_terms(),
rf.denominator.n_terms()
);
}
if num.is_zero() {
return Ok(Certificate {
method: CertMethod::RadicalField,
difference: ctx.num(0),
});
}
let den = reduce_mod_radicals(&rf.denominator, &relations).ok_or(CertDecline::Budget)?;
if den.is_zero() {
return Err(CertDecline::NotInField);
}
Err(CertDecline::NonZero)
}
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;
let mut candidates = collect_funs(current);
candidates
.sort_by_key(|(_, app)| app.children().first().map_or(0usize, |a| node_count(*a)));
for (name, app) in candidates {
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_certifies_via_radical_layer() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let verdict = certify_str(&ctx, "1/sqrt(1-x^2)", "asin(x)");
assert!(verdict.is_ok(), "expected a certificate, got: {verdict:?}");
}
#[test]
fn radical_layer_uses_the_constant_radical_relation() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let cert = certify_str(
&ctx,
"1/(x^2-2)",
"8^(-1/2)*log((2*x-8^(1/2))/(2*x+8^(1/2)))",
)
.expect("certificate");
assert_eq!(cert.method, CertMethod::RadicalField);
}
#[test]
fn radical_layer_merges_sqrt_head_and_fractional_power() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let cert = certify_str(&ctx, "x*(1-x^2)^(-1/2)", "-sqrt(1-x^2)").expect("certificate");
assert!(matches!(
cert.method,
CertMethod::RadicalField | CertMethod::ElementaryField | CertMethod::Structural
));
}
#[test]
fn merge_orders_integer_multiples_after_their_base() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let cert = certify_str(
&ctx,
"exp(2*x) + exp(x)*exp(2*x)",
"exp(2*x)/2 + exp(x)^3/3",
)
.expect("certificate");
assert!(matches!(
cert.method,
CertMethod::Structural | CertMethod::ElementaryField
));
}
#[test]
fn radical_layer_rejects_wrong_answers() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert!(
certify_str(
&ctx,
"1/(x^2-2)",
"8^(-1/2)*log((3*x-8^(1/2))/(2*x+8^(1/2)))"
)
.is_err()
);
}
}