use std::sync::{Arc, Mutex};
use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Zero};
use crate::api::context::Context;
use crate::api::expr::Ex;
use crate::base::arena::Arena;
use crate::base::combinatorics::factorial;
use crate::base::errors::SymplexError;
use crate::base::node::{ExprId, ExprNode};
use crate::base::numeric::Q;
use crate::base::walk;
use crate::calculus::series::{FnKind, TSeries, expand_meromorphic};
use crate::transforms::eval;
pub struct FormalPowerSeries {
ctx: Context,
var: ExprId,
point: ExprId,
offset: ExprId,
ram: u32,
valuation: i64,
log_terms: Vec<(i64, ExprId)>,
source: Source,
memo: Mutex<Vec<Coeff>>,
zero: ExprId,
}
#[derive(Clone, Copy, Debug)]
struct Coeff {
id: ExprId,
exact: bool,
}
#[derive(Clone, Debug)]
enum Known {
Elementary { kind: FnKind, c: ExprId, m: usize },
Ln { a: ExprId, c: ExprId, m: usize },
Binomial {
alpha: ExprId,
a: ExprId,
c: ExprId,
m: usize,
},
}
enum Source {
Known(Known),
Engine {
expr: ExprId,
svar: ExprId,
cache: Mutex<Option<TSeries>>,
},
Polynomial(Vec<ExprId>),
Refused {
expr: ExprId,
reason: String,
},
Add(Box<FormalPowerSeries>, Box<FormalPowerSeries>),
Sub(Box<FormalPowerSeries>, Box<FormalPowerSeries>),
Mul(Box<FormalPowerSeries>, Box<FormalPowerSeries>),
Scale(Box<FormalPowerSeries>, ExprId),
Compose(Box<FormalPowerSeries>, Box<FormalPowerSeries>),
Derivative(Box<FormalPowerSeries>),
Integral(Box<FormalPowerSeries>),
Inverse(Box<FormalPowerSeries>, i64),
Reversion(Box<FormalPowerSeries>),
Reindex {
inner: Box<FormalPowerSeries>,
mul: u32,
div: u32,
shift: i64,
},
}
impl Clone for FormalPowerSeries {
fn clone(&self) -> Self {
let memo = self.memo.lock().map(|m| m.clone()).unwrap_or_default();
let source = match &self.source {
Source::Known(k) => Source::Known(k.clone()),
Source::Engine { expr, svar, cache } => Source::Engine {
expr: *expr,
svar: *svar,
cache: Mutex::new(cache.lock().ok().and_then(|c| c.clone())),
},
Source::Polynomial(v) => Source::Polynomial(v.clone()),
Source::Refused { expr, reason } => Source::Refused {
expr: *expr,
reason: reason.clone(),
},
Source::Add(a, b) => Source::Add(a.clone(), b.clone()),
Source::Sub(a, b) => Source::Sub(a.clone(), b.clone()),
Source::Mul(a, b) => Source::Mul(a.clone(), b.clone()),
Source::Scale(a, c) => Source::Scale(a.clone(), *c),
Source::Compose(a, b) => Source::Compose(a.clone(), b.clone()),
Source::Derivative(a) => Source::Derivative(a.clone()),
Source::Integral(a) => Source::Integral(a.clone()),
Source::Inverse(a, l) => Source::Inverse(a.clone(), *l),
Source::Reversion(a) => Source::Reversion(a.clone()),
Source::Reindex {
inner,
mul,
div,
shift,
} => Source::Reindex {
inner: inner.clone(),
mul: *mul,
div: *div,
shift: *shift,
},
};
FormalPowerSeries {
ctx: self.ctx.clone(),
var: self.var,
point: self.point,
offset: self.offset,
ram: self.ram,
valuation: self.valuation,
log_terms: self.log_terms.clone(),
source,
memo: Mutex::new(memo),
zero: self.zero,
}
}
}
impl std::fmt::Debug for FormalPowerSeries {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "FormalPowerSeries({})", self.truncate(6))
}
}
fn rat_expr(arena: &mut Arena, r: Q) -> ExprId {
let nid = arena.intern_num(r);
arena.intern(ExprNode::Num(nid))
}
fn rat_i(n: i64) -> Q {
Ratio::from_integer(BigInt::from(n))
}
fn add_all(arena: &mut Arena, terms: &[ExprId]) -> ExprId {
let s = match terms.len() {
0 => arena.zero,
1 => terms[0],
_ => arena.add(terms),
};
eval::eval(arena, s)
}
fn mul_all(arena: &mut Arena, factors: &[ExprId]) -> ExprId {
let p = match factors.len() {
0 => arena.one,
1 => factors[0],
_ => arena.mul(factors),
};
eval::eval(arena, p)
}
fn pow_i(arena: &mut Arena, base: ExprId, n: i64) -> ExprId {
if n == 0 {
return arena.one;
}
if n == 1 {
return base;
}
let e = arena.int(n);
let p = arena.pow(base, e);
eval::eval(arena, p)
}
fn gen_binomial(arena: &mut Arena, alpha: ExprId, n: usize) -> ExprId {
if n == 0 {
return arena.one;
}
if let Some(a) = arena.as_num(alpha).cloned() {
let mut acc = Q::one();
for i in 0..n {
acc = acc * (&a - rat_i(i as i64)) / rat_i(i as i64 + 1);
}
return rat_expr(arena, acc);
}
let mut factors = Vec::with_capacity(n + 1);
for i in 0..n {
let ie = arena.int(-(i as i64));
factors.push(arena.add(&[alpha, ie]));
}
factors.push(rat_expr(arena, Q::new(BigInt::one(), factorial(n as u64))));
mul_all(arena, &factors)
}
fn poly_mul(arena: &mut Arena, a: &[ExprId], b: &[ExprId], n: usize) -> Vec<ExprId> {
let mut out = Vec::with_capacity(n);
for k in 0..n {
let mut terms = Vec::new();
for (i, &ai) in a.iter().enumerate().take(k + 1) {
let j = k - i;
if j >= b.len() || arena.is_zero_structural(ai) || arena.is_zero_structural(b[j]) {
continue;
}
terms.push(arena.mul(&[ai, b[j]]));
}
out.push(add_all(arena, &terms));
}
out
}
fn poly_inverse(arena: &mut Arena, a: &[ExprId], n: usize) -> Vec<ExprId> {
let a0 = a.first().copied().unwrap_or(arena.zero);
let inv_a0 = {
let m1 = arena.neg_one;
let p = arena.pow(a0, m1);
eval::eval(arena, p)
};
let mut b: Vec<ExprId> = Vec::with_capacity(n);
b.push(inv_a0);
for k in 1..n {
let mut terms = Vec::new();
for i in 1..=k {
let ai = a.get(i).copied().unwrap_or(arena.zero);
if arena.is_zero_structural(ai) || arena.is_zero_structural(b[k - i]) {
continue;
}
terms.push(arena.mul(&[ai, b[k - i]]));
}
let s = add_all(arena, &terms);
let v = arena.mul(&[inv_a0, s]);
let v = arena.neg(v);
b.push(eval::eval(arena, v));
}
b
}
fn is_zero_expr(arena: &mut Arena, e: ExprId) -> bool {
let v = eval::eval(arena, e);
arena.is_zero_structural(v) || arena.as_num(v).is_some_and(|r| r.is_zero())
}
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
match m.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
}
}
fn lcm_u32(a: u32, b: u32) -> Option<u32> {
let (a, b) = (u64::from(a), u64::from(b));
let l = a / crate::base::numeric::gcd_u64(a, b).max(1) * b;
u32::try_from(l).ok().filter(|&l| l <= MAX_RAMIFICATION)
}
const MAX_RAMIFICATION: u32 = 360;
const RAMIFIED_PLACEHOLDER: &str = "__fps_t";
fn as_integer(arena: &Arena, e: ExprId) -> Option<i64> {
use num_traits::ToPrimitive;
let q = arena.as_num(e)?;
if q.is_integer() {
q.to_integer().to_i64()
} else {
None
}
}
fn q_to_i64(q: &Q) -> Option<i64> {
use num_traits::ToPrimitive;
if q.is_integer() {
q.to_integer().to_i64()
} else {
None
}
}
fn rational_part(arena: &Arena, e: ExprId) -> Q {
if let Some(q) = arena.as_num(e) {
return q.clone();
}
let mut acc = Q::zero();
if let ExprNode::Add(args) = arena.node(e) {
for &t in args {
if let Some(q) = arena.as_num(t) {
acc += q;
}
}
}
acc
}
fn contains_singular(arena: &Arena, id: ExprId) -> bool {
walk::contains(arena, id, arena.nan)
|| walk::contains(arena, id, arena.complex_infinity)
|| walk::contains(arena, id, arena.infinity)
|| walk::contains(arena, id, arena.neg_infinity)
}
fn taylor_standin(arena: &mut Arena, f: ExprId, var: ExprId, point: ExprId, k: i64) -> ExprId {
let k = u64::try_from(k).unwrap_or(0);
let mut body = f;
for _ in 0..k {
body = arena.intern(ExprNode::Derivative(body, var));
}
let at = arena.make(ExprNode::Subs(body, var, point));
if k <= 1 {
return at;
}
let inv = rat_expr(arena, Q::new(BigInt::one(), factorial(k)));
arena.mul(&[inv, at])
}
fn as_monomial(arena: &mut Arena, arg: ExprId, var: ExprId) -> Option<(ExprId, usize)> {
let terms = crate::calculus::summation::sym_poly_in(arena, arg, var)?;
if terms.len() != 1 || terms[0].0 == 0 {
return None;
}
Some((terms[0].1, terms[0].0))
}
fn as_shifted_monomial(
arena: &mut Arena,
arg: ExprId,
var: ExprId,
) -> Option<(ExprId, ExprId, usize)> {
let terms = crate::calculus::summation::sym_poly_in(arena, arg, var)?;
if terms.len() != 2 || terms[0].0 != 0 {
return None;
}
let a = terms[0].1;
if is_zero_expr(arena, a) {
return None;
}
Some((a, terms[1].1, terms[1].0))
}
fn detect_known(arena: &mut Arena, expr: ExprId, var: ExprId) -> Option<Known> {
let node = arena.node(expr).clone();
let elementary = |kind: FnKind, arg: ExprId, arena: &mut Arena| -> Option<Known> {
let (c, m) = as_monomial(arena, arg, var)?;
Some(Known::Elementary { kind, c, m })
};
match node {
ExprNode::Exp(a) => elementary(FnKind::Exp, a, arena),
ExprNode::Sin(a) => elementary(FnKind::Sin, a, arena),
ExprNode::Cos(a) => elementary(FnKind::Cos, a, arena),
ExprNode::Sinh(a) => elementary(FnKind::Sinh, a, arena),
ExprNode::Cosh(a) => elementary(FnKind::Cosh, a, arena),
ExprNode::Tan(a) => elementary(FnKind::Tan, a, arena),
ExprNode::Tanh(a) => elementary(FnKind::Tanh, a, arena),
ExprNode::Atan(a) => elementary(FnKind::Atan, a, arena),
ExprNode::Atanh(a) => elementary(FnKind::Atanh, a, arena),
ExprNode::Asin(a) => elementary(FnKind::Asin, a, arena),
ExprNode::Asinh(a) => elementary(FnKind::Asinh, a, arena),
ExprNode::Erf(a) => elementary(FnKind::Erf, a, arena),
ExprNode::LambertW(a) => elementary(FnKind::LambertW, a, arena),
ExprNode::Ln(inner) => {
let (a, c, m) = as_shifted_monomial(arena, inner, var)?;
Some(Known::Ln { a, c, m })
}
ExprNode::Pow(base, exp) => {
if walk::contains(arena, exp, var) {
return None;
}
let (a, c, m) = as_shifted_monomial(arena, base, var)?;
Some(Known::Binomial {
alpha: exp,
a,
c,
m,
})
}
_ => None,
}
}
impl Known {
fn coefficient(&self, arena: &mut Arena, k: usize) -> ExprId {
match self {
Known::Elementary { kind, c, m } => {
if !k.is_multiple_of(*m) {
return arena.zero;
}
let n = k / m;
let base = kind.coefficient(arena, n);
if arena.is_zero_structural(base) {
return arena.zero;
}
let cn = pow_i(arena, *c, n as i64);
mul_all(arena, &[base, cn])
}
Known::Ln { a, c, m } => {
if k == 0 {
let l = arena.ln(*a);
return eval::eval(arena, l);
}
if !k.is_multiple_of(*m) {
return arena.zero;
}
let n = k / m;
let coeff = rat_expr(arena, FnKind::Ln1p.rational_coefficient(n));
let ratio = arena.div(*c, *a);
let rn = pow_i(arena, ratio, n as i64);
mul_all(arena, &[coeff, rn])
}
Known::Binomial { alpha, a, c, m } => {
if !k.is_multiple_of(*m) {
return arena.zero;
}
let n = k / m;
let bin = gen_binomial(arena, *alpha, n);
let ratio = arena.div(*c, *a);
let rn = pow_i(arena, ratio, n as i64);
let aa = arena.pow(*a, *alpha);
mul_all(arena, &[aa, bin, rn])
}
}
}
fn general_term(&self, arena: &mut Arena, k: ExprId) -> Option<ExprId> {
let one = arena.one;
let two = arena.int(2);
let m1 = arena.neg_one;
let pi = arena.pi;
let k_pi_2 = arena.mul(&[k, pi]);
let k_pi_2 = arena.div(k_pi_2, two);
let sin_k = arena.sin(k_pi_2);
let cos_k = arena.cos(k_pi_2);
let neg1_k = arena.pow(m1, k);
let odd_ind = {
let d = arena.sub(one, neg1_k);
arena.div(d, two)
};
let even_ind = {
let d = arena.add(&[one, neg1_k]);
arena.div(d, two)
};
let kf = arena.factorial(k);
let inv_kf = arena.div(one, kf);
let inv_k = arena.div(one, k);
let ge1 = arena.ge(k, one);
let zero = arena.zero;
let lt1 = arena.gt(one, k);
let piecewise_ge1 =
|arena: &mut Arena, v: ExprId| arena.piecewise(&[(v, ge1), (zero, lt1)]);
match self {
Known::Elementary { kind, c, m } => {
if *m != 1 {
return None;
}
let ck = arena.pow(*c, k);
let term = match kind {
FnKind::Exp => inv_kf,
FnKind::Sin => arena.mul(&[sin_k, inv_kf]),
FnKind::Cos => arena.mul(&[cos_k, inv_kf]),
FnKind::Sinh => arena.mul(&[odd_ind, inv_kf]),
FnKind::Cosh => arena.mul(&[even_ind, inv_kf]),
FnKind::Atan => {
let v = arena.mul(&[sin_k, inv_k]);
piecewise_ge1(arena, v)
}
FnKind::Atanh => {
let v = arena.mul(&[odd_ind, inv_k]);
piecewise_ge1(arena, v)
}
FnKind::Asin | FnKind::Asinh => {
let km1 = arena.sub(k, one);
let j = arena.div(km1, two);
let bin = arena.binomial(km1, j);
let four_j = arena.pow(two, km1); let den = arena.mul(&[four_j, k]);
let v = arena.div(bin, den);
let sel = if *kind == FnKind::Asin {
odd_ind
} else {
sin_k
};
let v = arena.mul(&[sel, v]);
piecewise_ge1(arena, v)
}
FnKind::Tan | FnKind::Tanh => {
let kp1 = arena.add(&[k, one]);
let p = arena.pow(two, kp1);
let pm1 = arena.sub(p, one);
let b = arena.bernoulli_number(kp1);
let f = arena.factorial(kp1);
let num = arena.mul(&[p, pm1, b]);
let v = arena.div(num, f);
let sel = if *kind == FnKind::Tan { sin_k } else { odd_ind };
arena.mul(&[sel, v])
}
FnKind::Erf => {
let km1 = arena.sub(k, one);
let j = arena.div(km1, two);
let jf = arena.factorial(j);
let den = arena.mul(&[jf, k]);
let sp = arena.sqrt(pi);
let pref = arena.div(two, sp);
let v = arena.div(pref, den);
let v = arena.mul(&[sin_k, v]);
piecewise_ge1(arena, v)
}
FnKind::LambertW => {
let nk = arena.neg(k);
let km1 = arena.sub(k, one);
let p = arena.pow(nk, km1);
let v = arena.mul(&[p, inv_kf]);
piecewise_ge1(arena, v)
}
FnKind::Ln1p => return None,
};
let r = arena.mul(&[term, ck]);
Some(eval::eval(arena, r))
}
Known::Ln { a, c, m } => {
if *m != 1 {
return None;
}
let ratio = arena.div(*c, *a);
let nr = arena.neg(ratio);
let p = arena.pow(nr, k);
let v = arena.mul(&[p, inv_k]);
let v = arena.neg(v);
let la = arena.ln(*a);
let eq0 = arena.eq_(k, zero);
let r = arena.piecewise(&[(la, eq0), (v, ge1)]);
Some(eval::eval(arena, r))
}
Known::Binomial { alpha, a, c, m } => {
if *m != 1 {
return None;
}
let neg_alpha = arena.neg(*alpha);
let rf = arena.rising_factorial(neg_alpha, k);
let bin = arena.mul(&[neg1_k, rf, inv_kf]);
let ratio = arena.div(*c, *a);
let rk = arena.pow(ratio, k);
let aa = arena.pow(*a, *alpha);
let r = arena.mul(&[aa, bin, rk]);
Some(eval::eval(arena, r))
}
}
}
}
pub(crate) fn fps(ctx: &Context, expr: ExprId, var: ExprId, point: ExprId) -> FormalPowerSeries {
let (whole, parts, zero) = {
let mut inner = ctx.inner.write();
let arena = &mut inner.arena;
let zero = arena.zero;
let shifted = if arena.is_zero_structural(point) {
expr
} else {
let v = arena.add(&[var, point]);
crate::transforms::subs::subs(arena, expr, var, v)
};
match build_term(ctx, arena, shifted, var, point) {
Some(s) => (Some(s), Vec::new(), zero),
None => {
let mut parts = Vec::new();
if let ExprNode::Add(args) = arena.node(shifted).clone() {
for t in args {
match build_term(ctx, arena, t, var, point) {
Some(s) => parts.push(s),
None => {
parts.clear();
break;
}
}
}
}
(None, parts, zero)
}
}
};
if let Some(s) = whole {
return s;
}
let mut parts = parts.into_iter();
if let Some(mut acc) = parts.next() {
let mut ok = true;
for p in parts {
match acc.add(&p) {
Ok(s) => acc = s,
Err(_) => {
ok = false;
break;
}
}
}
if ok {
return acc;
}
}
FormalPowerSeries::with_source(
ctx,
var,
point,
zero,
Source::Refused {
expr,
reason: "no Laurent or Puiseux expansion exists at this point".into(),
},
)
}
fn build_term(
ctx: &Context,
arena: &mut Arena,
f: ExprId,
var: ExprId,
point: ExprId,
) -> Option<FormalPowerSeries> {
let zero = arena.zero;
let make = |offset: ExprId, ram: u32, (source, valuation): (Source, i64)| {
let mut s = FormalPowerSeries::with_source(ctx, var, point, zero, source);
s.offset = offset;
s.ram = ram;
s.valuation = valuation;
s
};
if let Some(o) = ordinary(arena, f, var) {
return Some(make(zero, 1, o));
}
let (offset, h) = split_monomial(arena, f, var);
if h != f
&& let Some(o) = ordinary(arena, h, var)
{
return Some(make(offset, 1, o));
}
let mut d = 1u32;
let mut pows: Vec<(ExprId, ExprId, Q)> = Vec::new();
for id in walk::post_order_ids(arena, h) {
if let ExprNode::Pow(b, e) = *arena.node(id)
&& let Some(c) = positive_multiple_of(arena, b, var)
&& let Some(q) = arena.as_num(e)
&& !q.is_integer()
{
let den = u32::try_from(q.denom()).ok()?;
d = lcm_u32(d, den)?;
let q = q.clone();
let ce = arena.pow(c, e);
pows.push((id, eval::eval(arena, ce), q));
}
}
if d == 1 {
return None;
}
let t = arena.symbol(RAMIFIED_PLACEHOLDER);
if walk::contains(arena, h, t) {
return None;
}
let d_q = Q::from_integer(BigInt::from(d));
let mut g = h;
for (node, ce, q) in pows {
let m = q_to_i64(&(q * &d_q))?;
let tm = pow_i(arena, t, m);
let tm = mul_all(arena, &[ce, tm]);
g = crate::transforms::subs::subs(arena, g, node, tm);
}
let td = pow_i(arena, t, i64::from(d));
g = crate::transforms::subs::subs(arena, g, var, td);
g = eval::eval(arena, g);
if walk::contains(arena, g, var) {
return None;
}
let s = make(offset, d, ordinary(arena, g, t)?);
if !d.is_multiple_of(2) {
return Some(s);
}
let mt = arena.neg(t);
let gm = crate::transforms::subs::subs(arena, g, t, mt);
let gm = eval::eval(arena, gm);
let ng = arena.neg(g);
let ng = eval::eval(arena, ng);
if gm == g {
Some(s.reindexed(arena, offset, d / 2, 2, 1, 0))
} else if gm == ng {
let step = rat_expr(arena, Q::new(BigInt::one(), BigInt::from(d)));
let o = arena.add(&[offset, step]);
let o = eval::eval(arena, o);
Some(s.reindexed(arena, o, d / 2, 2, 1, -1))
} else {
Some(s)
}
}
fn ordinary(arena: &mut Arena, f: ExprId, svar: ExprId) -> Option<(Source, i64)> {
if let Some(k) = detect_known(arena, f, svar) {
return Some((Source::Known(k), 0));
}
if let Some(terms) = crate::calculus::summation::sym_poly_in(arena, f, svar) {
let deg = terms.iter().map(|(p, _)| *p).max().unwrap_or(0);
let mut coeffs = vec![arena.zero; deg + 1];
for (p, c) in terms {
coeffs[p] = c;
}
return Some((Source::Polynomial(coeffs), 0));
}
let ts = expand_meromorphic(arena, f, svar, 8).ok()?;
let valuation = ts.shift();
Some((
Source::Engine {
expr: f,
svar,
cache: Mutex::new(Some(ts)),
},
valuation,
))
}
fn positive_multiple_of(arena: &Arena, b: ExprId, var: ExprId) -> Option<ExprId> {
if b == var {
return Some(arena.one);
}
if let ExprNode::Mul(args) = arena.node(b)
&& args.len() == 2
{
for (c, v) in [(args[0], args[1]), (args[1], args[0])] {
if v == var && arena.as_num(c).is_some_and(|q| *q > Q::zero()) {
return Some(c);
}
}
}
None
}
fn split_monomial(arena: &mut Arena, f: ExprId, var: ExprId) -> (ExprId, ExprId) {
let zero = arena.zero;
let factors: Vec<ExprId> = match arena.node(f) {
ExprNode::Mul(args) => args.to_vec(),
_ => vec![f],
};
let mut exps = Vec::new();
let mut rest = Vec::new();
for t in factors {
match *arena.node(t) {
ExprNode::Pow(b, e)
if !walk::contains(arena, e, var)
&& positive_multiple_of(arena, b, var).is_some() =>
{
exps.push(e);
if let Some(c) = positive_multiple_of(arena, b, var)
&& c != arena.one
{
let ce = arena.pow(c, e);
rest.push(eval::eval(arena, ce));
}
}
ExprNode::Pow(b, n) if as_integer(arena, n).is_some() => match *arena.node(b) {
ExprNode::Pow(bb, e) if bb == var && !walk::contains(arena, e, var) => {
let en = arena.mul(&[e, n]);
exps.push(eval::eval(arena, en));
}
_ => rest.push(t),
},
_ => rest.push(t),
}
}
if exps.is_empty() {
return (zero, f);
}
let rho = add_all(arena, &exps);
if let Some(m) = as_integer(arena, rho) {
rest.push(pow_i(arena, var, m));
return (zero, mul_all(arena, &rest));
}
(rho, mul_all(arena, &rest))
}
impl FormalPowerSeries {
fn with_source(
ctx: &Context,
var: ExprId,
point: ExprId,
zero: ExprId,
source: Source,
) -> Self {
FormalPowerSeries {
ctx: ctx.clone(),
var,
point,
offset: zero,
ram: 1,
valuation: 0,
log_terms: Vec::new(),
source,
memo: Mutex::new(Vec::new()),
zero,
}
}
#[must_use]
pub fn from_coefficients(var: &Ex, point: &Ex, coeffs: &[Ex]) -> Self {
let point_id = var.checked_id(point);
let ids: Vec<ExprId> = coeffs.iter().map(|c| var.checked_id(c)).collect();
let ctx = var.context();
let zero = ctx.inner.read().arena.zero;
Self::with_source(&ctx, var.raw_id(), point_id, zero, Source::Polynomial(ids))
}
fn derived(&self, source: Source, valuation: i64) -> Self {
let mut s = Self::with_source(&self.ctx, self.var, self.point, self.zero, source);
s.offset = self.offset;
s.ram = self.ram;
s.valuation = valuation;
s
}
fn refused_like(&self, expr: ExprId, reason: &str) -> Self {
let source = Source::Refused {
expr,
reason: reason.to_string(),
};
Self::with_source(&self.ctx, self.var, self.point, self.zero, source)
}
fn reindexed(
self,
arena: &Arena,
offset: ExprId,
ram: u32,
mul: u32,
div: u32,
shift: i64,
) -> Self {
let (offset, shift) = match as_integer(arena, offset) {
Some(m) if m != 0 => (
self.zero,
shift.saturating_add(m.saturating_mul(i64::from(ram) * i64::from(mul))),
),
_ => (offset, shift),
};
if mul == 1 && div == 1 && shift == 0 && offset == self.offset && ram == self.ram {
return self;
}
let num = self
.valuation
.saturating_mul(i64::from(div))
.saturating_add(shift);
let m = i64::from(mul);
let valuation = num.div_euclid(m) + i64::from(num.rem_euclid(m) != 0);
let (ctx, var, point, zero) = (self.ctx.clone(), self.var, self.point, self.zero);
let log_terms = self.log_terms.clone();
let source = Source::Reindex {
inner: Box::new(self),
mul,
div,
shift,
};
let mut s = Self::with_source(&ctx, var, point, zero, source);
s.offset = offset;
s.ram = ram;
s.valuation = valuation;
s.log_terms = log_terms;
s
}
fn aligned(&self, offset: ExprId, ram: u32) -> Option<Self> {
if !ram.is_multiple_of(self.ram) {
return None;
}
let shift = {
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
let n = arena.neg(offset);
let delta = add_all(arena, &[self.offset, n]);
let q = arena.as_num(delta)?.clone();
q_to_i64(&(q * Q::from_integer(BigInt::from(ram))))?
};
let s = self.clone();
let inner = self.ctx.inner.read();
Some(s.reindexed(&inner.arena, offset, ram, 1, ram / self.ram, shift))
}
fn common_frame(&self, other: &Self) -> Option<(ExprId, u32)> {
let ram = lcm_u32(self.ram, other.ram)?;
if self.offset == other.offset {
return Some((self.offset, ram));
}
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
let n = arena.neg(self.offset);
let delta = add_all(arena, &[other.offset, n]);
let dq = arena.as_num(delta)?.clone();
let den = |q: &Q| u32::try_from(q.denom()).ok();
if let (Some(a), Some(b)) = (
arena.as_num(self.offset).cloned(),
arena.as_num(other.offset).cloned(),
) {
let r = lcm_u32(lcm_u32(ram, den(&a)?)?, den(&b)?)?;
return Some((arena.zero, r));
}
let r = lcm_u32(ram, den(&dq)?)?;
let offset = if dq >= Q::zero() {
self.offset
} else {
other.offset
};
Some((offset, r))
}
fn with_zero_offset(&self) -> Option<Self> {
if self.offset == self.zero {
return Some(self.clone());
}
let den = {
let inner = self.ctx.inner.read();
let q = inner.arena.as_num(self.offset)?;
u32::try_from(q.denom()).ok()?
};
self.aligned(self.zero, lcm_u32(self.ram, den)?)
}
fn ensure_usable(&self, op: &'static str, logs_ok: bool) -> Result<(), SymplexError> {
if let Source::Refused { reason, .. } = &self.source {
return Err(SymplexError::ComputationFailed {
operation: op,
reason: format!("the series does not exist: {reason}"),
});
}
if !logs_ok && !self.log_terms.is_empty() {
return Err(SymplexError::InvalidArgument {
operation: op,
reason: "series has a logarithmic term".into(),
});
}
Ok(())
}
fn is_zero_coeff(&self, c: Coeff) -> bool {
if !c.exact {
return false;
}
let mut inner = self.ctx.inner.write();
is_zero_expr(&mut inner.arena, c.id)
}
fn wrap(&self, id: ExprId) -> Ex {
Ex::from_raw_parts(self.ctx.id, Arc::clone(&self.ctx.inner), id)
}
#[must_use]
pub fn variable(&self) -> Ex {
self.wrap(self.var)
}
#[must_use]
pub fn point(&self) -> Ex {
self.wrap(self.point)
}
#[must_use]
pub fn context(&self) -> Context {
self.ctx.clone()
}
#[must_use]
pub fn offset(&self) -> Ex {
self.wrap(self.offset)
}
#[must_use]
pub fn ramification(&self) -> u32 {
self.ram
}
#[must_use]
pub fn exponent(&self, k: usize) -> Ex {
let id = {
let mut inner = self.ctx.inner.write();
self.exponent_id(&mut inner.arena, index(k))
};
self.wrap(id)
}
fn exponent_id(&self, arena: &mut Arena, k: i64) -> ExprId {
let step = rat_expr(arena, Q::new(BigInt::from(k), BigInt::from(self.ram)));
add_all(arena, &[self.offset, step])
}
fn exponent_index_in(&self, arena: &Arena, e: i64) -> Option<i64> {
let rho = arena.as_num(self.offset)?;
q_to_i64(
&((Q::from_integer(BigInt::from(e)) - rho) * Q::from_integer(BigInt::from(self.ram))),
)
}
#[must_use]
pub fn is_refused(&self) -> bool {
matches!(self.source, Source::Refused { .. })
}
fn check_compatible(&self, other: &Self, op: &'static str) -> Result<(), SymplexError> {
let _ = self.variable().checked_id(&other.variable());
if self.var != other.var || self.point != other.point {
return Err(SymplexError::InvalidArgument {
operation: op,
reason: "series must share the expansion variable and point".into(),
});
}
Ok(())
}
#[must_use]
pub fn coefficient(&self, k: usize) -> Ex {
self.wrap(self.coeff(index(k)).id)
}
pub fn try_coefficient(&self, k: usize) -> Result<Ex, SymplexError> {
self.ensure_usable("fps_coefficient", true)?;
let c = self.coeff(index(k));
if c.exact {
Ok(self.wrap(c.id))
} else {
Err(SymplexError::ComputationFailed {
operation: "fps_coefficient",
reason: "the expansion failed at this order".into(),
})
}
}
#[must_use]
pub fn coefficient_rational(&self, k: usize) -> Option<Q> {
let c = self.coeff(index(k));
if !c.exact {
return None;
}
let inner = self.ctx.inner.read();
inner.arena.as_num(c.id).cloned()
}
#[must_use]
pub fn coefficients(&self, n: usize) -> Vec<Ex> {
(0..n).map(|k| self.coefficient(k)).collect()
}
fn coeff(&self, k: i64) -> Coeff {
let zero = Coeff {
id: self.zero,
exact: true,
};
let Ok(idx) = usize::try_from(k.saturating_sub(self.valuation)) else {
return zero;
};
let mut memo = lock(&self.memo);
while memo.len() <= idx {
let next = self.valuation + memo.len() as i64;
let c = self.compute(next, &memo);
memo.push(c);
}
memo[idx]
}
fn coeffs(&self, lo: i64, hi: i64) -> Vec<Coeff> {
if hi < lo {
return Vec::new();
}
let _ = self.coeff(hi);
(lo..=hi).map(|k| self.coeff(k)).collect()
}
fn compute(&self, k: i64, prev: &[Coeff]) -> Coeff {
let exact = |id: ExprId| Coeff { id, exact: true };
let zero = exact(self.zero);
match &self.source {
Source::Known(known) => {
let Ok(k) = usize::try_from(k) else {
return zero;
};
let mut inner = self.ctx.inner.write();
exact(known.coefficient(&mut inner.arena, k))
}
Source::Engine { expr, svar, cache } => {
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
let mut guard = lock(cache);
let need = k + 1;
if guard.as_ref().is_none_or(|ts| ts.known() < need) {
let order =
need.max(guard.as_ref().map_or(8, |ts| ts.known().saturating_mul(2)));
let ts = expand_meromorphic(arena, *expr, *svar, order).or_else(|e| {
if order > need {
expand_meromorphic(arena, *expr, *svar, need)
} else {
Err(e)
}
});
if let Ok(ts) = ts {
*guard = Some(ts);
}
}
if let Some(ts) = guard.as_ref()
&& ts.known() >= need
{
let c = ts.coefficient(arena, k);
if !contains_singular(arena, c) {
return exact(c);
}
}
let id = taylor_standin(arena, *expr, *svar, self.zero, k);
Coeff { id, exact: false }
}
Source::Polynomial(c) => usize::try_from(k)
.ok()
.and_then(|k| c.get(k).copied())
.map_or(zero, exact),
Source::Refused { expr, .. } => {
if k < 0 {
return zero;
}
let mut inner = self.ctx.inner.write();
let id = taylor_standin(&mut inner.arena, *expr, self.var, self.point, k);
Coeff { id, exact: false }
}
Source::Add(a, b) | Source::Sub(a, b) => {
let x = a.coeff(k);
let y = b.coeff(k);
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
let y_id = if matches!(self.source, Source::Sub(..)) {
arena.neg(y.id)
} else {
y.id
};
Coeff {
id: add_all(arena, &[x.id, y_id]),
exact: x.exact && y.exact,
}
}
Source::Mul(a, b) => {
let (va, vb) = (a.valuation, b.valuation);
let xs = a.coeffs(va, k - vb);
let ys = b.coeffs(vb, k - va);
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
let mut terms = Vec::new();
let mut ex = true;
let top = ys.len();
for (i, x) in xs.iter().enumerate() {
let y = ys[top - 1 - i];
if (x.exact && arena.is_zero_structural(x.id))
|| (y.exact && arena.is_zero_structural(y.id))
{
continue;
}
ex &= x.exact && y.exact;
terms.push(arena.mul(&[x.id, y.id]));
}
Coeff {
id: add_all(arena, &terms),
exact: ex,
}
}
Source::Scale(a, c) => {
let x = a.coeff(k);
let mut inner = self.ctx.inner.write();
Coeff {
id: mul_all(&mut inner.arena, &[*c, x.id]),
exact: x.exact,
}
}
Source::Derivative(a) => {
let d = i64::from(a.ram);
let x = a.coeff(k + d);
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
let e = a.exponent_id(arena, k + d);
let mut terms = vec![arena.mul(&[e, x.id])];
for &(p, r) in &a.log_terms {
if a.exponent_index_in(arena, p - 1) == Some(k) {
terms.push(r);
}
}
Coeff {
id: add_all(arena, &terms),
exact: x.exact,
}
}
Source::Integral(a) => {
let d = i64::from(a.ram);
let x = a.coeff(k - d);
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
let e = a.exponent_id(arena, k - d);
let one = arena.one;
let e1 = add_all(arena, &[e, one]);
let mut terms = Vec::new();
if !is_zero_expr(arena, e1) && !arena.is_zero_structural(x.id) {
let m1 = arena.neg_one;
let inv = arena.pow(e1, m1);
terms.push(arena.mul(&[x.id, inv]));
}
for &(p, r) in &a.log_terms {
if a.exponent_index_in(arena, p + 1) == Some(k) {
let c = rat_expr(
arena,
Q::new(BigInt::from(-1), BigInt::from((p + 1) * (p + 1))),
);
terms.push(arena.mul(&[c, r]));
}
}
Coeff {
id: add_all(arena, &terms),
exact: x.exact,
}
}
Source::Inverse(a, l) => {
let j = k + l;
let xs = a.coeffs(*l, l + j);
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
let inv0 = {
let m1 = arena.neg_one;
let p = arena.pow(xs[0].id, m1);
eval::eval(arena, p)
};
let mut ex = xs[0].exact;
if j == 0 {
return Coeff {
id: inv0,
exact: ex,
};
}
let mut terms = Vec::new();
for (i, ai) in xs.iter().enumerate().skip(1) {
let bj = prev[usize::try_from(j).unwrap_or(0) - i];
if arena.is_zero_structural(ai.id) || arena.is_zero_structural(bj.id) {
continue;
}
ex &= ai.exact && bj.exact;
terms.push(arena.mul(&[ai.id, bj.id]));
}
let s = add_all(arena, &terms);
let v = arena.mul(&[inv0, s]);
let v = arena.neg(v);
Coeff {
id: eval::eval(arena, v),
exact: ex,
}
}
Source::Compose(f, g) => {
let Ok(ku) = usize::try_from(k) else {
return zero;
};
let fs = f.coeffs(0, k);
let gs = g.coeffs(0, k);
let ex = fs.iter().chain(gs.iter()).all(|c| c.exact);
let gs: Vec<ExprId> = gs.iter().map(|c| c.id).collect();
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
let n = ku + 1;
let mut power: Vec<ExprId> = vec![arena.zero; n];
power[0] = arena.one;
let mut terms = Vec::new();
for (i, fi) in fs.iter().enumerate() {
if i > 0 {
power = poly_mul(arena, &power, &gs, n);
}
if arena.is_zero_structural(fi.id) || arena.is_zero_structural(power[ku]) {
continue;
}
terms.push(arena.mul(&[fi.id, power[ku]]));
}
Coeff {
id: add_all(arena, &terms),
exact: ex,
}
}
Source::Reversion(f) => {
let Ok(ku) = usize::try_from(k) else {
return zero;
};
if ku == 0 {
return zero;
}
let fs = f.coeffs(0, k);
let ex = fs.iter().all(|c| c.exact);
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
let h: Vec<ExprId> = fs[1..].iter().map(|c| c.id).collect();
let inv_h = poly_inverse(arena, &h, ku);
let mut power: Vec<ExprId> = vec![arena.zero; ku];
power[0] = arena.one;
for _ in 0..ku {
power = poly_mul(arena, &power, &inv_h, ku);
}
let inv_k = rat_expr(arena, Q::new(BigInt::one(), BigInt::from(ku)));
Coeff {
id: mul_all(arena, &[inv_k, power[ku - 1]]),
exact: ex,
}
}
Source::Reindex {
inner,
mul,
div,
shift,
} => {
let num = k.saturating_mul(i64::from(*mul)).saturating_sub(*shift);
let dv = i64::from(*div);
if num.rem_euclid(dv) != 0 {
return zero;
}
inner.coeff(num.div_euclid(dv))
}
}
}
#[must_use]
pub fn has_closed_form(&self) -> bool {
match &self.source {
Source::Known(_) | Source::Polynomial(_) => true,
Source::Add(a, b) | Source::Sub(a, b) => a.has_closed_form() && b.has_closed_form(),
Source::Scale(a, _) => a.has_closed_form(),
Source::Derivative(a) => a.log_terms.is_empty() && a.has_closed_form(),
Source::Integral(a) => {
a.is_taylor_frame() && a.log_terms.is_empty() && a.has_closed_form()
}
Source::Reindex { inner, div, .. } => *div == 1 && inner.has_closed_form(),
_ => false,
}
}
fn is_taylor_frame(&self) -> bool {
self.offset == self.zero && self.ram == 1
}
#[must_use]
pub fn general_term(&self, k: &Ex) -> Option<Ex> {
let k_id = self.variable().checked_id(k);
let id = self.general_term_id(k_id)?;
Some(self.wrap(id))
}
fn general_term_id(&self, k: ExprId) -> Option<ExprId> {
match &self.source {
Source::Known(known) => {
let mut inner = self.ctx.inner.write();
known.general_term(&mut inner.arena, k)
}
Source::Polynomial(c) => {
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
let mut pairs = Vec::new();
for (i, &ci) in c.iter().enumerate() {
if arena.is_zero_structural(ci) {
continue;
}
let ie = arena.int(i as i64);
let cond = arena.eq_(k, ie);
pairs.push((ci, cond));
}
let deg = arena.int(c.len() as i64);
let rest = arena.ge(k, deg);
let zero = arena.zero;
pairs.push((zero, rest));
Some(arena.piecewise(&pairs))
}
Source::Add(a, b) | Source::Sub(a, b) => {
let x = a.general_term_id(k)?;
let y = b.general_term_id(k)?;
let mut inner = self.ctx.inner.write();
let y = if matches!(self.source, Source::Sub(..)) {
inner.arena.neg(y)
} else {
y
};
Some(add_all(&mut inner.arena, &[x, y]))
}
Source::Scale(a, c) => {
let x = a.general_term_id(k)?;
let mut inner = self.ctx.inner.write();
Some(mul_all(&mut inner.arena, &[*c, x]))
}
Source::Derivative(a) => {
if !a.log_terms.is_empty() {
return None;
}
let mut inner = self.ctx.inner.write();
let d = inner.arena.int(i64::from(a.ram));
let kpd = inner.arena.add(&[k, d]);
drop(inner);
let x = a.general_term_id(kpd)?;
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
let e = if a.ram == 1 { kpd } else { arena.div(kpd, d) };
let e = arena.add(&[a.offset, e]);
Some(mul_all(arena, &[e, x]))
}
Source::Integral(a) => {
if !(a.is_taylor_frame() && a.log_terms.is_empty()) {
return None;
}
let mut inner = self.ctx.inner.write();
let one = inner.arena.one;
let km1 = inner.arena.sub(k, one);
drop(inner);
let x = a.general_term_id(km1)?;
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
let v = arena.div(x, k);
let one = arena.one;
let zero = arena.zero;
let ge1 = arena.ge(k, one);
let lt1 = arena.gt(one, k);
Some(arena.piecewise(&[(v, ge1), (zero, lt1)]))
}
Source::Reindex {
inner: s,
mul,
div,
shift,
} => {
if *div != 1 {
return None;
}
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
let m = arena.int(i64::from(*mul));
let km = arena.mul(&[m, k]);
let sh = arena.int(-*shift);
let j = arena.add(&[km, sh]);
let j = eval::eval(arena, j);
drop(inner);
s.general_term_id(j)
}
_ => None,
}
}
#[must_use]
pub fn truncate(&self, n: usize) -> Ex {
let id = self.truncate_id(n);
self.wrap(id)
}
pub fn try_truncate(&self, n: usize) -> Result<Ex, SymplexError> {
self.ensure_usable("fps_truncate", true)?;
let end = self.index_end(n);
if self
.coeffs(self.valuation, end - 1)
.iter()
.any(|c| !c.exact)
{
return Err(SymplexError::ComputationFailed {
operation: "fps_truncate",
reason: "the expansion failed at this order".into(),
});
}
Ok(self.truncate(n))
}
fn index_end(&self, n: usize) -> i64 {
let inner = self.ctx.inner.read();
let r = rational_part(&inner.arena, self.offset);
let x = (Q::from_integer(BigInt::from(n)) - r) * Q::from_integer(BigInt::from(self.ram));
q_to_i64(&x.ceil())
.unwrap_or(self.valuation)
.max(self.valuation)
}
fn truncate_id(&self, n: usize) -> ExprId {
if let Source::Refused { expr, .. } = &self.source {
let mut inner = self.ctx.inner.write();
let order = inner.arena.int(i64::try_from(n).unwrap_or(i64::MAX));
return inner
.arena
.intern(ExprNode::Series(*expr, self.var, self.point, order));
}
let end = self.index_end(n);
let coeffs = self.coeffs(self.valuation, end - 1);
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
let base = if arena.is_zero_structural(self.point) {
self.var
} else {
arena.sub(self.var, self.point)
};
let mut terms = Vec::new();
for (i, c) in coeffs.iter().enumerate() {
if arena.is_zero_structural(c.id) {
continue;
}
let e = self.exponent_id(arena, self.valuation + i as i64);
let t = if arena.is_zero_structural(e) {
c.id
} else if e == arena.one {
arena.mul(&[c.id, base])
} else {
let p = arena.pow(base, e);
let p = eval::eval(arena, p);
arena.mul(&[c.id, p])
};
terms.push(t);
}
for &(p, r) in &self.log_terms {
if usize::try_from(p).is_ok_and(|p| p < n) {
let ln = arena.ln(base);
let bp = pow_i(arena, base, p);
terms.push(arena.mul(&[r, bp, ln]));
}
}
add_all(arena, &terms)
}
pub fn add(&self, other: &Self) -> Result<Self, SymplexError> {
self.add_sub(other, false, "fps_add")
}
pub fn sub(&self, other: &Self) -> Result<Self, SymplexError> {
self.add_sub(other, true, "fps_sub")
}
fn add_sub(&self, other: &Self, sub: bool, op: &'static str) -> Result<Self, SymplexError> {
self.check_compatible(other, op)?;
self.ensure_usable(op, true)?;
other.ensure_usable(op, true)?;
let mismatch = || SymplexError::InvalidArgument {
operation: op,
reason: "the exponents of the two series differ by a non-rational amount".into(),
};
let (offset, ram) = self.common_frame(other).ok_or_else(mismatch)?;
let a = self.aligned(offset, ram).ok_or_else(mismatch)?;
let b = other.aligned(offset, ram).ok_or_else(mismatch)?;
let mut logs = a.log_terms.clone();
{
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
for &(p, r) in &b.log_terms {
let r = if sub { arena.neg(r) } else { r };
match logs.iter_mut().find(|(q, _)| *q == p) {
Some(e) => e.1 = add_all(arena, &[e.1, r]),
None => logs.push((p, r)),
}
}
}
let valuation = a.valuation.min(b.valuation);
let source = if sub {
Source::Sub(Box::new(a.clone()), Box::new(b))
} else {
Source::Add(Box::new(a.clone()), Box::new(b))
};
let mut s = a.derived(source, valuation);
s.log_terms = logs;
Ok(s)
}
pub fn mul(&self, other: &Self) -> Result<Self, SymplexError> {
self.check_compatible(other, "fps_mul")?;
self.ensure_usable("fps_mul", false)?;
other.ensure_usable("fps_mul", false)?;
let too_fine = || SymplexError::InvalidArgument {
operation: "fps_mul",
reason: "ramification too large".into(),
};
let ram = lcm_u32(self.ram, other.ram).ok_or_else(too_fine)?;
let a = self.aligned(self.offset, ram).ok_or_else(too_fine)?;
let b = other.aligned(other.offset, ram).ok_or_else(too_fine)?;
let offset = {
let mut inner = self.ctx.inner.write();
add_all(&mut inner.arena, &[a.offset, b.offset])
};
let valuation = a.valuation.saturating_add(b.valuation);
let mut s = a.derived(Source::Mul(Box::new(a.clone()), Box::new(b)), valuation);
s.offset = offset;
let inner = self.ctx.inner.read();
Ok(s.reindexed(&inner.arena, offset, ram, 1, 1, 0))
}
#[must_use]
pub fn scale(&self, c: &Ex) -> Self {
let cid = self.variable().checked_id(c);
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
if let Source::Refused { expr, reason } = &self.source {
let e = mul_all(arena, &[cid, *expr]);
drop(inner);
return self.refused_like(e, reason);
}
let logs = self
.log_terms
.iter()
.map(|&(p, r)| (p, mul_all(arena, &[cid, r])))
.collect();
drop(inner);
let mut s = self.derived(Source::Scale(Box::new(self.clone()), cid), self.valuation);
s.log_terms = logs;
s
}
pub fn compose(&self, other: &Self) -> Result<Self, SymplexError> {
self.check_compatible(other, "fps_compose")?;
self.ensure_usable("fps_compose", false)?;
other.ensure_usable("fps_compose", false)?;
if !self.is_taylor_frame() || !self.vanishes_up_to(-1) {
return Err(SymplexError::InvalidArgument {
operation: "fps_compose",
reason: "outer series must be a power series".into(),
});
}
let g = other
.with_zero_offset()
.filter(|g| g.vanishes_up_to(0))
.ok_or_else(|| SymplexError::InvalidArgument {
operation: "fps_compose",
reason: "inner series must have zero constant term".into(),
})?;
let mut s = g.derived(
Source::Compose(Box::new(self.clone()), Box::new(g.clone())),
0,
);
s.offset = self.zero;
Ok(s)
}
fn vanishes_up_to(&self, top: i64) -> bool {
if self.valuation < -4096 {
return false;
}
(self.valuation..=top).all(|i| self.is_zero_coeff(self.coeff(i)))
}
#[must_use]
pub fn derivative(&self) -> Self {
if let Source::Refused { expr, reason } = &self.source {
let d = {
let mut inner = self.ctx.inner.write();
inner.arena.intern(ExprNode::Derivative(*expr, self.var))
};
return self.refused_like(d, reason);
}
let mut valuation = self.valuation.saturating_sub(i64::from(self.ram));
let mut logs = Vec::new();
{
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
for &(p, r) in &self.log_terms {
if let Some(i) = self.exponent_index_in(arena, p - 1) {
valuation = valuation.min(i);
}
if p > 0 {
let pe = arena.int(p);
logs.push((p - 1, mul_all(arena, &[pe, r])));
}
}
}
let mut s = self.derived(Source::Derivative(Box::new(self.clone())), valuation);
s.log_terms = logs;
s
}
#[must_use]
pub fn integral(&self) -> Self {
if let Source::Refused { expr, reason } = &self.source {
let i = {
let mut inner = self.ctx.inner.write();
inner.arena.intern(ExprNode::Integral(*expr, self.var))
};
return self.refused_like(i, reason);
}
let residue_index = {
let inner = self.ctx.inner.read();
self.exponent_index_in(&inner.arena, -1)
};
let mut logs = Vec::new();
if let Some(i) = residue_index
&& i >= self.valuation
{
let c = self.coeff(i);
if !self.is_zero_coeff(c) {
logs.push((0, c.id));
}
}
let mut valuation = self.valuation.saturating_add(i64::from(self.ram));
{
let mut inner = self.ctx.inner.write();
let arena = &mut inner.arena;
for &(p, r) in &self.log_terms {
if let Some(i) = self.exponent_index_in(arena, p + 1) {
valuation = valuation.min(i);
}
let c = rat_expr(arena, Q::new(BigInt::one(), BigInt::from(p + 1)));
logs.push((p + 1, mul_all(arena, &[c, r])));
}
}
let mut s = self.derived(Source::Integral(Box::new(self.clone())), valuation);
s.log_terms = logs;
s
}
pub fn inverse(&self) -> Result<Self, SymplexError> {
self.ensure_usable("fps_inverse", false)?;
let not_invertible = || SymplexError::InvalidArgument {
operation: "fps_inverse",
reason: "series has zero constant term (not invertible as a power series)".into(),
};
if self.valuation < -4096 {
return Err(not_invertible());
}
let mut lead = None;
for i in self.valuation..=0 {
let c = self.coeff(i);
if !c.exact {
return Err(not_invertible());
}
if !self.is_zero_coeff(c) {
lead = Some(i);
break;
}
}
let l = lead.ok_or_else(not_invertible)?;
let offset = {
let mut inner = self.ctx.inner.write();
let n = inner.arena.neg(self.offset);
eval::eval(&mut inner.arena, n)
};
let mut s = self.derived(Source::Inverse(Box::new(self.clone()), l), -l);
s.offset = offset;
Ok(s)
}
pub fn reversion(&self) -> Result<Self, SymplexError> {
self.ensure_usable("fps_reversion", false)?;
let ok = self.is_taylor_frame() && self.vanishes_up_to(0) && {
let a1 = self.coeff(1);
a1.exact && !self.is_zero_coeff(a1)
};
if !ok {
return Err(SymplexError::InvalidArgument {
operation: "fps_reversion",
reason: "reversion needs a_0 = 0 and a_1 ≠ 0".into(),
});
}
Ok(self.derived(Source::Reversion(Box::new(self.clone())), 0))
}
}
fn index(k: usize) -> i64 {
i64::try_from(k).unwrap_or(i64::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
fn rat(p: i64, q: i64) -> Q {
Q::new(BigInt::from(p), BigInt::from(q))
}
#[test]
fn exp_coefficients_and_general_term() {
let ctx = Context::new();
let x = ctx.symbol("x");
let k = ctx.symbol("k");
let s = x.exp().fps_maclaurin(&x);
assert!(s.has_closed_form());
assert_eq!(s.coefficient_rational(0), Some(rat(1, 1)));
assert_eq!(s.coefficient_rational(5), Some(rat(1, 120)));
assert_eq!(s.general_term(&k).unwrap().to_string(), "1/k!");
}
#[test]
fn sin_cos_sinh_cosh_coefficients() {
let ctx = Context::new();
let x = ctx.symbol("x");
let s = x.sin().fps_maclaurin(&x);
assert_eq!(s.coefficient_rational(1), Some(rat(1, 1)));
assert_eq!(s.coefficient_rational(2), Some(rat(0, 1)));
assert_eq!(s.coefficient_rational(3), Some(rat(-1, 6)));
assert_eq!(s.coefficient_rational(5), Some(rat(1, 120)));
let c = x.cos().fps_maclaurin(&x);
assert_eq!(c.coefficient_rational(2), Some(rat(-1, 2)));
assert_eq!(c.coefficient_rational(4), Some(rat(1, 24)));
let sh = x.sinh().fps_maclaurin(&x);
assert_eq!(sh.coefficient_rational(3), Some(rat(1, 6)));
let ch = x.cosh().fps_maclaurin(&x);
assert_eq!(ch.coefficient_rational(2), Some(rat(1, 2)));
}
#[test]
fn general_terms_match_coefficients() {
let ctx = Context::new();
let x = ctx.symbol("x");
let k = ctx.symbol("k");
let funcs = [
x.sin(),
x.cos(),
x.sinh(),
x.cosh(),
x.atan(),
x.atanh(),
x.tan(),
x.tanh(),
x.asin(),
x.asinh(),
x.lambertw(),
(&ctx.int(1) + &x).ln(),
(&ctx.int(1) - &x).powi(-1),
(&ctx.int(1) + &x).pow(&ctx.rational(1, 2)),
];
for f in funcs {
let s = f.fps_maclaurin(&x);
let gt = s
.general_term(&k)
.unwrap_or_else(|| panic!("no general term for {f}"));
for i in 0..8usize {
let a = s.coefficient(i).eval();
let b = gt.subs_i64(&k, i as i64).eval();
let av = a.eval_f64().unwrap();
let bv = b
.eval_f64()
.unwrap_or_else(|_| panic!("general term of {f} at {i}: {b}"));
assert!(
(av - bv).abs() < 1e-12,
"{f}: a_{i} = {a} but general term gives {b}"
);
}
}
}
#[test]
fn scaled_argument_and_shifted_log() {
let ctx = Context::new();
let x = ctx.symbol("x");
let s = (&x * 2).exp().fps_maclaurin(&x);
assert_eq!(s.coefficient_rational(3), Some(rat(4, 3)));
let s = x.powi(2).sin().fps_maclaurin(&x);
assert_eq!(s.coefficient_rational(2), Some(rat(1, 1)));
assert_eq!(s.coefficient_rational(3), Some(rat(0, 1)));
assert_eq!(s.coefficient_rational(6), Some(rat(-1, 6)));
let s = (&ctx.int(2) + &x).ln().fps_maclaurin(&x);
assert_eq!(s.coefficient(0).to_string(), "ln(2)");
assert_eq!(s.coefficient_rational(1), Some(rat(1, 2)));
assert_eq!(s.coefficient_rational(2), Some(rat(-1, 8)));
let s = (&ctx.int(2) - &x).powi(-1).fps_maclaurin(&x);
assert_eq!(s.coefficient_rational(0), Some(rat(1, 2)));
assert_eq!(s.coefficient_rational(2), Some(rat(1, 8)));
}
#[test]
fn engine_fallback_and_laurent() {
let ctx = Context::new();
let x = ctx.symbol("x");
let s = (&x.exp() + &x.sin()).fps_maclaurin(&x);
assert!(!s.has_closed_form());
assert_eq!(s.coefficient_rational(0), Some(rat(1, 1)));
assert_eq!(s.coefficient_rational(1), Some(rat(2, 1)));
assert_eq!(s.coefficient_rational(3), Some(rat(0, 1)));
let f = &ctx.int(1) / &(&x * &(&ctx.int(1) - &x));
let s = f.fps_maclaurin(&x);
assert_eq!(s.coefficient_rational(0), Some(rat(1, 1)));
assert_eq!(s.truncate(2).to_string(), "x + 1/x + 1");
}
#[test]
fn arithmetic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let e = x.exp().fps_maclaurin(&x);
let s = x.sin().fps_maclaurin(&x);
let c = x.cos().fps_maclaurin(&x);
let one = s.mul(&s).unwrap().add(&c.mul(&c).unwrap()).unwrap();
assert_eq!(one.coefficient_rational(0), Some(rat(1, 1)));
for k in 1..8 {
assert_eq!(one.coefficient_rational(k), Some(rat(0, 1)), "k={k}");
}
let ds = s.derivative();
for k in 0..8 {
assert_eq!(ds.coefficient_rational(k), c.coefficient_rational(k));
}
let ic = c.integral();
for k in 0..8 {
assert_eq!(ic.coefficient_rational(k), s.coefficient_rational(k));
}
let em = (-&x).exp().fps_maclaurin(&x);
let prod = e.mul(&em).unwrap();
for k in 1..6 {
assert_eq!(prod.coefficient_rational(k), Some(rat(0, 1)));
}
let inv = e.inverse().unwrap();
for k in 0..6 {
assert_eq!(inv.coefficient_rational(k), em.coefficient_rational(k));
}
let two_e = e.scale(&ctx.int(2));
assert_eq!(two_e.coefficient_rational(2), Some(rat(1, 1)));
let es = e.compose(&s).unwrap();
assert_eq!(es.coefficient_rational(4), Some(rat(-1, 8)));
assert_eq!(es.coefficient_rational(5), Some(rat(-1, 15)));
let at = x.tan().fps_maclaurin(&x).reversion().unwrap();
assert_eq!(at.coefficient_rational(3), Some(rat(-1, 3)));
assert_eq!(at.coefficient_rational(5), Some(rat(1, 5)));
let em1 = e
.sub(&FormalPowerSeries::from_coefficients(
&x,
&ctx.int(0),
&[ctx.int(1)],
))
.unwrap();
let l = em1.reversion().unwrap();
assert_eq!(l.coefficient_rational(2), Some(rat(-1, 2)));
assert_eq!(l.coefficient_rational(3), Some(rat(1, 3)));
assert_eq!(l.coefficient_rational(4), Some(rat(-1, 4)));
}
#[test]
fn preconditions() {
let ctx = Context::new();
let x = ctx.symbol("x");
let s = x.sin().fps_maclaurin(&x);
assert!(s.inverse().is_err());
assert!(x.cos().fps_maclaurin(&x).reversion().is_err());
assert!(
x.exp()
.fps_maclaurin(&x)
.compose(&x.cos().fps_maclaurin(&x))
.is_err()
);
let y = ctx.symbol("y");
assert!(s.add(&y.sin().fps_maclaurin(&y)).is_err());
}
#[test]
fn nonzero_point() {
let ctx = Context::new();
let x = ctx.symbol("x");
let one = ctx.int(1);
let s = x.exp().fps(&x, &one);
assert_eq!(s.coefficient(2).to_string(), "1/2*E");
let t = s.truncate(3);
assert!(t.to_string().contains("x - 1"), "{t}");
}
}