use crate::base::arena::Arena;
use crate::base::assumptions::{AssumptionCache, Props};
use crate::base::node::{ExprId, ExprNode};
use crate::base::walk;
use rustc_hash::FxHashMap;
pub(crate) fn expand_log(arena: &mut Arena, expr: ExprId) -> ExprId {
expand_log_with(arena, expr, false)
}
pub(crate) fn expand_log_with(arena: &mut Arena, expr: ExprId, force: bool) -> ExprId {
let post_order = walk::post_order_ids(arena, expr);
let mut cache: FxHashMap<ExprId, ExprId> = FxHashMap::default();
let mut assumptions = AssumptionCache::new();
for &id in &post_order {
let node = arena.node(id).clone();
let expanded = match node {
ExprNode::Ln(inner) => {
let inner = cache.get(&inner).copied().unwrap_or(inner);
expand_ln_node_guarded(arena, &mut assumptions, inner, force)
}
ExprNode::Add(ref children) => {
let new: smallvec::SmallVec<[ExprId; 6]> = children
.iter()
.map(|&c| cache.get(&c).copied().unwrap_or(c))
.collect();
if new == *children {
id
} else {
arena.add(&new)
}
}
ExprNode::Mul(ref children) => {
let new: smallvec::SmallVec<[ExprId; 6]> = children
.iter()
.map(|&c| cache.get(&c).copied().unwrap_or(c))
.collect();
if new == *children {
id
} else {
arena.mul(&new)
}
}
ExprNode::Pow(base, exp) => {
let nb = cache.get(&base).copied().unwrap_or(base);
let ne = cache.get(&exp).copied().unwrap_or(exp);
if nb == base && ne == exp {
id
} else {
arena.pow(nb, ne)
}
}
ExprNode::Neg(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
if ni == inner { id } else { arena.neg(ni) }
}
_ => {
crate::base::walk::rebuild_with_cache(arena, id, &cache)
}
};
cache.insert(id, expanded);
}
cache.get(&expr).copied().unwrap_or(expr)
}
pub(crate) fn expand_ln_node_guarded(
arena: &mut Arena,
assumptions: &mut AssumptionCache,
inner: ExprId,
force: bool,
) -> ExprId {
let mut work: Vec<(ExprId, ExprId)> = vec![(arena.one, inner)];
let mut terms: smallvec::SmallVec<[ExprId; 6]> = smallvec::SmallVec::new();
while let Some((coeff, arg)) = work.pop() {
match arena.node(arg).clone() {
ExprNode::Mul(ref children) => {
let mut split = false;
let mut rest: smallvec::SmallVec<[ExprId; 6]> = smallvec::SmallVec::new();
for &c in children {
if force || assumptions.query(arena, c, Props::POSITIVE) == Some(true) {
work.push((coeff, c));
split = true;
} else if c != arena.neg_one
&& assumptions.query(arena, c, Props::NEGATIVE) == Some(true)
{
let neg_c = arena.neg(c);
work.push((coeff, neg_c));
rest.push(arena.neg_one);
split = true;
} else {
rest.push(c);
}
}
if !split {
let ln_arg = arena.ln(arg);
terms.push(arena.mul(&[coeff, ln_arg]));
} else if !rest.is_empty() {
let remaining = arena.mul(&rest);
if remaining == arena.neg_one {
let i_pi = arena.mul(&[arena.i_unit, arena.pi]);
terms.push(arena.mul(&[coeff, i_pi]));
} else if remaining != arena.one {
work.push((coeff, remaining));
}
}
}
ExprNode::Pow(base, exp) if pow_log_splits(arena, assumptions, base, exp, force) => {
let c = arena.mul(&[coeff, exp]);
work.push((c, base));
}
ExprNode::Exp(w) if force || assumptions.query(arena, w, Props::REAL) == Some(true) => {
terms.push(arena.mul(&[coeff, w]));
}
_ => {
let ln_arg = arena.ln(arg);
terms.push(arena.mul(&[coeff, ln_arg]));
}
}
}
match terms.len() {
0 => arena.zero,
1 => terms[0],
_ => arena.add(&terms),
}
}
fn pow_log_splits(
arena: &Arena,
assumptions: &mut AssumptionCache,
base: ExprId,
exp: ExprId,
force: bool,
) -> bool {
if force {
return true;
}
if assumptions.query(arena, exp, Props::REAL) != Some(true) {
return false;
}
let in_principal_range = arena
.as_num(exp)
.is_some_and(|e| *e > crate::base::numeric::qi(-1) && *e <= crate::base::numeric::qi(1));
in_principal_range || assumptions.query(arena, base, Props::POSITIVE) == Some(true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::arena::Arena;
fn sym(a: &mut Arena, name: &str) -> ExprId {
a.symbol(name)
}
fn display(a: &Arena, id: ExprId) -> String {
a.display(id).to_string()
}
fn positive(a: &mut Arena, name: &str) -> ExprId {
let s = a.symbol(name);
if let ExprNode::Symbol(sid) = *a.node(s) {
let mut asm = crate::base::assumptions::Assumptions::default();
asm.assert_true(Props::POSITIVE);
asm.forward_chain();
a.set_symbol_assumptions(sid, asm);
}
s
}
fn forced(a: &mut Arena, e: ExprId) -> String {
let r = expand_log_with(a, e, true);
display(a, r)
}
fn guarded(a: &mut Arena, e: ExprId) -> String {
let r = expand_log(a, e);
display(a, r)
}
#[test]
fn forced_expand_ln_product() {
let mut a = Arena::new();
let (x, y) = (sym(&mut a, "x"), sym(&mut a, "y"));
let product = a.mul(&[x, y]);
let expr = a.ln(product);
assert_eq!(forced(&mut a, expr), "ln(x) + ln(y)");
}
#[test]
fn forced_expand_ln_power_and_quotient() {
let mut a = Arena::new();
let (x, y) = (sym(&mut a, "x"), sym(&mut a, "y"));
let two = a.int(2);
let x2 = a.pow(x, two);
let e = a.ln(x2);
assert_eq!(forced(&mut a, e), "2*ln(x)");
let q = a.div(x, y);
let e = a.ln(q);
assert_eq!(forced(&mut a, e), "-ln(y) + ln(x)");
}
#[test]
fn expand_ln_bare_symbol_unchanged() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let expr = a.ln(x);
assert_eq!(guarded(&mut a, expr), "ln(x)");
assert_eq!(forced(&mut a, expr), "ln(x)");
}
#[test]
fn expand_log_inside_sin() {
let mut a = Arena::new();
let (x, y) = (positive(&mut a, "x"), positive(&mut a, "y"));
let product = a.mul(&[x, y]);
let ln_product = a.ln(product);
let expr = a.sin(ln_product);
assert_eq!(guarded(&mut a, expr), "sin(ln(x) + ln(y))");
}
#[test]
fn expand_ln_nested_in_one_pass() {
let mut a = Arena::new();
let (x, y) = (positive(&mut a, "x"), sym(&mut a, "y"));
let two = a.int(2);
let x2 = a.pow(x, two);
let product = a.mul(&[x2, y]);
let expr = a.ln(product);
assert_eq!(guarded(&mut a, expr), "2*ln(x) + ln(y)");
}
#[test]
fn guarded_expand_splits_off_positive_factors() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let e = a.mul(&[two, x]);
let e = a.ln(e);
assert_eq!(guarded(&mut a, e), "ln(2) + ln(x)");
}
#[test]
fn guarded_expand_moves_the_sign_of_a_negative_factor() {
let mut a = Arena::new();
let (x, p) = (sym(&mut a, "x"), positive(&mut a, "p"));
let (m2, m3) = (a.int(-2), a.int(-3));
let e = a.mul(&[m2, x]);
let e = a.ln(e);
assert_eq!(guarded(&mut a, e), "ln(2) + ln(-x)");
let e = a.mul(&[m3, p]);
let e = a.ln(e);
assert_eq!(guarded(&mut a, e), "pi*I + ln(3) + ln(p)");
}
#[test]
fn guarded_expand_keeps_powers_outside_the_principal_range() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let (two, neg_one) = (a.int(2), a.int(-1));
for exp in [two, neg_one] {
let p = a.pow(x, exp);
let e = a.ln(p);
assert_eq!(expand_log(&mut a, e), e);
}
let half = a.rational(1, 2);
let e = a.pow(x, half);
let e = a.ln(e);
assert_eq!(guarded(&mut a, e), "1/2*ln(x)");
let third = a.rational(1, 3);
let e = a.pow(x, third);
let e = a.ln(e);
assert_eq!(guarded(&mut a, e), "1/3*ln(x)");
}
#[test]
fn guarded_expand_of_exp_needs_a_real_exponent() {
let mut a = Arena::new();
let (x, p) = (sym(&mut a, "x"), positive(&mut a, "p"));
let ex = a.exp(x);
let e = a.ln(ex);
assert_eq!(expand_log(&mut a, e), e);
assert_eq!(forced(&mut a, e), "x");
let ep = a.exp(p);
let e = a.ln(ep);
assert_eq!(guarded(&mut a, e), "p");
}
#[test]
fn guarded_expand_requires_positive_factors() {
let mut a = Arena::new();
let (x, y) = (sym(&mut a, "x"), sym(&mut a, "y"));
let xy = a.mul(&[x, y]);
let e = a.ln(xy);
assert_eq!(expand_log_with(&mut a, e, false), e);
let forced = expand_log_with(&mut a, e, true);
assert_eq!(display(&a, forced), "ln(x) + ln(y)");
for s in [x, y] {
if let ExprNode::Symbol(sid) = *a.node(s) {
let mut asm = crate::base::assumptions::Assumptions::default();
asm.assert_true(crate::base::assumptions::Props::POSITIVE);
asm.forward_chain();
a.set_symbol_assumptions(sid, asm);
}
}
let guarded = expand_log_with(&mut a, e, false);
assert_eq!(display(&a, guarded), "ln(x) + ln(y)");
}
#[test]
fn forced_expand_ln_of_product_with_power_factor() {
let mut a = Arena::new();
let (x, y) = (sym(&mut a, "x"), sym(&mut a, "y"));
let two = a.int(2);
let y2 = a.pow(y, two);
let xy2 = a.mul(&[x, y2]);
let e = a.ln(xy2);
assert_eq!(forced(&mut a, e), "2*ln(y) + ln(x)");
}
}