use super::asymptotic::AsymptoticError;
use super::asymptotic_common::{
as_rational_function, complex_roots, gate_accept, qp_degree, qp_eval, qp_is_zero,
rational_to_expr, verification_points, AsymptoticReport, Hypothesis, QPoly, Rigor,
DEFAULT_SLACK,
};
use crate::kernel::{ExprId, ExprPool};
use crate::simplify::simplify;
use rug::Rational;
const DOMINANCE_MARGIN: f64 = 1e-6;
const MAX_MULTIPLICITY: usize = 8;
const GATE_INDICES: [usize; 4] = [24, 32, 40, 48];
pub fn coefficient_asymptotics(
gf: ExprId,
z: ExprId,
n: ExprId,
pool: &ExprPool,
) -> Result<AsymptoticReport, AsymptoticError> {
if z == n {
return Err(AsymptoticError::InvalidTermCount);
}
let rf = as_rational_function(gf, z, pool).ok_or(AsymptoticError::UnsupportedScale)?;
let num = rf.num.clone();
let den = rf.den.clone();
if qp_is_zero(&den) {
return Err(AsymptoticError::UnsupportedScale);
}
if qp_eval(&den, &Rational::from(0)) == 0 {
return Err(AsymptoticError::UnsupportedScale);
}
if qp_is_zero(&num) {
return Err(AsymptoticError::GateFailed);
}
if qp_degree(&den) == 0 {
return Err(AsymptoticError::UnsupportedScale);
}
let mut derivation = Vec::new();
let den_f64: Vec<f64> = den.iter().map(|c| c.to_f64()).collect();
let roots = complex_roots(&den_f64).ok_or(AsymptoticError::UnsupportedScale)?;
if roots.is_empty() {
return Err(AsymptoticError::UnsupportedScale);
}
let mut moduli: Vec<(f64, usize)> = roots
.iter()
.enumerate()
.map(|(i, r)| (r.abs(), i))
.collect();
moduli.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let (rho_mod, rho_idx) = moduli[0];
if !(rho_mod.is_finite() && rho_mod > 0.0) {
return Err(AsymptoticError::UnsupportedScale);
}
let equal_modulus: Vec<usize> = moduli
.iter()
.filter(|(m, _)| (m - rho_mod).abs() <= DOMINANCE_MARGIN * rho_mod.max(1.0))
.map(|(_, i)| *i)
.collect();
let rho = roots[rho_idx];
if rho.im.abs() > DOMINANCE_MARGIN * rho_mod.max(1.0) {
return Err(AsymptoticError::UnsupportedScale);
}
let multiplicity = equal_modulus
.iter()
.filter(|&&i| {
let r = roots[i];
(r.re - rho.re).abs() <= DOMINANCE_MARGIN * rho_mod.max(1.0)
&& (r.im - rho.im).abs() <= DOMINANCE_MARGIN * rho_mod.max(1.0)
})
.count();
if multiplicity == 0 || multiplicity > MAX_MULTIPLICITY {
return Err(AsymptoticError::UnsupportedScale);
}
if equal_modulus.len() > multiplicity {
return Err(AsymptoticError::UnsupportedScale);
}
derivation.push(format!(
"dominant pole at z ≈ {:.12} with multiplicity {multiplicity}",
rho.re
));
let want = GATE_INDICES[GATE_INDICES.len() - 1] + 1;
let coeffs = series_coefficients(&num, &den, want).ok_or(AsymptoticError::UnsupportedScale)?;
let growth = 1.0 / rho.re;
let shape = |k: usize| -> f64 {
let nn = k as f64;
nn.powi(multiplicity as i32 - 1) * growth.powi(k as i32)
};
let k_hi = GATE_INDICES[GATE_INDICES.len() - 1];
let k_lo = GATE_INDICES[GATE_INDICES.len() / 2];
if k_hi == k_lo {
return Err(AsymptoticError::GateFailed);
}
let ratio_at = |k: usize| -> Option<f64> {
let sh = shape(k);
if !sh.is_finite() || sh == 0.0 {
return None;
}
let v = coeffs[k].to_f64() / sh;
if v.is_finite() {
Some(v)
} else {
None
}
};
let c_hi = ratio_at(k_hi).ok_or(AsymptoticError::GateFailed)?;
let c_lo = ratio_at(k_lo).ok_or(AsymptoticError::GateFailed)?;
let c_const = (c_hi * k_hi as f64 - c_lo * k_lo as f64) / (k_hi as f64 - k_lo as f64);
if !c_const.is_finite() || c_const == 0.0 {
return Err(AsymptoticError::GateFailed);
}
derivation.push(format!(
"leading constant by Richardson extrapolation of a_k/shape(k) at k = {k_lo}, {k_hi}: \
{c_lo} , {c_hi} -> {c_const}"
));
let points: Vec<f64> = GATE_INDICES.iter().map(|&k| k as f64).collect();
let oracle: Vec<f64> = GATE_INDICES.iter().map(|&k| coeffs[k].to_f64()).collect();
if oracle.iter().any(|v| !v.is_finite()) {
return Err(AsymptoticError::GateFailed);
}
let term_vals: Vec<Vec<f64>> = vec![GATE_INDICES.iter().map(|&k| c_const * shape(k)).collect()];
let accepted = gate_accept(&oracle, &term_vals, DEFAULT_SLACK);
if accepted == 0 {
return Err(AsymptoticError::GateFailed);
}
let verification = verification_points(&points, &oracle, &term_vals, accepted);
let c_expr = float_to_expr(c_const, pool);
let growth_expr = float_to_expr(growth, pool);
let mut factors = vec![c_expr];
if multiplicity > 1 {
factors.push(pool.pow(n, pool.integer((multiplicity - 1) as i32)));
}
factors.push(pool.pow(growth_expr, n));
let term = simplify(pool.mul(factors), pool).value;
Ok(AsymptoticReport {
method: "singularity-analysis",
var: n,
terms: vec![term],
rigor: Rigor::NumericallyConsistent,
hypotheses: vec![
Hypothesis::checked(
"the generating function is rational and regular at the origin, so the \
coefficient sequence exists",
),
Hypothesis::checked(
"the singularity of smallest modulus is unique and real, so the transfer \
theorem yields a single power-law term",
),
Hypothesis::assumed(
"the poles were located numerically, so uniqueness is decided against a \
relative separation tolerance rather than proved",
),
Hypothesis::assumed(
"the leading constant was fitted from the exact series, not derived in \
closed form",
),
],
verification,
derivation,
})
}
fn series_coefficients(num: &QPoly, den: &QPoly, count: usize) -> Option<Vec<Rational>> {
let d0 = den.first()?.clone();
if d0 == 0 {
return None;
}
let mut out: Vec<Rational> = Vec::with_capacity(count);
for k in 0..count {
let mut acc = num.get(k).cloned().unwrap_or_else(|| Rational::from(0));
for j in 1..=k {
if let Some(dj) = den.get(j) {
if *dj != 0 {
acc -= Rational::from(dj * &out[k - j]);
}
}
}
out.push(Rational::from(&acc / &d0));
}
Some(out)
}
fn float_to_expr(v: f64, pool: &ExprPool) -> ExprId {
match Rational::from_f64(v) {
Some(q) => {
let scale = rug::Integer::from(1_000_000_000_000_i64);
let scaled = (q * Rational::from(scale.clone())).round();
rational_to_expr(&Rational::from((scaled.numer().clone(), scale)), pool)
}
None => pool.integer(0_i32),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Domain;
fn env() -> (ExprPool, ExprId, ExprId) {
let pool = ExprPool::new();
let z = pool.symbol("z", Domain::Real);
let n = pool.symbol("n", Domain::Real);
(pool, z, n)
}
#[test]
fn fibonacci_growth_is_the_golden_ratio() {
let (pool, z, n) = env();
let one = pool.integer(1_i32);
let den = pool.add(vec![
one,
pool.mul(vec![pool.integer(-1_i32), z]),
pool.mul(vec![pool.integer(-1_i32), z, z]),
]);
let gf = pool.mul(vec![one, pool.pow(den, pool.integer(-1_i32))]);
let r = coefficient_asymptotics(gf, z, n, &pool).expect("expansion");
assert_eq!(r.method, "singularity-analysis");
assert_eq!(r.terms.len(), 1);
let mut env_map = std::collections::HashMap::new();
env_map.insert(n, 40.0);
let approx = crate::jit::eval_interp(r.terms[0], &env_map, &pool).expect("evaluates");
let (mut a, mut b) = (0u64, 1u64);
for _ in 0..41 {
let t = a + b;
a = b;
b = t;
}
let truth = a as f64;
assert!(
(approx - truth).abs() / truth < 1e-6,
"[z^40]: expansion {approx} vs truth {truth}"
);
assert!(r.max_relative_error().unwrap() < 1e-6);
}
#[test]
fn double_pole_gives_linear_factor() {
let (pool, z, n) = env();
let one = pool.integer(1_i32);
let base = pool.add(vec![one, pool.mul(vec![pool.integer(-1_i32), z])]);
let gf = pool.pow(base, pool.integer(-2_i32));
let r = coefficient_asymptotics(gf, z, n, &pool).expect("expansion");
let rel = |ni: f64| -> f64 {
let mut env_map = std::collections::HashMap::new();
env_map.insert(n, ni);
let approx = crate::jit::eval_interp(r.terms[0], &env_map, &pool).expect("evaluates");
let truth = ni + 1.0;
(approx - truth).abs() / truth
};
let (e100, e1000) = (rel(100.0), rel(1000.0));
assert!(e100 < 0.05, "relative error at n=100 too large: {e100}");
assert!(
e1000 < e100 / 2.0,
"relative error must shrink with n: {e100} -> {e1000}"
);
}
#[test]
fn refuses_competing_dominant_singularities() {
let (pool, z, n) = env();
let one = pool.integer(1_i32);
let den = pool.add(vec![one, pool.mul(vec![pool.integer(-1_i32), z, z])]);
let gf = pool.mul(vec![one, pool.pow(den, pool.integer(-1_i32))]);
let err = coefficient_asymptotics(gf, z, n, &pool).expect_err("must decline");
assert!(matches!(err, AsymptoticError::UnsupportedScale));
}
#[test]
fn refuses_pole_at_the_origin() {
let (pool, z, n) = env();
let gf = pool.pow(z, pool.integer(-1_i32));
assert!(coefficient_asymptotics(gf, z, n, &pool).is_err());
}
#[test]
fn refuses_non_rational_input() {
let (pool, z, n) = env();
let gf = pool.func("exp", vec![z]);
let err = coefficient_asymptotics(gf, z, n, &pool).expect_err("must decline");
assert!(matches!(err, AsymptoticError::UnsupportedScale));
}
#[test]
fn refuses_polynomial_input() {
let (pool, z, n) = env();
let gf = pool.add(vec![pool.integer(1_i32), z]);
assert!(coefficient_asymptotics(gf, z, n, &pool).is_err());
}
}