use rug::{Integer, Rational};
use std::collections::BTreeMap;
use crate::flint::FlintPoly;
use crate::integrate::risch::number_field::NumberField;
type KElem = Vec<Rational>;
type KBi = BTreeMap<(Rational, u32), KElem>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PuiseuxSeries {
pub ramification: u64,
pub terms: Vec<(Rational, Rational)>,
pub order: Option<Rational>,
}
type Bi = BTreeMap<(Rational, u32), Rational>;
type Edge = (Rational, Vec<((Rational, u32), Rational)>);
fn rzero() -> Rational {
Rational::from(0)
}
pub fn puiseux_at_zero(coeffs: &[(u32, u32, Rational)], prec: u32) -> Vec<PuiseuxSeries> {
let mut f: Bi = BTreeMap::new();
for (i, j, a) in coeffs {
if *a != 0 {
*f.entry((Rational::from(*i), *j)).or_insert_with(rzero) += a;
}
}
f.retain(|_, a| *a != 0);
if f.is_empty() {
return Vec::new();
}
factor_min_x(&mut f);
let prec_r = Rational::from(prec);
let mut out = Vec::new();
let mut f0: BTreeMap<u32, Rational> = BTreeMap::new();
for ((xe, ye), a) in &f {
if *xe == 0 {
*f0.entry(*ye).or_insert_with(rzero) += a;
}
}
let f0_dense = dense(&f0);
for c0 in rational_roots(&f0_dense) {
let g = shift_y(&f, &c0);
for (mut terms, exact) in lift(&g, &prec_r, 0) {
if c0 != 0 {
terms.insert(0, (rzero(), c0.clone()));
}
terms.retain(|(e, _)| exact || *e < prec_r);
terms.sort_by(|a, b| a.0.cmp(&b.0));
let e = terms.iter().fold(1u64, |acc, (ex, _)| {
lcm_u64(acc, ex.denom().to_u64().unwrap_or(1))
});
out.push(PuiseuxSeries {
ramification: e,
terms,
order: if exact { None } else { Some(prec_r.clone()) },
});
}
}
out
}
fn lift(g: &Bi, prec: &Rational, depth: u32) -> Vec<(Vec<(Rational, Rational)>, bool)> {
const MAX_DEPTH: u32 = 64;
let mut g = g.clone();
g.retain(|_, a| *a != 0);
if g.is_empty() {
return vec![(Vec::new(), true)]; }
if depth > MAX_DEPTH {
return vec![(Vec::new(), false)];
}
let mut result: Vec<(Vec<(Rational, Rational)>, bool)> = Vec::new();
let m0 = g.keys().map(|(_, j)| *j).min().unwrap_or(0);
if m0 > 0 {
result.push((Vec::new(), true));
let shifted: Bi = g
.into_iter()
.map(|((xe, ye), a)| ((xe, ye - m0), a))
.collect();
g = shifted;
}
for (q, edge) in newton_edges(&g) {
if q <= 0 {
continue; }
let mut phi: BTreeMap<u32, Rational> = BTreeMap::new();
for ((_, j), a) in &edge {
*phi.entry(*j).or_insert_with(rzero) += a;
}
for c in rational_roots(&dense(&phi)) {
if c == 0 {
continue;
}
if prec.clone() - &q <= 0 {
result.push((vec![(q.clone(), c.clone())], false));
continue;
}
let g1 = substitute(&g, &q, &c);
for (sub, exact) in lift(&g1, &(prec.clone() - &q), depth + 1) {
let mut terms = vec![(q.clone(), c.clone())];
for (gamma, b) in sub {
terms.push((q.clone() + &gamma, b));
}
result.push((terms, exact));
}
}
}
result
}
pub(crate) fn newton_edges_keys(keys: &[(Rational, u32)]) -> Vec<(Rational, Vec<(Rational, u32)>)> {
let mut lo: BTreeMap<u32, Rational> = BTreeMap::new();
for (xe, ye) in keys {
lo.entry(*ye)
.and_modify(|m| {
if xe < m {
*m = xe.clone();
}
})
.or_insert_with(|| xe.clone());
}
let pts: Vec<(u32, Rational)> = lo.into_iter().collect(); if pts.len() < 2 {
return Vec::new();
}
let mut hull: Vec<(u32, Rational)> = Vec::new();
for p in pts {
while hull.len() >= 2 {
let a = &hull[hull.len() - 2];
let b = &hull[hull.len() - 1];
let lhs = Rational::from(b.0 as i64 - a.0 as i64) * (p.1.clone() - &b.1);
let rhs = Rational::from(p.0 as i64 - b.0 as i64) * (b.1.clone() - &a.1);
if lhs - rhs <= 0 {
hull.pop();
} else {
break;
}
}
hull.push(p);
}
let mut edges = Vec::new();
for w in hull.windows(2) {
let (j1, i1) = (&w[0].0, &w[0].1);
let (j2, i2) = (&w[1].0, &w[1].1);
let dj = Rational::from(*j2 as i64 - *j1 as i64);
let q = (i1.clone() - i2) / dj; if q <= 0 {
continue;
}
let val = i1.clone() + q.clone() * Rational::from(*j1 as i64);
let on_edge: Vec<(Rational, u32)> = keys
.iter()
.filter(|(xe, ye)| xe.clone() + q.clone() * Rational::from(*ye as i64) == val)
.cloned()
.collect();
edges.push((q, on_edge));
}
edges
}
fn newton_edges(g: &Bi) -> Vec<Edge> {
let keys: Vec<(Rational, u32)> = g.keys().cloned().collect();
let mut edges = Vec::new();
for (q, on_edge) in newton_edges_keys(&keys) {
let monos: Vec<((Rational, u32), Rational)> = on_edge
.into_iter()
.map(|k| {
let a = g[&k].clone();
(k, a)
})
.collect();
edges.push((q, monos));
}
edges
}
fn substitute(g: &Bi, q: &Rational, c: &Rational) -> Bi {
let mut g1: Bi = BTreeMap::new();
for ((xe, ye), a) in g {
let j = *ye;
let new_xe = xe.clone() + q.clone() * Rational::from(j as i64);
for l in 0..=j {
let binom = Rational::from(binomial(j, l));
let cpow = rat_pow(c, j - l);
let coeff = a.clone() * &binom * &cpow;
if coeff != 0 {
*g1.entry((new_xe.clone(), l)).or_insert_with(rzero) += &coeff;
}
}
}
g1.retain(|_, a| *a != 0);
factor_min_x(&mut g1);
g1
}
fn factor_min_x(g: &mut Bi) {
let Some(v) = g.keys().map(|(xe, _)| xe.clone()).min() else {
return;
};
if v == 0 {
return;
}
*g = std::mem::take(g)
.into_iter()
.map(|((xe, ye), a)| ((xe - &v, ye), a))
.collect();
}
fn shift_y(f: &Bi, c0: &Rational) -> Bi {
if *c0 == 0 {
return f.clone();
}
let mut g: Bi = BTreeMap::new();
for ((xe, ye), a) in f {
let j = *ye;
for l in 0..=j {
let binom = Rational::from(binomial(j, l));
let cpow = rat_pow(c0, j - l);
let coeff = a.clone() * &binom * &cpow;
if coeff != 0 {
*g.entry((xe.clone(), l)).or_insert_with(rzero) += &coeff;
}
}
}
g.retain(|_, a| *a != 0);
g
}
fn dense(m: &BTreeMap<u32, Rational>) -> Vec<Rational> {
let Some(&maxd) = m.keys().max() else {
return Vec::new();
};
let mut v = vec![rzero(); maxd as usize + 1];
for (d, c) in m {
v[*d as usize] = c.clone();
}
v
}
fn rational_roots(p: &[Rational]) -> Vec<Rational> {
let mut hi = p.len();
while hi > 0 && p[hi - 1] == 0 {
hi -= 1;
}
let p = &p[..hi];
if p.is_empty() {
return Vec::new();
}
let mut roots = Vec::new();
let mut lo = 0usize;
while lo < p.len() && p[lo] == 0 {
lo += 1;
}
if lo > 0 {
roots.push(rzero());
}
let p = &p[lo..];
if p.len() <= 1 {
return roots; }
let mut den_lcm = Integer::from(1);
for c in p {
den_lcm = den_lcm.lcm(c.denom());
}
let ints: Vec<Integer> = p
.iter()
.map(|c| {
(c.clone() * Rational::from(den_lcm.clone()))
.numer()
.clone()
})
.collect();
let a0 = ints[0].clone().abs();
let an = ints[ints.len() - 1].clone().abs();
let pdiv = divisors(&a0);
let qdiv = divisors(&an);
let mut seen: Vec<Rational> = Vec::new();
for pn in &pdiv {
for qn in &qdiv {
for sign in [1i32, -1] {
let cand = Rational::from((Integer::from(sign) * pn.clone(), qn.clone()));
if seen.contains(&cand) {
continue;
}
if eval_int_poly(&ints, &cand) == 0 {
seen.push(cand);
}
}
}
}
roots.extend(seen);
roots
}
fn eval_int_poly(coeffs: &[Integer], c: &Rational) -> Rational {
let mut acc = rzero();
for a in coeffs.iter().rev() {
acc = acc * c + Rational::from(a.clone());
}
acc
}
fn divisors(n: &Integer) -> Vec<Integer> {
let n = n.clone().abs();
if n == 0 {
return vec![Integer::from(1)];
}
let mut ds = Vec::new();
let mut d = Integer::from(1);
while Integer::from(&d * &d) <= n {
if n.is_divisible(&d) {
ds.push(d.clone());
let other = n.clone() / &d;
if other != d {
ds.push(other);
}
}
d += 1;
}
ds
}
fn binomial(n: u32, k: u32) -> Integer {
if k > n {
return Integer::from(0);
}
let mut num = Integer::from(1);
for t in 0..k {
num *= Integer::from(n - t);
}
let mut den = Integer::from(1);
for t in 1..=k {
den *= Integer::from(t);
}
num / den
}
fn rat_pow(c: &Rational, e: u32) -> Rational {
let mut acc = Rational::from(1);
for _ in 0..e {
acc *= c;
}
acc
}
fn lcm_u64(a: u64, b: u64) -> u64 {
if a == 0 || b == 0 {
return 0;
}
a / gcd_u64(a, b) * b
}
fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}
pub fn puiseux_at(
coeffs: &[(u32, u32, Rational)],
alpha: &Rational,
prec: u32,
) -> Vec<PuiseuxSeries> {
if *alpha == 0 {
return puiseux_at_zero(coeffs, prec);
}
let mut shifted: BTreeMap<(u32, u32), Rational> = BTreeMap::new();
for (i, j, a) in coeffs {
for m in 0..=*i {
let binom = Rational::from(binomial(*i, m));
let apow = rat_pow(alpha, *i - m);
let c = a.clone() * &binom * &apow;
if c != 0 {
*shifted.entry((m, *j)).or_insert_with(rzero) += &c;
}
}
}
let flat: Vec<(u32, u32, Rational)> =
shifted.into_iter().map(|((i, j), a)| (i, j, a)).collect();
puiseux_at_zero(&flat, prec)
}
#[derive(Clone, Debug)]
pub struct AlgPuiseuxSeries {
pub minpoly: Option<Vec<Rational>>,
pub conjugates: usize,
pub ramification: u64,
pub terms: Vec<(Rational, KElem)>,
pub order: Option<Rational>,
}
pub fn puiseux_at_zero_algebraic(
coeffs: &[(u32, u32, Rational)],
prec: u32,
) -> Vec<AlgPuiseuxSeries> {
let mut out: Vec<AlgPuiseuxSeries> = puiseux_at_zero(coeffs, prec)
.into_iter()
.map(|s| AlgPuiseuxSeries {
minpoly: None,
conjugates: 1,
ramification: s.ramification,
terms: s.terms.into_iter().map(|(e, c)| (e, vec![c])).collect(),
order: s.order,
})
.collect();
let mut f: Bi = BTreeMap::new();
for (i, j, a) in coeffs {
if *a != 0 {
*f.entry((Rational::from(*i), *j)).or_insert_with(rzero) += a;
}
}
f.retain(|_, a| *a != 0);
if f.is_empty() {
return out;
}
factor_min_x(&mut f);
let prec_r = Rational::from(prec);
let mut f0: BTreeMap<u32, Rational> = BTreeMap::new();
for ((xe, ye), a) in &f {
if *xe == 0 {
*f0.entry(*ye).or_insert_with(rzero) += a;
}
}
let f0_dense = dense(&f0);
if f0_dense.first().map(|c| *c == 0).unwrap_or(true) {
out.extend(collect_algebraic(&f, &prec_r, &[]));
}
for (fac, deg) in factor_over_q(&f0_dense) {
if deg == 1 {
let c0 = -fac[0].clone();
out.extend(collect_algebraic(
&shift_y(&f, &c0),
&prec_r,
&[(rzero(), c0.clone())],
));
} else {
let nf = NumberField::new(fac.clone());
let theta = nf.reduce(&vec![Rational::from(0), Rational::from(1)]);
let gk = substitute_k(&nf, &embed(&nf, &f), &rzero(), &theta);
for (terms, exact) in lift_k(&nf, &gk, &prec_r, 0) {
let mut full = vec![(rzero(), theta.clone())];
full.extend(terms);
out.push(make_alg_series(
Some(fac.clone()),
deg,
full,
exact,
&prec_r,
));
}
}
}
out
}
fn collect_algebraic(
g: &Bi,
prec: &Rational,
prefix: &[(Rational, Rational)],
) -> Vec<AlgPuiseuxSeries> {
let mut g = g.clone();
g.retain(|_, a| *a != 0);
if g.is_empty() {
return Vec::new();
}
let m0 = g.keys().map(|(_, j)| *j).min().unwrap_or(0);
if m0 > 0 {
g = g
.into_iter()
.map(|((xe, ye), a)| ((xe, ye - m0), a))
.collect();
}
let mut out = Vec::new();
let keys: Vec<(Rational, u32)> = g.keys().cloned().collect();
for (q, on_edge) in newton_edges_keys(&keys) {
let mut phi: BTreeMap<u32, Rational> = BTreeMap::new();
for k in &on_edge {
*phi.entry(k.1).or_insert_with(rzero) += &g[k];
}
for (fac, deg) in factor_over_q(&dense(&phi)) {
if deg == 1 {
let c = -fac[0].clone();
if c == 0 || prec.clone() - &q <= 0 {
continue;
}
let mut np = prefix.to_vec();
np.push((q.clone(), c.clone()));
out.extend(collect_algebraic(
&substitute(&g, &q, &c),
&(prec.clone() - &q),
&np,
));
} else {
let nf = NumberField::new(fac.clone());
let theta = nf.reduce(&vec![Rational::from(0), Rational::from(1)]);
if prec.clone() - &q <= 0 {
let mut full = embed_prefix(&nf, prefix);
full.push((q.clone(), theta));
out.push(make_alg_series(Some(fac.clone()), deg, full, false, prec));
continue;
}
let gk = substitute_k(&nf, &embed(&nf, &g), &q, &theta);
for (sub, exact) in lift_k(&nf, &gk, &(prec.clone() - &q), 0) {
let mut full = embed_prefix(&nf, prefix);
full.push((q.clone(), theta.clone()));
for (gamma, b) in sub {
full.push((q.clone() + &gamma, b));
}
out.push(make_alg_series(Some(fac.clone()), deg, full, exact, prec));
}
}
}
}
out
}
fn lift_k(
nf: &NumberField,
g: &KBi,
prec: &Rational,
depth: u32,
) -> Vec<(Vec<(Rational, KElem)>, bool)> {
const MAX_DEPTH: u32 = 48;
let mut g = g.clone();
g.retain(|_, a| !NumberField::is_zero(a));
if g.is_empty() {
return vec![(Vec::new(), true)];
}
if depth > MAX_DEPTH {
return vec![(Vec::new(), false)];
}
let mut result = Vec::new();
let m0 = g.keys().map(|(_, j)| *j).min().unwrap_or(0);
if m0 > 0 {
result.push((Vec::new(), true));
g = g
.into_iter()
.map(|((xe, ye), a)| ((xe, ye - m0), a))
.collect();
}
let keys: Vec<(Rational, u32)> = g.keys().cloned().collect();
for (q, on_edge) in newton_edges_keys(&keys) {
let mut phi: BTreeMap<u32, KElem> = BTreeMap::new();
for k in &on_edge {
let e = phi.entry(k.1).or_default();
*e = nf.add(e, &g[k]);
}
let Some(c) = k_linear_root(nf, &phi) else {
continue; };
if prec.clone() - &q <= 0 {
result.push((vec![(q.clone(), c)], false));
continue;
}
let gk = substitute_k(nf, &g, &q, &c);
for (sub, exact) in lift_k(nf, &gk, &(prec.clone() - &q), depth + 1) {
let mut terms = vec![(q.clone(), c.clone())];
for (gamma, b) in sub {
terms.push((q.clone() + &gamma, b));
}
result.push((terms, exact));
}
}
result
}
fn k_linear_root(nf: &NumberField, phi: &BTreeMap<u32, KElem>) -> Option<KElem> {
let nz: Vec<(&u32, &KElem)> = phi
.iter()
.filter(|(_, c)| !NumberField::is_zero(c))
.collect();
if nz.len() != 2 {
return None;
}
let (j1, a1) = nz[0];
let (j2, a2) = nz[1];
if *j2 != *j1 + 1 {
return None; }
let inv = nf.inv(a2)?;
Some(nf.neg(&nf.mul(a1, &inv)))
}
fn substitute_k(nf: &NumberField, g: &KBi, q: &Rational, c: &KElem) -> KBi {
let mut g1: KBi = BTreeMap::new();
for ((xe, ye), a) in g {
let j = *ye;
let new_xe = xe.clone() + q.clone() * Rational::from(j as i64);
for l in 0..=j {
let binom = k_from_int(nf, &binomial(j, l));
let cpow = k_pow(nf, c, j - l);
let coeff = nf.mul(&nf.mul(a, &binom), &cpow);
if !NumberField::is_zero(&coeff) {
let e = g1.entry((new_xe.clone(), l)).or_default();
*e = nf.add(e, &coeff);
}
}
}
g1.retain(|_, a| !NumberField::is_zero(a));
factor_min_x_k(&mut g1);
g1
}
fn factor_min_x_k(g: &mut KBi) {
let Some(v) = g.keys().map(|(xe, _)| xe.clone()).min() else {
return;
};
if v == 0 {
return;
}
*g = std::mem::take(g)
.into_iter()
.map(|((xe, ye), a)| ((xe - &v, ye), a))
.collect();
}
fn embed(nf: &NumberField, g: &Bi) -> KBi {
g.iter()
.map(|((xe, ye), a)| ((xe.clone(), *ye), nf.reduce(&vec![a.clone()])))
.collect()
}
fn embed_prefix(nf: &NumberField, prefix: &[(Rational, Rational)]) -> Vec<(Rational, KElem)> {
prefix
.iter()
.map(|(e, c)| (e.clone(), nf.reduce(&vec![c.clone()])))
.collect()
}
fn k_from_int(nf: &NumberField, n: &Integer) -> KElem {
nf.reduce(&vec![Rational::from(n.clone())])
}
fn k_pow(nf: &NumberField, c: &KElem, e: u32) -> KElem {
let mut acc = nf.reduce(&vec![Rational::from(1)]);
for _ in 0..e {
acc = nf.mul(&acc, c);
}
acc
}
fn make_alg_series(
minpoly: Option<Vec<Rational>>,
conjugates: usize,
mut terms: Vec<(Rational, KElem)>,
exact: bool,
prec: &Rational,
) -> AlgPuiseuxSeries {
terms.retain(|(e, c)| (exact || *e < *prec) && !NumberField::is_zero(c));
terms.sort_by(|a, b| a.0.cmp(&b.0));
let e = terms.iter().fold(1u64, |acc, (ex, _)| {
lcm_u64(acc, ex.denom().to_u64().unwrap_or(1))
});
AlgPuiseuxSeries {
minpoly,
conjugates,
ramification: e,
terms,
order: if exact { None } else { Some(prec.clone()) },
}
}
fn factor_over_q(p: &[Rational]) -> Vec<(Vec<Rational>, usize)> {
let p = {
let mut hi = p.len();
while hi > 0 && p[hi - 1] == 0 {
hi -= 1;
}
p[..hi].to_vec()
};
let lo = p.iter().position(|c| *c != 0).unwrap_or(p.len());
let psi = &p[lo..];
if psi.len() <= 1 {
return Vec::new();
}
let mut den_lcm = Integer::from(1);
for c in psi {
den_lcm = den_lcm.lcm(c.denom());
}
let ints: Vec<Integer> = psi
.iter()
.map(|c| {
(c.clone() * Rational::from(den_lcm.clone()))
.numer()
.clone()
})
.collect();
let fp = FlintPoly::from_rug_coefficients(&ints);
let Ok((_unit, facs)) = fp.factor_over_z() else {
return Vec::new();
};
let mut out = Vec::new();
for (fpoly, _mult) in facs {
let deg = fpoly.degree();
if deg < 1 {
continue;
}
let icoeffs = fpoly.coefficients(); let qcoeffs: Vec<Rational> = icoeffs.iter().map(|&c| Rational::from(c)).collect();
let lead = qcoeffs[deg as usize].clone();
let monic: Vec<Rational> = qcoeffs.iter().map(|c| c.clone() / &lead).collect();
out.push((monic, deg as usize));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn r(n: i64) -> Rational {
Rational::from(n)
}
fn rr(n: i64, d: i64) -> Rational {
Rational::from((n, d))
}
fn has_term(s: &PuiseuxSeries, exp: Rational, coeff: Rational) -> bool {
s.terms.iter().any(|(e, c)| *e == exp && *c == coeff)
}
fn verify_branches(f: &[(u32, u32, Rational)], prec: u32) {
let br = puiseux_at_zero(f, prec);
assert!(!br.is_empty(), "expected at least one branch");
for s in &br {
for &x0 in &[0.01_f64, 0.03, 0.07] {
let y: f64 = s
.terms
.iter()
.map(|(e, c)| c.to_f64() * x0.powf(e.to_f64()))
.sum();
let fval: f64 = f
.iter()
.map(|(i, j, a)| a.to_f64() * x0.powi(*i as i32) * y.powi(*j as i32))
.sum();
let tol = 1e-6 + 50.0 * x0.powf(prec as f64);
assert!(
fval.abs() < tol,
"branch {s:?}: F({x0}, y)={fval} not O(x^{prec})"
);
}
}
}
#[test]
fn back_substitution_soundness() {
verify_branches(&[(0, 2, r(1)), (1, 0, r(-1))], 4); verify_branches(&[(0, 2, r(1)), (3, 0, r(-1))], 5); verify_branches(&[(0, 3, r(1)), (1, 0, r(-1))], 4); verify_branches(&[(0, 2, r(1)), (2, 0, r(-1)), (3, 0, r(-1))], 5); verify_branches(
&[(0, 2, r(1)), (1, 1, r(-2)), (2, 0, r(1)), (3, 0, r(-1))],
4,
); }
#[test]
fn puiseux_at_base_point() {
let f = [(0, 2, r(1)), (1, 0, r(-1)), (0, 0, r(1))]; let br = puiseux_at(&f, &r(1), 3);
assert_eq!(br.len(), 2);
for s in &br {
assert_eq!(s.ramification, 2);
assert!(has_term(s, rr(1, 2), r(1)) || has_term(s, rr(1, 2), r(-1)));
}
}
#[test]
fn algebraic_cube_root_of_unity() {
let br = puiseux_at_zero_algebraic(&[(0, 3, r(1)), (1, 0, r(-1))], 2);
let total: usize = br.iter().map(|s| s.conjugates).sum();
assert_eq!(total, 3, "branches: {br:?}");
let alg = br
.iter()
.find(|s| s.minpoly.is_some())
.expect("an algebraic branch");
assert_eq!(alg.conjugates, 2);
assert_eq!(alg.minpoly.as_ref().unwrap(), &vec![r(1), r(1), r(1)]);
assert_eq!(alg.ramification, 3);
assert_eq!(alg.terms[0].0, rr(1, 3));
assert_eq!(alg.terms[0].1, vec![r(0), r(1)]); let nf = NumberField::new(vec![r(1), r(1), r(1)]);
let theta = vec![r(0), r(1)];
let theta3 = nf.mul(&nf.mul(&theta, &theta), &theta);
assert_eq!(nf.reduce(&theta3), vec![r(1)]);
}
#[test]
fn algebraic_constant_branches() {
let br = puiseux_at_zero_algebraic(&[(0, 2, r(1)), (0, 0, r(-2))], 2);
let alg = br
.iter()
.find(|s| s.minpoly.is_some())
.expect("an algebraic branch");
assert_eq!(alg.conjugates, 2);
assert_eq!(alg.minpoly.as_ref().unwrap(), &vec![r(-2), r(0), r(1)]);
assert_eq!(alg.terms.len(), 1);
assert_eq!(alg.terms[0], (r(0), vec![r(0), r(1)]));
}
#[test]
fn algebraic_includes_rational_branches() {
let br = puiseux_at_zero_algebraic(&[(0, 2, r(1)), (1, 0, r(-1))], 3);
let total: usize = br.iter().map(|s| s.conjugates).sum();
assert_eq!(total, 2);
assert!(br.iter().all(|s| s.minpoly.is_none()));
}
#[test]
fn sqrt_x() {
let f = [(0, 2, r(1)), (1, 0, r(-1))];
let br = puiseux_at_zero(&f, 3);
assert_eq!(br.len(), 2);
for s in &br {
assert_eq!(s.ramification, 2);
assert!(has_term(s, rr(1, 2), r(1)) || has_term(s, rr(1, 2), r(-1)));
}
}
#[test]
fn cusp_y2_eq_x3() {
let f = [(0, 2, r(1)), (3, 0, r(-1))];
let br = puiseux_at_zero(&f, 4);
assert_eq!(br.len(), 2);
for s in &br {
assert_eq!(s.ramification, 2);
assert!(has_term(s, rr(3, 2), r(1)) || has_term(s, rr(3, 2), r(-1)));
}
}
#[test]
fn cbrt_x_principal_branch() {
let f = [(0, 3, r(1)), (1, 0, r(-1))];
let br = puiseux_at_zero(&f, 2);
assert_eq!(br.len(), 1);
assert_eq!(br[0].ramification, 3);
assert!(has_term(&br[0], rr(1, 3), r(1)));
}
#[test]
fn double_root_recursion() {
let f = [(0, 2, r(1)), (1, 1, r(-2)), (2, 0, r(1)), (3, 0, r(-1))];
let br = puiseux_at_zero(&f, 3);
assert_eq!(br.len(), 2, "branches: {br:?}");
for s in &br {
assert!(has_term(s, r(1), r(1)), "leading x term: {s:?}");
assert!(has_term(s, rr(3, 2), r(1)) || has_term(s, rr(3, 2), r(-1)));
}
}
#[test]
fn multi_term_taylor_branch() {
let f = [(0, 2, r(1)), (2, 0, r(-1)), (3, 0, r(-1))];
let br = puiseux_at_zero(&f, 4);
assert_eq!(br.len(), 2);
let plus = br
.iter()
.find(|s| has_term(s, r(1), r(1)))
.expect("a +x branch");
assert_eq!(plus.ramification, 1);
assert!(has_term(plus, r(2), rr(1, 2)), "x² coeff ½: {plus:?}");
assert!(has_term(plus, r(3), rr(-1, 8)), "x³ coeff −⅛: {plus:?}");
}
#[test]
fn nonzero_constant_branch() {
let f = [(0, 2, r(1)), (0, 1, r(-1)), (1, 1, r(-1)), (1, 0, r(1))];
let br = puiseux_at_zero(&f, 3);
assert!(br.iter().any(|s| has_term(s, r(0), r(1))));
assert!(br.iter().any(|s| has_term(s, r(1), r(1))));
}
}