1use crate::factor::{mod_inverse, modpow};
21use logicaffeine_base::BigInt;
22
23#[inline]
24fn ib(x: i64) -> BigInt {
25 BigInt::from_i64(x)
26}
27#[inline]
28fn rp(a: &BigInt, p: &BigInt) -> BigInt {
29 let r = a.div_rem(p).map(|(_, r)| r).unwrap_or_else(|| a.clone());
30 if r.is_negative() {
31 r.add(p)
32 } else {
33 r
34 }
35}
36#[inline]
37fn mm(a: &BigInt, b: &BigInt, p: &BigInt) -> BigInt {
38 rp(&a.mul(b), p)
39}
40#[inline]
41fn am(a: &BigInt, b: &BigInt, p: &BigInt) -> BigInt {
42 rp(&a.add(b), p)
43}
44#[inline]
45fn sm(a: &BigInt, b: &BigInt, p: &BigInt) -> BigInt {
46 rp(&a.sub(b), p)
47}
48
49pub type Quad = [BigInt; 3];
51
52pub fn poly_mul(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
54 if a.is_empty() || b.is_empty() {
55 return Vec::new();
56 }
57 let mut r = vec![ib(0); a.len() + b.len() - 1];
58 for (i, ai) in a.iter().enumerate() {
59 for (j, bj) in b.iter().enumerate() {
60 r[i + j] = am(&r[i + j], &mm(ai, bj, p), p);
61 }
62 }
63 r
64}
65
66pub fn poly_deriv(a: &[BigInt], p: &BigInt) -> Vec<BigInt> {
68 (1..a.len()).map(|i| mm(&ib(i as i64), &a[i], p)).collect()
69}
70
71fn poly_sub(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
72 let n = a.len().max(b.len());
73 (0..n)
74 .map(|i| sm(a.get(i).unwrap_or(&ib(0)), b.get(i).unwrap_or(&ib(0)), p))
75 .collect()
76}
77
78fn poly_trim(mut a: Vec<BigInt>) -> Vec<BigInt> {
80 while a.len() > 1 && a.last().is_some_and(|c| c.is_zero()) {
81 a.pop();
82 }
83 a
84}
85
86#[derive(Clone, Debug)]
88pub struct Richelot {
89 pub delta: BigInt,
91 pub domain: Vec<BigInt>,
93 pub codomain: Vec<BigInt>,
95 pub dual: [Vec<BigInt>; 3],
97}
98
99impl Richelot {
100 pub fn is_split(&self) -> bool {
103 self.delta.is_zero()
104 }
105}
106
107pub fn richelot(g: &[Quad; 3], p: &BigInt) -> Richelot {
109 let m = |i: usize, j: usize| &g[i][2 - j]; let det = {
112 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);
113 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);
114 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);
115 am(&sm(&t1, &t2, p), &t3, p)
116 };
117
118 let quad = |q: &Quad| vec![q[0].clone(), q[1].clone(), q[2].clone()];
120 let dual_of = |j: usize, k: usize| {
121 let (gj, gk) = (quad(&g[j]), quad(&g[k]));
122 let (dj, dk) = (poly_deriv(&gj, p), poly_deriv(&gk, p));
123 poly_trim(poly_sub(&poly_mul(&gj, &dk, p), &poly_mul(&dj, &gk, p), p))
124 };
125 let dual = [dual_of(1, 2), dual_of(2, 0), dual_of(0, 1)];
126
127 let domain = poly_mul(&poly_mul(&quad(&g[0]), &quad(&g[1]), p), &quad(&g[2]), p);
128 let codomain = poly_mul(&poly_mul(&dual[0], &dual[1], p), &dual[2], p);
129
130 Richelot { delta: det, domain, codomain, dual }
131}
132
133pub fn richelot_partitions(roots: &[BigInt; 6], p: &BigInt) -> Vec<[Quad; 3]> {
136 fn quad_from(ra: &BigInt, rb: &BigInt, p: &BigInt) -> Quad {
137 [mm(ra, rb, p), sm(&ib(0), &am(ra, rb, p), p), ib(1)]
139 }
140 let idx = [0usize, 1, 2, 3, 4, 5];
141 let mut out = Vec::new();
142 for a in 1..6 {
144 let rest1: Vec<usize> = idx.iter().copied().filter(|&x| x != 0 && x != a).collect();
145 let b0 = rest1[0];
146 for bi in 1..rest1.len() {
147 let b = rest1[bi];
148 let rest2: Vec<usize> = rest1.iter().copied().filter(|&x| x != b0 && x != b).collect();
149 let (c, d) = (rest2[0], rest2[1]);
150 out.push([
151 quad_from(&roots[0], &roots[a], p),
152 quad_from(&roots[b0], &roots[b], p),
153 quad_from(&roots[c], &roots[d], p),
154 ]);
155 }
156 }
157 out
158}
159
160pub fn find_split_neighbour(roots: &[BigInt; 6], p: &BigInt) -> Option<[Quad; 3]> {
166 richelot_partitions(roots, p).into_iter().find(|g| richelot(g, p).is_split())
167}
168
169pub fn poly_eval(a: &[BigInt], x: &BigInt, p: &BigInt) -> BigInt {
171 let mut acc = ib(0);
172 for c in a.iter().rev() {
173 acc = am(&mm(&acc, x, p), c, p);
174 }
175 acc
176}
177
178#[derive(Clone, Debug)]
183pub struct SplitGenus2 {
184 pub sextic: Vec<BigInt>,
186 pub e1: Vec<BigInt>,
188 pub e2: Vec<BigInt>,
190 pub p: BigInt,
191}
192
193pub fn split_jacobian_from_cubic(g: &[BigInt], p: &BigInt) -> SplitGenus2 {
197 let z = ib(0);
198 let sextic = vec![
199 rp(&g[0], p), z.clone(), rp(&g[1], p), z.clone(), rp(&g[2], p), z.clone(), rp(&g[3], p),
200 ];
201 SplitGenus2 {
202 sextic,
203 e1: g.iter().map(|c| rp(c, p)).collect(),
204 e2: g.iter().rev().map(|c| rp(c, p)).collect(),
205 p: p.clone(),
206 }
207}
208
209#[derive(Clone, Debug)]
217pub struct MatchedPairGlue {
218 pub sextic: Vec<BigInt>,
220 pub e1: Vec<BigInt>,
222 pub e2: Vec<BigInt>,
224 pub p: BigInt,
225}
226
227pub fn glue_shared_2torsion(c: &[BigInt; 3], p: &BigInt) -> MatchedPairGlue {
230 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);
232 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 {
235 sextic,
236 e1: poly_mul(&lin(ib(2)), &core, p), e2: poly_mul(&lin(ib(-2)), &core, p), p: p.clone(),
239 }
240}
241
242pub fn surface_is_reducible(roots: &[BigInt; 6], p: &BigInt) -> bool {
247 find_split_neighbour(roots, p).is_some()
248}
249
250pub fn select_splitting_branch(branches: &[[BigInt; 6]], p: &BigInt) -> Option<usize> {
255 branches.iter().position(|r| surface_is_reducible(r, p))
256}
257
258fn pdeg(a: &[BigInt]) -> isize {
266 (0..a.len()).rev().find(|&i| !a[i].is_zero()).map(|i| i as isize).unwrap_or(-1)
267}
268fn padd(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
269 let n = a.len().max(b.len());
270 poly_trim((0..n).map(|i| am(a.get(i).unwrap_or(&ib(0)), b.get(i).unwrap_or(&ib(0)), p)).collect())
271}
272fn psub(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
273 poly_trim(poly_sub(a, b, p))
274}
275fn pmul(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
276 poly_trim(poly_mul(a, b, p))
277}
278fn pscale(a: &[BigInt], s: &BigInt, p: &BigInt) -> Vec<BigInt> {
279 poly_trim(a.iter().map(|c| mm(c, s, p)).collect())
280}
281fn pneg(a: &[BigInt], p: &BigInt) -> Vec<BigInt> {
282 pscale(a, &sm(&ib(0), &ib(1), p), p)
283}
284fn pmonic(a: &[BigInt], p: &BigInt) -> Vec<BigInt> {
285 let d = pdeg(a);
286 if d < 0 {
287 return vec![ib(0)];
288 }
289 pscale(a, &mod_inverse(&a[d as usize], p).unwrap(), p)
290}
291fn pdivmod(a: &[BigInt], b: &[BigInt], p: &BigInt) -> (Vec<BigInt>, Vec<BigInt>) {
293 let db = pdeg(b);
294 assert!(db >= 0, "division by the zero polynomial");
295 let binv = mod_inverse(&b[db as usize], p).unwrap();
296 let mut r = poly_trim(a.to_vec());
297 let mut q = vec![ib(0)];
298 while pdeg(&r) >= db {
299 let dr = pdeg(&r);
300 let shift = (dr - db) as usize;
301 let coef = mm(&r[dr as usize], &binv, p);
302 if q.len() <= shift {
303 q.resize(shift + 1, ib(0));
304 }
305 q[shift] = am(&q[shift], &coef, p);
306 let mut sub = vec![ib(0); shift];
307 sub.extend(b.iter().map(|c| mm(c, &coef, p)));
308 r = psub(&r, &sub, p);
309 }
310 (poly_trim(q), poly_trim(r))
311}
312fn pmod(a: &[BigInt], m: &[BigInt], p: &BigInt) -> Vec<BigInt> {
313 pdivmod(a, m, p).1
314}
315fn pdiv_exact(a: &[BigInt], b: &[BigInt], p: &BigInt) -> Vec<BigInt> {
316 pdivmod(a, b, p).0
317}
318fn pext_gcd(a: &[BigInt], b: &[BigInt], p: &BigInt) -> (Vec<BigInt>, Vec<BigInt>, Vec<BigInt>) {
320 let (mut r0, mut r1) = (poly_trim(a.to_vec()), poly_trim(b.to_vec()));
321 let (mut s0, mut s1) = (vec![ib(1)], vec![ib(0)]);
322 let (mut t0, mut t1) = (vec![ib(0)], vec![ib(1)]);
323 while pdeg(&r1) >= 0 {
324 let (q, r) = pdivmod(&r0, &r1, p);
325 r0 = r1;
326 r1 = r;
327 let ns = psub(&s0, &pmul(&q, &s1, p), p);
328 s0 = s1;
329 s1 = ns;
330 let nt = psub(&t0, &pmul(&q, &t1, p), p);
331 t0 = t1;
332 t1 = nt;
333 }
334 let d = pdeg(&r0);
335 if d > 0 || (d == 0 && r0[0] != ib(1)) {
336 let inv = mod_inverse(&r0[d as usize], p).unwrap();
337 r0 = pscale(&r0, &inv, p);
338 s0 = pscale(&s0, &inv, p);
339 t0 = pscale(&t0, &inv, p);
340 }
341 (r0, s0, t0)
342}
343
344#[derive(Clone, Debug, PartialEq, Eq)]
346pub struct Mumford {
347 pub u: Vec<BigInt>,
348 pub v: Vec<BigInt>,
349}
350
351pub fn jac_identity() -> Mumford {
353 Mumford { u: vec![ib(1)], v: vec![ib(0)] }
354}
355
356pub fn jac_negate(d: &Mumford, p: &BigInt) -> Mumford {
358 Mumford { u: d.u.clone(), v: pmod(&pneg(&d.v, p), &d.u, p) }
359}
360
361fn jac_reduce(mut u: Vec<BigInt>, mut v: Vec<BigInt>, f: &[BigInt], p: &BigInt) -> Mumford {
363 while pdeg(&u) > 2 {
364 let up = pdiv_exact(&psub(f, &pmul(&v, &v, p), p), &u, p);
365 let vp = pmod(&pneg(&v, p), &up, p);
366 u = up;
367 v = vp;
368 }
369 let um = pmonic(&u, p);
370 let vm = pmod(&v, &um, p);
371 Mumford { u: um, v: vm }
372}
373
374pub fn cantor_add(d1: &Mumford, d2: &Mumford, f: &[BigInt], p: &BigInt) -> Mumford {
376 let (u1, v1, u2, v2) = (&d1.u, &d1.v, &d2.u, &d2.v);
377 let (g1, e1, e2) = pext_gcd(u1, u2, p);
379 let vsum = padd(v1, v2, p);
380 let (d, c1, c2) = pext_gcd(&g1, &vsum, p);
381 let s1 = pmul(&c1, &e1, p);
382 let s2 = pmul(&c1, &e2, p);
383 let s3 = c2;
384 let u = pdiv_exact(&pmul(u1, u2, p), &pmul(&d, &d, p), p);
386 let num = padd(
387 &padd(&pmul(&pmul(&s1, u1, p), v2, p), &pmul(&pmul(&s2, u2, p), v1, p), p),
388 &pmul(&s3, &padd(&pmul(v1, v2, p), f, p), p),
389 p,
390 );
391 let v = pmod(&pdiv_exact(&num, &d, p), &u, p);
392 jac_reduce(u, v, f, p)
393}
394
395pub fn jac_scalar_mul(n: u128, d: &Mumford, f: &[BigInt], p: &BigInt) -> Mumford {
397 let mut result = jac_identity();
398 let mut base = d.clone();
399 let mut k = n;
400 while k > 0 {
401 if k & 1 == 1 {
402 result = cantor_add(&result, &base, f, p);
403 }
404 base = cantor_add(&base, &base, f, p);
405 k >>= 1;
406 }
407 result
408}
409
410pub fn jac_element_order(d: &Mumford, group_order: u128, f: &[BigInt], p: &BigInt) -> u128 {
413 let id = jac_identity();
414 let mut primes = Vec::new();
415 let (mut m, mut q) = (group_order, 2u128);
416 while q * q <= m {
417 if m % q == 0 {
418 primes.push(q);
419 while m % q == 0 {
420 m /= q;
421 }
422 }
423 q += 1;
424 }
425 if m > 1 {
426 primes.push(m);
427 }
428 let mut n = group_order;
429 for &pr in &primes {
430 while n % pr == 0 && jac_scalar_mul(n / pr, d, f, p) == id {
431 n /= pr;
432 }
433 }
434 n
435}
436
437fn legendre(a: &BigInt, p: &BigInt) -> i64 {
439 let ar = rp(a, p);
440 if ar.is_zero() {
441 return 0;
442 }
443 let e = p.sub(&ib(1)).div_rem(&ib(2)).map(|(q, _)| q).unwrap();
444 if modpow(&ar, &e, p) == ib(1) {
445 1
446 } else {
447 -1
448 }
449}
450
451pub fn count_curve_fp(f: &[BigInt], p: &BigInt, inf: i64) -> i64 {
453 let pu = p.to_i64().unwrap();
454 (0..pu).fold(inf, |n, x| n + 1 + legendre(&poly_eval(f, &ib(x), p), p))
455}
456
457fn count_curve_fp2(f: &[BigInt], p: &BigInt, inf: i64) -> i64 {
460 use crate::fp2::{fp2_add, fp2_const, fp2_is_zero, fp2_mul, fp2_pow, Fp2};
461 let pu = p.to_i64().unwrap();
462 let exp = (((pu as i128) * (pu as i128) - 1) / 2) as u64;
463 let coeffs: Vec<Fp2> = f.iter().map(|c| (rp(c, p), ib(0))).collect();
464 let mut n = inf;
465 for a in 0..pu {
466 for b in 0..pu {
467 let beta: Fp2 = (ib(a), ib(b));
468 let mut acc = fp2_const(0, p);
469 for c in coeffs.iter().rev() {
470 acc = fp2_add(&fp2_mul(&acc, &beta, p), c, p);
471 }
472 n += 1 + if fp2_is_zero(&acc) {
473 0
474 } else if fp2_pow(&acc, exp, p) == fp2_const(1, p) {
475 1
476 } else {
477 -1
478 };
479 }
480 }
481 n
482}
483
484pub fn genus2_jacobian_order(sextic: &[BigInt], p: &BigInt) -> i128 {
488 let pu = p.to_i64().unwrap() as i128;
489 let n1 = count_curve_fp(sextic, p, 2) as i128; let n2 = count_curve_fp2(sextic, p, 2) as i128;
491 let t1 = (pu + 1) - n1; let t2 = (pu * pu + 1) - n2; 1 + pu * pu - (1 + pu) * t1 + (t1 * t1 - t2) / 2
495}
496
497pub fn genus2_jacobian_order_general(f: &[BigInt], p: &BigInt) -> i128 {
502 let deg = pdeg(f);
503 let pu = p.to_i64().unwrap() as i128;
504 let inf_p = if deg % 2 == 1 {
505 1
506 } else if legendre(&f[deg as usize], p) == 1 {
507 2
508 } else {
509 0
510 };
511 let inf_p2 = if deg % 2 == 1 { 1 } else { 2 };
512 let n1 = count_curve_fp(f, p, inf_p) as i128;
513 let n2 = count_curve_fp2(f, p, inf_p2) as i128;
514 let t1 = (pu + 1) - n1;
515 let t2 = (pu * pu + 1) - n2;
516 1 + pu * pu - (1 + pu) * t1 + (t1 * t1 - t2) / 2
517}
518
519fn fp_sqrt(a: &BigInt, p: &BigInt) -> Option<BigInt> {
521 let a = rp(a, p);
522 if a.is_zero() {
523 return Some(ib(0));
524 }
525 let exp = p.add(&ib(1)).div_rem(&ib(4)).map(|(q, _)| q)?;
526 let r = modpow(&a, &exp, p);
527 (mm(&r, &r, p) == a).then_some(r)
528}
529
530fn quad_roots(q: &[BigInt], p: &BigInt) -> Option<[BigInt; 2]> {
532 if q.len() < 3 || q[2].is_zero() {
533 return None;
534 }
535 let (c0, c1, c2) = (&q[0], &q[1], &q[2]);
536 let disc = sm(&mm(c1, c1, p), &mm(&ib(4), &mm(c0, c2, p), p), p);
537 let sq = fp_sqrt(&disc, p)?;
538 let inv = mod_inverse(&mm(&ib(2), c2, p), p)?;
539 let neg_b = sm(&ib(0), c1, p);
540 Some([mm(&sm(&neg_b, &sq, p), &inv, p), mm(&am(&neg_b, &sq, p), &inv, p)])
541}
542
543fn codomain_roots(dual: &[Vec<BigInt>; 3], p: &BigInt) -> Option<[BigInt; 6]> {
548 let a = quad_roots(&dual[0], p)?;
549 let b = quad_roots(&dual[1], p)?;
550 let c = quad_roots(&dual[2], p)?;
551 Some([a[0].clone(), a[1].clone(), b[0].clone(), b[1].clone(), c[0].clone(), c[1].clone()])
552}
553
554pub fn richelot_chain(roots: &[BigInt; 6], depth: usize, p: &BigInt) -> Option<Vec<usize>> {
560 for (i, g) in richelot_partitions(roots, p).into_iter().enumerate() {
561 let r = richelot(&g, p);
562 if r.is_split() {
563 return Some(vec![i]);
564 }
565 if depth > 0 {
566 if let Some(next) = codomain_roots(&r.dual, p) {
567 if let Some(mut path) = richelot_chain(&next, depth - 1, p) {
568 path.insert(0, i);
569 return Some(path);
570 }
571 }
572 }
573 }
574 None
575}
576
577#[derive(Clone, Debug, PartialEq, Eq)]
579pub enum GuidedWalk {
580 SplitAt(usize),
582 Ended([BigInt; 6]),
584 Stuck(usize),
587}
588
589pub fn guided_chain(roots: &[BigInt; 6], path: &[usize], p: &BigInt) -> GuidedWalk {
595 let mut cur = roots.clone();
596 for (step, &idx) in path.iter().enumerate() {
597 let parts = richelot_partitions(&cur, p);
598 if idx >= parts.len() {
599 return GuidedWalk::Stuck(step);
600 }
601 let r = richelot(&parts[idx], p);
602 if r.is_split() {
603 return GuidedWalk::SplitAt(step);
604 }
605 match codomain_roots(&r.dual, p) {
606 Some(next) => cur = next,
607 None => return GuidedWalk::Stuck(step),
608 }
609 }
610 GuidedWalk::Ended(cur)
611}
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616
617 fn big(s: &str) -> BigInt {
618 BigInt::parse_decimal(s).unwrap()
619 }
620
621 #[test]
622 fn poly_arithmetic_is_correct() {
623 let p = big("101");
624 assert_eq!(poly_mul(&[ib(1), ib(2)], &[ib(3), ib(1)], &p), vec![ib(3), ib(7), ib(2)]);
626 assert_eq!(poly_deriv(&[ib(5), ib(3), ib(4)], &p), vec![ib(3), ib(8)]);
628 }
629
630 #[test]
631 fn richelot_delta_and_dual_quadratics_are_correct() {
632 let p = big("101");
633 let g = [
635 [ib(1), ib(2), ib(1)], [ib(3), ib(1), ib(1)], [ib(0), ib(4), ib(1)], ];
639 let r = richelot(&g, &p);
640 assert_eq!(r.delta, big("98"), "Richelot δ = det of the quadratic coefficients");
643 for h in &r.dual {
645 assert!(h.len() <= 3, "Hᵢ is a quadratic");
646 }
647 assert_eq!(r.domain.len(), 7, "domain is a sextic (7 coefficients)");
649 assert!(!r.is_split(), "generic (independent) quadratics ⟹ δ ≠ 0 ⟹ not split");
650 }
651
652 #[test]
653 fn dependent_quadratics_give_a_reducible_split_surface() {
654 let p = big("101");
655 let g1 = [ib(1), ib(2), ib(1)];
657 let g2 = [ib(3), ib(1), ib(1)];
658 let g3 = [am(&g1[0], &g2[0], &p), am(&g1[1], &g2[1], &p), am(&g1[2], &g2[2], &p)];
659 let r = richelot(&[g1, g2, g3], &p);
660 assert_eq!(r.delta, ib(0), "dependent quadratics ⟹ δ = 0");
661 assert!(r.is_split(), "δ = 0 ⟹ reducible: the abelian surface splits into a product of curves");
662 }
663
664 #[test]
665 fn chain_walk_finds_a_split_across_the_2_2_graph() {
666 let p = big("101");
667 let roots = [ib(1), ib(2), ib(3), ib(4), ib(5), ib(6)];
670 let parts = richelot_partitions(&roots, &p);
671 assert_eq!(parts.len(), 15, "a genus-2 Jacobian has exactly 15 (2,2)-neighbours");
672 for g in &parts {
674 let r = richelot(g, &p);
675 assert_eq!(r.domain.len(), 7, "each neighbour has a sextic domain");
676 }
677 let any_split = parts.iter().any(|g| richelot(g, &p).is_split());
679 let found = find_split_neighbour(&roots, &p);
680 assert_eq!(found.is_some(), any_split, "find_split_neighbour agrees with the exhaustive scan");
681 }
682
683 #[test]
684 fn quadratic_root_extraction_recovers_the_weierstrass_points() {
685 let p = big("103"); let q = [big("37"), big("76"), ib(1)];
688 let roots = quad_roots(&q, &p).expect("rational roots");
689 let mut got: Vec<i64> = roots.iter().map(|r| r.to_i64().unwrap()).collect();
690 got.sort();
691 assert_eq!(got, vec![7, 20], "the quadratic formula recovers both Weierstrass points");
692 for r in &roots {
693 assert!(poly_eval(&q, r, &p).is_zero(), "each extracted point is a genuine root");
694 }
695 }
696
697 #[test]
698 fn a_guided_chain_step_lands_on_the_codomain_surface() {
699 let p = big("103");
700 let roots = [ib(1), ib(2), ib(3), ib(4), ib(5), ib(6)];
701 let parts = richelot_partitions(&roots, &p);
704 let idx = (0..parts.len()).find(|&i| !richelot(&parts[i], &p).is_split()).unwrap();
705 let r = richelot(&parts[idx], &p);
706 match guided_chain(&roots, &[idx], &p) {
707 GuidedWalk::Ended(next) => {
708 for pt in &next {
709 assert!(poly_eval(&r.codomain, pt, &p).is_zero(), "next surface's 2-torsion ⊂ codomain");
710 }
711 }
712 GuidedWalk::Stuck(_) => {} GuidedWalk::SplitAt(_) => panic!("chose a non-split neighbour"),
714 }
715 }
716
717 #[test]
718 fn the_recursive_chain_finds_a_split_and_the_guided_walk_confirms_it() {
719 let p = big("103");
720 let mut s = 0x9e3779b97f4a7c15u64;
723 let mut next = || {
724 s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
725 ((s >> 33) % 100 + 1) as i64
726 };
727 let mut confirmed = false;
728 for _ in 0..400 {
729 let mut vals = [0i64; 6];
730 for v in &mut vals {
731 *v = next();
732 }
733 if (0..6).any(|i| (i + 1..6).any(|j| vals[i] == vals[j])) {
735 continue;
736 }
737 let roots: [BigInt; 6] = core::array::from_fn(|i| ib(vals[i]));
738 if let Some(path) = richelot_chain(&roots, 0, &p) {
739 assert_eq!(path.len(), 1, "a depth-0 walk reports a single-step chain");
741 assert!(
742 richelot(&richelot_partitions(&roots, &p)[path[0]], &p).is_split(),
743 "the returned neighbour is genuinely split"
744 );
745 assert_eq!(
746 guided_chain(&roots, &path, &p),
747 GuidedWalk::SplitAt(0),
748 "following the guidance path reproduces the split"
749 );
750 confirmed = true;
751 break;
752 }
753 }
754 assert!(confirmed, "split-bearing configurations exist and both walks agree on them");
755 }
756
757 #[test]
758 fn a_guided_path_off_the_split_ends_or_gets_stuck_but_never_lies() {
759 let p = big("103");
760 let roots = [ib(2), ib(9), ib(11), ib(30), ib(41), ib(58)];
761 match guided_chain(&roots, &[3, 1], &p) {
764 GuidedWalk::Ended(final_roots) => {
765 assert!(final_roots.iter().all(|r| !r.is_negative()), "final points are field elements");
766 }
767 GuidedWalk::SplitAt(step) => assert!(step < 2, "a split is reported within the path"),
768 GuidedWalk::Stuck(step) => assert!(step < 2, "leaving 𝔽_p is reported at a real step"),
769 }
770 }
771
772 #[test]
773 fn even_sextic_glues_two_elliptic_curves_into_a_split_jacobian() {
774 let p = big("103"); let g = [ib(-6), ib(11), ib(-6), ib(1)];
777 let sg = split_jacobian_from_cubic(&g, &p);
778 assert_eq!(sg.sextic.len(), 7, "C is a sextic y² = g(x²)");
779 assert_eq!(sg.e2, vec![ib(1), rp(&ib(-6), &p), rp(&ib(11), &p), rp(&ib(-6), &p)], "E₂ is the reversed cubic");
780
781 let mut points = 0;
783 for x in 0..103i64 {
784 let xb = ib(x);
785 let fx = poly_eval(&sg.sextic, &xb, &p);
786 let Some(y) = fp_sqrt(&fx, &p) else { continue };
787 points += 1;
788 let u = mm(&xb, &xb, &p);
790 assert_eq!(mm(&y, &y, &p), poly_eval(&sg.e1, &u, &p), "ψ₁(P) lies on E₁");
791 if x != 0 {
793 let xi = mod_inverse(&xb, &p).unwrap();
794 let v = mm(&xi, &xi, &p);
795 let yv = mm(&y, &mm(&xi, &mm(&xi, &xi, &p), &p), &p);
796 assert_eq!(mm(&yv, &yv, &p), poly_eval(&sg.e2, &v, &p), "ψ₂(P) lies on E₂");
797 }
798 }
799 assert!(points > 5, "C has genuine rational points to validate the maps on");
800
801 let split = [[ib(-1), ib(0), ib(1)], [ib(-2), ib(0), ib(1)], [ib(-3), ib(0), ib(1)]];
804 let r = richelot(&split, &p);
805 assert_eq!(r.delta, ib(0), "the ±-splitting has δ = 0");
806 assert!(r.is_split(), "even sextic ⟹ Richelot-reducible ⟹ Jac(C) ~ E₁ × E₂ (a genuine (2,2)-split)");
807 }
808
809 #[test]
810 fn split_jacobian_order_equals_the_product_of_its_elliptic_quotients() {
811 let p = big("103");
815 let g = [ib(-6), ib(11), ib(-6), ib(1)]; let sg = split_jacobian_from_cubic(&g, &p);
817 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);
820 assert_eq!(jac, e1 * e2, "#Jac(C) = #E₁·#E₂ — the split, verified at the level of point counts");
821 let bound = 2 * (103f64.sqrt() as i128) + 2;
823 assert!((e1 - 104).abs() <= bound && (e2 - 104).abs() <= bound, "#Eᵢ obey the Hasse bound");
824 }
825
826 #[test]
827 fn general_matched_pair_gluing_validates_by_jacobian_order() {
828 let p = big("103");
831 for c in [[ib(3), ib(7), ib(20)], [ib(1), ib(5), ib(9)], [ib(4), ib(11), ib(30)]] {
832 let g = glue_shared_2torsion(&c, &p);
833 assert_eq!(g.sextic.len(), 7, "C is a genus-2 sextic");
834 assert_eq!(g.e1.len(), 5, "E₁ is a quartic model");
835 let e1 = count_curve_fp(&g.e1, &p, 2) as i128; let e2 = count_curve_fp(&g.e2, &p, 2) as i128;
837 let jac = genus2_jacobian_order(&g.sextic, &p);
838 assert_eq!(jac, e1 * e2, "#Jac(C) = #E₁·#E₂ for c = {c:?} — matched-pair gluing verified (Tate)");
839 }
840 let g = glue_shared_2torsion(&[ib(3), ib(7), ib(20)], &p);
842 assert_ne!(g.e1, g.e2, "the two elliptic quotients are distinct (they differ in the fourth root)");
843 }
844
845 #[test]
846 fn the_split_test_is_the_per_digit_oracle() {
847 let p = big("103");
848 let recip = |r: i64| (ib(r), mod_inverse(&ib(r), &p).unwrap());
851 let (a0, a1) = recip(2);
852 let (b0, b1) = recip(5);
853 let (c0, c1) = recip(7);
854 let consistent = [a0, a1, b0, b1, c0, c1];
855 assert!(surface_is_reducible(&consistent, &p), "the glued surface splits — the correct digit");
856
857 let mut inconsistent: Option<[BigInt; 6]> = None;
861 for seed in 1..200i64 {
862 let vals: Vec<i64> = (0..6).map(|i| (seed * 41 + i * i * 7 + i * 3) % 101 + 1).collect();
863 if (0..6).any(|i| (i + 1..6).any(|j| vals[i] == vals[j])) {
864 continue; }
866 let rs: [BigInt; 6] = core::array::from_fn(|i| ib(vals[i]));
867 if !surface_is_reducible(&rs, &p) {
868 inconsistent = Some(rs);
869 break;
870 }
871 }
872 let inconsistent = inconsistent.expect("a generic (irreducible) branch exists");
873 assert!(!surface_is_reducible(&inconsistent, &p), "a wrong branch does not split — the oracle prunes it");
874
875 let branches = [inconsistent.clone(), consistent.clone(), inconsistent];
877 let sel = select_splitting_branch(&branches, &p).expect("the oracle selects a splitting branch");
878 assert_eq!(sel, 1, "the split-test selects exactly the consistent (gluing) branch");
879 assert!(surface_is_reducible(&branches[sel], &p), "and the selected branch genuinely splits");
880 }
881
882 #[test]
883 fn genus2_jacobian_cantor_arithmetic_is_a_group_of_order_hash_jac() {
884 let p = big("103");
886 let lin = |r: i64| vec![sm(&ib(0), &ib(r), &p), ib(1)]; let mut f = vec![ib(1)];
888 for r in [0, 1, 2, 3, 4] {
889 f = pmul(&f, &lin(r), &p);
890 }
891 assert_eq!(pdeg(&f), 5, "a quintic ⟹ genus 2");
892
893 let d = (0..103i64)
895 .find_map(|x0| {
896 let fx = poly_eval(&f, &ib(x0), &p);
897 fp_sqrt(&fx, &p).filter(|y| !y.is_zero()).map(|y0| Mumford {
898 u: vec![sm(&ib(0), &ib(x0), &p), ib(1)],
899 v: vec![y0],
900 })
901 })
902 .expect("a rational non-Weierstrass point on C");
903
904 let id = jac_identity();
905 assert_eq!(cantor_add(&d, &id, &f, &p), d, "D + 0 = D");
907 assert_eq!(cantor_add(&d, &jac_negate(&d, &p), &f, &p), id, "D + (−D) = 0");
908 let d2 = cantor_add(&d, &d, &f, &p);
909 assert_eq!(
910 cantor_add(&d2, &d, &f, &p),
911 cantor_add(&d, &d2, &f, &p),
912 "associativity: (2D)+D = D+(2D)"
913 );
914 assert!(pdeg(&d2.u) <= 2, "reduced class has deg u ≤ 2");
916 assert_eq!(pmod(&psub(&f, &pmul(&d2.v, &d2.v, &p), &p), &d2.u, &p), vec![ib(0)], "u | (f − v²)");
917
918 let pu = 103i128;
921 let n1 = count_curve_fp(&f, &p, 1) as i128;
922 let n2 = count_curve_fp2(&f, &p, 1) as i128;
923 let (t1, t2) = ((pu + 1) - n1, (pu * pu + 1) - n2);
924 let jac = 1 + pu * pu - (1 + pu) * t1 + (t1 * t1 - t2) / 2;
925 assert!(jac > 0, "#Jac is a positive integer");
926 assert_eq!(
927 jac_scalar_mul(jac as u128, &d, &f, &p),
928 id,
929 "#Jac · D = 0 — Cantor's law is a genuine group of exactly the order the L-polynomial predicts"
930 );
931 }
932
933 #[test]
934 fn richelot_two_two_kernel_and_two_power_torsion() {
935 let p = big("103");
937 let lin = |r: i64| vec![sm(&ib(0), &ib(r), &p), ib(1)]; let mut f = vec![ib(1)];
939 for r in [0, 1, 2, 3, 4] {
940 f = pmul(&f, &lin(r), &p);
941 }
942 let id = jac_identity();
943 let pu = 103i128;
944 let n1 = count_curve_fp(&f, &p, 1) as i128;
945 let n2 = count_curve_fp2(&f, &p, 1) as i128;
946 let (t1, t2) = ((pu + 1) - n1, (pu * pu + 1) - n2);
947 let jac = (1 + pu * pu - (1 + pu) * t1 + (t1 * t1 - t2) / 2) as u128;
948
949 let d1 = Mumford { u: lin(0), v: vec![ib(0)] };
952 let d2 = Mumford { u: lin(1), v: vec![ib(0)] };
953 assert_eq!(jac_element_order(&d1, jac, &f, &p), 2, "D₁ is 2-torsion (a Weierstrass point)");
954 assert_eq!(jac_element_order(&d2, jac, &f, &p), 2, "D₂ is 2-torsion");
955 let d3 = cantor_add(&d1, &d2, &f, &p);
956 assert_eq!(jac_element_order(&d3, jac, &f, &p), 2, "D₁+D₂ is 2-torsion");
957 assert_ne!(d3, id, "D₁, D₂ are independent");
958 assert_eq!(cantor_add(&d1, &d3, &f, &p), d2, "closed: D₁+(D₁+D₂) = D₂");
960 assert_eq!(cantor_add(&d2, &d3, &f, &p), d1, "closed: D₂+(D₁+D₂) = D₁");
961
962 let odd = {
964 let mut m = jac;
965 while m % 2 == 0 {
966 m /= 2;
967 }
968 m
969 };
970 let two_power = (5..103i64)
972 .find_map(|x0| {
973 let y = fp_sqrt(&poly_eval(&f, &ib(x0), &p), &p).filter(|y| !y.is_zero())?;
974 let dp = jac_scalar_mul(odd, &Mumford { u: lin(x0), v: vec![y] }, &f, &p);
975 (dp != id).then_some(dp)
976 })
977 .expect("a class with nontrivial 2-power torsion");
978 let e_ord = jac_element_order(&two_power, jac, &f, &p);
979 assert!(e_ord.is_power_of_two() && e_ord >= 2, "genuine 2^e-torsion, e ≥ 1: order {e_ord}");
980 assert_eq!(jac_scalar_mul(e_ord, &two_power, &f, &p), id, "and 2^e kills it");
981 }
984
985 #[test]
986 fn richelot_dual_is_genuinely_isogenous_equal_jacobian_order() {
987 let p = big("103");
991 for g in [
992 [[ib(1), ib(0), ib(1)], [ib(2), ib(1), ib(1)], [ib(5), ib(3), ib(1)]],
993 [[ib(7), ib(2), ib(1)], [ib(1), ib(4), ib(1)], [ib(3), ib(0), ib(1)]],
994 ] {
995 let r = richelot(&g, &p);
996 assert_ne!(r.delta, ib(0), "generic quadratics ⟹ δ ≠ 0 ⟹ a genuine genus-2 isogeny (not a split)");
997 let jac_c = genus2_jacobian_order_general(&r.domain, &p);
998 let dinv = mod_inverse(&r.delta, &p).unwrap();
1000 let cprime = pscale(&r.codomain, &dinv, &p);
1001 let jac_cp = genus2_jacobian_order_general(&cprime, &p);
1002 assert_eq!(jac_c, jac_cp, "#Jac(C) = #Jac(C') — the Richelot dual is genuinely isogenous (Tate)");
1003 }
1004 }
1005}