use crate::base::arena::Arena;
use crate::base::node::{ExprId, ExprNode, SymbolId};
use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Zero};
fn binom(n: u64, k: u64) -> BigInt {
if k > n {
return BigInt::zero();
}
if k == 0 || k == n {
return BigInt::one();
}
let k = k.min(n - k);
let mut result = BigInt::one();
for i in 0..k {
result = result * BigInt::from(n - i) / BigInt::from(i + 1);
}
result
}
fn contains_var(arena: &Arena, expr: ExprId, var_sym: SymbolId) -> bool {
crate::base::walk::has_free_symbol(arena, expr, var_sym)
}
fn as_i64(arena: &Arena, id: ExprId) -> Option<i64> {
let r = arena.as_num(id)?;
if !r.is_integer() {
return None;
}
let big = r.to_integer();
i64::try_from(&big).ok()
}
#[derive(Clone, Copy)]
struct TrigPower {
arg: ExprId,
exp: i64,
}
fn extract_power(
arena: &Arena,
factor: ExprId,
func: fn(&ExprNode) -> Option<ExprId>,
) -> Option<TrigPower> {
if let Some(arg) = func(arena.node(factor)) {
return Some(TrigPower { arg, exp: 1 });
}
match arena.node(factor) {
ExprNode::Pow(base, exp) => {
let arg = func(arena.node(*base))?;
Some(TrigPower {
arg,
exp: as_i64(arena, *exp)?,
})
}
_ => None,
}
}
fn sin_arg(node: &ExprNode) -> Option<ExprId> {
match node {
ExprNode::Sin(u) => Some(*u),
_ => None,
}
}
fn cos_arg(node: &ExprNode) -> Option<ExprId> {
match node {
ExprNode::Cos(u) => Some(*u),
_ => None,
}
}
fn tan_arg(node: &ExprNode) -> Option<ExprId> {
match node {
ExprNode::Tan(u) => Some(*u),
_ => None,
}
}
fn sinh_arg(node: &ExprNode) -> Option<ExprId> {
match node {
ExprNode::Sinh(u) => Some(*u),
_ => None,
}
}
fn cosh_arg(node: &ExprNode) -> Option<ExprId> {
match node {
ExprNode::Cosh(u) => Some(*u),
_ => None,
}
}
fn linear_rate(arena: &mut Arena, u: ExprId, var: ExprId, var_sym: SymbolId) -> Option<ExprId> {
if u == var {
return Some(arena.one);
}
if !contains_var(arena, u, var_sym) {
return None;
}
let a = crate::transforms::diff::diff(arena, u, var);
(a != arena.zero && !contains_var(arena, a, var_sym)).then_some(a)
}
const MAX_TRIG_POWER: i64 = 256;
fn within_power_bound(m: i64, n: i64) -> bool {
m.checked_abs().is_some_and(|a| a <= MAX_TRIG_POWER)
&& n.checked_abs().is_some_and(|a| a <= MAX_TRIG_POWER)
}
#[cfg(test)]
fn extract_sin_power(arena: &Arena, factor: ExprId, var: ExprId) -> Option<i64> {
extract_power(arena, factor, sin_arg)
.filter(|p| p.arg == var)
.map(|p| p.exp)
}
#[cfg(test)]
fn extract_cos_power(arena: &Arena, factor: ExprId, var: ExprId) -> Option<i64> {
extract_power(arena, factor, cos_arg)
.filter(|p| p.arg == var)
.map(|p| p.exp)
}
const MAX_LINEARIZE_DEGREE: u32 = 12;
pub(crate) fn linearize_sin_cos(arena: &mut Arena, u: ExprId, m: u32, n: u32) -> Option<ExprId> {
use std::collections::BTreeMap;
if m + n > MAX_LINEARIZE_DEGREE {
return None;
}
let half = Ratio::new(BigInt::one(), BigInt::from(2));
let mut terms: BTreeMap<u32, (Ratio<BigInt>, Ratio<BigInt>)> = BTreeMap::new();
terms.insert(0, (Ratio::one(), Ratio::zero()));
let zero = || (Ratio::<BigInt>::zero(), Ratio::<BigInt>::zero());
for step in 0..m + n {
let by_sin = step < m;
let mut next: BTreeMap<u32, (Ratio<BigInt>, Ratio<BigInt>)> = BTreeMap::new();
for (k, (a, b)) in terms {
let (ah, bh) = (&a * &half, &b * &half);
if by_sin {
let up = next.entry(k + 1).or_insert_with(zero);
up.1 += &ah;
up.0 -= &bh;
let down = next.entry(k.abs_diff(1)).or_insert_with(zero);
if k >= 1 {
down.1 -= &ah;
} else {
down.1 += &ah;
}
down.0 += &bh;
} else {
let up = next.entry(k + 1).or_insert_with(zero);
up.0 += &ah;
up.1 += &bh;
let down = next.entry(k.abs_diff(1)).or_insert_with(zero);
down.0 += &ah;
if k >= 1 {
down.1 += &bh;
} else {
down.1 -= &bh;
}
}
}
terms = next;
}
let mut out: Vec<ExprId> = Vec::new();
for (k, (a, b)) in terms {
if k == 0 {
if !a.is_zero() {
out.push(arena.num_ratio(a));
}
continue;
}
let k_id = arena.int(i64::from(k));
let ku = arena.mul(&[k_id, u]);
for (c, is_sin) in [(a, false), (b, true)] {
if c.is_zero() {
continue;
}
let g = if is_sin { arena.sin(ku) } else { arena.cos(ku) };
let c_id = arena.num_ratio(c);
out.push(arena.mul(&[c_id, g]));
}
}
Some(match out.len() {
0 => arena.zero,
1 => out[0],
_ => arena.add(&out),
})
}
fn trig_exp_polynomial(arena: &mut Arena, e: ExprId, var: ExprId, var_sym: SymbolId) -> bool {
let mut arguments: Vec<ExprId> = Vec::new();
let mut stack = vec![e];
let mut seen: rustc_hash::FxHashSet<ExprId> = rustc_hash::FxHashSet::default();
while let Some(id) = stack.pop() {
if !seen.insert(id) || !contains_var(arena, id, var_sym) {
continue;
}
match arena.node(id) {
ExprNode::Symbol(_) => {}
ExprNode::Add(ch) | ExprNode::Mul(ch) => stack.extend(ch.iter().copied()),
ExprNode::Pow(b, n) if as_i64(arena, *n).is_some_and(|k| k >= 0) => stack.push(*b),
ExprNode::Exp(u) | ExprNode::Sin(u) | ExprNode::Cos(u) => arguments.push(*u),
_ => return false,
}
}
arguments
.into_iter()
.all(|u| linear_rate(arena, u, var, var_sym).is_some())
}
pub(crate) fn linearize_trig_powers(
arena: &mut Arena,
factors: &[ExprId],
var: ExprId,
var_sym: SymbolId,
) -> Option<ExprId> {
let mut groups: Vec<(ExprId, u32, u32)> = Vec::new();
for &f in factors {
let found = match extract_power(arena, f, sin_arg) {
Some(p) => Some((p, true)),
None => extract_power(arena, f, cos_arg).map(|p| (p, false)),
};
let Some((p, is_sin)) = found else {
continue;
};
let e = u32::try_from(p.exp).ok()?;
match groups.iter_mut().find(|g| g.0 == p.arg) {
Some(g) if is_sin => g.1 += e,
Some(g) => g.2 += e,
None => groups.push(if is_sin { (p.arg, e, 0) } else { (p.arg, 0, e) }),
}
}
let &(u, m, n) = groups.iter().find(|g| g.1 + g.2 >= 2)?;
for &f in factors {
if !trig_exp_polynomial(arena, f, var, var_sym) {
return None;
}
}
let is_group_member = |arena: &Arena, f: ExprId| {
extract_power(arena, f, sin_arg)
.or_else(|| extract_power(arena, f, cos_arg))
.is_some_and(|p| p.arg == u)
};
let mut rest: Vec<ExprId> = factors
.iter()
.copied()
.filter(|&f| !is_group_member(arena, f))
.collect();
let linear = linearize_sin_cos(arena, u, m, n)?;
rest.push(linear);
Some(arena.mul(&rest))
}
fn over_rate(arena: &mut Arena, big_f: ExprId, rate: ExprId) -> ExprId {
if rate == arena.one {
big_f
} else {
arena.div(big_f, rate)
}
}
pub(crate) fn sin_pow_integrate(arena: &mut Arena, n: i64, var: ExprId) -> ExprId {
if n == 0 {
return var; }
if n == 1 {
let cos_x = arena.cos(var);
return arena.neg(cos_x);
}
if n == -1 {
let sin_x = arena.sin(var);
let cos_x = arena.cos(var);
let neg_one_id = arena.int(-1);
let csc_x = arena.pow(sin_x, neg_one_id); let cot_x = arena.mul(&[cos_x, csc_x]); let sum = arena.add(&[csc_x, cot_x]);
let abs_sum = arena.abs(sum);
let ln_val = arena.ln(abs_sum);
return arena.neg(ln_val);
}
if n < -1 {
let cos_x = arena.cos(var);
let sin_x = arena.sin(var);
let exp_id = arena.int(n + 1);
let sin_pow = arena.pow(sin_x, exp_id);
let inv = arena.rational(1, n + 1);
let first_term = arena.mul(&[inv, cos_x, sin_pow]);
let coeff = arena.rational(n + 2, n + 1);
let recursive = sin_pow_integrate(arena, n + 2, var);
let second_term = arena.mul(&[coeff, recursive]);
return arena.add(&[first_term, second_term]);
}
let cos_x = arena.cos(var);
let sin_x = arena.sin(var);
let sin_pow = if n - 1 == 1 {
sin_x
} else {
let n_minus_1 = arena.int(n - 1);
arena.pow(sin_x, n_minus_1)
};
let neg_inv_n = arena.rational(-1, n);
let first_term = arena.mul(&[neg_inv_n, cos_x, sin_pow]);
let coeff = arena.rational(n - 1, n);
let recursive = sin_pow_integrate(arena, n - 2, var);
let second_term = arena.mul(&[coeff, recursive]);
arena.add(&[first_term, second_term])
}
pub(crate) fn cos_pow_integrate(arena: &mut Arena, n: i64, var: ExprId) -> ExprId {
if n == 0 {
return var; }
if n == 1 {
return arena.sin(var);
}
if n == -1 {
let cos_x = arena.cos(var);
let neg_one_id = arena.int(-1);
let sec_x = arena.pow(cos_x, neg_one_id); let tan_x = arena.tan(var);
let sum = arena.add(&[sec_x, tan_x]);
let abs_sum = arena.abs(sum);
return arena.ln(abs_sum);
}
if n < -1 {
let sin_x = arena.sin(var);
let cos_x = arena.cos(var);
let exp_id = arena.int(n + 1);
let cos_pow = arena.pow(cos_x, exp_id);
let neg_inv = arena.rational(-1, n + 1);
let first_term = arena.mul(&[neg_inv, sin_x, cos_pow]);
let coeff = arena.rational(n + 2, n + 1);
let recursive = cos_pow_integrate(arena, n + 2, var);
let second_term = arena.mul(&[coeff, recursive]);
return arena.add(&[first_term, second_term]);
}
let sin_x = arena.sin(var);
let cos_x = arena.cos(var);
let cos_pow = if n - 1 == 1 {
cos_x
} else {
let n_minus_1 = arena.int(n - 1);
arena.pow(cos_x, n_minus_1)
};
let inv_n = arena.rational(1, n);
let first_term = arena.mul(&[inv_n, sin_x, cos_pow]);
let coeff = arena.rational(n - 1, n);
let recursive = cos_pow_integrate(arena, n - 2, var);
let second_term = arena.mul(&[coeff, recursive]);
arena.add(&[first_term, second_term])
}
pub(crate) fn sin_cos_integrate(arena: &mut Arena, m: i64, n: i64, var: ExprId) -> ExprId {
if m == 0 {
return cos_pow_integrate(arena, n, var);
}
if n == 0 {
return sin_pow_integrate(arena, m, var);
}
if m > 0 && m % 2 != 0 {
let cos_x = arena.cos(var);
return odd_power_substitution(arena, (m - 1) / 2, n, cos_x, true);
}
if n > 0 && n % 2 != 0 {
let sin_x = arena.sin(var);
return odd_power_substitution(arena, (n - 1) / 2, m, sin_x, false);
}
if n >= 2 && m < 0 {
return even_power_expansion(arena, n / 2, m, var, sin_pow_integrate);
}
if m >= 2 && n < 0 {
return even_power_expansion(arena, m / 2, n, var, cos_pow_integrate);
}
if m < 0 && n < 0 {
return both_negative_powers(arena, m, n, var);
}
debug_assert!(m % 2 == 0 && n % 2 == 0 && m >= 2 && n >= 2);
let sin_x = arena.sin(var);
let cos_x = arena.cos(var);
let mn = m + n;
let sin_pow = if m - 1 == 1 {
sin_x
} else {
let exp = arena.int(m - 1);
arena.pow(sin_x, exp)
};
let cos_pow = {
let exp = arena.int(n + 1);
arena.pow(cos_x, exp)
};
let neg_inv_mn = arena.rational(-1, mn);
let first_term = arena.mul(&[neg_inv_mn, sin_pow, cos_pow]);
let coeff = arena.rational(m - 1, mn);
let recursive = sin_cos_integrate(arena, m - 2, n, var);
let second_term = arena.mul(&[coeff, recursive]);
arena.add(&[first_term, second_term])
}
fn odd_power_substitution(arena: &mut Arena, k: i64, e: i64, u: ExprId, negate: bool) -> ExprId {
let mut terms: Vec<ExprId> = Vec::new();
for j in 0..=k {
let mut c = binom(k as u64, j as u64);
if (j % 2 != 0) != negate {
c = -c;
}
let power = e + 2 * j + 1;
let term = if power == 0 {
let abs_u = arena.abs(u);
let ln = arena.ln(abs_u);
let coeff = arena.intern_num(Ratio::from_integer(c));
let coeff = arena.intern(ExprNode::Num(coeff));
arena.mul(&[coeff, ln])
} else {
let ratio = Ratio::new(c, BigInt::from(power));
let coeff = arena.intern_num(ratio);
let coeff = arena.intern(ExprNode::Num(coeff));
let pow_id = arena.int(power);
let u_pow = arena.pow(u, pow_id);
arena.mul(&[coeff, u_pow])
};
terms.push(term);
}
arena.add(&terms)
}
fn even_power_expansion(
arena: &mut Arena,
h: i64,
e: i64,
var: ExprId,
single: fn(&mut Arena, i64, ExprId) -> ExprId,
) -> ExprId {
let mut terms: Vec<ExprId> = Vec::new();
for j in 0..=h {
let mut c = binom(h as u64, j as u64);
if j % 2 != 0 {
c = -c;
}
let integral = single(arena, e + 2 * j, var);
let coeff = arena.intern_num(Ratio::from_integer(c));
let coeff = arena.intern(ExprNode::Num(coeff));
terms.push(arena.mul(&[coeff, integral]));
}
arena.add(&terms)
}
fn both_negative_powers(arena: &mut Arena, m: i64, n: i64, var: ExprId) -> ExprId {
use std::collections::BTreeMap;
let mut open: BTreeMap<(i64, i64), BigInt> = BTreeMap::new();
let mut done: BTreeMap<(i64, i64), BigInt> = BTreeMap::new();
open.insert((m, n), BigInt::one());
while let Some(((a, b), c)) = open.pop_first() {
if a >= 0 || b >= 0 {
*done.entry((a, b)).or_insert_with(BigInt::zero) += c;
continue;
}
*open.entry((a + 2, b)).or_insert_with(BigInt::zero) += &c;
*open.entry((a, b + 2)).or_insert_with(BigInt::zero) += c;
}
let mut terms: Vec<ExprId> = Vec::new();
for ((a, b), c) in done {
let integral = sin_cos_integrate(arena, a, b, var);
let coeff = arena.intern_num(Ratio::from_integer(c));
let coeff = arena.intern(ExprNode::Num(coeff));
terms.push(arena.mul(&[coeff, integral]));
}
arena.add(&terms)
}
pub(crate) fn sinh_squared_integrate(arena: &mut Arena, var: ExprId) -> ExprId {
let two = arena.int(2);
let two_x = arena.mul(&[two, var]);
let sinh_2x = arena.sinh(two_x);
let quarter = arena.rational(1, 4);
let first_term = arena.mul(&[quarter, sinh_2x]);
let half = arena.rational(1, 2);
let second_term = arena.mul(&[half, var]);
arena.sub(first_term, second_term)
}
pub(crate) fn cosh_squared_integrate(arena: &mut Arena, var: ExprId) -> ExprId {
let two = arena.int(2);
let two_x = arena.mul(&[two, var]);
let sinh_2x = arena.sinh(two_x);
let quarter = arena.rational(1, 4);
let first_term = arena.mul(&[quarter, sinh_2x]);
let half = arena.rational(1, 2);
let second_term = arena.mul(&[half, var]);
arena.add(&[first_term, second_term])
}
pub(crate) fn try_trig_power_integral(
arena: &mut Arena,
expr: ExprId,
var: ExprId,
var_sym: SymbolId,
) -> Option<ExprId> {
let node = arena.node(expr).clone();
if let Some(p) = extract_power(arena, expr, sin_arg) {
if (0..2).contains(&p.exp) || !within_power_bound(p.exp, 0) {
return None;
}
let rate = linear_rate(arena, p.arg, var, var_sym)?;
let big_f = sin_pow_integrate(arena, p.exp, p.arg);
return Some(over_rate(arena, big_f, rate));
}
if let Some(p) = extract_power(arena, expr, cos_arg) {
if (0..2).contains(&p.exp) || !within_power_bound(p.exp, 0) {
return None;
}
let rate = linear_rate(arena, p.arg, var, var_sym)?;
let big_f = cos_pow_integrate(arena, p.exp, p.arg);
return Some(over_rate(arena, big_f, rate));
}
if let Some(p) = extract_power(arena, expr, sinh_arg)
&& p.exp == 2
{
let rate = linear_rate(arena, p.arg, var, var_sym)?;
let big_f = sinh_squared_integrate(arena, p.arg);
return Some(over_rate(arena, big_f, rate));
}
if let Some(p) = extract_power(arena, expr, cosh_arg)
&& p.exp == 2
{
let rate = linear_rate(arena, p.arg, var, var_sym)?;
let big_f = cosh_squared_integrate(arena, p.arg);
return Some(over_rate(arena, big_f, rate));
}
if let Some(p) = extract_power(arena, expr, tan_arg)
&& p.exp < 0
&& within_power_bound(p.exp, -p.exp)
{
let rate = linear_rate(arena, p.arg, var, var_sym)?;
let big_f = sin_cos_integrate(arena, p.exp, -p.exp, p.arg);
return Some(over_rate(arena, big_f, rate));
}
if let ExprNode::Mul(ref children) = node {
let mut sin_exp: i64 = 0;
let mut cos_exp: i64 = 0;
let mut other_factors: Vec<ExprId> = Vec::new();
let mut found_tan = false;
let mut arg: Option<ExprId> = None;
let mut same_arg = |p: TrigPower| -> bool { *arg.get_or_insert(p.arg) == p.arg };
for &child in children.iter() {
if let Some(p) = extract_power(arena, child, sin_arg) {
if !same_arg(p) {
return None;
}
sin_exp = sin_exp.checked_add(p.exp)?;
} else if let Some(p) = extract_power(arena, child, cos_arg) {
if !same_arg(p) {
return None;
}
cos_exp = cos_exp.checked_add(p.exp)?;
} else if let Some(p) = extract_power(arena, child, tan_arg) {
if !same_arg(p) {
return None;
}
sin_exp = sin_exp.checked_add(p.exp)?;
cos_exp = cos_exp.checked_sub(p.exp)?;
found_tan = true;
} else if contains_var(arena, child, var_sym) {
return None;
} else {
other_factors.push(child);
}
}
let u = arg?;
let dominated_by_basic = sin_exp == 0 && cos_exp == 1 || sin_exp == 1 && cos_exp == 0;
if dominated_by_basic && !found_tan {
return None;
}
if !within_power_bound(sin_exp, cos_exp) {
return None;
}
let rate = linear_rate(arena, u, var, var_sym)?;
let trig_result = sin_cos_integrate(arena, sin_exp, cos_exp, u);
let trig_result = over_rate(arena, trig_result, rate);
if other_factors.is_empty() {
return Some(trig_result);
}
other_factors.push(trig_result);
return Some(arena.mul(&other_factors));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::arena::Arena;
#[test]
fn sin_zero() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = sin_pow_integrate(&mut a, 0, x);
assert_eq!(result, x);
}
#[test]
fn sin_first() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = sin_pow_integrate(&mut a, 1, x);
let s = a.display(result).to_string();
assert!(s.contains("cos"), "expected −cos(x), got: {s}");
}
#[test]
fn sin_squared() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = sin_pow_integrate(&mut a, 2, x);
let s = a.display(result).to_string();
assert!(
s.contains("cos") && s.contains("sin"),
"expected reduction formula terms, got: {s}"
);
}
#[test]
fn sin_cubed() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = sin_pow_integrate(&mut a, 3, x);
let s = a.display(result).to_string();
assert!(s.contains("cos"), "expected cos terms, got: {s}");
}
#[test]
fn sin_fourth() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = sin_pow_integrate(&mut a, 4, x);
let s = a.display(result).to_string();
assert!(!s.is_empty(), "got: {s}");
}
#[test]
fn sin_neg2_is_neg_cot() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = sin_pow_integrate(&mut a, -2, x);
let s = a.display(result).to_string();
assert!(
!s.contains("Integral"),
"expected evaluated result, got unevaluated: {s}"
);
assert!(
s.contains("cos") && s.contains("sin"),
"expected cos/sin terms for −cot(x), got: {s}"
);
}
#[test]
fn cos_zero() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = cos_pow_integrate(&mut a, 0, x);
assert_eq!(result, x);
}
#[test]
fn cos_first() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = cos_pow_integrate(&mut a, 1, x);
let s = a.display(result).to_string();
assert!(s.contains("sin"), "expected sin(x), got: {s}");
}
#[test]
fn cos_squared() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = cos_pow_integrate(&mut a, 2, x);
let s = a.display(result).to_string();
assert!(
s.contains("cos") && s.contains("sin"),
"expected reduction formula terms, got: {s}"
);
}
#[test]
fn cos_cubed() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = cos_pow_integrate(&mut a, 3, x);
let s = a.display(result).to_string();
assert!(s.contains("sin"), "expected sin terms, got: {s}");
}
#[test]
fn cos_neg1_is_ln_sec_tan() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = cos_pow_integrate(&mut a, -1, x);
let s = a.display(result).to_string();
assert!(
!s.contains("Integral"),
"expected evaluated result, got unevaluated: {s}"
);
assert!(
s.contains("ln"),
"expected ln term for ln|sec+tan|, got: {s}"
);
}
#[test]
fn cos_neg2_is_tan() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = cos_pow_integrate(&mut a, -2, x);
let s = a.display(result).to_string();
assert!(
!s.contains("Integral"),
"expected evaluated result, got unevaluated: {s}"
);
assert!(
s.contains("sin") && s.contains("cos"),
"expected sin/cos terms for tan(x), got: {s}"
);
}
#[test]
fn sin1_cos1() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = sin_cos_integrate(&mut a, 1, 1, x);
let s = a.display(result).to_string();
assert!(s.contains("cos"), "expected cos term, got: {s}");
}
#[test]
fn sin2_cos1() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = sin_cos_integrate(&mut a, 2, 1, x);
let s = a.display(result).to_string();
assert!(s.contains("sin"), "expected sin term, got: {s}");
}
#[test]
fn sin1_cos2() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = sin_cos_integrate(&mut a, 1, 2, x);
let s = a.display(result).to_string();
assert!(s.contains("cos"), "expected cos term, got: {s}");
}
#[test]
fn sin3_cos2() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = sin_cos_integrate(&mut a, 3, 2, x);
let s = a.display(result).to_string();
assert!(s.contains("cos"), "expected cos terms, got: {s}");
}
#[test]
fn sin2_cos3() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = sin_cos_integrate(&mut a, 2, 3, x);
let s = a.display(result).to_string();
assert!(s.contains("sin"), "expected sin terms, got: {s}");
}
#[test]
fn sin2_cos2_both_even() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = sin_cos_integrate(&mut a, 2, 2, x);
let s = a.display(result).to_string();
assert!(!s.is_empty(), "got: {s}");
}
#[test]
fn sin_cos_m_zero_delegates() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = sin_cos_integrate(&mut a, 0, 3, x);
let direct = cos_pow_integrate(&mut a, 3, x);
assert_eq!(result, direct);
}
#[test]
fn sin_cos_n_zero_delegates() {
let mut a = Arena::new();
let x = a.symbol("x");
let result = sin_cos_integrate(&mut a, 3, 0, x);
let direct = sin_pow_integrate(&mut a, 3, x);
assert_eq!(result, direct);
}
#[test]
fn try_detect_sin_squared() {
let mut a = Arena::new();
let x = a.symbol("x");
let var_sym = match a.node(x) {
ExprNode::Symbol(sid) => *sid,
_ => unreachable!(),
};
let sin_x = a.sin(x);
let two = a.int(2);
let expr = a.pow(sin_x, two);
let result = try_trig_power_integral(&mut a, expr, x, var_sym);
assert!(result.is_some(), "should detect sin²(x)");
let s = a.display(result.unwrap()).to_string();
assert!(
s.contains("cos") && s.contains("sin"),
"expected reduction formula, got: {s}"
);
}
#[test]
fn try_detect_cos_cubed() {
let mut a = Arena::new();
let x = a.symbol("x");
let var_sym = match a.node(x) {
ExprNode::Symbol(sid) => *sid,
_ => unreachable!(),
};
let cos_x = a.cos(x);
let three = a.int(3);
let expr = a.pow(cos_x, three);
let result = try_trig_power_integral(&mut a, expr, x, var_sym);
assert!(result.is_some(), "should detect cos³(x)");
let s = a.display(result.unwrap()).to_string();
assert!(s.contains("sin"), "expected sin terms, got: {s}");
}
#[test]
fn try_detect_sin_cos_product() {
let mut a = Arena::new();
let x = a.symbol("x");
let var_sym = match a.node(x) {
ExprNode::Symbol(sid) => *sid,
_ => unreachable!(),
};
let sin_x = a.sin(x);
let cos_x = a.cos(x);
let two = a.int(2);
let three = a.int(3);
let sin2 = a.pow(sin_x, two);
let cos3 = a.pow(cos_x, three);
let expr = a.mul(&[sin2, cos3]);
let result = try_trig_power_integral(&mut a, expr, x, var_sym);
assert!(result.is_some(), "should detect sin²·cos³");
}
#[test]
fn try_detect_bare_sin_cos_product() {
let mut a = Arena::new();
let x = a.symbol("x");
let var_sym = match a.node(x) {
ExprNode::Symbol(sid) => *sid,
_ => unreachable!(),
};
let sin_x = a.sin(x);
let cos_x = a.cos(x);
let expr = a.mul(&[sin_x, cos_x]);
let result = try_trig_power_integral(&mut a, expr, x, var_sym);
assert!(result.is_some(), "should detect sin(x)·cos(x)");
}
#[test]
fn try_detect_with_constant_factor() {
let mut a = Arena::new();
let x = a.symbol("x");
let var_sym = match a.node(x) {
ExprNode::Symbol(sid) => *sid,
_ => unreachable!(),
};
let sin_x = a.sin(x);
let cos_x = a.cos(x);
let two = a.int(2);
let three_exp = a.int(3);
let sin2 = a.pow(sin_x, two);
let cos3 = a.pow(cos_x, three_exp);
let three = a.int(3);
let expr = a.mul(&[three, sin2, cos3]);
let result = try_trig_power_integral(&mut a, expr, x, var_sym);
assert!(
result.is_some(),
"should detect 3·sin²·cos³ with constant factor"
);
}
#[test]
fn try_returns_none_for_non_trig() {
let mut a = Arena::new();
let x = a.symbol("x");
let var_sym = match a.node(x) {
ExprNode::Symbol(sid) => *sid,
_ => unreachable!(),
};
let two = a.int(2);
let expr = a.pow(x, two);
let result = try_trig_power_integral(&mut a, expr, x, var_sym);
assert!(result.is_none(), "x² is not a trig power integral");
}
#[test]
fn try_returns_none_for_mixed_non_trig() {
let mut a = Arena::new();
let x = a.symbol("x");
let var_sym = match a.node(x) {
ExprNode::Symbol(sid) => *sid,
_ => unreachable!(),
};
let sin_x = a.sin(x);
let two = a.int(2);
let sin2 = a.pow(sin_x, two);
let expr = a.mul(&[x, sin2]);
let result = try_trig_power_integral(&mut a, expr, x, var_sym);
assert!(
result.is_none(),
"x·sin²(x) has a non-trig dependent factor"
);
}
#[test]
fn binom_values() {
assert_eq!(binom(0, 0), BigInt::from(1));
assert_eq!(binom(1, 0), BigInt::from(1));
assert_eq!(binom(1, 1), BigInt::from(1));
assert_eq!(binom(4, 2), BigInt::from(6));
assert_eq!(binom(5, 0), BigInt::from(1));
assert_eq!(binom(5, 5), BigInt::from(1));
assert_eq!(binom(5, 3), BigInt::from(10));
assert_eq!(binom(6, 3), BigInt::from(20));
assert_eq!(binom(3, 5), BigInt::from(0)); }
#[test]
fn extract_sin_power_bare() {
let mut a = Arena::new();
let x = a.symbol("x");
let sin_x = a.sin(x);
assert_eq!(extract_sin_power(&a, sin_x, x), Some(1));
}
#[test]
fn extract_sin_power_squared() {
let mut a = Arena::new();
let x = a.symbol("x");
let sin_x = a.sin(x);
let two = a.int(2);
let sin2 = a.pow(sin_x, two);
assert_eq!(extract_sin_power(&a, sin2, x), Some(2));
}
#[test]
fn extract_cos_power_bare() {
let mut a = Arena::new();
let x = a.symbol("x");
let cos_x = a.cos(x);
assert_eq!(extract_cos_power(&a, cos_x, x), Some(1));
}
#[test]
fn extract_cos_power_cubed() {
let mut a = Arena::new();
let x = a.symbol("x");
let cos_x = a.cos(x);
let three = a.int(3);
let cos3 = a.pow(cos_x, three);
assert_eq!(extract_cos_power(&a, cos3, x), Some(3));
}
#[test]
fn extract_returns_none_for_non_trig() {
let mut a = Arena::new();
let x = a.symbol("x");
let two = a.int(2);
let x2 = a.pow(x, two);
assert_eq!(extract_sin_power(&a, x2, x), None);
assert_eq!(extract_cos_power(&a, x2, x), None);
}
}