use std::sync::Arc;
use crate::numerical::elementary::eval_expr_single;
use crate::numerical::special::bernoulli_poly;
use crate::numerical::special::digamma_numerical;
use crate::numerical::special::hurwitz_zeta;
use crate::symbolic::core::Expr;
#[derive(Debug, Clone)]
pub struct IndefiniteSumConfig {
pub h: f64,
pub step: f64,
pub peak_sub: usize,
pub smooth_sub: usize,
pub contour_height: f64,
pub tolerance: f64,
}
impl Default for IndefiniteSumConfig {
fn default() -> Self {
Self {
h: 0.0,
step: 1.0,
peak_sub: 20,
smooth_sub: 40,
contour_height: 20.0,
tolerance: 1e-10,
}
}
}
#[must_use]
pub fn try_closed_form_sum(
body: &Expr,
var: &str,
) -> Option<Expr> {
if let Expr::Dag(node) = body {
if let Ok(converted) = node.to_expr() {
return try_closed_form_sum(&converted, var);
}
}
if !expr_contains_var(body, var) {
return Some(Expr::new_mul(body.clone(), Expr::new_variable(var)));
}
if let Expr::Add(a, b) = body {
if let (Some(sa), Some(sb)) = (try_closed_form_sum(a, var), try_closed_form_sum(b, var)) {
return Some(Expr::new_add(sa, sb));
}
}
if let Expr::Sub(a, b) = body {
if let (Some(sa), Some(sb)) = (try_closed_form_sum(a, var), try_closed_form_sum(b, var)) {
return Some(Expr::new_sub(sa, sb));
}
}
if let Expr::Neg(a) = body {
if let Some(sa) = try_closed_form_sum(a, var) {
return Some(Expr::new_neg(sa));
}
}
if let Expr::Mul(a, b) = body {
if !expr_contains_var(a, var) {
if let Some(sb) = try_closed_form_sum(b, var) {
return Some(Expr::new_mul(a.as_ref().clone(), sb));
}
} else if !expr_contains_var(b, var) {
if let Some(sa) = try_closed_form_sum(a, var) {
return Some(Expr::new_mul(sa, b.as_ref().clone()));
}
}
}
if let Expr::AddList(terms) = body {
let mut summed_terms = Vec::new();
let mut all_success = true;
for term in terms {
if let Some(st) = try_closed_form_sum(term, var) {
summed_terms.push(st);
} else {
all_success = false;
break;
}
}
if all_success {
return Some(Expr::AddList(summed_terms));
}
}
if let Expr::MulList(factors) = body {
let (const_factors, var_factors): (Vec<_>, Vec<_>) = factors
.iter()
.cloned()
.partition(|f| !expr_contains_var(f, var));
if !const_factors.is_empty() {
let rest = if var_factors.is_empty() {
Expr::Constant(1.0)
} else if var_factors.len() == 1 {
var_factors[0].clone()
} else {
Expr::MulList(var_factors)
};
if let Some(sum_rest) = try_closed_form_sum(&rest, var) {
let c = if const_factors.len() == 1 {
const_factors[0].clone()
} else {
Expr::MulList(const_factors)
};
return Some(Expr::new_mul(c, sum_rest));
}
}
}
match body {
| Expr::Power(base, exp) if is_pure_var(exp.as_ref(), var) && !expr_contains_var(base, var) => {
let a = base.as_ref().clone();
let denom = Expr::new_sub(a.clone(), Expr::new_constant(1.0));
Some(Expr::new_div(
Expr::new_pow(a, Expr::new_variable(var)),
denom,
))
},
| Expr::Exp(inner) if is_pure_var(inner.as_ref(), var) => {
let e_minus_1 = Expr::new_constant(std::f64::consts::E - 1.0);
Some(Expr::new_div(Expr::Exp(inner.clone()), e_minus_1))
},
| Expr::Exp(inner) => {
if let Some(a) = extract_linear_coeff(inner, var) {
let ea = (a.exp() - 1.0).abs();
if ea > 1e-15 {
let denom = Expr::new_constant(a.exp() - 1.0);
return Some(Expr::new_div(body.clone(), denom));
}
}
None
},
| Expr::Sin(inner) => {
if let Some(a) = extract_linear_coeff(inner, var) {
let half_a_sin = (a / 2.0).sin();
if half_a_sin.abs() > 1e-15 {
let denom = Expr::new_constant(2.0 * half_a_sin);
let x_expr = Expr::new_variable(var);
let two_x_m1 = Expr::new_sub(
Expr::new_mul(Expr::new_constant(2.0), x_expr),
Expr::new_constant(1.0),
);
let arg = Expr::new_mul(Expr::new_constant(a / 2.0), two_x_m1);
let neg_cos = Expr::Neg(Arc::new(Expr::Cos(Arc::new(arg))));
return Some(Expr::new_div(neg_cos, denom));
}
}
None
},
| Expr::Cos(inner) => {
if let Some(a) = extract_linear_coeff(inner, var) {
let half_a_sin = (a / 2.0).sin();
if half_a_sin.abs() > 1e-15 {
let denom = Expr::new_constant(2.0 * half_a_sin);
let x_expr = Expr::new_variable(var);
let two_x_m1 = Expr::new_sub(
Expr::new_mul(Expr::new_constant(2.0), x_expr),
Expr::new_constant(1.0),
);
let arg = Expr::new_mul(Expr::new_constant(a / 2.0), two_x_m1);
return Some(Expr::new_div(Expr::Sin(Arc::new(arg)), denom));
}
}
None
},
| Expr::Log(inner) if is_pure_var(inner.as_ref(), var) => {
Some(Expr::Log(Arc::new(Expr::Gamma(Arc::new(
Expr::new_variable(var),
)))))
},
| Expr::Variable(v) if v == var => {
Some(Expr::BinaryList(
"hurwitz_zeta_antidiff".to_string(),
Arc::new(Expr::Constant(1.0)),
Arc::new(Expr::new_variable(var)),
))
},
| Expr::Power(base, exp) if is_pure_var(base.as_ref(), var) && !expr_contains_var(exp, var) => {
Some(Expr::BinaryList(
"hurwitz_zeta_antidiff".to_string(),
exp.clone(),
Arc::new(Expr::new_variable(var)),
))
},
| Expr::Div(num, den)
if matches!(num.as_ref(), Expr::Constant(c) if (*c - 1.0).abs() < 1e-15)
&& is_pure_var(den.as_ref(), var) =>
{
Some(Expr::Digamma(Arc::new(Expr::new_variable(var))))
},
| _ => None,
}
}
pub fn eval_antidiff(
anti_diff: &Expr,
var: &str,
x_val: f64,
) -> Result<f64, String> {
match anti_diff {
| Expr::BinaryList(tag, a_arc, _) if tag == "hurwitz_zeta_antidiff" => {
let a = eval_expr_single(a_arc, var, x_val)?;
let result = hurwitz_zeta(-a, 1.0) - hurwitz_zeta(-a, x_val);
Ok(result)
},
| Expr::Digamma(inner) => {
let z = eval_expr_single(inner, var, x_val)?;
Ok(digamma_numerical(z))
},
| Expr::Log(inner) => {
if let Expr::Gamma(gamma_inner) = inner.as_ref() {
let z = eval_expr_single(gamma_inner, var, x_val)?;
if z > 0.0 {
return Ok(statrs::function::gamma::ln_gamma(z));
}
}
eval_expr_single(anti_diff, var, x_val)
},
| _ => eval_expr_single(anti_diff, var, x_val),
}
}
pub fn eval_normalized(
anti_diff: &Expr,
var: &str,
x_val: f64,
h: f64,
) -> Result<f64, String> {
let fx = eval_antidiff(anti_diff, var, x_val)?;
let fh = eval_antidiff(anti_diff, var, h)?;
Ok(fx - fh)
}
#[cfg(feature = "compute")]
pub struct AbelPlanaEngine {
config: IndefiniteSumConfig,
expr: Expr,
var: String,
}
#[cfg(feature = "compute")]
impl AbelPlanaEngine {
pub fn new(
expr: Expr,
var: impl Into<String>,
config: IndefiniteSumConfig,
) -> Self {
Self {
config,
expr,
var: var.into(),
}
}
pub fn eval(
&self,
x: f64,
) -> Result<f64, String> {
let step = self.config.step;
if step == 0.0 {
return Err("Step size cannot be zero".to_string());
}
let h = self.config.h;
let tol = self.config.tolerance;
let n_steps = ((x - h) / step).round() as i64;
let mut sum = 0.0_f64;
if n_steps >= 0 {
for k in 0..n_steps {
let t = h + (k as f64) * step;
sum += eval_expr_single(&self.expr, &self.var, t)
.map_err(|e| format!("eval at t={t}: {e}"))?;
}
} else {
for k in n_steps..0 {
let t = h + (k as f64) * step;
sum -= eval_expr_single(&self.expr, &self.var, t)
.map_err(|e| format!("eval at t={t}: {e}"))?;
}
}
let frac = (x - h) / step - (n_steps as f64);
if frac.abs() < 1e-12 {
return Ok(sum);
}
let base = h + (n_steps as f64) * step;
let frac_dist = frac * step;
let expr_ref = &self.expr;
let var_ref = self.var.as_str();
let output = quadrature::integrate(
|t| eval_expr_single(expr_ref, var_ref, base + t).unwrap_or(0.0),
0.0,
frac_dist,
tol,
);
Ok(sum + output.integral)
}
}
pub fn eval_indefinite_product_numerical(
x_val: f64,
f: &dyn Fn(f64) -> Result<f64, String>,
h: f64,
step: f64,
) -> Result<f64, String> {
let n_steps = ((x_val - h) / step).round() as i64;
if step == 0.0 {
return Err("Step size cannot be zero".to_string());
}
let mut log_sum = 0.0_f64;
if n_steps >= 0 {
for k in 0..n_steps {
let t = h + (k as f64) * step;
let fval = f(t).map_err(|e| format!("product eval at t={t}: {e}"))?;
if fval <= 0.0 {
return Err(format!("f({t}) = {fval} ≤ 0: log undefined"));
}
log_sum += fval.ln();
}
} else {
for k in n_steps..0 {
let t = h + (k as f64) * step;
let fval = f(t).map_err(|e| format!("product eval at t={t}: {e}"))?;
if fval <= 0.0 {
return Err(format!("f({t}) = {fval} ≤ 0: log undefined"));
}
log_sum -= fval.ln();
}
}
Ok(log_sum.exp())
}
#[must_use]
pub fn series_antidiff(
coeffs: &[f64],
p: f64,
z: f64,
) -> f64 {
let mut result = 0.0;
for (m, &cm) in coeffs.iter().enumerate() {
if cm.abs() < 1e-20 {
continue;
}
let s = -(m as f64);
let zeta_0 = -bernoulli_poly(m as u32 + 1, 1.0) / (m as f64 + 1.0);
let zeta_shift = hurwitz_zeta(s, z - p + 1.0);
result += cm * (zeta_0 - zeta_shift);
}
result
}
#[must_use]
pub fn compute_taylor_coeffs_numerical(
expr: &Expr,
var: &str,
center: f64,
n_terms: usize,
) -> Vec<f64> {
let h = 1e-3;
let mut diffs: Vec<f64> = (0..n_terms)
.map(|k| eval_expr_single(expr, var, center + k as f64 * h).unwrap_or(f64::NAN))
.collect();
let mut coeffs = Vec::with_capacity(n_terms);
let mut h_pow = 1.0_f64;
let mut fact = 1.0_f64;
for m in 0..n_terms {
if m > 0 {
h_pow *= h;
fact *= m as f64;
for k in 0..(n_terms - m) {
diffs[k] = diffs[k + 1] - diffs[k];
}
}
coeffs.push(diffs[0] / (h_pow * fact));
}
coeffs
}
pub fn eval_indefinite_sum_numerical(
expr: &Expr,
var: &str,
x_val: f64,
config: &IndefiniteSumConfig,
) -> Result<f64, String> {
let h = config.h;
if let Some(anti_diff) = try_closed_form_sum(expr, var) {
let fx = eval_antidiff(&anti_diff, var, x_val)?;
let fh = eval_antidiff(&anti_diff, var, h)?;
return Ok(fx - fh);
}
#[cfg(feature = "compute")]
{
let engine = AbelPlanaEngine::new(expr.clone(), var.to_string(), config.clone());
if let Ok(val) = engine.eval(x_val) {
return Ok(val);
}
}
let coeffs = compute_taylor_coeffs_numerical(expr, var, h, 8);
if coeffs.iter().any(|c| c.is_nan() || c.is_infinite()) {
return Err(format!(
"series fallback: Taylor coefficients invalid near x={h}"
));
}
Ok(series_antidiff(&coeffs, h, x_val))
}
fn is_pure_var(
expr: &Expr,
var: &str,
) -> bool {
match expr {
| Expr::Variable(v) => v == var,
| Expr::Dag(node) => {
node.to_expr()
.map(|e| matches!(e, Expr::Variable(v) if v == var))
.unwrap_or(false)
},
| _ => false,
}
}
#[must_use]
pub fn expr_contains_var(
expr: &Expr,
var: &str,
) -> bool {
if let Expr::Dag(node) = expr {
if let Ok(converted) = node.to_expr() {
return expr_contains_var(&converted, var);
}
}
match expr {
| Expr::Variable(v) => v == var,
| Expr::Constant(_)
| Expr::BigInt(_)
| Expr::Rational(_)
| Expr::Boolean(_)
| Expr::Pi
| Expr::E
| Expr::Infinity
| Expr::NegativeInfinity => false,
| Expr::Add(a, b)
| Expr::Sub(a, b)
| Expr::Mul(a, b)
| Expr::Div(a, b)
| Expr::Power(a, b) => expr_contains_var(a, var) || expr_contains_var(b, var),
| Expr::Neg(a)
| Expr::Sin(a)
| Expr::Cos(a)
| Expr::Tan(a)
| Expr::Exp(a)
| Expr::Log(a)
| Expr::Abs(a)
| Expr::Sqrt(a)
| Expr::Gamma(a)
| Expr::Digamma(a) => expr_contains_var(a, var),
| Expr::AddList(v) | Expr::MulList(v) => v.iter().any(|e| expr_contains_var(e, var)),
| _ => false,
}
}
#[must_use]
pub fn extract_linear_coeff(
expr: &Expr,
var: &str,
) -> Option<f64> {
if let Expr::Dag(node) = expr {
if let Ok(converted) = node.to_expr() {
return extract_linear_coeff(&converted, var);
}
}
match expr {
| Expr::Variable(v) if v == var => Some(1.0),
| Expr::Neg(inner) => extract_linear_coeff(inner, var).map(|c| -c),
| Expr::Mul(a, b) => {
match (a.as_ref(), b.as_ref()) {
| (Expr::Constant(c), Expr::Variable(v)) if v == var => Some(*c),
| (Expr::Variable(v), Expr::Constant(c)) if v == var => Some(*c),
| _ => None,
}
},
| _ => None,
}
}