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};
#[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 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") {
gens.push(GenInfo {
kind: GenKind::Constant,
atom: _ctx.var("I"),
arg: _ctx.var("I"),
dt: KElem::zero(0),
});
}
for (name, app) in collect_funs(expr) {
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 is_rational_constant(arg) {
return None;
}
for g in &gens {
if algebraically_dependent(kind, arg, g) {
return None;
}
}
gens.push(GenInfo {
kind,
atom: app,
arg,
dt: KElem::zero(0),
});
}
let n = 1 + gens.len();
for i in 0..gens.len() {
let (done, rest) = gens.split_at_mut(i);
let g = &mut rest[0];
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, 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))
}
fn algebraically_dependent(kind: GenKind, arg: Atom, existing: &GenInfo) -> bool {
if arg == existing.atom {
return true;
}
let u = arg;
let v = existing.arg;
match (kind, existing.kind) {
(GenKind::Log, GenKind::Log) => {
let ratio = is_rational_constant_div(u, v);
let product = is_rational_constant_mul(u, v);
let powers = [2, 3, -2, -3]
.iter()
.any(|&k| pow_int_eq(u, v, k) || pow_int_eq(v, u, k));
ratio || product || powers
}
(GenKind::Exp, GenKind::Exp) => {
is_rational_constant_sub(u, v) || is_rational_constant_sum(u, v)
}
_ => false,
}
}
fn is_rational_constant_sum(u: Atom, v: Atom) -> bool {
if let AtomNode::Mul(factors) = u.node()
&& factors.len() == 2
&& matches!(factors[0].node(), AtomNode::Num(-1))
&& factors[1] == v
{
return true;
}
if let AtomNode::Mul(factors) = v.node()
&& factors.len() == 2
&& matches!(factors[0].node(), AtomNode::Num(-1))
&& factors[1] == u
{
return true;
}
match u.node() {
AtomNode::Add(args) if args.len() == 2 => {
let neg_v = matches!(args[0].node(), AtomNode::Mul(f)
if f.len() == 2 && matches!(f[0].node(), AtomNode::Num(-1)) && f[1] == v);
if (neg_v && is_rational_constant(args[1]))
|| (args[0] == v && is_rational_constant(args[1]))
{
return true;
}
}
_ => {}
}
false
}
fn is_rational_constant_div(u: Atom, v: Atom) -> bool {
structurally_proportional(u, v)
}
fn is_rational_constant_mul(u: Atom, v: Atom) -> bool {
matches!(v.node(), AtomNode::Pow(b, e) if matches!(e.node(), AtomNode::Num(-1)) && structurally_proportional(u, *b))
}
fn is_rational_constant_sub(u: Atom, v: Atom) -> bool {
match u.node() {
AtomNode::Add(args) if args.len() == 2 => {
(args[0] == v && is_rational_constant(args[1]))
|| (args[1] == v && is_rational_constant(args[0]))
}
_ => false,
}
}
fn pow_int_eq(u: Atom, v: Atom, k: i64) -> bool {
matches!(u.node(), AtomNode::Pow(b, e) if *b == v && matches!(e.node(), AtomNode::Num(n) if *n == k))
}
fn structurally_proportional(u: Atom, v: Atom) -> bool {
if u == v {
return true;
}
strip_const_factor(u) == Some(v) || strip_const_factor(v) == Some(u)
}
fn strip_const_factor(atom: Atom) -> Option<Atom> {
if let AtomNode::Mul(args) = atom.node()
&& args.len() == 2
{
if is_rational_constant(args[0]) {
return Some(args[1]);
}
if is_rational_constant(args[1]) {
return Some(args[0]);
}
}
None
}
#[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_rejects_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])]),
]);
assert!(build_tower(&ctx, expr, sym("x")).is_none());
}
#[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_rejects_dependent_exps() {
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)])]),
]);
assert!(build_tower(&ctx, expr, sym("x")).is_none());
}
#[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_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);
}
}