use crate::factor::{mod_inverse, modpow};
use logicaffeine_base::BigInt;
#[inline]
fn ib(x: i64) -> BigInt {
BigInt::from_i64(x)
}
#[inline]
fn rp(a: &BigInt, p: &BigInt) -> BigInt {
let r = a.div_rem(p).map(|(_, r)| r).unwrap_or_else(|| a.clone());
if r.is_negative() {
r.add(p)
} else {
r
}
}
#[inline]
fn mm(a: &BigInt, b: &BigInt, p: &BigInt) -> BigInt {
rp(&a.mul(b), p)
}
#[inline]
fn am(a: &BigInt, b: &BigInt, p: &BigInt) -> BigInt {
rp(&a.add(b), p)
}
#[inline]
fn sm(a: &BigInt, b: &BigInt, p: &BigInt) -> BigInt {
rp(&a.sub(b), p)
}
pub type Quad = [BigInt; 3];
pub fn poly_mul(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
if a.is_empty() || b.is_empty() {
return Vec::new();
}
let mut r = vec![ib(0); a.len() + b.len() - 1];
for (i, ai) in a.iter().enumerate() {
for (j, bj) in b.iter().enumerate() {
r[i + j] = am(&r[i + j], &mm(ai, bj, p), p);
}
}
r
}
pub fn poly_deriv(a: &[BigInt], p: &BigInt) -> Vec<BigInt> {
(1..a.len()).map(|i| mm(&ib(i as i64), &a[i], p)).collect()
}
fn poly_sub(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
let n = a.len().max(b.len());
(0..n)
.map(|i| sm(a.get(i).unwrap_or(&ib(0)), b.get(i).unwrap_or(&ib(0)), p))
.collect()
}
fn poly_trim(mut a: Vec<BigInt>) -> Vec<BigInt> {
while a.len() > 1 && a.last().is_some_and(|c| c.is_zero()) {
a.pop();
}
a
}
#[derive(Clone, Debug)]
pub struct Richelot {
pub delta: BigInt,
pub domain: Vec<BigInt>,
pub codomain: Vec<BigInt>,
pub dual: [Vec<BigInt>; 3],
}
impl Richelot {
pub fn is_split(&self) -> bool {
self.delta.is_zero()
}
}
pub fn richelot(g: &[Quad; 3], p: &BigInt) -> Richelot {
let m = |i: usize, j: usize| &g[i][2 - j]; let det = {
let t1 = mm(m(0, 0), &sm(&mm(m(1, 1), m(2, 2), p), &mm(m(1, 2), m(2, 1), p), p), p);
let t2 = mm(m(0, 1), &sm(&mm(m(1, 0), m(2, 2), p), &mm(m(1, 2), m(2, 0), p), p), p);
let t3 = mm(m(0, 2), &sm(&mm(m(1, 0), m(2, 1), p), &mm(m(1, 1), m(2, 0), p), p), p);
am(&sm(&t1, &t2, p), &t3, p)
};
let quad = |q: &Quad| vec![q[0].clone(), q[1].clone(), q[2].clone()];
let dual_of = |j: usize, k: usize| {
let (gj, gk) = (quad(&g[j]), quad(&g[k]));
let (dj, dk) = (poly_deriv(&gj, p), poly_deriv(&gk, p));
poly_trim(poly_sub(&poly_mul(&gj, &dk, p), &poly_mul(&dj, &gk, p), p))
};
let dual = [dual_of(1, 2), dual_of(2, 0), dual_of(0, 1)];
let domain = poly_mul(&poly_mul(&quad(&g[0]), &quad(&g[1]), p), &quad(&g[2]), p);
let codomain = poly_mul(&poly_mul(&dual[0], &dual[1], p), &dual[2], p);
Richelot { delta: det, domain, codomain, dual }
}
pub fn richelot_partitions(roots: &[BigInt; 6], p: &BigInt) -> Vec<[Quad; 3]> {
fn quad_from(ra: &BigInt, rb: &BigInt, p: &BigInt) -> Quad {
[mm(ra, rb, p), sm(&ib(0), &am(ra, rb, p), p), ib(1)]
}
let idx = [0usize, 1, 2, 3, 4, 5];
let mut out = Vec::new();
for a in 1..6 {
let rest1: Vec<usize> = idx.iter().copied().filter(|&x| x != 0 && x != a).collect();
let b0 = rest1[0];
for bi in 1..rest1.len() {
let b = rest1[bi];
let rest2: Vec<usize> = rest1.iter().copied().filter(|&x| x != b0 && x != b).collect();
let (c, d) = (rest2[0], rest2[1]);
out.push([
quad_from(&roots[0], &roots[a], p),
quad_from(&roots[b0], &roots[b], p),
quad_from(&roots[c], &roots[d], p),
]);
}
}
out
}
pub fn find_split_neighbour(roots: &[BigInt; 6], p: &BigInt) -> Option<[Quad; 3]> {
richelot_partitions(roots, p).into_iter().find(|g| richelot(g, p).is_split())
}
pub fn poly_eval(a: &[BigInt], x: &BigInt, p: &BigInt) -> BigInt {
let mut acc = ib(0);
for c in a.iter().rev() {
acc = am(&mm(&acc, x, p), c, p);
}
acc
}
#[derive(Clone, Debug)]
pub struct SplitGenus2 {
pub sextic: Vec<BigInt>,
pub e1: Vec<BigInt>,
pub e2: Vec<BigInt>,
pub p: BigInt,
}
pub fn split_jacobian_from_cubic(g: &[BigInt], p: &BigInt) -> SplitGenus2 {
let z = ib(0);
let sextic = vec![
rp(&g[0], p), z.clone(), rp(&g[1], p), z.clone(), rp(&g[2], p), z.clone(), rp(&g[3], p),
];
SplitGenus2 {
sextic,
e1: g.iter().map(|c| rp(c, p)).collect(),
e2: g.iter().rev().map(|c| rp(c, p)).collect(),
p: p.clone(),
}
}
#[derive(Clone, Debug)]
pub struct MatchedPairGlue {
pub sextic: Vec<BigInt>,
pub e1: Vec<BigInt>,
pub e2: Vec<BigInt>,
pub p: BigInt,
}
pub fn glue_shared_2torsion(c: &[BigInt; 3], p: &BigInt) -> MatchedPairGlue {
let quad = |ci: &BigInt| vec![ib(1), rp(ci, p), ib(1)]; let sextic = poly_mul(&poly_mul(&quad(&c[0]), &quad(&c[1]), p), &quad(&c[2]), p);
let lin = |r: BigInt| vec![rp(&r, p), ib(1)]; let core = poly_mul(&poly_mul(&lin(c[0].clone()), &lin(c[1].clone()), p), &lin(c[2].clone()), p); MatchedPairGlue {
sextic,
e1: poly_mul(&lin(ib(2)), &core, p), e2: poly_mul(&lin(ib(-2)), &core, p), p: p.clone(),
}
}
pub fn surface_is_reducible(roots: &[BigInt; 6], p: &BigInt) -> bool {
find_split_neighbour(roots, p).is_some()
}
pub fn select_splitting_branch(branches: &[[BigInt; 6]], p: &BigInt) -> Option<usize> {
branches.iter().position(|r| surface_is_reducible(r, p))
}
fn pdeg(a: &[BigInt]) -> isize {
(0..a.len()).rev().find(|&i| !a[i].is_zero()).map(|i| i as isize).unwrap_or(-1)
}
fn padd(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
let n = a.len().max(b.len());
poly_trim((0..n).map(|i| am(a.get(i).unwrap_or(&ib(0)), b.get(i).unwrap_or(&ib(0)), p)).collect())
}
fn psub(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
poly_trim(poly_sub(a, b, p))
}
fn pmul(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
poly_trim(poly_mul(a, b, p))
}
fn pscale(a: &[BigInt], s: &BigInt, p: &BigInt) -> Vec<BigInt> {
poly_trim(a.iter().map(|c| mm(c, s, p)).collect())
}
fn pneg(a: &[BigInt], p: &BigInt) -> Vec<BigInt> {
pscale(a, &sm(&ib(0), &ib(1), p), p)
}
fn pmonic(a: &[BigInt], p: &BigInt) -> Vec<BigInt> {
let d = pdeg(a);
if d < 0 {
return vec![ib(0)];
}
pscale(a, &mod_inverse(&a[d as usize], p).unwrap(), p)
}
fn pdivmod(a: &[BigInt], b: &[BigInt], p: &BigInt) -> (Vec<BigInt>, Vec<BigInt>) {
let db = pdeg(b);
assert!(db >= 0, "division by the zero polynomial");
let binv = mod_inverse(&b[db as usize], p).unwrap();
let mut r = poly_trim(a.to_vec());
let mut q = vec![ib(0)];
while pdeg(&r) >= db {
let dr = pdeg(&r);
let shift = (dr - db) as usize;
let coef = mm(&r[dr as usize], &binv, p);
if q.len() <= shift {
q.resize(shift + 1, ib(0));
}
q[shift] = am(&q[shift], &coef, p);
let mut sub = vec![ib(0); shift];
sub.extend(b.iter().map(|c| mm(c, &coef, p)));
r = psub(&r, &sub, p);
}
(poly_trim(q), poly_trim(r))
}
fn pmod(a: &[BigInt], m: &[BigInt], p: &BigInt) -> Vec<BigInt> {
pdivmod(a, m, p).1
}
fn pdiv_exact(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
pdivmod(a, b, p).0
}
fn pext_gcd(a: &[BigInt], b: &[BigInt], p: &BigInt) -> (Vec<BigInt>, Vec<BigInt>, Vec<BigInt>) {
let (mut r0, mut r1) = (poly_trim(a.to_vec()), poly_trim(b.to_vec()));
let (mut s0, mut s1) = (vec![ib(1)], vec![ib(0)]);
let (mut t0, mut t1) = (vec![ib(0)], vec![ib(1)]);
while pdeg(&r1) >= 0 {
let (q, r) = pdivmod(&r0, &r1, p);
r0 = r1;
r1 = r;
let ns = psub(&s0, &pmul(&q, &s1, p), p);
s0 = s1;
s1 = ns;
let nt = psub(&t0, &pmul(&q, &t1, p), p);
t0 = t1;
t1 = nt;
}
let d = pdeg(&r0);
if d > 0 || (d == 0 && r0[0] != ib(1)) {
let inv = mod_inverse(&r0[d as usize], p).unwrap();
r0 = pscale(&r0, &inv, p);
s0 = pscale(&s0, &inv, p);
t0 = pscale(&t0, &inv, p);
}
(r0, s0, t0)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Mumford {
pub u: Vec<BigInt>,
pub v: Vec<BigInt>,
}
pub fn jac_identity() -> Mumford {
Mumford { u: vec![ib(1)], v: vec![ib(0)] }
}
pub fn jac_negate(d: &Mumford, p: &BigInt) -> Mumford {
Mumford { u: d.u.clone(), v: pmod(&pneg(&d.v, p), &d.u, p) }
}
fn jac_reduce(mut u: Vec<BigInt>, mut v: Vec<BigInt>, f: &[BigInt], p: &BigInt) -> Mumford {
while pdeg(&u) > 2 {
let up = pdiv_exact(&psub(f, &pmul(&v, &v, p), p), &u, p);
let vp = pmod(&pneg(&v, p), &up, p);
u = up;
v = vp;
}
let um = pmonic(&u, p);
let vm = pmod(&v, &um, p);
Mumford { u: um, v: vm }
}
pub fn cantor_add(d1: &Mumford, d2: &Mumford, f: &[BigInt], p: &BigInt) -> Mumford {
let (u1, v1, u2, v2) = (&d1.u, &d1.v, &d2.u, &d2.v);
let (g1, e1, e2) = pext_gcd(u1, u2, p);
let vsum = padd(v1, v2, p);
let (d, c1, c2) = pext_gcd(&g1, &vsum, p);
let s1 = pmul(&c1, &e1, p);
let s2 = pmul(&c1, &e2, p);
let s3 = c2;
let u = pdiv_exact(&pmul(u1, u2, p), &pmul(&d, &d, p), p);
let num = padd(
&padd(&pmul(&pmul(&s1, u1, p), v2, p), &pmul(&pmul(&s2, u2, p), v1, p), p),
&pmul(&s3, &padd(&pmul(v1, v2, p), f, p), p),
p,
);
let v = pmod(&pdiv_exact(&num, &d, p), &u, p);
jac_reduce(u, v, f, p)
}
pub fn jac_scalar_mul(n: u128, d: &Mumford, f: &[BigInt], p: &BigInt) -> Mumford {
let mut result = jac_identity();
let mut base = d.clone();
let mut k = n;
while k > 0 {
if k & 1 == 1 {
result = cantor_add(&result, &base, f, p);
}
base = cantor_add(&base, &base, f, p);
k >>= 1;
}
result
}
pub fn jac_element_order(d: &Mumford, group_order: u128, f: &[BigInt], p: &BigInt) -> u128 {
let id = jac_identity();
let mut primes = Vec::new();
let (mut m, mut q) = (group_order, 2u128);
while q * q <= m {
if m % q == 0 {
primes.push(q);
while m % q == 0 {
m /= q;
}
}
q += 1;
}
if m > 1 {
primes.push(m);
}
let mut n = group_order;
for &pr in &primes {
while n % pr == 0 && jac_scalar_mul(n / pr, d, f, p) == id {
n /= pr;
}
}
n
}
fn legendre(a: &BigInt, p: &BigInt) -> i64 {
let ar = rp(a, p);
if ar.is_zero() {
return 0;
}
let e = p.sub(&ib(1)).div_rem(&ib(2)).map(|(q, _)| q).unwrap();
if modpow(&ar, &e, p) == ib(1) {
1
} else {
-1
}
}
pub fn count_curve_fp(f: &[BigInt], p: &BigInt, inf: i64) -> i64 {
let pu = p.to_i64().unwrap();
(0..pu).fold(inf, |n, x| n + 1 + legendre(&poly_eval(f, &ib(x), p), p))
}
fn count_curve_fp2(f: &[BigInt], p: &BigInt, inf: i64) -> i64 {
use crate::fp2::{fp2_add, fp2_const, fp2_is_zero, fp2_mul, fp2_pow, Fp2};
let pu = p.to_i64().unwrap();
let exp = (((pu as i128) * (pu as i128) - 1) / 2) as u64;
let coeffs: Vec<Fp2> = f.iter().map(|c| (rp(c, p), ib(0))).collect();
let mut n = inf;
for a in 0..pu {
for b in 0..pu {
let beta: Fp2 = (ib(a), ib(b));
let mut acc = fp2_const(0, p);
for c in coeffs.iter().rev() {
acc = fp2_add(&fp2_mul(&acc, &beta, p), c, p);
}
n += 1 + if fp2_is_zero(&acc) {
0
} else if fp2_pow(&acc, exp, p) == fp2_const(1, p) {
1
} else {
-1
};
}
}
n
}
pub fn genus2_jacobian_order(sextic: &[BigInt], p: &BigInt) -> i128 {
let pu = p.to_i64().unwrap() as i128;
let n1 = count_curve_fp(sextic, p, 2) as i128; let n2 = count_curve_fp2(sextic, p, 2) as i128;
let t1 = (pu + 1) - n1; let t2 = (pu * pu + 1) - n2; 1 + pu * pu - (1 + pu) * t1 + (t1 * t1 - t2) / 2
}
pub fn genus2_jacobian_order_general(f: &[BigInt], p: &BigInt) -> i128 {
let deg = pdeg(f);
let pu = p.to_i64().unwrap() as i128;
let inf_p = if deg % 2 == 1 {
1
} else if legendre(&f[deg as usize], p) == 1 {
2
} else {
0
};
let inf_p2 = if deg % 2 == 1 { 1 } else { 2 };
let n1 = count_curve_fp(f, p, inf_p) as i128;
let n2 = count_curve_fp2(f, p, inf_p2) as i128;
let t1 = (pu + 1) - n1;
let t2 = (pu * pu + 1) - n2;
1 + pu * pu - (1 + pu) * t1 + (t1 * t1 - t2) / 2
}
fn fp_sqrt(a: &BigInt, p: &BigInt) -> Option<BigInt> {
let a = rp(a, p);
if a.is_zero() {
return Some(ib(0));
}
let exp = p.add(&ib(1)).div_rem(&ib(4)).map(|(q, _)| q)?;
let r = modpow(&a, &exp, p);
(mm(&r, &r, p) == a).then_some(r)
}
fn quad_roots(q: &[BigInt], p: &BigInt) -> Option<[BigInt; 2]> {
if q.len() < 3 || q[2].is_zero() {
return None;
}
let (c0, c1, c2) = (&q[0], &q[1], &q[2]);
let disc = sm(&mm(c1, c1, p), &mm(&ib(4), &mm(c0, c2, p), p), p);
let sq = fp_sqrt(&disc, p)?;
let inv = mod_inverse(&mm(&ib(2), c2, p), p)?;
let neg_b = sm(&ib(0), c1, p);
Some([mm(&sm(&neg_b, &sq, p), &inv, p), mm(&am(&neg_b, &sq, p), &inv, p)])
}
fn codomain_roots(dual: &[Vec<BigInt>; 3], p: &BigInt) -> Option<[BigInt; 6]> {
let a = quad_roots(&dual[0], p)?;
let b = quad_roots(&dual[1], p)?;
let c = quad_roots(&dual[2], p)?;
Some([a[0].clone(), a[1].clone(), b[0].clone(), b[1].clone(), c[0].clone(), c[1].clone()])
}
pub fn richelot_chain(roots: &[BigInt; 6], depth: usize, p: &BigInt) -> Option<Vec<usize>> {
for (i, g) in richelot_partitions(roots, p).into_iter().enumerate() {
let r = richelot(&g, p);
if r.is_split() {
return Some(vec![i]);
}
if depth > 0 {
if let Some(next) = codomain_roots(&r.dual, p) {
if let Some(mut path) = richelot_chain(&next, depth - 1, p) {
path.insert(0, i);
return Some(path);
}
}
}
}
None
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum GuidedWalk {
SplitAt(usize),
Ended([BigInt; 6]),
Stuck(usize),
}
pub fn guided_chain(roots: &[BigInt; 6], path: &[usize], p: &BigInt) -> GuidedWalk {
let mut cur = roots.clone();
for (step, &idx) in path.iter().enumerate() {
let parts = richelot_partitions(&cur, p);
if idx >= parts.len() {
return GuidedWalk::Stuck(step);
}
let r = richelot(&parts[idx], p);
if r.is_split() {
return GuidedWalk::SplitAt(step);
}
match codomain_roots(&r.dual, p) {
Some(next) => cur = next,
None => return GuidedWalk::Stuck(step),
}
}
GuidedWalk::Ended(cur)
}
#[cfg(test)]
mod tests {
use super::*;
fn big(s: &str) -> BigInt {
BigInt::parse_decimal(s).unwrap()
}
#[test]
fn poly_arithmetic_is_correct() {
let p = big("101");
assert_eq!(poly_mul(&[ib(1), ib(2)], &[ib(3), ib(1)], &p), vec![ib(3), ib(7), ib(2)]);
assert_eq!(poly_deriv(&[ib(5), ib(3), ib(4)], &p), vec![ib(3), ib(8)]);
}
#[test]
fn richelot_delta_and_dual_quadratics_are_correct() {
let p = big("101");
let g = [
[ib(1), ib(2), ib(1)], [ib(3), ib(1), ib(1)], [ib(0), ib(4), ib(1)], ];
let r = richelot(&g, &p);
assert_eq!(r.delta, big("98"), "Richelot δ = det of the quadratic coefficients");
for h in &r.dual {
assert!(h.len() <= 3, "Hᵢ is a quadratic");
}
assert_eq!(r.domain.len(), 7, "domain is a sextic (7 coefficients)");
assert!(!r.is_split(), "generic (independent) quadratics ⟹ δ ≠ 0 ⟹ not split");
}
#[test]
fn dependent_quadratics_give_a_reducible_split_surface() {
let p = big("101");
let g1 = [ib(1), ib(2), ib(1)];
let g2 = [ib(3), ib(1), ib(1)];
let g3 = [am(&g1[0], &g2[0], &p), am(&g1[1], &g2[1], &p), am(&g1[2], &g2[2], &p)];
let r = richelot(&[g1, g2, g3], &p);
assert_eq!(r.delta, ib(0), "dependent quadratics ⟹ δ = 0");
assert!(r.is_split(), "δ = 0 ⟹ reducible: the abelian surface splits into a product of curves");
}
#[test]
fn chain_walk_finds_a_split_across_the_2_2_graph() {
let p = big("101");
let roots = [ib(1), ib(2), ib(3), ib(4), ib(5), ib(6)];
let parts = richelot_partitions(&roots, &p);
assert_eq!(parts.len(), 15, "a genus-2 Jacobian has exactly 15 (2,2)-neighbours");
for g in &parts {
let r = richelot(g, &p);
assert_eq!(r.domain.len(), 7, "each neighbour has a sextic domain");
}
let any_split = parts.iter().any(|g| richelot(g, &p).is_split());
let found = find_split_neighbour(&roots, &p);
assert_eq!(found.is_some(), any_split, "find_split_neighbour agrees with the exhaustive scan");
}
#[test]
fn quadratic_root_extraction_recovers_the_weierstrass_points() {
let p = big("103"); let q = [big("37"), big("76"), ib(1)];
let roots = quad_roots(&q, &p).expect("rational roots");
let mut got: Vec<i64> = roots.iter().map(|r| r.to_i64().unwrap()).collect();
got.sort();
assert_eq!(got, vec![7, 20], "the quadratic formula recovers both Weierstrass points");
for r in &roots {
assert!(poly_eval(&q, r, &p).is_zero(), "each extracted point is a genuine root");
}
}
#[test]
fn a_guided_chain_step_lands_on_the_codomain_surface() {
let p = big("103");
let roots = [ib(1), ib(2), ib(3), ib(4), ib(5), ib(6)];
let parts = richelot_partitions(&roots, &p);
let idx = (0..parts.len()).find(|&i| !richelot(&parts[i], &p).is_split()).unwrap();
let r = richelot(&parts[idx], &p);
match guided_chain(&roots, &[idx], &p) {
GuidedWalk::Ended(next) => {
for pt in &next {
assert!(poly_eval(&r.codomain, pt, &p).is_zero(), "next surface's 2-torsion ⊂ codomain");
}
}
GuidedWalk::Stuck(_) => {} GuidedWalk::SplitAt(_) => panic!("chose a non-split neighbour"),
}
}
#[test]
fn the_recursive_chain_finds_a_split_and_the_guided_walk_confirms_it() {
let p = big("103");
let mut s = 0x9e3779b97f4a7c15u64;
let mut next = || {
s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
((s >> 33) % 100 + 1) as i64
};
let mut confirmed = false;
for _ in 0..400 {
let mut vals = [0i64; 6];
for v in &mut vals {
*v = next();
}
if (0..6).any(|i| (i + 1..6).any(|j| vals[i] == vals[j])) {
continue;
}
let roots: [BigInt; 6] = core::array::from_fn(|i| ib(vals[i]));
if let Some(path) = richelot_chain(&roots, 0, &p) {
assert_eq!(path.len(), 1, "a depth-0 walk reports a single-step chain");
assert!(
richelot(&richelot_partitions(&roots, &p)[path[0]], &p).is_split(),
"the returned neighbour is genuinely split"
);
assert_eq!(
guided_chain(&roots, &path, &p),
GuidedWalk::SplitAt(0),
"following the guidance path reproduces the split"
);
confirmed = true;
break;
}
}
assert!(confirmed, "split-bearing configurations exist and both walks agree on them");
}
#[test]
fn a_guided_path_off_the_split_ends_or_gets_stuck_but_never_lies() {
let p = big("103");
let roots = [ib(2), ib(9), ib(11), ib(30), ib(41), ib(58)];
match guided_chain(&roots, &[3, 1], &p) {
GuidedWalk::Ended(final_roots) => {
assert!(final_roots.iter().all(|r| !r.is_negative()), "final points are field elements");
}
GuidedWalk::SplitAt(step) => assert!(step < 2, "a split is reported within the path"),
GuidedWalk::Stuck(step) => assert!(step < 2, "leaving 𝔽_p is reported at a real step"),
}
}
#[test]
fn even_sextic_glues_two_elliptic_curves_into_a_split_jacobian() {
let p = big("103"); let g = [ib(-6), ib(11), ib(-6), ib(1)];
let sg = split_jacobian_from_cubic(&g, &p);
assert_eq!(sg.sextic.len(), 7, "C is a sextic y² = g(x²)");
assert_eq!(sg.e2, vec![ib(1), rp(&ib(-6), &p), rp(&ib(11), &p), rp(&ib(-6), &p)], "E₂ is the reversed cubic");
let mut points = 0;
for x in 0..103i64 {
let xb = ib(x);
let fx = poly_eval(&sg.sextic, &xb, &p);
let Some(y) = fp_sqrt(&fx, &p) else { continue };
points += 1;
let u = mm(&xb, &xb, &p);
assert_eq!(mm(&y, &y, &p), poly_eval(&sg.e1, &u, &p), "ψ₁(P) lies on E₁");
if x != 0 {
let xi = mod_inverse(&xb, &p).unwrap();
let v = mm(&xi, &xi, &p);
let yv = mm(&y, &mm(&xi, &mm(&xi, &xi, &p), &p), &p);
assert_eq!(mm(&yv, &yv, &p), poly_eval(&sg.e2, &v, &p), "ψ₂(P) lies on E₂");
}
}
assert!(points > 5, "C has genuine rational points to validate the maps on");
let split = [[ib(-1), ib(0), ib(1)], [ib(-2), ib(0), ib(1)], [ib(-3), ib(0), ib(1)]];
let r = richelot(&split, &p);
assert_eq!(r.delta, ib(0), "the ±-splitting has δ = 0");
assert!(r.is_split(), "even sextic ⟹ Richelot-reducible ⟹ Jac(C) ~ E₁ × E₂ (a genuine (2,2)-split)");
}
#[test]
fn split_jacobian_order_equals_the_product_of_its_elliptic_quotients() {
let p = big("103");
let g = [ib(-6), ib(11), ib(-6), ib(1)]; let sg = split_jacobian_from_cubic(&g, &p);
let e1 = count_curve_fp(&sg.e1, &p, 1) as i128; let e2 = count_curve_fp(&sg.e2, &p, 1) as i128; let jac = genus2_jacobian_order(&sg.sextic, &p);
assert_eq!(jac, e1 * e2, "#Jac(C) = #E₁·#E₂ — the split, verified at the level of point counts");
let bound = 2 * (103f64.sqrt() as i128) + 2;
assert!((e1 - 104).abs() <= bound && (e2 - 104).abs() <= bound, "#Eᵢ obey the Hasse bound");
}
#[test]
fn general_matched_pair_gluing_validates_by_jacobian_order() {
let p = big("103");
for c in [[ib(3), ib(7), ib(20)], [ib(1), ib(5), ib(9)], [ib(4), ib(11), ib(30)]] {
let g = glue_shared_2torsion(&c, &p);
assert_eq!(g.sextic.len(), 7, "C is a genus-2 sextic");
assert_eq!(g.e1.len(), 5, "E₁ is a quartic model");
let e1 = count_curve_fp(&g.e1, &p, 2) as i128; let e2 = count_curve_fp(&g.e2, &p, 2) as i128;
let jac = genus2_jacobian_order(&g.sextic, &p);
assert_eq!(jac, e1 * e2, "#Jac(C) = #E₁·#E₂ for c = {c:?} — matched-pair gluing verified (Tate)");
}
let g = glue_shared_2torsion(&[ib(3), ib(7), ib(20)], &p);
assert_ne!(g.e1, g.e2, "the two elliptic quotients are distinct (they differ in the fourth root)");
}
#[test]
fn the_split_test_is_the_per_digit_oracle() {
let p = big("103");
let recip = |r: i64| (ib(r), mod_inverse(&ib(r), &p).unwrap());
let (a0, a1) = recip(2);
let (b0, b1) = recip(5);
let (c0, c1) = recip(7);
let consistent = [a0, a1, b0, b1, c0, c1];
assert!(surface_is_reducible(&consistent, &p), "the glued surface splits — the correct digit");
let mut inconsistent: Option<[BigInt; 6]> = None;
for seed in 1..200i64 {
let vals: Vec<i64> = (0..6).map(|i| (seed * 41 + i * i * 7 + i * 3) % 101 + 1).collect();
if (0..6).any(|i| (i + 1..6).any(|j| vals[i] == vals[j])) {
continue; }
let rs: [BigInt; 6] = core::array::from_fn(|i| ib(vals[i]));
if !surface_is_reducible(&rs, &p) {
inconsistent = Some(rs);
break;
}
}
let inconsistent = inconsistent.expect("a generic (irreducible) branch exists");
assert!(!surface_is_reducible(&inconsistent, &p), "a wrong branch does not split — the oracle prunes it");
let branches = [inconsistent.clone(), consistent.clone(), inconsistent];
let sel = select_splitting_branch(&branches, &p).expect("the oracle selects a splitting branch");
assert_eq!(sel, 1, "the split-test selects exactly the consistent (gluing) branch");
assert!(surface_is_reducible(&branches[sel], &p), "and the selected branch genuinely splits");
}
#[test]
fn genus2_jacobian_cantor_arithmetic_is_a_group_of_order_hash_jac() {
let p = big("103");
let lin = |r: i64| vec![sm(&ib(0), &ib(r), &p), ib(1)]; let mut f = vec![ib(1)];
for r in [0, 1, 2, 3, 4] {
f = pmul(&f, &lin(r), &p);
}
assert_eq!(pdeg(&f), 5, "a quintic ⟹ genus 2");
let d = (0..103i64)
.find_map(|x0| {
let fx = poly_eval(&f, &ib(x0), &p);
fp_sqrt(&fx, &p).filter(|y| !y.is_zero()).map(|y0| Mumford {
u: vec![sm(&ib(0), &ib(x0), &p), ib(1)],
v: vec![y0],
})
})
.expect("a rational non-Weierstrass point on C");
let id = jac_identity();
assert_eq!(cantor_add(&d, &id, &f, &p), d, "D + 0 = D");
assert_eq!(cantor_add(&d, &jac_negate(&d, &p), &f, &p), id, "D + (−D) = 0");
let d2 = cantor_add(&d, &d, &f, &p);
assert_eq!(
cantor_add(&d2, &d, &f, &p),
cantor_add(&d, &d2, &f, &p),
"associativity: (2D)+D = D+(2D)"
);
assert!(pdeg(&d2.u) <= 2, "reduced class has deg u ≤ 2");
assert_eq!(pmod(&psub(&f, &pmul(&d2.v, &d2.v, &p), &p), &d2.u, &p), vec![ib(0)], "u | (f − v²)");
let pu = 103i128;
let n1 = count_curve_fp(&f, &p, 1) as i128;
let n2 = count_curve_fp2(&f, &p, 1) as i128;
let (t1, t2) = ((pu + 1) - n1, (pu * pu + 1) - n2);
let jac = 1 + pu * pu - (1 + pu) * t1 + (t1 * t1 - t2) / 2;
assert!(jac > 0, "#Jac is a positive integer");
assert_eq!(
jac_scalar_mul(jac as u128, &d, &f, &p),
id,
"#Jac · D = 0 — Cantor's law is a genuine group of exactly the order the L-polynomial predicts"
);
}
#[test]
fn richelot_two_two_kernel_and_two_power_torsion() {
let p = big("103");
let lin = |r: i64| vec![sm(&ib(0), &ib(r), &p), ib(1)]; let mut f = vec![ib(1)];
for r in [0, 1, 2, 3, 4] {
f = pmul(&f, &lin(r), &p);
}
let id = jac_identity();
let pu = 103i128;
let n1 = count_curve_fp(&f, &p, 1) as i128;
let n2 = count_curve_fp2(&f, &p, 1) as i128;
let (t1, t2) = ((pu + 1) - n1, (pu * pu + 1) - n2);
let jac = (1 + pu * pu - (1 + pu) * t1 + (t1 * t1 - t2) / 2) as u128;
let d1 = Mumford { u: lin(0), v: vec![ib(0)] };
let d2 = Mumford { u: lin(1), v: vec![ib(0)] };
assert_eq!(jac_element_order(&d1, jac, &f, &p), 2, "D₁ is 2-torsion (a Weierstrass point)");
assert_eq!(jac_element_order(&d2, jac, &f, &p), 2, "D₂ is 2-torsion");
let d3 = cantor_add(&d1, &d2, &f, &p);
assert_eq!(jac_element_order(&d3, jac, &f, &p), 2, "D₁+D₂ is 2-torsion");
assert_ne!(d3, id, "D₁, D₂ are independent");
assert_eq!(cantor_add(&d1, &d3, &f, &p), d2, "closed: D₁+(D₁+D₂) = D₂");
assert_eq!(cantor_add(&d2, &d3, &f, &p), d1, "closed: D₂+(D₁+D₂) = D₁");
let odd = {
let mut m = jac;
while m % 2 == 0 {
m /= 2;
}
m
};
let two_power = (5..103i64)
.find_map(|x0| {
let y = fp_sqrt(&poly_eval(&f, &ib(x0), &p), &p).filter(|y| !y.is_zero())?;
let dp = jac_scalar_mul(odd, &Mumford { u: lin(x0), v: vec![y] }, &f, &p);
(dp != id).then_some(dp)
})
.expect("a class with nontrivial 2-power torsion");
let e_ord = jac_element_order(&two_power, jac, &f, &p);
assert!(e_ord.is_power_of_two() && e_ord >= 2, "genuine 2^e-torsion, e ≥ 1: order {e_ord}");
assert_eq!(jac_scalar_mul(e_ord, &two_power, &f, &p), id, "and 2^e kills it");
}
#[test]
fn richelot_dual_is_genuinely_isogenous_equal_jacobian_order() {
let p = big("103");
for g in [
[[ib(1), ib(0), ib(1)], [ib(2), ib(1), ib(1)], [ib(5), ib(3), ib(1)]],
[[ib(7), ib(2), ib(1)], [ib(1), ib(4), ib(1)], [ib(3), ib(0), ib(1)]],
] {
let r = richelot(&g, &p);
assert_ne!(r.delta, ib(0), "generic quadratics ⟹ δ ≠ 0 ⟹ a genuine genus-2 isogeny (not a split)");
let jac_c = genus2_jacobian_order_general(&r.domain, &p);
let dinv = mod_inverse(&r.delta, &p).unwrap();
let cprime = pscale(&r.codomain, &dinv, &p);
let jac_cp = genus2_jacobian_order_general(&cprime, &p);
assert_eq!(jac_c, jac_cp, "#Jac(C) = #Jac(C') — the Richelot dual is genuinely isogenous (Tate)");
}
}
}