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 log_combine(arena: &mut Arena, expr: ExprId) -> ExprId {
log_combine_with(arena, expr, false)
}
pub(crate) fn log_combine_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 combined = match node {
ExprNode::Mul(ref children) => {
let new: smallvec::SmallVec<[ExprId; 6]> = children
.iter()
.map(|&c| cache.get(&c).copied().unwrap_or(c))
.collect();
combine_mul_ln(arena, &mut assumptions, id, children, &new, force)
}
ExprNode::Add(ref children) => {
let new: smallvec::SmallVec<[ExprId; 6]> = children
.iter()
.map(|&c| cache.get(&c).copied().unwrap_or(c))
.collect();
combine_add_ln(arena, &mut assumptions, id, children, &new, force)
}
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) }
}
ExprNode::Ln(inner) => {
let ni = cache.get(&inner).copied().unwrap_or(inner);
if ni == inner { id } else { arena.ln(ni) }
}
_ => {
crate::base::walk::rebuild_with_cache(arena, id, &cache)
}
};
cache.insert(id, combined);
}
cache.get(&expr).copied().unwrap_or(expr)
}
fn combine_mul_ln(
arena: &mut Arena,
assumptions: &mut AssumptionCache,
id: ExprId,
original: &[ExprId],
new: &[ExprId],
force: bool,
) -> ExprId {
let mut ln_factor: Option<(usize, ExprId)> = None;
let mut ln_count: usize = 0;
for (i, &child) in new.iter().enumerate() {
if let ExprNode::Ln(inner) = *arena.node(child) {
ln_count += 1;
if ln_count == 1 {
ln_factor = Some((i, inner));
}
}
}
let (idx, ln_arg) = match ln_factor {
Some(f) if ln_count == 1 && new.len() >= 2 => f,
_ => return if new == original { id } else { arena.mul(new) },
};
let coeff_factors: smallvec::SmallVec<[ExprId; 6]> = new
.iter()
.enumerate()
.filter(|&(i, _)| i != idx)
.map(|(_, &c)| c)
.collect();
let coeff = if coeff_factors.len() == 1 {
coeff_factors[0]
} else {
arena.mul(&coeff_factors)
};
let coeff_in_principal_range = arena
.as_num(coeff)
.is_some_and(|c| *c > crate::base::numeric::qi(-1) && *c <= crate::base::numeric::qi(1));
let guard_ok = force
|| coeff_in_principal_range
|| (assumptions.query(arena, ln_arg, Props::POSITIVE) == Some(true)
&& assumptions.query(arena, coeff, Props::REAL) == Some(true));
if !guard_ok {
return if new == original { id } else { arena.mul(new) };
}
let powered = arena.pow(ln_arg, coeff);
arena.ln(powered)
}
fn combine_add_ln(
arena: &mut Arena,
assumptions: &mut AssumptionCache,
id: ExprId,
original: &[ExprId],
new: &[ExprId],
force: bool,
) -> ExprId {
let mut ln_inner_args: smallvec::SmallVec<[ExprId; 6]> = smallvec::SmallVec::new();
let mut unknown: smallvec::SmallVec<[(ExprId, ExprId); 6]> = smallvec::SmallVec::new();
let mut others: smallvec::SmallVec<[ExprId; 6]> = smallvec::SmallVec::new();
for &child in new {
if let ExprNode::Ln(inner) = *arena.node(child) {
if force || assumptions.query(arena, inner, Props::POSITIVE) == Some(true) {
ln_inner_args.push(inner);
} else {
unknown.push((child, inner));
}
} else {
others.push(child);
}
}
if unknown.len() == 1 {
ln_inner_args.push(unknown[0].1);
} else {
others.extend(unknown.iter().map(|&(child, _)| child));
}
if ln_inner_args.len() >= 2 {
let product = arena.mul(&ln_inner_args);
let combined_ln = arena.ln(product);
if others.is_empty() {
combined_ln
} else {
others.push(combined_ln);
arena.add(&others)
}
} else if new == original {
id
} else {
arena.add(new)
}
}
#[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 = log_combine_with(a, e, true);
display(a, r)
}
fn guarded(a: &mut Arena, e: ExprId) -> String {
let r = log_combine(a, e);
display(a, r)
}
#[test]
fn forced_combine_two_and_three_logs() {
let mut a = Arena::new();
let (x, y, z) = (sym(&mut a, "x"), sym(&mut a, "y"), sym(&mut a, "z"));
let (lnx, lny, lnz) = (a.ln(x), a.ln(y), a.ln(z));
let two = a.add(&[lnx, lny]);
assert_eq!(forced(&mut a, two), "ln(x*y)");
let three = a.add(&[lnx, lny, lnz]);
assert_eq!(forced(&mut a, three), "ln(x*y*z)");
}
#[test]
fn forced_combine_coeff_log() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let two = a.int(2);
let lnx = a.ln(x);
let expr = a.mul(&[two, lnx]);
assert_eq!(forced(&mut a, expr), "ln(x^2)");
}
#[test]
fn no_combine_single_log() {
let mut a = Arena::new();
let x = sym(&mut a, "x");
let lnx = a.ln(x);
let one = a.int(1);
let expr = a.add(&[lnx, one]);
assert_eq!(log_combine(&mut a, expr), expr);
assert_eq!(log_combine_with(&mut a, expr, true), expr);
}
#[test]
fn forced_combine_keeps_other_terms() {
let mut a = Arena::new();
let (x, y, z) = (sym(&mut a, "x"), sym(&mut a, "y"), sym(&mut a, "z"));
let (lnx, lny) = (a.ln(x), a.ln(y));
let expr = a.add(&[lnx, lny, z]);
assert_eq!(forced(&mut a, expr), "z + ln(x*y)");
}
#[test]
fn combine_logs_inside_exp() {
let mut a = Arena::new();
let (x, y) = (positive(&mut a, "x"), positive(&mut a, "y"));
let (ln_x, ln_y) = (a.ln(x), a.ln(y));
let sum = a.add(&[ln_x, ln_y]);
let expr = a.exp(sum);
assert_eq!(guarded(&mut a, expr), "exp(ln(x*y))");
}
#[test]
fn bare_symbol_unchanged() {
let mut a = Arena::new();
let (x, y) = (sym(&mut a, "x"), sym(&mut a, "y"));
let expr = a.add(&[x, y]);
assert_eq!(log_combine(&mut a, expr), expr);
}
#[test]
fn guarded_combine_leaves_two_unknown_logs() {
let mut a = Arena::new();
let (x, y) = (sym(&mut a, "x"), sym(&mut a, "y"));
let (lnx, lny) = (a.ln(x), a.ln(y));
let e = a.add(&[lnx, lny]);
assert_eq!(log_combine(&mut a, e), e);
}
#[test]
fn guarded_combine_joins_positive_logs_with_one_other() {
let mut a = Arena::new();
let (x, y, p) = (sym(&mut a, "x"), sym(&mut a, "y"), positive(&mut a, "p"));
let two = a.int(2);
let (ln2, lnx, lny, lnp) = (a.ln(two), a.ln(x), a.ln(y), a.ln(p));
let e = a.add(&[ln2, lnx]);
assert_eq!(guarded(&mut a, e), "ln(2*x)");
let e = a.add(&[lnp, lnx]);
assert_eq!(guarded(&mut a, e), "ln(p*x)");
let e = a.add(&[ln2, lnp, lnx, lny]);
assert_eq!(guarded(&mut a, e), "ln(x) + ln(y) + ln(2*p)");
}
#[test]
fn guarded_combine_coefficient_needs_positive_argument_or_principal_range() {
let mut a = Arena::new();
let (x, p) = (sym(&mut a, "x"), positive(&mut a, "p"));
let (two, neg_one, half) = (a.int(2), a.int(-1), a.rational(1, 2));
let (lnx, lnp) = (a.ln(x), a.ln(p));
let e = a.mul(&[two, lnp]);
assert_eq!(guarded(&mut a, e), "ln(p^2)");
for c in [two, neg_one] {
let e = a.mul(&[c, lnx]);
assert_eq!(log_combine(&mut a, e), e);
}
let e = a.mul(&[half, lnx]);
assert_eq!(guarded(&mut a, e), "ln(sqrt(x))");
}
}