use rug::{Integer, Rational};
use std::collections::BTreeMap;
use crate::flint::FlintPoly;
use crate::integrate::risch::number_field::{KPoly, 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);
let (subs, _missed) = lift_tower(&nf, &gk, &prec_r, 0, deg);
for sub in subs {
let kfinal = NumberField::new(sub.mp.clone());
let theta_f = embed_elem(&kfinal, &theta, &sub.theta_in_f);
let mut full = vec![(rzero(), theta_f)];
full.extend(sub.terms);
out.push(make_alg_series(
Some(sub.mp.clone()),
sub.conjugates,
full,
sub.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);
let (subs, _missed) = lift_tower(&nf, &gk, &(prec.clone() - &q), 0, deg);
for sub in subs {
let kfinal = NumberField::new(sub.mp.clone());
let mut full: Vec<(Rational, KElem)> = prefix
.iter()
.map(|(e, c)| (e.clone(), kfinal.from_rational(c)))
.collect();
let theta_f = embed_elem(&kfinal, &theta, &sub.theta_in_f);
full.push((q.clone(), theta_f));
full.extend(sub.terms);
out.push(make_alg_series(
Some(sub.mp.clone()),
sub.conjugates,
full,
sub.exact,
prec,
));
}
}
}
}
out
}
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()) },
}
}
pub(crate) 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
}
#[derive(Clone, Debug)]
pub struct AlgBasePuiseuxSeries {
pub alpha_minpoly: Vec<Rational>,
pub conjugates: usize,
pub ramification: u64,
pub terms: Vec<(Rational, KElem)>,
pub order: Option<Rational>,
}
#[derive(Clone, Debug)]
pub struct TowerPuiseuxSeries {
pub branch: AlgBasePuiseuxSeries,
pub coeff_minpoly: Vec<Rational>,
}
pub fn puiseux_at_algebraic(
coeffs: &[(u32, u32, Rational)],
alpha_minpoly: &[Rational],
prec: u32,
) -> (Vec<AlgBasePuiseuxSeries>, usize) {
let deg_alpha = {
let mut d = alpha_minpoly.len();
while d > 0 && alpha_minpoly[d - 1] == 0 {
d -= 1;
}
d.saturating_sub(1)
};
if deg_alpha <= 1 {
let alpha = if deg_alpha == 0 {
rzero()
} else {
-alpha_minpoly[0].clone() / alpha_minpoly[1].clone()
};
let branches = puiseux_at(coeffs, &alpha, prec);
let out = branches
.into_iter()
.map(|s| AlgBasePuiseuxSeries {
alpha_minpoly: vec![Rational::from(1)],
conjugates: 1,
ramification: s.ramification,
terms: s.terms.into_iter().map(|(e, c)| (e, vec![c])).collect(),
order: s.order,
})
.collect();
return (out, 0);
}
let nf = NumberField::new(alpha_minpoly.to_vec());
let alpha = nf.reduce(&vec![rzero(), Rational::from(1)]); let prec_r = Rational::from(prec);
let mut f = shift_x_alpha(&nf, coeffs, &alpha);
factor_min_x_k(&mut f);
if f.is_empty() {
return (Vec::new(), 0);
}
let mut f0: BTreeMap<u32, KElem> = BTreeMap::new();
for ((xe, ye), a) in &f {
if *xe == 0 {
let e = f0.entry(*ye).or_default();
*e = nf.add(e, a);
}
}
let f0_dense = k_dense(&nf, &f0);
let mut out: Vec<AlgBasePuiseuxSeries> = Vec::new();
let mut skipped = 0usize;
let (roots, missed) = k_roots_in_field(&nf, &f0_dense);
skipped += missed;
for c0 in roots {
let g = if NumberField::is_zero(&c0) {
f.clone()
} else {
shift_y_k(&nf, &f, &c0)
};
let prefix: Vec<(Rational, KElem)> = if NumberField::is_zero(&c0) {
Vec::new()
} else {
vec![(rzero(), c0.clone())]
};
let (lifted, missed) = lift_k_counted(&nf, &g, &prec_r, 0);
skipped += missed;
for (sub, exact) in lifted {
let mut full = prefix.clone();
full.extend(sub);
out.push(make_alg_base_series(
alpha_minpoly,
deg_alpha,
full,
exact,
&prec_r,
));
}
}
(out, skipped)
}
pub fn puiseux_at_algebraic_tower(
coeffs: &[(u32, u32, Rational)],
alpha_minpoly: &[Rational],
prec: u32,
) -> (Vec<TowerPuiseuxSeries>, usize) {
let deg_alpha = {
let mut d = alpha_minpoly.len();
while d > 0 && alpha_minpoly[d - 1] == 0 {
d -= 1;
}
d.saturating_sub(1)
};
if deg_alpha <= 1 {
let alpha = if deg_alpha == 0 {
rzero()
} else {
-alpha_minpoly[0].clone() / alpha_minpoly[1].clone()
};
let branches = puiseux_at(coeffs, &alpha, prec);
let out = branches
.into_iter()
.map(|s| TowerPuiseuxSeries {
branch: AlgBasePuiseuxSeries {
alpha_minpoly: vec![Rational::from(1)],
conjugates: 1,
ramification: s.ramification,
terms: s.terms.into_iter().map(|(e, c)| (e, vec![c])).collect(),
order: s.order,
},
coeff_minpoly: vec![Rational::from(1)],
})
.collect();
return (out, 0);
}
let nf = NumberField::new(alpha_minpoly.to_vec());
let alpha = nf.reduce(&vec![rzero(), Rational::from(1)]); let prec_r = Rational::from(prec);
let mut f = shift_x_alpha(&nf, coeffs, &alpha);
factor_min_x_k(&mut f);
if f.is_empty() {
return (Vec::new(), 0);
}
let mut f0: BTreeMap<u32, KElem> = BTreeMap::new();
for ((xe, ye), a) in &f {
if *xe == 0 {
let e = f0.entry(*ye).or_default();
*e = nf.add(e, a);
}
}
let f0_dense = k_dense(&nf, &f0);
let mut out: Vec<TowerPuiseuxSeries> = Vec::new();
let mut skipped = 0usize;
let (roots, kfactors) = k_roots_and_factors(&nf, &f0_dense);
for c0 in roots {
let g = if NumberField::is_zero(&c0) {
f.clone()
} else {
shift_y_k(&nf, &f, &c0)
};
let in_k_prefix = !NumberField::is_zero(&c0);
let (lifted, missed) = lift_tower(&nf, &g, &prec_r, 0, deg_alpha);
skipped += missed;
for sub in lifted {
let kfinal = NumberField::new(sub.mp.clone());
let mut full = Vec::new();
if in_k_prefix {
let c0_f = embed_elem(&kfinal, &c0, &sub.theta_in_f);
full.push((rzero(), c0_f));
}
full.extend(sub.terms);
out.push(make_tower_series(
alpha_minpoly,
&sub.mp,
sub.conjugates,
full,
sub.exact,
&prec_r,
));
}
}
for (chi, fdeg) in kfactors {
let Some((kp, theta_img, z_img)) = build_compositum(&nf, &chi) else {
skipped += fdeg; continue;
};
let f_kp = embed_bivariate(&kp, &f, &theta_img);
let g = shift_y_k(&kp, &f_kp, &z_img);
let conj_kp = kp.degree().max(0) as usize;
let (lifted, missed) = lift_tower(&kp, &g, &prec_r, 0, conj_kp);
skipped += missed;
for sub in lifted {
let kfinal = NumberField::new(sub.mp.clone());
let c0_f = embed_elem(&kfinal, &z_img, &sub.theta_in_f);
let mut full = vec![(rzero(), c0_f)];
full.extend(sub.terms);
out.push(make_tower_series(
alpha_minpoly,
&sub.mp,
sub.conjugates,
full,
sub.exact,
&prec_r,
));
}
}
(out, skipped)
}
fn shift_x_alpha(nf: &NumberField, coeffs: &[(u32, u32, Rational)], alpha: &KElem) -> KBi {
let mut f: KBi = BTreeMap::new();
for (i, j, a) in coeffs {
if *a == 0 {
continue;
}
let ak = nf.reduce(&vec![a.clone()]);
for m in 0..=*i {
let binom = k_from_int(nf, &binomial(*i, m));
let apow = k_pow(nf, alpha, *i - m);
let coeff = nf.mul(&nf.mul(&ak, &binom), &apow);
if !NumberField::is_zero(&coeff) {
let e = f.entry((Rational::from(m), *j)).or_default();
*e = nf.add(e, &coeff);
}
}
}
f.retain(|_, a| !NumberField::is_zero(a));
f
}
fn shift_y_k(nf: &NumberField, f: &KBi, c0: &KElem) -> KBi {
let mut g: KBi = BTreeMap::new();
for ((xe, ye), a) in f {
let j = *ye;
for l in 0..=j {
let binom = k_from_int(nf, &binomial(j, l));
let cpow = k_pow(nf, c0, j - l);
let coeff = nf.mul(&nf.mul(a, &binom), &cpow);
if !NumberField::is_zero(&coeff) {
let e = g.entry((xe.clone(), l)).or_default();
*e = nf.add(e, &coeff);
}
}
}
g.retain(|_, a| !NumberField::is_zero(a));
g
}
fn k_dense(nf: &NumberField, m: &BTreeMap<u32, KElem>) -> Vec<KElem> {
let Some(&maxd) = m.keys().max() else {
return Vec::new();
};
let mut v = vec![NumberField::k_zero(); maxd as usize + 1];
for (d, c) in m {
v[*d as usize] = nf.reduce(c);
}
v
}
fn k_roots_in_field(nf: &NumberField, p: &[KElem]) -> (Vec<KElem>, usize) {
let mut hi = p.len();
while hi > 0 && NumberField::is_zero(&p[hi - 1]) {
hi -= 1;
}
let p = &p[..hi];
if p.is_empty() {
return (Vec::new(), 0);
}
let mut roots: Vec<KElem> = Vec::new();
let mut lo = 0usize;
while lo < p.len() && NumberField::is_zero(&p[lo]) {
lo += 1;
}
if lo > 0 {
roots.push(NumberField::k_zero());
}
let work: Vec<KElem> = p[lo..].iter().map(|c| nf.reduce(c)).collect();
if work.len() <= 1 {
return (roots, 0); }
let mut skipped = 0usize;
let norm = k_norm_poly(nf, &work);
for (g, _deg) in factor_over_q(&norm) {
let gk: KPoly = g.iter().map(|c| nf.reduce(&vec![c.clone()])).collect();
let Some(h) = nf.kpoly_gcd(&work, &gk) else {
continue;
};
let hdeg = NumberField::kdeg(&h);
if hdeg == 1 {
let inv = match nf.inv(&h[1]) {
Some(i) => i,
None => {
skipped += 1;
continue;
}
};
roots.push(nf.neg(&nf.mul(&h[0], &inv)));
} else if hdeg >= 2 {
skipped += hdeg as usize;
}
}
(roots, skipped)
}
fn k_norm_poly(nf: &NumberField, p: &[KElem]) -> Vec<Rational> {
let m_alpha = nf.modulus().clone();
let d_alpha = (m_alpha.len() as i64 - 1).max(0) as usize;
let cdeg_bound = p.len().saturating_sub(1) * d_alpha;
let n_pts = cdeg_bound + 1;
let mut xs: Vec<Rational> = Vec::with_capacity(n_pts);
let mut ys: Vec<Rational> = Vec::with_capacity(n_pts);
let mut s: i64 = 0;
while xs.len() < n_pts {
let cs = Rational::from(s);
let mut phi_t: Vec<Rational> = Vec::new();
let mut spow = Rational::from(1);
for pk in p {
for (i, coeff) in pk.iter().enumerate() {
if i >= phi_t.len() {
phi_t.resize(i + 1, rzero());
}
phi_t[i] += coeff.clone() * &spow;
}
spow *= &cs;
}
let r = q_resultant(&m_alpha, &phi_t);
xs.push(cs);
ys.push(r);
s += 1;
}
lagrange_interpolate(&xs, &ys)
}
fn q_resultant(a: &[Rational], b: &[Rational]) -> Rational {
let mut a = trim_q(a.to_vec());
let mut b = trim_q(b.to_vec());
if a.is_empty() || b.is_empty() {
return rzero();
}
let mut res = Rational::from(1);
loop {
let da = a.len() - 1;
let db = b.len() - 1;
if db == 0 {
res *= rat_pow_q(&b[0], da as u32);
return res;
}
let rem = q_rem(&a, &b);
let drem = if rem.is_empty() { 0 } else { rem.len() - 1 };
let sign = if (da * db) % 2 == 0 {
Rational::from(1)
} else {
Rational::from(-1)
};
let lc_b = b[db].clone();
res *= sign * rat_pow_q(&lc_b, (da - drem) as u32);
if rem.is_empty() {
return rzero();
}
a = b;
b = rem;
}
}
fn q_rem(a: &[Rational], b: &[Rational]) -> Vec<Rational> {
let mut r = trim_q(a.to_vec());
let b = trim_q(b.to_vec());
let db = b.len() - 1;
let lc_inv = Rational::from(1) / b[db].clone();
loop {
let dr = if r.is_empty() { 0 } else { r.len() - 1 };
if r.is_empty() || dr < db {
break;
}
let factor = r[dr].clone() * &lc_inv;
let shift = dr - db;
for (i, bc) in b.iter().enumerate() {
r[shift + i] -= factor.clone() * bc;
}
r = trim_q(r);
}
r
}
fn trim_q(mut p: Vec<Rational>) -> Vec<Rational> {
while p.last().is_some_and(|c| *c == 0) {
p.pop();
}
p
}
fn rat_pow_q(c: &Rational, e: u32) -> Rational {
let mut acc = Rational::from(1);
for _ in 0..e {
acc *= c;
}
acc
}
fn lagrange_interpolate(xs: &[Rational], ys: &[Rational]) -> Vec<Rational> {
let n = xs.len();
let mut acc: Vec<Rational> = vec![rzero(); n];
for i in 0..n {
let mut basis = vec![Rational::from(1)];
let mut denom = Rational::from(1);
for j in 0..n {
if i == j {
continue;
}
let mut nb = vec![rzero(); basis.len() + 1];
for (k, c) in basis.iter().enumerate() {
nb[k] += -xs[j].clone() * c;
nb[k + 1] += c.clone();
}
basis = nb;
denom *= xs[i].clone() - &xs[j];
}
let scale = ys[i].clone() / denom;
for (k, c) in basis.iter().enumerate() {
acc[k] += c.clone() * &scale;
}
}
trim_q(acc)
}
type KLiftedBranch = (Vec<(Rational, KElem)>, bool);
fn lift_k_counted(
nf: &NumberField,
g: &KBi,
prec: &Rational,
depth: u32,
) -> (Vec<KLiftedBranch>, usize) {
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)], 0);
}
if depth > MAX_DEPTH {
return (vec![(Vec::new(), false)], 0);
}
let mut result = Vec::new();
let mut skipped = 0usize;
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 phi_dense = k_dense(nf, &phi);
let (roots, missed) = k_roots_in_field(nf, &phi_dense);
skipped += missed;
for c in roots {
if NumberField::is_zero(&c) {
continue;
}
if prec.clone() - &q <= 0 {
result.push((vec![(q.clone(), c)], false));
continue;
}
let gk = substitute_k(nf, &g, &q, &c);
let (sub_branches, sub_missed) =
lift_k_counted(nf, &gk, &(prec.clone() - &q), depth + 1);
skipped += sub_missed;
for (sub, exact) in sub_branches {
let mut terms = vec![(q.clone(), c.clone())];
for (gamma, b) in sub {
terms.push((q.clone() + &gamma, b));
}
result.push((terms, exact));
}
}
}
(result, skipped)
}
fn make_alg_base_series(
alpha_minpoly: &[Rational],
conjugates: usize,
mut terms: Vec<(Rational, KElem)>,
exact: bool,
prec: &Rational,
) -> AlgBasePuiseuxSeries {
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))
});
AlgBasePuiseuxSeries {
alpha_minpoly: alpha_minpoly.to_vec(),
conjugates,
ramification: e,
terms,
order: if exact { None } else { Some(prec.clone()) },
}
}
fn make_tower_series(
alpha_minpoly: &[Rational],
coeff_minpoly: &[Rational],
conjugates: usize,
terms: Vec<(Rational, KElem)>,
exact: bool,
prec: &Rational,
) -> TowerPuiseuxSeries {
let branch = make_alg_base_series(alpha_minpoly, conjugates, terms, exact, prec);
TowerPuiseuxSeries {
branch,
coeff_minpoly: coeff_minpoly.to_vec(),
}
}
const TOWER_DEGREE_CAP: usize = 16;
type KFactor = (KPoly, usize);
struct TowerBranch {
mp: Vec<Rational>,
conjugates: usize,
theta_in_f: KElem,
terms: Vec<(Rational, KElem)>,
exact: bool,
}
fn lift_tower(
nf: &NumberField,
g: &KBi,
prec: &Rational,
depth: u32,
conj_base: usize,
) -> (Vec<TowerBranch>, usize) {
const MAX_DEPTH: u32 = 48;
let mut g = g.clone();
g.retain(|_, a| !NumberField::is_zero(a));
let here_mp = nf.modulus().clone();
let ident: KElem = nf.reduce(&vec![rzero(), Rational::from(1)]); let mk_leaf = |exact: bool| TowerBranch {
mp: here_mp.clone(),
conjugates: conj_base,
theta_in_f: ident.clone(),
terms: Vec::new(),
exact,
};
if g.is_empty() {
return (vec![mk_leaf(true)], 0);
}
if depth > MAX_DEPTH {
return (vec![mk_leaf(false)], 0);
}
let mut result: Vec<TowerBranch> = Vec::new();
let mut skipped = 0usize;
let m0 = g.keys().map(|(_, j)| *j).min().unwrap_or(0);
if m0 > 0 {
result.push(mk_leaf(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 phi_dense = k_dense(nf, &phi);
let (roots, kfactors) = k_roots_and_factors(nf, &phi_dense);
for c in roots {
if NumberField::is_zero(&c) {
continue;
}
if prec.clone() - &q <= 0 {
result.push(TowerBranch {
mp: here_mp.clone(),
conjugates: conj_base,
theta_in_f: ident.clone(),
terms: vec![(q.clone(), c)],
exact: false,
});
continue;
}
let gk = substitute_k(nf, &g, &q, &c);
let (subs, sub_missed) =
lift_tower(nf, &gk, &(prec.clone() - &q), depth + 1, conj_base);
skipped += sub_missed;
for sub in subs {
let kfinal = NumberField::new(sub.mp.clone());
let head_c = embed_elem(&kfinal, &c, &sub.theta_in_f);
let mut terms = vec![(q.clone(), head_c)];
for (gamma, b) in sub.terms {
terms.push((q.clone() + &gamma, b));
}
result.push(TowerBranch {
mp: sub.mp,
conjugates: sub.conjugates,
theta_in_f: sub.theta_in_f,
terms,
exact: sub.exact,
});
}
}
for (chi, fdeg) in kfactors {
let Some((kp, theta_img, z_img)) = build_compositum(nf, &chi) else {
skipped += fdeg; continue;
};
let conj_kp = kp.degree().max(0) as usize;
let g_kp = embed_bivariate(&kp, &g, &theta_img);
if prec.clone() - &q <= 0 {
result.push(TowerBranch {
mp: kp.modulus().clone(),
conjugates: conj_kp,
theta_in_f: theta_img.clone(),
terms: vec![(q.clone(), z_img)],
exact: false,
});
continue;
}
let gk = substitute_k(&kp, &g_kp, &q, &z_img);
let (subs, sub_missed) = lift_tower(&kp, &gk, &(prec.clone() - &q), depth + 1, conj_kp);
skipped += sub_missed;
for sub in subs {
let kfinal = NumberField::new(sub.mp.clone());
let head_c = embed_elem(&kfinal, &z_img, &sub.theta_in_f);
let theta_old_in_final = embed_elem(&kfinal, &theta_img, &sub.theta_in_f);
let mut terms = vec![(q.clone(), head_c)];
for (gamma, b) in sub.terms {
terms.push((q.clone() + &gamma, b));
}
result.push(TowerBranch {
mp: sub.mp,
conjugates: sub.conjugates,
theta_in_f: theta_old_in_final,
terms,
exact: sub.exact,
});
}
}
}
(result, skipped)
}
fn k_roots_and_factors(nf: &NumberField, p: &[KElem]) -> (Vec<KElem>, Vec<KFactor>) {
let mut hi = p.len();
while hi > 0 && NumberField::is_zero(&p[hi - 1]) {
hi -= 1;
}
let p = &p[..hi];
if p.is_empty() {
return (Vec::new(), Vec::new());
}
let mut roots: Vec<KElem> = Vec::new();
let mut lo = 0usize;
while lo < p.len() && NumberField::is_zero(&p[lo]) {
lo += 1;
}
if lo > 0 {
roots.push(NumberField::k_zero());
}
let work: Vec<KElem> = p[lo..].iter().map(|c| nf.reduce(c)).collect();
if work.len() <= 1 {
return (roots, Vec::new());
}
let mut factors: Vec<KFactor> = Vec::new();
for (h, hdeg) in kfactor_over_k(nf, &work) {
if hdeg == 1 {
if let Some(inv) = nf.inv(&h[1]) {
roots.push(nf.neg(&nf.mul(&h[0], &inv)));
}
} else {
factors.push((h, hdeg));
}
}
(roots, factors)
}
fn kfactor_over_k(nf: &NumberField, p: &[KElem]) -> Vec<KFactor> {
let mut hi = p.len();
while hi > 0 && NumberField::is_zero(&p[hi - 1]) {
hi -= 1;
}
let p = &p[..hi];
let mut lo = 0usize;
while lo < p.len() && NumberField::is_zero(&p[lo]) {
lo += 1;
}
let work: KPoly = p[lo..].iter().map(|c| nf.reduce(c)).collect();
if NumberField::kdeg(&work) < 1 {
return Vec::new();
}
let Some(phi) = nf.kpoly_monic(&work) else {
return Vec::new();
};
let theta: KElem = nf.reduce(&vec![rzero(), Rational::from(1)]);
for s in 0..=8i64 {
let phi_s = if s == 0 {
phi.clone()
} else {
let shift = nf.mul(&nf.from_int(-s), &theta); kpoly_shift_z(nf, &phi, &shift)
};
let norm = k_norm_poly(nf, &phi_s);
if norm.len() <= 1 || !is_squarefree_q(&norm) {
continue;
}
let mut out: Vec<KFactor> = Vec::new();
let mut ok = true;
for (g, _deg) in factor_over_q(&norm) {
let gk: KPoly = g.iter().map(|c| nf.reduce(&vec![c.clone()])).collect();
let Some(h) = nf.kpoly_gcd(&phi_s, &gk) else {
ok = false;
break;
};
let hdeg = NumberField::kdeg(&h);
if hdeg < 1 {
ok = false; break;
}
let h_back = if s == 0 {
h
} else {
let shift = nf.mul(&nf.from_int(s), &theta);
kpoly_shift_z(nf, &h, &shift)
};
let Some(h_monic) = nf.kpoly_monic(&h_back) else {
ok = false;
break;
};
let d = NumberField::kdeg(&h_monic) as usize;
out.push((h_monic, d));
}
if ok && !out.is_empty() {
return out;
}
}
let d = NumberField::kdeg(&phi) as usize;
vec![(phi, d)]
}
fn kpoly_shift_z(nf: &NumberField, phi: &[KElem], c: &KElem) -> KPoly {
let mut acc: KPoly = Vec::new();
let mut zpc_pow: KPoly = vec![nf.from_int(1)]; let zpc: KPoly = vec![c.clone(), nf.from_int(1)]; for (k, pk) in phi.iter().enumerate() {
if k > 0 {
zpc_pow = nf.kpoly_mul(&zpc_pow, &zpc);
}
if !NumberField::is_zero(pk) {
let term = nf.kpoly_scale(&zpc_pow, pk);
acc = nf.kpoly_add(&acc, &term);
}
}
NumberField::kpoly_trim(acc)
}
fn is_squarefree_q(f: &[Rational]) -> bool {
let f = trim_q(f.to_vec());
if f.len() <= 1 {
return true;
}
let mut df: Vec<Rational> = Vec::with_capacity(f.len().saturating_sub(1));
for (k, c) in f.iter().enumerate().skip(1) {
df.push(Rational::from(k as i64) * c);
}
let g = q_gcd(&f, &df);
g.len() <= 1
}
fn q_gcd(a: &[Rational], b: &[Rational]) -> Vec<Rational> {
let mut a = trim_q(a.to_vec());
let mut b = trim_q(b.to_vec());
while !b.is_empty() {
let r = q_rem(&a, &b);
a = b;
b = trim_q(r);
}
if let Some(lc) = a.last().cloned() {
if lc != 0 {
for c in a.iter_mut() {
*c /= &lc;
}
}
}
a
}
#[allow(clippy::type_complexity)]
fn build_compositum(nf: &NumberField, chi: &[KElem]) -> Option<(NumberField, KElem, KElem)> {
let n = nf.degree().max(0) as usize; let chi_monic = nf.kpoly_monic(chi)?;
let d = NumberField::kdeg(&chi_monic).max(0) as usize; if d < 2 || n == 0 {
return None;
}
let big = n * d; if big > TOWER_DEGREE_CAP {
return None;
}
let tred = |a: &[KElem]| -> KPoly {
match nf.kpoly_divrem(a, &chi_monic) {
Some((_q, rem)) => NumberField::kpoly_trim(rem),
None => NumberField::kpoly_trim(a.to_vec()),
}
};
let tmul = |a: &[KElem], b: &[KElem]| -> KPoly { tred(&nf.kpoly_mul(a, b)) };
let flat = |a: &[KElem]| -> Vec<Rational> {
let mut v = vec![rzero(); big];
for (j, cj) in a.iter().enumerate().take(d) {
let cjr = nf.reduce(cj);
for (i, ci) in cjr.iter().enumerate().take(n) {
v[j * n + i] = ci.clone();
}
}
v
};
let z_elem: KPoly = vec![NumberField::k_zero(), nf.from_int(1)]; let theta_elem: KPoly = vec![nf.reduce(&vec![rzero(), Rational::from(1)])];
for lambda in 1..=8i64 {
let theta_p =
tred(&nf.kpoly_add(&theta_elem, &nf.kpoly_scale(&z_elem, &nf.from_int(lambda))));
let one_t: KPoly = vec![nf.from_int(1)];
let mut powers: Vec<KPoly> = vec![one_t.clone()];
let mut cur = one_t.clone();
for _ in 0..big {
cur = tmul(&cur, &theta_p);
powers.push(cur.clone());
}
let cols: Vec<Vec<Rational>> = (0..big).map(|k| flat(&powers[k])).collect();
let Some(mcoef) = solve_q_columns(&cols, &flat(&powers[big]), big) else {
continue; };
let mut mprime = vec![rzero(); big + 1];
mprime[big] = Rational::from(1);
for (k, mk) in mcoef.iter().enumerate() {
mprime[k] = -mk.clone();
}
let Some(theta_in) = solve_q_columns(&cols, &flat(&theta_elem), big) else {
continue;
};
let Some(z_in) = solve_q_columns(&cols, &flat(&z_elem), big) else {
continue;
};
let kprime = NumberField::new(trim_q(mprime));
return Some((
kprime.clone(),
kprime.reduce(&trim_q(theta_in)),
kprime.reduce(&trim_q(z_in)),
));
}
None
}
fn solve_q_columns(cols: &[Vec<Rational>], rhs: &[Rational], n: usize) -> Option<Vec<Rational>> {
let mut a: Vec<Vec<Rational>> = (0..n)
.map(|i| {
let mut row: Vec<Rational> = (0..n).map(|j| cols[j][i].clone()).collect();
row.push(rhs[i].clone());
row
})
.collect();
for col in 0..n {
let piv = (col..n).find(|&rr| a[rr][col] != 0)?;
a.swap(col, piv);
let inv = Rational::from(1) / a[col][col].clone();
for v in a[col].iter_mut() {
*v *= &inv;
}
for rr in 0..n {
if rr != col && a[rr][col] != 0 {
let f = a[rr][col].clone();
#[allow(clippy::needless_range_loop)]
for k in col..=n {
let s = f.clone() * &a[col][k];
a[rr][k] -= s;
}
}
}
}
Some((0..n).map(|i| a[i][n].clone()).collect())
}
fn embed_elem(kp: &NumberField, c: &KElem, theta_img: &KElem) -> KElem {
let mut acc = NumberField::k_zero();
for ci in c.iter().rev() {
acc = kp.mul(&acc, theta_img);
if *ci != 0 {
acc = kp.add(&acc, &kp.from_rational(ci));
}
}
acc
}
fn embed_bivariate(kp: &NumberField, g: &KBi, theta_img: &KElem) -> KBi {
let mut out: KBi = BTreeMap::new();
for (k, a) in g {
let e = embed_elem(kp, a, theta_img);
if !NumberField::is_zero(&e) {
out.insert(k.clone(), e);
}
}
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))));
}
fn kt_mul_trunc(nf: &NumberField, a: &[KElem], b: &[KElem], n: usize) -> Vec<KElem> {
let mut r = vec![NumberField::k_zero(); n];
for (i, ca) in a.iter().enumerate() {
if i >= n || NumberField::is_zero(ca) {
continue;
}
for (j, cb) in b.iter().enumerate() {
if i + j >= n {
break;
}
let p = nf.mul(ca, cb);
r[i + j] = nf.add(&r[i + j], &p);
}
}
r
}
fn kt_pow_trunc(nf: &NumberField, p: &[KElem], e: u32, n: usize) -> Vec<KElem> {
let mut acc = vec![nf.reduce(&vec![r(1)])];
for _ in 0..e {
acc = kt_mul_trunc(nf, &acc, p, n);
}
acc
}
fn verify_alg_branch(
coeffs: &[(u32, u32, Rational)],
alpha_minpoly: &[Rational],
s: &AlgBasePuiseuxSeries,
prec: u32,
) {
let nf = NumberField::new(alpha_minpoly.to_vec());
let alpha = nf.reduce(&vec![r(0), r(1)]); let e = s.ramification;
let n = (e * prec as u64) as usize + 1;
let mut xpoly = vec![NumberField::k_zero(); e as usize + 1];
xpoly[0] = alpha.clone();
xpoly[e as usize] = nf.reduce(&vec![r(1)]);
let mut ypoly = vec![NumberField::k_zero(); n];
for (exp, c) in &s.terms {
let te = exp.clone() * Rational::from(e as i64);
assert!(*te.denom() == 1, "exponent·e must be integral: {te}");
let idx = te.numer().to_i64().unwrap();
assert!(idx >= 0);
let idx = idx as usize;
if idx < n {
ypoly[idx] = nf.add(&ypoly[idx], c);
}
}
let mut fpoly = vec![NumberField::k_zero(); n];
for (i, j, a) in coeffs {
if *a == 0 {
continue;
}
let xi = kt_pow_trunc(&nf, &xpoly, *i, n);
let yj = kt_pow_trunc(&nf, &ypoly, *j, n);
let term = kt_mul_trunc(&nf, &xi, &yj, n);
let ak = nf.reduce(&vec![a.clone()]);
for (idx, tc) in term.iter().enumerate() {
let scaled = nf.mul(&ak, tc);
fpoly[idx] = nf.add(&fpoly[idx], &scaled);
}
}
for (idx, c) in fpoly.iter().enumerate() {
assert!(
NumberField::is_zero(c),
"alg branch {s:?}: F(α+t^{e}, y)[t^{idx}] = {c:?} ≠ 0 (not ≡0 mod t^{n})"
);
}
}
#[test]
fn alg_base_degenerate_matches_rational() {
let f = [(0, 2, r(1)), (1, 0, r(-1)), (0, 0, r(2))]; let alpha_minpoly = vec![r(-2), r(1)]; let (br, skipped) = puiseux_at_algebraic(&f, &alpha_minpoly, 3);
assert_eq!(skipped, 0);
let rat = puiseux_at(&f, &r(2), 3);
assert_eq!(br.len(), rat.len());
assert_eq!(br.len(), 2);
for s in &br {
assert_eq!(s.conjugates, 1);
assert_eq!(s.ramification, 2);
assert_eq!(s.terms[0].0, rr(1, 2));
let lead = &s.terms[0].1;
assert!(lead == &vec![r(1)] || lead == &vec![r(-1)]);
verify_alg_branch(&f, &alpha_minpoly, s, 3);
}
}
#[test]
fn alg_base_rational_branches_at_sqrt2() {
let f = [(0, 2, r(1)), (1, 1, r(-1)), (2, 1, r(-1)), (3, 0, r(1))];
let mp = vec![r(-2), r(0), r(1)]; let (br, skipped) = puiseux_at_algebraic(&f, &mp, 4);
assert_eq!(skipped, 0, "branches: {br:?}");
assert_eq!(br.len(), 2, "branches: {br:?}");
let total: usize = br.iter().map(|s| s.conjugates).sum();
assert_eq!(total, 4); for s in &br {
assert_eq!(s.conjugates, 2);
assert_eq!(s.ramification, 1); verify_alg_branch(&f, &mp, s, 4);
}
let consts: Vec<KElem> = br
.iter()
.map(|s| {
s.terms
.iter()
.find(|(e, _)| *e == r(0))
.map(|(_, c)| c.clone())
.unwrap_or_default()
})
.collect();
assert!(
consts.iter().any(|c| *c == vec![r(0), r(1)]),
"√2 const: {consts:?}"
);
assert!(
consts.iter().any(|c| *c == vec![r(2)]),
"2 const: {consts:?}"
);
}
#[test]
fn alg_base_constant_algebraic_node_in_field() {
let f = [(0, 2, r(1)), (2, 1, r(1)), (0, 1, r(-2))]; let mp = vec![r(-2), r(0), r(1)]; let (br, skipped) = puiseux_at_algebraic(&f, &mp, 4);
assert_eq!(skipped, 0, "branches: {br:?}");
assert!(!br.is_empty(), "branches: {br:?}");
for s in &br {
assert_eq!(s.conjugates, 2);
assert_eq!(s.ramification, 1); verify_alg_branch(&f, &mp, s, 4);
}
}
#[test]
fn alg_base_ramified_needs_further_extension_is_counted() {
let f = [(0, 2, r(1)), (2, 0, r(-1)), (0, 0, r(2))]; let mp = vec![r(-2), r(0), r(1)]; let (br, skipped) = puiseux_at_algebraic(&f, &mp, 3);
assert!(
br.iter()
.all(|s| s.terms.iter().all(|(e, _)| *e != rr(1, 2))),
"no bogus ramified branch should be returned: {br:?}"
);
assert_eq!(skipped, 2, "two sheets need a further extension: {br:?}");
for s in &br {
verify_alg_branch(&f, &mp, s, 3);
}
}
#[test]
fn alg_base_node_at_sqrt2_needs_extension_is_counted() {
let f = [
(0, 2, r(1)),
(5, 0, r(-1)),
(4, 0, r(-1)),
(3, 0, r(4)),
(2, 0, r(4)),
(1, 0, r(-4)),
(0, 0, r(-4)),
];
let mp = vec![r(-2), r(0), r(1)]; let (br, skipped) = puiseux_at_algebraic(&f, &mp, 3);
assert_eq!(skipped, 2, "two node sheets escape ℚ(√2): {br:?}");
for s in &br {
verify_alg_branch(&f, &mp, s, 3);
}
}
fn verify_alg_branch_tower(
coeffs: &[(u32, u32, Rational)],
alpha_minpoly: &[Rational],
s: &TowerPuiseuxSeries,
prec: u32,
) {
let eval = |p: &[Rational], x: f64| -> f64 {
p.iter().rev().fold(0.0, |acc, c| acc * x + c.to_f64())
};
let mut tprimes = Vec::new();
let (lo, hi, steps) = (-6.0_f64, 6.0_f64, 24000);
let dx = (hi - lo) / steps as f64;
let mut prev = eval(&s.coeff_minpoly, lo);
for k in 1..=steps {
let x = lo + dx * k as f64;
let cur = eval(&s.coeff_minpoly, x);
if prev == 0.0 || (prev < 0.0) != (cur < 0.0) {
let (mut a, mut b) = (x - dx, x);
for _ in 0..60 {
let mid = 0.5 * (a + b);
if (eval(&s.coeff_minpoly, a) < 0.0) != (eval(&s.coeff_minpoly, mid) < 0.0) {
b = mid;
} else {
a = mid;
}
}
tprimes.push(0.5 * (a + b));
}
prev = cur;
}
assert!(
!tprimes.is_empty(),
"coeff_minpoly should have a real root for the numeric check: {s:?}"
);
let alpha_roots: Vec<f64> = {
let mut rts = Vec::new();
let mut prev = eval(alpha_minpoly, lo);
for k in 1..=steps {
let x = lo + dx * k as f64;
let cur = eval(alpha_minpoly, x);
if prev == 0.0 || (prev < 0.0) != (cur < 0.0) {
let (mut a, mut b) = (x - dx, x);
for _ in 0..60 {
let mid = 0.5 * (a + b);
if (eval(alpha_minpoly, a) < 0.0) != (eval(alpha_minpoly, mid) < 0.0) {
b = mid;
} else {
a = mid;
}
}
rts.push(0.5 * (a + b));
}
prev = cur;
}
rts
};
for tp in &tprimes {
let mut best = f64::INFINITY;
for a0 in &alpha_roots {
for &delta in &[0.002_f64, 0.005, 0.011] {
let x = a0 + delta;
let y: f64 = s
.branch
.terms
.iter()
.map(|(exp, c)| eval(c, *tp) * delta.powf(exp.to_f64()))
.sum();
let fval: f64 = coeffs
.iter()
.map(|(i, j, a)| a.to_f64() * x.powi(*i as i32) * y.powi(*j as i32))
.sum();
let tol = 1e-6 + 200.0 * delta.powf(prec as f64);
best = best.min(fval.abs() / tol);
}
}
assert!(
best < 1.0,
"tower branch {s:?} at θ'₀={tp}: F not O(δ^{prec}) for any α₀ ({alpha_roots:?})"
);
}
}
#[test]
fn alg_base_ramified_tower_collapse_sqrt2_to_quartic() {
let f = [(0, 2, r(1)), (2, 0, r(-1)), (0, 0, r(2))]; let mp = vec![r(-2), r(0), r(1)]; let (br, skipped) = puiseux_at_algebraic_tower(&f, &mp, 3);
assert_eq!(skipped, 0, "tower closes — nothing skipped: {br:?}");
let ram: Vec<&TowerPuiseuxSeries> = br
.iter()
.filter(|s| s.branch.terms.iter().any(|(e, _)| *e == rr(1, 2)))
.collect();
assert!(!ram.is_empty(), "ramified branch must be returned: {br:?}");
for s in &ram {
assert_eq!(s.branch.ramification, 2);
assert_ne!(s.coeff_minpoly, mp, "coeff field must extend ℚ(√2)");
assert_eq!(s.coeff_minpoly.len() - 1, 4, "deg K' = 4: {s:?}");
assert_eq!(s.branch.conjugates, 4);
verify_alg_branch_tower(&f, &mp, s, 3);
}
let (k_only, k_skipped) = puiseux_at_algebraic(&f, &mp, 3);
assert_eq!(k_skipped, 2, "K-only entry skip-counts the two sheets");
assert!(
k_only
.iter()
.all(|s| s.terms.iter().all(|(e, _)| *e != rr(1, 2))),
"K-only entry returns no ramified branch: {k_only:?}"
);
}
#[test]
fn alg_base_node_tower_collapse_sqrt2() {
let f = [
(0, 2, r(1)),
(5, 0, r(-1)),
(4, 0, r(-1)),
(3, 0, r(4)),
(2, 0, r(4)),
(1, 0, r(-4)),
(0, 0, r(-4)),
];
let mp = vec![r(-2), r(0), r(1)]; let (br, skipped) = puiseux_at_algebraic_tower(&f, &mp, 3);
assert_eq!(skipped, 0, "tower closes — nothing skipped: {br:?}");
let tower: Vec<&TowerPuiseuxSeries> = br.iter().filter(|s| s.coeff_minpoly != mp).collect();
assert!(!tower.is_empty(), "node tower branches returned: {br:?}");
for s in &tower {
assert_eq!(s.coeff_minpoly.len() - 1, 4, "deg K' = 4: {s:?}");
assert_eq!(s.branch.conjugates, 4);
verify_alg_branch_tower(&f, &mp, s, 3);
}
}
#[test]
fn alg_base_constant_tower_sqrt2_over_sqrt3() {
let f = [(0, 2, r(1)), (0, 0, r(-2))]; let mp = vec![r(-3), r(0), r(1)]; let (br, skipped) = puiseux_at_algebraic_tower(&f, &mp, 2);
assert_eq!(skipped, 0, "tower closes: {br:?}");
assert_eq!(
br.len(),
1,
"one class (±√2 are conjugate over ℚ(√3)): {br:?}"
);
let s = &br[0];
assert_ne!(s.coeff_minpoly, mp, "coeff field must extend ℚ(√3)");
assert_eq!(s.coeff_minpoly.len() - 1, 4, "deg ℚ(√3,√2) = 4: {s:?}");
assert_eq!(s.branch.conjugates, 4);
verify_alg_branch_tower(&f, &mp, s, 2);
}
#[test]
fn zero_genuine_two_level_tower_y4_minus_2x2() {
let f = [(0, 4, r(1)), (2, 0, r(-2))]; let br = puiseux_at_zero_algebraic(&f, 2);
let total: usize = br.iter().map(|s| s.conjugates).sum();
assert_eq!(total, 4, "all four sheets recovered: {br:?}");
for s in &br {
assert!(
s.terms.iter().any(|(e, _)| *e == rr(1, 2)),
"leading x^{{1/2}}: {s:?}"
);
}
assert!(
br.iter().any(|s| s
.minpoly
.as_ref()
.map(|m| m.len() - 1 == 4)
.unwrap_or(false)),
"a degree-4 tower class expected: {br:?}"
);
}
#[test]
fn alg_base_conjugate_count_bookkeeping() {
let f = [(0, 2, r(1)), (1, 1, r(-1)), (2, 1, r(-1)), (3, 0, r(1))];
let mp = vec![r(-2), r(0), r(0), r(1)]; let (br, skipped) = puiseux_at_algebraic(&f, &mp, 3);
assert_eq!(skipped, 0, "branches: {br:?}");
let total: usize = br.iter().map(|s| s.conjugates).sum();
assert_eq!(total, 6, "two classes × 3 conjugates: {br:?}");
for s in &br {
assert_eq!(s.conjugates, 3);
verify_alg_branch(&f, &mp, s, 3);
}
}
}