use ocas_atom::walk::collect_funs;
use ocas_atom::{Atom, AtomArena, AtomNode, Symbol};
use super::convert::atom_to_rational_extended;
use super::elem::{KElem, KPoly};
use super::merge;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum GenKind {
Constant,
Log,
Exp,
}
pub(crate) struct GenInfo<'a> {
pub kind: GenKind,
pub atom: Atom<'a>,
pub arg: Atom<'a>,
pub dt: KElem,
}
pub(crate) struct Tower<'a> {
pub x: Atom<'a>,
pub expr: Atom<'a>,
pub gens: Vec<GenInfo<'a>>,
}
impl<'a> Tower<'a> {
pub fn n_vars(&self) -> usize {
1 + self.gens.len()
}
pub fn gen_atoms(&self) -> Vec<Atom<'a>> {
let mut v = Vec::with_capacity(self.n_vars());
v.push(self.x);
v.extend(self.gens.iter().map(|g| g.atom));
v
}
}
pub(crate) fn tower_diff(e: &KElem, gens: &[GenInfo]) -> KElem {
let mut acc = e.partial_deriv(0);
for (i, g) in gens.iter().enumerate() {
acc = acc.add(&e.partial_deriv(i + 1).mul(&g.dt));
}
acc
}
pub(crate) fn tower_diff_kpoly(p: &KPoly, gens: &[GenInfo], dt_top: &KElem) -> KPoly {
let derived = KPoly {
top: p.top,
coeffs: p.coeffs.iter().map(|c| tower_diff(c, gens)).collect(),
n_vars: p.n_vars,
};
derived.add(&p.derivative_dt().mul_kelem(dt_top))
}
pub(crate) fn build_tower<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> Option<Tower<'a>> {
let x = ctx.var(var.as_str());
if !only_integer_powers(expr) {
return None;
}
let mut gens: Vec<GenInfo<'a>> = Vec::new();
if contains_var(expr, "I") {
return None;
}
let mut current = expr;
loop {
let mut rewritten = 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,
_ => return None,
};
let args = app.children();
if args.len() != 1 {
return None;
}
let arg = args[0];
if let Some(merge) = merge::merge_candidate(ctx, kind, arg, app, &gens, var) {
if let Some(constant) = merge.constant
&& !gens.iter().any(|g| g.atom == constant)
{
gens.push(GenInfo {
kind: GenKind::Constant,
atom: constant,
arg: constant,
dt: KElem::zero(0),
});
}
current = merge::substitute_atom(ctx, current, app, merge.replacement);
rewritten = true;
break;
}
if is_rational_constant(arg) {
return None;
}
gens.push(GenInfo {
kind,
atom: app,
arg,
dt: KElem::zero(0),
});
}
if !rewritten {
break;
}
}
let n = 1 + gens.len();
for i in 0..gens.len() {
let (done, rest) = gens.split_at_mut(i);
let g = &mut rest[0];
if g.kind == GenKind::Constant {
g.dt = KElem::zero(n);
continue;
}
let mut prefix_atoms = Vec::with_capacity(i + 1);
prefix_atoms.push(x);
prefix_atoms.extend(done.iter().map(|d| d.atom));
let u_rf = atom_to_rational_extended(g.arg, &prefix_atoms, n)?;
let u_k = KElem::new(u_rf.numerator, u_rf.denominator);
let du = tower_diff(&u_k, done);
g.dt = match g.kind {
GenKind::Constant => KElem::zero(n),
GenKind::Log => du.div(&u_k)?,
GenKind::Exp => du.mul(&KElem::var(i + 1, n)),
};
}
Some(Tower {
x,
expr: current,
gens,
})
}
fn is_rational_constant(atom: Atom) -> bool {
atom_to_rational_extended(atom, &[], 0).is_some()
}
fn contains_var(atom: Atom, name: &str) -> bool {
match atom.node() {
AtomNode::Var(s) => s.as_str() == name,
AtomNode::Num(_) => false,
AtomNode::Add(args) | AtomNode::Mul(args) | AtomNode::Fun(_, args) => {
args.iter().any(|a| contains_var(*a, name))
}
AtomNode::Pow(b, e) => contains_var(*b, name) || contains_var(*e, name),
}
}
fn only_integer_powers(atom: Atom) -> bool {
if let Some((base, exp)) = atom.binary_children() {
let ok = matches!(exp.node(), AtomNode::Num(_))
|| (is_rational_constant(base) && is_rational_constant(exp));
return ok && only_integer_powers(base) && only_integer_powers(exp);
}
atom.children().iter().all(|c| only_integer_powers(*c))
}
#[cfg(test)]
mod tests {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;
use ocas_domain::{Domain, Rational, RationalDomain};
use super::*;
fn sym(name: &str) -> Symbol {
Symbol::new(name)
}
#[test]
fn tower_single_log() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.add(&[ctx.fun("log", &[x]), ctx.num(1)]);
let tower = build_tower(&ctx, expr, sym("x")).expect("tower");
assert_eq!(tower.gens.len(), 1);
assert_eq!(tower.gens[0].kind, GenKind::Log);
let one_over_x = KElem::one(2).div(&KElem::var(0, 2)).unwrap();
assert!(tower.gens[0].dt.eq_cross(&one_over_x));
}
#[test]
fn tower_nested_exp_log() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let log_x = ctx.fun("log", &[x]);
let expr = ctx.fun("exp", &[ctx.mul(&[x, log_x])]);
let tower = build_tower(&ctx, expr, sym("x")).expect("tower");
assert_eq!(tower.gens.len(), 2);
assert_eq!(tower.gens[0].kind, GenKind::Log);
assert_eq!(tower.gens[1].kind, GenKind::Exp);
let log_var = KElem::var(1, 3);
let exp_var = KElem::var(2, 3);
let expect = log_var.add(&KElem::one(3)).mul(&exp_var);
assert!(tower.gens[1].dt.eq_cross(&expect));
}
#[test]
fn tower_merges_dependent_logs() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.add(&[
ctx.fun("log", &[x]),
ctx.fun("log", &[ctx.mul(&[ctx.num(2), x])]),
]);
let tower = build_tower(&ctx, expr, sym("x")).expect("tower");
assert_eq!(tower.gens.len(), 2);
assert_eq!(tower.gens[0].kind, GenKind::Log);
assert_eq!(tower.gens[1].kind, GenKind::Constant);
assert_eq!(tower.gens[1].atom.to_string(), "log(2)");
assert!(tower.gens[1].dt.is_zero());
assert_eq!(tower.expr.to_string(), "(log(x)) + ((log(x)) + (log(2)))");
}
#[test]
fn tower_allows_independent_logs() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.add(&[
ctx.fun("log", &[x]),
ctx.fun("log", &[ctx.add(&[x, ctx.num(1)])]),
]);
let tower = build_tower(&ctx, expr, sym("x")).expect("tower");
assert_eq!(tower.gens.len(), 2);
}
#[test]
fn tower_merges_reciprocal_exponentials() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.mul(&[
ctx.fun("exp", &[x]),
ctx.fun("exp", &[ctx.mul(&[ctx.num(-1), x])]),
]);
let tower = build_tower(&ctx, expr, sym("x")).expect("tower");
assert_eq!(tower.gens.len(), 1);
assert_eq!(tower.gens[0].kind, GenKind::Exp);
assert_eq!(tower.expr.to_string(), "(exp(x))*((exp(x))^-1)");
}
#[test]
fn tower_merges_shifted_exponentials() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.mul(&[
ctx.fun("exp", &[x]),
ctx.fun("exp", &[ctx.add(&[x, ctx.num(1)])]),
]);
let tower = build_tower(&ctx, expr, sym("x")).expect("tower");
assert_eq!(tower.gens.len(), 2);
assert_eq!(tower.gens[1].kind, GenKind::Constant);
assert_eq!(tower.gens[1].atom.to_string(), "exp(1)");
}
#[test]
fn tower_rejects_trig_and_sqrt_power() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
assert!(build_tower(&ctx, ctx.fun("sin", &[x]), sym("x")).is_none());
let sqrt_x = ctx.pow(x, ctx.pow(ctx.num(2), ctx.num(-1)));
assert!(build_tower(&ctx, sqrt_x, sym("x")).is_none());
}
#[test]
fn tower_rejects_log_of_constant() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.add(&[ctx.fun("log", &[ctx.num(3)]), x]);
assert!(build_tower(&ctx, expr, sym("x")).is_none());
}
#[test]
fn tower_rejects_the_imaginary_unit_for_now() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.fun("exp", &[ctx.mul(&[ctx.var("I"), x])]);
assert!(build_tower(&ctx, expr, sym("x")).is_none());
}
#[test]
fn tower_diff_polynomial() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.fun("exp", &[x]);
let tower = build_tower(&ctx, expr, sym("x")).expect("tower");
let xt = KElem::var(0, 2).mul(&KElem::var(1, 2));
let d = tower_diff(&xt, &tower.gens);
let t = KElem::var(1, 2);
let expect = t.add(&KElem::var(0, 2).mul(&t));
assert!(d.eq_cross(&expect));
let _ = RationalDomain.one();
let _ = Rational::new(1, 1);
}
}