#![allow(clippy::needless_range_loop)]
use crate::base::arena::Arena;
use crate::base::node::{ExprId, ExprNode};
use crate::base::walk;
use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::ToPrimitive;
use rustc_hash::FxHashMap;
pub(crate) fn combsimp(arena: &mut Arena, expr: ExprId) -> ExprId {
let post_order = walk::post_order_ids(arena, expr);
let mut cache: FxHashMap<ExprId, ExprId> = FxHashMap::default();
for &id in &post_order {
let rebuilt = crate::base::walk::rebuild_with_cache(arena, id, &cache);
let simplified = combsimp_node(arena, rebuilt);
cache.insert(id, simplified);
}
cache.get(&expr).copied().unwrap_or(expr)
}
fn combsimp_node(arena: &mut Arena, id: ExprId) -> ExprId {
let node = arena.node(id).clone();
match node {
ExprNode::Mul(ref children) => {
let children_vec: Vec<ExprId> = children.iter().copied().collect();
simplify_factorial_mul(arena, id, &children_vec)
}
_ => id,
}
}
struct FactorialTerm {
child_idx: usize,
arg: ExprId,
}
fn simplify_factorial_mul(arena: &mut Arena, original: ExprId, children: &[ExprId]) -> ExprId {
let neg_one = Ratio::from_integer(BigInt::from(-1));
let mut numer_facts: Vec<FactorialTerm> = Vec::new();
let mut denom_facts: Vec<FactorialTerm> = Vec::new();
for (idx, &child) in children.iter().enumerate() {
match arena.node(child).clone() {
ExprNode::Factorial(arg) => {
numer_facts.push(FactorialTerm {
child_idx: idx,
arg,
});
}
ExprNode::Pow(base, exp) => {
if let Some(val) = arena.as_num(exp)
&& *val == neg_one
{
match arena.node(base).clone() {
ExprNode::Factorial(arg) => {
denom_facts.push(FactorialTerm {
child_idx: idx,
arg,
});
}
ExprNode::Mul(ref mul_children) => {
let mut all_factorial = true;
let mut fact_args = Vec::new();
for &mc in mul_children.iter() {
if let ExprNode::Factorial(arg) = arena.node(mc).clone() {
fact_args.push(arg);
} else {
all_factorial = false;
break;
}
}
if all_factorial {
for arg in fact_args {
denom_facts.push(FactorialTerm {
child_idx: idx,
arg,
});
}
}
}
_ => {}
}
}
}
_ => {}
}
}
for ni in 0..numer_facts.len() {
for di in 0..denom_facts.len() {
let n_arg = numer_facts[ni].arg;
let d_arg = denom_facts[di].arg;
if n_arg == d_arg {
return remove_and_replace(
arena,
children,
original,
&[numer_facts[ni].child_idx, denom_facts[di].child_idx],
None,
);
}
let diff_expr = arena.sub(n_arg, d_arg);
let diff_eval = crate::transforms::eval::eval(arena, diff_expr);
if let Some(val) = arena.as_num(diff_eval).cloned() {
if !val.is_integer() {
continue;
}
let k: Option<i64> = val.to_integer().to_i64();
let k = match k {
Some(k) => k,
None => continue,
};
if k > 0 && k <= 20 {
let replacement = if k == 1 {
n_arg
} else {
let mut factors = Vec::with_capacity(k as usize);
for i in 1..=k {
let offset = arena.int(i);
let factor = arena.add(&[d_arg, offset]);
let factor_eval = crate::transforms::eval::eval(arena, factor);
factors.push(factor_eval);
}
arena.mul(&factors)
};
return remove_and_replace(
arena,
children,
original,
&[numer_facts[ni].child_idx, denom_facts[di].child_idx],
Some(replacement),
);
} else if k == 0 {
return remove_and_replace(
arena,
children,
original,
&[numer_facts[ni].child_idx, denom_facts[di].child_idx],
None,
);
}
}
}
}
if numer_facts.len() == 1 && denom_facts.len() == 2 {
let n_arg = numer_facts[0].arg;
let k1_arg = denom_facts[0].arg;
let k2_arg = denom_facts[1].arg;
let sum_expr = arena.add(&[k1_arg, k2_arg]);
let sum_eval = crate::transforms::eval::eval(arena, sum_expr);
let diff_expr = arena.sub(n_arg, sum_eval);
let diff_eval = crate::transforms::eval::eval(arena, diff_expr);
if arena.is_zero_structural(diff_eval) {
let binom = arena.binomial(n_arg, k1_arg);
return remove_and_replace(
arena,
children,
original,
&[
numer_facts[0].child_idx,
denom_facts[0].child_idx,
denom_facts[1].child_idx,
],
Some(binom),
);
}
}
original
}
fn remove_and_replace(
arena: &mut Arena,
children: &[ExprId],
_original: ExprId,
used_indices: &[usize],
replacement: Option<ExprId>,
) -> ExprId {
let mut new_children: Vec<ExprId> = children
.iter()
.enumerate()
.filter(|&(idx, _)| !used_indices.contains(&idx))
.map(|(_, &c)| c)
.collect();
if let Some(r) = replacement {
new_children.push(r);
}
match new_children.len() {
0 => arena.one,
1 => new_children[0],
_ => arena.mul(&new_children),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sym(arena: &mut Arena, name: &str) -> ExprId {
arena.symbol(name)
}
fn display(arena: &Arena, id: ExprId) -> String {
arena.display(id).to_string()
}
#[test]
fn factorial_ratio_concrete_5_over_4() {
let mut arena = Arena::new();
let five = arena.int(5);
let four = arena.int(4);
let five_fact = arena.factorial(five);
let four_fact = arena.factorial(four);
let ratio = arena.div(five_fact, four_fact);
let evaled = crate::transforms::eval::eval(&mut arena, ratio);
let result = combsimp(&mut arena, evaled);
assert_eq!(display(&arena, result), "5");
}
#[test]
fn factorial_ratio_symbolic_n_over_n_minus_1() {
let mut arena = Arena::new();
let n = sym(&mut arena, "n");
let one = arena.int(1);
let n_minus_1 = arena.sub(n, one);
let n_fact = arena.factorial(n);
let nm1_fact = arena.factorial(n_minus_1);
let ratio = arena.div(n_fact, nm1_fact);
let result = combsimp(&mut arena, ratio);
assert_eq!(display(&arena, result), "n");
}
#[test]
fn factorial_ratio_same_cancels() {
let mut arena = Arena::new();
let n = sym(&mut arena, "n");
let n_fact = arena.factorial(n);
let ratio = arena.div(n_fact, n_fact);
let result = combsimp(&mut arena, ratio);
assert_eq!(display(&arena, result), "1");
}
#[test]
fn factorial_ratio_difference_2() {
let mut arena = Arena::new();
let n = sym(&mut arena, "n");
let two = arena.int(2);
let n_minus_2 = arena.sub(n, two);
let n_fact = arena.factorial(n);
let nm2_fact = arena.factorial(n_minus_2);
let ratio = arena.div(n_fact, nm2_fact);
let result = combsimp(&mut arena, ratio);
let s = display(&arena, result);
assert!(
!s.contains('!') && !s.contains("factorial"),
"should not contain factorials: {s}"
);
}
#[test]
fn combsimp_no_factorial_unchanged() {
let mut arena = Arena::new();
let x = sym(&mut arena, "x");
let y = sym(&mut arena, "y");
let expr = arena.add(&[x, y]);
let result = combsimp(&mut arena, expr);
assert_eq!(result, expr);
}
#[test]
fn binomial_detection() {
let mut arena = Arena::new();
let n = sym(&mut arena, "n");
let k = sym(&mut arena, "k");
let n_minus_k = arena.sub(n, k);
let n_fact = arena.factorial(n);
let k_fact = arena.factorial(k);
let nmk_fact = arena.factorial(n_minus_k);
let denom = arena.mul(&[k_fact, nmk_fact]);
let ratio = arena.div(n_fact, denom);
let result = combsimp(&mut arena, ratio);
let s = display(&arena, result);
assert!(
s.contains("C(") || s.contains("binomial"),
"should produce binomial coefficient, got: {s}"
);
}
}