From 8bf8781e495d87c026e752d4a712233a27237ef7 Mon Sep 17 00:00:00 2001 From: allaun Date: Thu, 2 Jul 2026 20:38:22 -0500 Subject: [PATCH] feat(semisym): modular fast path + scar-map ingest skeleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - modp.rs: order-3 jet AD over 𝔽_p (multi-prime, robust exact-escalation, named edge receipts, Miller–Rabin modulus guard); 272Γ— at Δ₇ centroid, gate-preserving (β‰’0 mod p is a proof; PROPER via division-free certificate) - main.rs: --mod flag, validate-modp cross-check, and collision-fixed survey filenames (numer-denom; reduced numerators silently folded ~6435 pts onto 330) - ingest_semisym_scar.rs: receipt β†’ ene.rrc_classifications dry-run mapper, float-free exact JSONB metrics (no Q16/IEEE), --apply gated behind explicit --dsn - CITATION.cff: credit Kritchevsky "Everything Is Logarithms" for the exact / log-in-prime-basis scar-map encoding Co-Authored-By: Claude Opus 4.8 --- CITATION.cff | 4 +- rust/semisym/Cargo.lock | 207 +++++++ rust/semisym/src/main.rs | 194 ++++++- rust/semisym/src/modp.rs | 832 ++++++++++++++++++++++++++++ rust/src/bin/ingest_semisym_scar.rs | 246 ++++++++ 5 files changed, 1462 insertions(+), 21 deletions(-) create mode 100644 rust/semisym/Cargo.lock create mode 100644 rust/semisym/src/modp.rs create mode 100644 rust/src/bin/ingest_semisym_scar.rs diff --git a/CITATION.cff b/CITATION.cff index 5f6dc52c..4de7841d 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -17,7 +17,7 @@ authors: given-names: "Brandon" alias: "allaunthefox" affiliation: "Independent Researcher" - orcid: "https://orcid.org/0000-0000-0000-0000" + orcid: "https://orcid.org/0009-0000-1594-0095" repository: "https://github.com/allaunthefox/SilverSight" repository-code: "https://github.com/allaunthefox/SilverSight" url: "https://github.com/allaunthefox/SilverSight" @@ -178,7 +178,7 @@ references: given-names: "Alex" date-published: "2026-05-25" url: "https://alexkritchevsky.com/2026/05/25/everything-is-logarithms.html" - notes: "Binds to SilverSight formal/CoreFormalism/HachimojiLUT.lean and formal/CoreFormalism/ChentsovFinite.lean. Core claim: logarithms are coordinate-free objects." + notes: "Binds to SilverSight formal/CoreFormalism/HachimojiLUT.lean and formal/CoreFormalism/ChentsovFinite.lean. Core claim: logarithms are coordinate-free objects. Also informed rust/src/bin/ingest_semisym_scar.rs (semisym scar-map ingest): the prime-factorization-as-logarithm identity (log n = Sum_p nu_p(n) log p) motivated the exact, float-free encoding of scar-map metrics (rationals kept exact, no Q16.16/IEEE float) and the planned prime-exponent nu_p log-vector field over the precomputed prime-LUT basis." - type: article title: "Stabilizing Recurrent Dynamics for Test-Time Scalable Latent Reasoning in Looped Language Models" diff --git a/rust/semisym/Cargo.lock b/rust/semisym/Cargo.lock new file mode 100644 index 00000000..751d5fba --- /dev/null +++ b/rust/semisym/Cargo.lock @@ -0,0 +1,207 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "semisym" +version = "0.1.0" +dependencies = [ + "num-bigint", + "num-rational", + "num-traits", + "rayon", + "serde", + "serde_json", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/rust/semisym/src/main.rs b/rust/semisym/src/main.rs index 8d470ce0..11238b1c 100644 --- a/rust/semisym/src/main.rs +++ b/rust/semisym/src/main.rs @@ -36,7 +36,9 @@ use std::fs; use std::path::PathBuf; use std::str::FromStr; -type Q = BigRational; +mod modp; + +pub(crate) type Q = BigRational; fn q(n: i64, d: i64) -> Q { Q::new(BigInt::from(n), BigInt::from(d)) @@ -56,12 +58,12 @@ fn parse_q(s: &str) -> Q { // ── symmetric index helpers (n vars) ───────────────────────────────── #[inline] -fn i2(n: usize, i: usize, j: usize) -> usize { +pub(crate) fn i2(n: usize, i: usize, j: usize) -> usize { let (i, j) = if i <= j { (i, j) } else { (j, i) }; i * n - i * (i + 1) / 2 + j } -fn i3_table(n: usize) -> (Vec<[usize; 3]>, Vec>>) { +pub(crate) fn i3_table(n: usize) -> (Vec<[usize; 3]>, Vec>>) { let mut list = Vec::new(); let mut idx = vec![vec![vec![0usize; n]; n]; n]; for i in 0..n { @@ -226,7 +228,7 @@ impl JetCtx { // ── receipts ───────────────────────────────────────────────────────── #[derive(Serialize)] -struct Receipt { +pub(crate) struct Receipt { schema: &'static str, m: usize, geometrization: String, @@ -250,7 +252,7 @@ fn qs(x: &Q) -> String { // ── geometry pipeline at a rational point ──────────────────────────── -fn drift_beta(m: usize) -> Vec { +pub(crate) fn drift_beta(m: usize) -> Vec { // BraidStateN.lean rossbyDriftFromChirality weights, cycled let base = [q(1, 1), q(-1, 1), q(1, 2), q(0, 1)]; let w: Vec = (0..m).map(|i| base[i % 4].clone()).collect(); @@ -258,7 +260,7 @@ fn drift_beta(m: usize) -> Vec { w.iter().map(|wi| wi - &mean).collect() } -fn probe(m: usize, point: &[Q]) -> Receipt { +pub(crate) fn probe(m: usize, point: &[Q]) -> Receipt { let n = m - 1; let cx = JetCtx::new(n); let ps: Vec = (0..n).map(|i| cx.coord(i, point[i].clone())).collect(); @@ -572,10 +574,19 @@ fn mat_inv(cx: &JetCtx, a: &[Vec], n: usize) -> Vec> { inv } -fn signature(gm: &[Vec]) -> (usize, usize) { - // Exact signature by symmetric (congruence) elimination β€” robust to - // vanishing leading minors, which Sylvester's criterion is not. - // Congruences A ↦ Eα΅€AE preserve signature (Sylvester's law of inertia). +pub(crate) fn signature(gm: &[Vec]) -> (usize, usize) { + // Panicking wrapper for the exact/oracle path, where a degenerate g' + // block is not expected. The robust modular path uses signature_opt and + // records an edge receipt instead of crashing. + signature_opt(gm).expect("degenerate metric block (rank deficiency)") +} + +/// Exact signature by symmetric (congruence) elimination β€” robust to vanishing +/// leading minors, which Sylvester's criterion is not. Congruences A ↦ Eα΅€AE +/// preserve signature (Sylvester's law of inertia). Returns None (rather than +/// panicking) when the block is rank-deficient / degenerate β€” a genuine +/// geometric edge (e.g. a null drift direction) that callers record explicitly. +pub(crate) fn signature_opt(gm: &[Vec]) -> Option<(usize, usize)> { let n = gm.len(); let mut a: Vec> = gm.to_vec(); let (mut pos, mut neg) = (0usize, 0usize); @@ -598,7 +609,7 @@ fn signature(gm: &[Vec]) -> (usize, usize) { a[t][k] += v; } } else { - panic!("degenerate metric block (rank deficiency)"); + return None; // rank-deficient block (e.g. null drift) β€” edge case } } if a[k][k] > Q::zero() { @@ -621,9 +632,10 @@ fn signature(gm: &[Vec]) -> (usize, usize) { a[k][i] = Q::zero(); } } - (pos, neg) + Some((pos, neg)) } +#[allow(dead_code)] fn det(a: &[Vec]) -> Q { let n = a.len(); let mut m = a.to_vec(); @@ -694,6 +706,7 @@ fn main() { let mut grid = 2u64; let mut ckpt = PathBuf::from("semisym_ckpt"); let mut max_points = 200usize; + let mut primes: Option> = None; // Some β‡’ modular fast path let mut i = 2; while i < args.len() { match args[i].as_str() { @@ -705,18 +718,49 @@ fn main() { "--grid" => { grid = args[i + 1].parse().unwrap(); i += 2; } "--checkpoint" => { ckpt = PathBuf::from(&args[i + 1]); i += 2; } "--max-points" => { max_points = args[i + 1].parse().unwrap(); i += 2; } + "--mod" => { + // bare `--mod` uses the default multi-prime set; `--mod p1,p2,..` + // overrides (each must be prime β€” guarded below). + let list = args.get(i + 1).and_then(|s| { + s.split(',').map(|x| x.parse::()).collect::, _>>().ok() + }); + match list { + Some(l) if !l.is_empty() => { primes = Some(l); i += 2; } + _ => { primes = Some(modp::DEFAULT_PRIMES.to_vec()); i += 1; } + } + } other => panic!("unknown arg {other}"), } } + // primality guard: a composite modulus makes the Fermat inverse β€” and every + // modular "proof" β€” silently unsound, so reject it up front (edge accounted + // for, not assumed away). + if let Some(ref ps) = primes { + for &p in ps { + assert!( + modp::is_prime(p), + "--mod modulus {p} is not prime; modular soundness requires a prime field" + ); + } + } + match cmd { "probe" => { let n = m - 1; let pt = point.unwrap_or_else(|| { (0..n).map(|_| Q::new(BigInt::one(), BigInt::from(m as i64))).collect() }); - let r = probe(m, &pt); - println!("{}", serde_json::to_string_pretty(&r).unwrap()); + match &primes { + Some(ps) => { + let (_verdict, json) = modp::probe_survey(m, &pt, ps); + println!("{json}"); + } + None => { + let r = probe(m, &pt); + println!("{}", serde_json::to_string_pretty(&r).unwrap()); + } + } } "survey" => { fs::create_dir_all(&ckpt).unwrap(); @@ -727,22 +771,56 @@ fn main() { let stride = pts.len() / max_points; pts = pts.into_iter().step_by(stride.max(1)).take(max_points).collect(); } - eprintln!("survey: {} lattice points, checkpoint {}", pts.len(), ckpt.display()); + let tag = match &primes { + Some(ps) => format!("modular (primes={ps:?})"), + None => "exact β„š".into(), + }; + eprintln!( + "survey [{tag}]: {} lattice points, checkpoint {}", + pts.len(), + ckpt.display() + ); let done: Vec<(usize, String)> = pts .par_iter() .enumerate() .map(|(idx, pt)| { let mut name = format!("m{m}_g{grid}_"); for x in pt { - let _ = write!(name, "{}_", x.numer()); + // encode numer AND denom: reduced numerators alone + // collide (1/16,1/8,1/4,1/2 all β†’ "1"), silently folding + // distinct lattice points onto one checkpoint file + let _ = write!(name, "{}-{}_", x.numer(), x.denom()); } let file = ckpt.join(format!("{name}.json")); if file.exists() { return (idx, "skip".to_string()); } - let r = probe(m, pt); - let v = r.verdict.clone(); - fs::write(&file, serde_json::to_string_pretty(&r).unwrap()).unwrap(); + // catch_unwind backstop: an UNFORESEEN panic on one point + // must never abort the whole survey (which would discard all + // in-flight rayon work). Assume it happens; contain it. + let compute = std::panic::AssertUnwindSafe(|| match &primes { + Some(ps) => modp::probe_survey(m, pt, ps), + None => { + let r = probe(m, pt); + (r.verdict.clone(), serde_json::to_string_pretty(&r).unwrap()) + } + }); + let (v, json) = match std::panic::catch_unwind(compute) { + Ok(x) => x, + Err(_) => { + let pt_s: Vec = pt.iter().map(|x| x.to_string()).collect(); + let e = serde_json::json!({ + "schema": "semisym_receipt_edge_v1", + "m": m, + "point": pt_s, + "verdict": "PANIC_CAUGHT", + "reason": "unforeseen_panic", + "note": "contained by survey backstop; needs manual review", + }); + ("PANIC_CAUGHT".to_string(), serde_json::to_string_pretty(&e).unwrap()) + } + }; + fs::write(&file, json).unwrap(); (idx, v) }) .collect(); @@ -752,6 +830,84 @@ fn main() { } println!("{counts:?}"); } + // Cross-check the modular path against the exact-β„š receipt oracle: + // for every exact receipt in --checkpoint, rerun modp at that point and + // assert method agreement (different arithmetic, so this is a genuine + // independent check, not the same-algorithm trap that hid the old + // signature bug). Prints any mismatch; exits nonzero if any found. + "validate-modp" => { + let p = primes + .as_ref() + .and_then(|v| v.first().copied()) + .unwrap_or(modp::DEFAULT_PRIMES[0]); + let entries: Vec = fs::read_dir(&ckpt) + .unwrap_or_else(|_| panic!("no checkpoint dir {}", ckpt.display())) + .filter_map(|e| e.ok().map(|e| e.path())) + .filter(|p| p.extension().is_some_and(|x| x == "json")) + .collect(); + let mut checked = 0usize; + let mut mismatches = 0usize; + let mut poles = 0usize; + for path in &entries { + let txt = fs::read_to_string(path).unwrap(); + let v: serde_json::Value = serde_json::from_str(&txt).unwrap(); + // only compare against EXACT receipts + if v.get("schema").and_then(|s| s.as_str()) != Some("semisym_receipt_v1") { + continue; + } + let mm = v["m"].as_u64().unwrap() as usize; + let pt: Vec = v["point"].as_array().unwrap().iter() + .map(|s| parse_q(s.as_str().unwrap())).collect(); + let exact_verdict = v["verdict"].as_str().unwrap(); + let exact_nabla = v["nabla_r_nonzero"].as_u64().unwrap() as usize; + let exact_sig = ( + v["gprime_signature"][0].as_u64().unwrap() as usize, + v["gprime_signature"][1].as_u64().unwrap() as usize, + ); + let r = match modp::probe_modp(mm, &pt, p) { + Ok(r) => r, + Err(reason) => { + // exact receipt exists β‡’ point is interior/non-degenerate, + // so a geometric edge here is a genuine disagreement + mismatches += 1; + eprintln!("MISMATCH {}: modp edge '{reason}' but exact receipt present", path.display()); + continue; + } + }; + checked += 1; + if r.modular_pole { + // expected edge for a single prime, not a correctness failure + poles += 1; + continue; + } + let mut problems = Vec::new(); + if r.gprime_signature != exact_sig { + problems.push(format!( + "signature exact {exact_sig:?} vs modp {:?}", + r.gprime_signature + )); + } + // modp certified-nonzero must be a subset of exact nonzero + if r.nabla_r_certified_nonzero > exact_nabla { + problems.push(format!( + "βˆ‡R certified {} > exact nonzero {}", + r.nabla_r_certified_nonzero, exact_nabla + )); + } + // if exact says PROPER, modp (sound) must not disagree + if exact_verdict == "PROPER" && r.sound && r.verdict != "PROPER" { + problems.push(format!("exact PROPER vs modp {}", r.verdict)); + } + if !problems.is_empty() { + mismatches += 1; + eprintln!("MISMATCH {}: {}", path.display(), problems.join("; ")); + } + } + println!("validate-modp: checked {checked} exact receipts, {mismatches} mismatches, {poles} single-prime poles (p={p})"); + if mismatches > 0 { + std::process::exit(1); + } + } other => panic!("unknown command {other}"), } } diff --git a/rust/semisym/src/modp.rs b/rust/semisym/src/modp.rs new file mode 100644 index 00000000..97680654 --- /dev/null +++ b/rust/semisym/src/modp.rs @@ -0,0 +1,832 @@ +//! modp β€” modular fast path for the Β§0 covariant semi-symmetry discriminator. +//! +//! Same order-3 jet AD pipeline as the exact `main.rs`, but the scalar is a +//! finite field 𝔽_p (u64 mod a large prime) instead of BigRational. This kills +//! the denominator blowup that makes off-centre lattice points take 30+ minutes +//! in exact β„š: 𝔽_p elements are single machine words, arithmetic is O(1), and +//! memory is flat. Typical speedup is 100–1000Γ— per point. +//! +//! SOUNDNESS (the OTOM gate is preserved for the verdicts we actually get): +//! * A curvature/βˆ‡R component β‰’ 0 mod p is a *proof* it is β‰  0 over β„š +//! (p ∀ numerator). So "βˆ‡R β‰  0" (not locally symmetric) is a hard proof. +//! * PROPER (not pseudosymmetric) is certified WITHOUT division: pick a +//! reference pair (rrβ‚€,qqβ‚€) with qqβ‚€ β‰  0; if any pair has +//! rrΒ·qqβ‚€ βˆ’ rrβ‚€Β·qq β‰’ 0 mod p, then RΒ·R β‰  LΒ·Q(g,R) for the only possible +//! L = rrβ‚€/qqβ‚€ β€” a hard proof over β„š. If all Q-side entries vanish but some +//! RΒ·R entry is β‰’ 0, that is also PROPER (no finite L works). +//! * The ONLY verdicts needing an exact recheck are the all-zero ones +//! (candidate locally-symmetric / semisymmetric / pseudosymmetric): there a +//! modular zero could be a false zero (p | numerator). We have never hit one +//! in the exact survey, and they escalate to the β„š path automatically. +//! +//! Signature and g'(Ξ²,Ξ²) stay in exact β„š (they need order/sign, meaningless in +//! 𝔽_p) β€” but they are an nΓ—n value-only computation, not the bottleneck. +//! +//! A modular pivot may vanish (p | a nonzero-over-β„š pivot) β†’ probe returns a +//! Pole; the caller retries with another prime or falls back to exact. + +use num_bigint::BigInt; +use num_rational::BigRational; +use num_traits::Zero; + +use crate::{drift_beta, i2, i3_table, signature_opt, Q}; + +// ── 𝔽_p, p = 2Β³ΒΉ βˆ’ 1 (Mersenne prime; products fit in u64) ──────────── + +pub const DEFAULT_PRIME: u64 = 2_147_483_647; // 2^31 - 1 + +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Fp { + v: u64, + p: u64, +} + +impl Fp { + #[inline] + fn new(v: u64, p: u64) -> Fp { + Fp { v: v % p, p } + } + #[inline] + fn zero(p: u64) -> Fp { + Fp { v: 0, p } + } + #[inline] + fn one(p: u64) -> Fp { + Fp { v: 1, p } + } + #[inline] + fn is_zero(self) -> bool { + self.v == 0 + } + #[inline] + fn add(self, o: Fp) -> Fp { + let s = self.v + o.v; + Fp { v: if s >= self.p { s - self.p } else { s }, p: self.p } + } + #[inline] + fn sub(self, o: Fp) -> Fp { + Fp { + v: if self.v >= o.v { self.v - o.v } else { self.v + self.p - o.v }, + p: self.p, + } + } + #[inline] + fn mul(self, o: Fp) -> Fp { + // p < 2^31 β‡’ product < 2^62 < 2^64, no overflow + Fp { v: (self.v * o.v) % self.p, p: self.p } + } + #[inline] + fn neg(self) -> Fp { + Fp { v: if self.v == 0 { 0 } else { self.p - self.v }, p: self.p } + } + fn pow(self, mut e: u64) -> Fp { + let mut r = Fp::one(self.p); + let mut b = self; + while e > 0 { + if e & 1 == 1 { + r = r.mul(b); + } + b = b.mul(b); + e >>= 1; + } + r + } + /// Fermat inverse; returns None at a modular pole (self ≑ 0). + fn inv(self) -> Option { + if self.is_zero() { + None + } else { + Some(self.pow(self.p - 2)) + } + } + fn from_i64(x: i64, p: u64) -> Fp { + let m = x.rem_euclid(p as i64); + Fp { v: m as u64, p } + } + /// Reduce a BigRational mod p; None if p | denominator. + fn from_q(qr: &BigRational, p: u64) -> Option { + let n = bigint_mod(qr.numer(), p); + let d = bigint_mod(qr.denom(), p); + Some(Fp::new(n, p).mul(Fp::new(d, p).inv()?)) + } +} + +fn bigint_mod(b: &BigInt, p: u64) -> u64 { + let r = (b % BigInt::from(p)).to_string(); + // r is a decimal in (-p, p); parse with sign + let x: i64 = r.parse().unwrap_or(0); + x.rem_euclid(p as i64) as u64 +} + +// ── order-3 jets over 𝔽_p ───────────────────────────────────────────── + +#[derive(Clone)] +struct JetF { + v: Fp, + d1: Vec, + d2: Vec, + d3: Vec, +} + +struct CtxF { + n: usize, + n2: usize, + n3: usize, + p: u64, + t3: Vec<[usize; 3]>, + t3idx: Vec>>, +} + +impl CtxF { + fn new(n: usize, p: u64) -> Self { + let (t3, t3idx) = i3_table(n); + CtxF { n, n2: n * (n + 1) / 2, n3: t3.len(), p, t3, t3idx } + } + fn zero(&self) -> JetF { + JetF { + v: Fp::zero(self.p), + d1: vec![Fp::zero(self.p); self.n], + d2: vec![Fp::zero(self.p); self.n2], + d3: vec![Fp::zero(self.p); self.n3], + } + } + fn constant(&self, c: Fp) -> JetF { + let mut j = self.zero(); + j.v = c; + j + } + fn coord(&self, i: usize, val: Fp) -> JetF { + let mut j = self.zero(); + j.v = val; + j.d1[i] = Fp::one(self.p); + j + } + fn add(&self, a: &JetF, b: &JetF) -> JetF { + let mut c = a.clone(); + c.v = c.v.add(b.v); + for i in 0..self.n { + c.d1[i] = c.d1[i].add(b.d1[i]); + } + for i in 0..self.n2 { + c.d2[i] = c.d2[i].add(b.d2[i]); + } + for i in 0..self.n3 { + c.d3[i] = c.d3[i].add(b.d3[i]); + } + c + } + fn sub(&self, a: &JetF, b: &JetF) -> JetF { + let mut c = a.clone(); + c.v = c.v.sub(b.v); + for i in 0..self.n { + c.d1[i] = c.d1[i].sub(b.d1[i]); + } + for i in 0..self.n2 { + c.d2[i] = c.d2[i].sub(b.d2[i]); + } + for i in 0..self.n3 { + c.d3[i] = c.d3[i].sub(b.d3[i]); + } + c + } + fn neg(&self, a: &JetF) -> JetF { + self.sub(&self.zero(), a) + } + fn mul(&self, a: &JetF, b: &JetF) -> JetF { + let n = self.n; + let mut c = self.zero(); + c.v = a.v.mul(b.v); + for i in 0..n { + c.d1[i] = a.d1[i].mul(b.v).add(a.v.mul(b.d1[i])); + } + for i in 0..n { + for j in i..n { + let ij = i2(n, i, j); + c.d2[ij] = a.d2[ij] + .mul(b.v) + .add(a.d1[i].mul(b.d1[j])) + .add(a.d1[j].mul(b.d1[i])) + .add(a.v.mul(b.d2[ij])); + } + } + for (id, t) in self.t3.iter().enumerate() { + let [i, j, k] = *t; + c.d3[id] = a.d3[id] + .mul(b.v) + .add(a.d2[i2(n, i, j)].mul(b.d1[k])) + .add(a.d2[i2(n, i, k)].mul(b.d1[j])) + .add(a.d2[i2(n, j, k)].mul(b.d1[i])) + .add(a.d1[i].mul(b.d2[i2(n, j, k)])) + .add(a.d1[j].mul(b.d2[i2(n, i, k)])) + .add(a.d1[k].mul(b.d2[i2(n, i, j)])) + .add(a.v.mul(b.d3[id])); + } + c + } + /// reciprocal 1/b; None at a modular pole (b.v ≑ 0). + fn inv(&self, b: &JetF) -> Option { + let n = self.n; + let bv_inv = b.v.inv()?; + let mut r = self.zero(); + r.v = bv_inv; + for i in 0..n { + r.d1[i] = b.d1[i].mul(r.v).neg().mul(bv_inv); + } + for i in 0..n { + for j in i..n { + let ij = i2(n, i, j); + let s = b.d2[ij] + .mul(r.v) + .add(b.d1[i].mul(r.d1[j])) + .add(b.d1[j].mul(r.d1[i])); + r.d2[ij] = s.neg().mul(bv_inv); + } + } + for (id, t) in self.t3.iter().enumerate() { + let [i, j, k] = *t; + let s = b.d3[id] + .mul(r.v) + .add(b.d2[i2(n, i, j)].mul(r.d1[k])) + .add(b.d2[i2(n, i, k)].mul(r.d1[j])) + .add(b.d2[i2(n, j, k)].mul(r.d1[i])) + .add(b.d1[i].mul(r.d2[i2(n, j, k)])) + .add(b.d1[j].mul(r.d2[i2(n, i, k)])) + .add(b.d1[k].mul(r.d2[i2(n, i, j)])); + r.d3[id] = s.neg().mul(bv_inv); + } + Some(r) + } + fn div(&self, a: &JetF, b: &JetF) -> Option { + Some(self.mul(a, &self.inv(b)?)) + } + fn shift(&self, a: &JetF, dir: usize) -> JetF { + let n = self.n; + let mut s = self.zero(); + s.v = a.d1[dir]; + for i in 0..n { + s.d1[i] = a.d2[i2(n, dir, i)]; + } + for i in 0..n { + for j in i..n { + s.d2[i2(n, i, j)] = a.d3[self.t3idx[dir][i][j]]; + } + } + s + } +} + +// ── modular receipt ─────────────────────────────────────────────────── + +#[derive(serde::Serialize)] +pub struct ReceiptModp { + pub schema: &'static str, + pub m: usize, + pub prime: u64, + pub geometrization: String, + pub point: Vec, + pub baseline_constant_curvature_quarter: bool, + pub gprime_signature: (usize, usize), + pub gprime_beta_norm: String, + /// components of βˆ‡R that are β‰’ 0 mod p β€” each a PROOF of β‰  0 over β„š + pub nabla_r_certified_nonzero: usize, + pub nabla_r_total: usize, + pub rr_nonzero_modp: usize, + /// index tuple + nonzero residue certifying PROPER, if found + pub proper_certificate: Option<(Vec, String)>, + pub verdict: String, + /// true β‡’ the verdict is a hard proof; false β‡’ needs exact β„š recheck + pub sound: bool, + pub modular_pole: bool, +} + +/// Exact β„š value-only metric block at the point β†’ g', signature input & Ξ²-norm. +/// Returns a named edge instead of a bare None so the caller can record WHY a +/// point has no valid drift-flip geometry (these edges are assumed to occur on +/// every large run, not treated as unreachable): +/// "outside_simplex" β€” Ξ£p_i β‰₯ 1 (p_m ≀ 0), point not interior +/// "null_drift" β€” g(Ξ²,Ξ²) = 0, the reflection g βˆ’ 2Ξ²β™­βŠ—Ξ²β™­/g(Ξ²,Ξ²) is +/// undefined (drift direction is g-null); itself a +/// meaningful scar-map feature, not a failure. +fn exact_gprime(m: usize, point: &[Q]) -> Result<(Vec>, Q), &'static str> { + use num_traits::One; + let n = m - 1; + if point.len() < n { + return Err("outside_simplex"); + } + let mut pm = Q::one(); + for pv in point.iter().take(n) { + if pv <= &Q::zero() { + return Err("outside_simplex"); + } + pm -= pv; + } + if pm <= Q::zero() { + return Err("outside_simplex"); + } + let inv_pm = Q::one() / ± + let mut g = vec![vec![Q::zero(); n]; n]; + for i in 0..n { + for j in 0..n { + g[i][j] = if i == j { + Q::one() / &point[i] + &inv_pm + } else { + inv_pm.clone() + }; + } + } + let beta_full = drift_beta(m); + let beta: Vec = beta_full[..n].to_vec(); + let mut beta_flat = vec![Q::zero(); n]; + for i in 0..n { + for j in 0..n { + beta_flat[i] += &g[i][j] * &beta[j]; + } + } + let mut bnorm = Q::zero(); + for i in 0..n { + bnorm += &beta[i] * &beta_flat[i]; + } + if bnorm.is_zero() { + return Err("null_drift"); // g-null drift: reflection undefined (edge) + } + let mut gp = vec![vec![Q::zero(); n]; n]; + for i in 0..n { + for j in 0..n { + gp[i][j] = &g[i][j] - &(Q::new(BigInt::from(2), BigInt::from(1)) * &beta_flat[i] * &beta_flat[j] / &bnorm); + } + } + let mut bb = Q::zero(); + for i in 0..n { + for j in 0..n { + bb += &gp[i][j] * &beta[i] * &beta[j]; + } + } + Ok((gp, bb)) +} + +fn curvature_f( + cx: &CtxF, + g: &[Vec], + n: usize, +) -> Option<(Vec>>>, Vec>>>, Vec>>)> { + let ginv = mat_inv_f(cx, g, n)?; + let half = cx.constant(Fp::new(1, cx.p).mul(Fp::new(2, cx.p).inv().unwrap())); + let mut gamma = vec![vec![vec![cx.zero(); n]; n]; n]; + for l in 0..n { + for i in 0..n { + for j in i..n { + let mut s = cx.zero(); + for k in 0..n { + let t = cx.sub( + &cx.add(&cx.shift(&g[k][j], i), &cx.shift(&g[k][i], j)), + &cx.shift(&g[i][j], k), + ); + s = cx.add(&s, &cx.mul(&ginv[l][k], &t)); + } + let v = cx.mul(&half, &s); + gamma[l][i][j] = v.clone(); + gamma[l][j][i] = v; + } + } + } + let mut rup = vec![vec![vec![vec![cx.zero(); n]; n]; n]; n]; + for rho in 0..n { + for sg in 0..n { + for mu in 0..n { + for nu in (mu + 1)..n { + let mut t = cx.sub( + &cx.shift(&gamma[rho][nu][sg], mu), + &cx.shift(&gamma[rho][mu][sg], nu), + ); + for lam in 0..n { + t = cx.add(&t, &cx.mul(&gamma[rho][mu][lam], &gamma[lam][nu][sg])); + t = cx.sub(&t, &cx.mul(&gamma[rho][nu][lam], &gamma[lam][mu][sg])); + } + rup[rho][sg][mu][nu] = t.clone(); + rup[rho][sg][nu][mu] = cx.neg(&t); + } + } + } + } + let mut rdn = vec![vec![vec![vec![cx.zero(); n]; n]; n]; n]; + for rho in 0..n { + for sg in 0..n { + for mu in 0..n { + for nu in (mu + 1)..n { + let mut s = cx.zero(); + for lam in 0..n { + s = cx.add(&s, &cx.mul(&g[rho][lam], &rup[lam][sg][mu][nu])); + } + rdn[rho][sg][mu][nu] = s.clone(); + rdn[rho][sg][nu][mu] = cx.neg(&s); + } + } + } + } + Some((rup, rdn, gamma)) +} + +fn mat_inv_f(cx: &CtxF, a: &[Vec], n: usize) -> Option>> { + let mut m: Vec> = a.to_vec(); + let mut inv: Vec> = (0..n) + .map(|i| { + (0..n) + .map(|j| if i == j { cx.constant(Fp::one(cx.p)) } else { cx.zero() }) + .collect() + }) + .collect(); + for col in 0..n { + let piv = (col..n).find(|&r| !m[r][col].v.is_zero())?; // None β‡’ modular pole + m.swap(col, piv); + inv.swap(col, piv); + let pinv = cx.inv(&m[col][col])?; + for j in 0..n { + m[col][j] = cx.mul(&m[col][j], &pinv); + inv[col][j] = cx.mul(&inv[col][j], &pinv); + } + for r in 0..n { + let entry_zero = m[r][col].v.is_zero() + && m[r][col].d1.iter().all(|x| x.is_zero()) + && m[r][col].d2.iter().all(|x| x.is_zero()) + && m[r][col].d3.iter().all(|x| x.is_zero()); + if r != col && !entry_zero { + let f = m[r][col].clone(); + for j in 0..n { + let t = cx.mul(&f, &m[col][j]); + m[r][j] = cx.sub(&m[r][j], &t); + let t = cx.mul(&f, &inv[col][j]); + inv[r][j] = cx.sub(&inv[r][j], &t); + } + } + } + } + Some(inv) +} + +fn check_constant_curvature_f(cx: &CtxF, g: &[Vec], n: usize) -> bool { + let (_, rdn, _) = match curvature_f(cx, g, n) { + Some(x) => x, + None => return false, + }; + let quarter = Fp::new(1, cx.p).mul(Fp::new(4, cx.p).inv().unwrap()); + for i in 0..n { + for j in 0..n { + for k in 0..n { + for l in 0..n { + let expect = quarter.mul(g[i][k].v.mul(g[j][l].v).sub(g[i][l].v.mul(g[j][k].v))); + if rdn[i][j][k][l].v != expect { + return false; + } + } + } + } + } + true +} + +/// Single-prime modular probe. +/// Err(reason) β€” a prime-independent geometric edge (outside_simplex +/// / null_drift / degenerate_signature_block); the +/// caller records it once, does not retry other primes. +/// Ok(r), r.modular_pole β€” this prime divides a nonzero-over-β„š quantity; the +/// caller retries another prime. +/// Ok(r), r.sound β€” PROPER is a hard proof (see module header). +/// Ok(r), !r.sound β€” a candidate all-zero verdict; caller escalates β„š. +pub fn probe_modp(m: usize, point: &[Q], p: u64) -> Result { + let n = m - 1; + + // exact β„š side: signature + Ξ²-norm (needs sign/order). Named edges bubble up. + let (gm_q, bb) = exact_gprime(m, point)?; + let sig = signature_opt(&gm_q).ok_or("degenerate_signature_block")?; + + let cx = CtxF::new(n, p); + let pole_receipt = |base_ok: bool| ReceiptModp { + schema: "semisym_receipt_modp_v1", + m, + prime: p, + geometrization: "drift_flip".into(), + point: point.iter().map(|x| x.to_string()).collect(), + baseline_constant_curvature_quarter: base_ok, + gprime_signature: sig, + gprime_beta_norm: bb.to_string(), + nabla_r_certified_nonzero: 0, + nabla_r_total: n.pow(5), + rr_nonzero_modp: 0, + proper_certificate: None, + verdict: "MODULAR_POLE".into(), + sound: false, + modular_pole: true, + }; + + // build 𝔽_p coord jets (p | a coord denominator β‡’ modular pole, retry prime) + let ps: Vec = match (0..n) + .map(|i| Fp::from_q(&point[i], p).map(|c| cx.coord(i, c))) + .collect::>>() + { + Some(v) => v, + None => return Ok(pole_receipt(false)), + }; + let mut pm = cx.constant(Fp::one(p)); + for pj in &ps { + pm = cx.sub(&pm, pj); + } + let inv_pm = match cx.inv(&pm) { + Some(x) => x, + None => return Ok(pole_receipt(false)), + }; + let mut g = vec![vec![cx.zero(); n]; n]; + for i in 0..n { + let inv_pi = match cx.inv(&ps[i]) { + Some(x) => x, + None => return Ok(pole_receipt(false)), + }; + for j in 0..n { + g[i][j] = if i == j { cx.add(&inv_pi, &inv_pm) } else { inv_pm.clone() }; + } + } + let base_ok = check_constant_curvature_f(&cx, &g, n); + + // drift-flip g' + let beta_full = drift_beta(m); + let bjets: Vec = match (0..n) + .map(|i| Fp::from_q(&beta_full[i], p).map(|b| cx.constant(b))) + .collect::>>() + { + Some(v) => v, + None => return Ok(pole_receipt(base_ok)), + }; + let mut beta_flat = vec![cx.zero(); n]; + for i in 0..n { + for j in 0..n { + beta_flat[i] = cx.add(&beta_flat[i], &cx.mul(&g[i][j], &bjets[j])); + } + } + let mut bnorm = cx.zero(); + for i in 0..n { + bnorm = cx.add(&bnorm, &cx.mul(&bjets[i], &beta_flat[i])); + } + let two = cx.constant(Fp::new(2, p)); + let mut gp = vec![vec![cx.zero(); n]; n]; + for i in 0..n { + for j in 0..n { + let corr = match cx.div(&cx.mul(&two, &cx.mul(&beta_flat[i], &beta_flat[j])), &bnorm) { + Some(x) => x, + None => return Ok(pole_receipt(base_ok)), + }; + gp[i][j] = cx.sub(&g[i][j], &corr); + } + } + + let (rup, rdn, gamma) = match curvature_f(&cx, &gp, n) { + Some(x) => x, + None => return Ok(pole_receipt(base_ok)), + }; + + // βˆ‡R β€” count certified-nonzero components + let mut nabla_nz = 0usize; + for a in 0..n { + for i in 0..n { + for jj in 0..n { + for k in 0..n { + for l in 0..n { + let mut v = rdn[i][jj][k][l].d1[a]; + for mm in 0..n { + v = v.sub(gamma[mm][a][i].v.mul(rdn[mm][jj][k][l].v)); + v = v.sub(gamma[mm][a][jj].v.mul(rdn[i][mm][k][l].v)); + v = v.sub(gamma[mm][a][k].v.mul(rdn[i][jj][mm][l].v)); + v = v.sub(gamma[mm][a][l].v.mul(rdn[i][jj][k][mm].v)); + } + if !v.is_zero() { + nabla_nz += 1; + } + } + } + } + } + } + + // RΒ·R and Q(g,R): collect pairs, certify PROPER by cross-multiplication + let mut rr_nz = 0usize; + let mut pairs: Vec<(Fp, Fp, Vec)> = Vec::new(); + for a in 0..n { + for b in (a + 1)..n { + for i in 0..n { + for jj in 0..n { + for k in 0..n { + for l in 0..n { + let mut rr = Fp::zero(p); + let mut qq = Fp::zero(p); + for mm in 0..n { + rr = rr.sub(rup[mm][i][a][b].v.mul(rdn[mm][jj][k][l].v)); + rr = rr.sub(rup[mm][jj][a][b].v.mul(rdn[i][mm][k][l].v)); + rr = rr.sub(rup[mm][k][a][b].v.mul(rdn[i][jj][mm][l].v)); + rr = rr.sub(rup[mm][l][a][b].v.mul(rdn[i][jj][k][mm].v)); + } + let gv = |r: usize, c: usize| -> Fp { gp[r][c].v }; + let wedge = |mm: usize, idx: usize| -> Fp { + let mut w = Fp::zero(p); + if mm == a { + w = w.add(gv(b, idx)); + } + if mm == b { + w = w.sub(gv(a, idx)); + } + w + }; + for mm in 0..n { + qq = qq.sub(wedge(mm, i).mul(rdn[mm][jj][k][l].v)); + qq = qq.sub(wedge(mm, jj).mul(rdn[i][mm][k][l].v)); + qq = qq.sub(wedge(mm, k).mul(rdn[i][jj][mm][l].v)); + qq = qq.sub(wedge(mm, l).mul(rdn[i][jj][k][mm].v)); + } + if !rr.is_zero() { + rr_nz += 1; + } + if !(rr.is_zero() && qq.is_zero()) { + pairs.push((rr, qq, vec![a, b, i, jj, k, l])); + } + } + } + } + } + } + } + + // PROPER certificate: reference pair with qq β‰  0, then any pair breaking + // rrΒ·qqβ‚€ βˆ’ rrβ‚€Β·qq ≑ 0. If all qq ≑ 0 but some rr β‰  0 β‡’ also PROPER. + let mut proper_cert: Option<(Vec, String)> = None; + if let Some((rr0, qq0, _)) = pairs.iter().find(|(_, q, _)| !q.is_zero()).cloned() { + for (rr, qq, idx) in &pairs { + let cross = rr.mul(qq0).sub(rr0.mul(*qq)); + if !cross.is_zero() { + proper_cert = Some((idx.clone(), cross.v.to_string())); + break; + } + } + } else if let Some((_, _, idx)) = pairs.iter().find(|(r, _, _)| !r.is_zero()) { + // no Q-side entry nonzero but RΒ·R nonzero β‡’ no finite L works + proper_cert = Some((idx.clone(), "rr_nonzero_q_all_zero".into())); + } + + let (verdict, sound) = if nabla_nz == 0 { + ("CANDIDATE_LOCALLY_SYMMETRIC(needs exact)".to_string(), false) + } else if rr_nz == 0 { + ("CANDIDATE_SEMISYMMETRIC(needs exact)".to_string(), false) + } else if proper_cert.is_some() { + ("PROPER".to_string(), true) // hard proof: βˆ‡Rβ‰ 0 (certified) & not pseudosym + } else { + ("CANDIDATE_PSEUDOSYMMETRIC(needs exact)".to_string(), false) + }; + + Ok(ReceiptModp { + schema: "semisym_receipt_modp_v1", + m, + prime: p, + geometrization: "drift_flip".into(), + point: point.iter().map(|x| x.to_string()).collect(), + baseline_constant_curvature_quarter: base_ok, + gprime_signature: sig, + gprime_beta_norm: bb.to_string(), + nabla_r_certified_nonzero: nabla_nz, + nabla_r_total: n.pow(5), + rr_nonzero_modp: rr_nz, + proper_certificate: proper_cert, + verdict, + sound, + modular_pole: false, + }) +} + +// ── robust orchestration: assume edges happen on every run ──────────── +// +// GUARANTEE: probe_survey resolves EVERY point to exactly one of +// (a) a proven-PROPER modular receipt (fast, hard proof over β„š), +// (b) a sound exact-β„š receipt (auto-escalation), or +// (c) a recorded edge receipt (geometric edge, no crash). +// It never panics on a known edge and never emits an unsound verdict. + +/// A geometric / arithmetic edge recorded in-band (never a panic). +#[derive(serde::Serialize)] +pub struct EdgeReceipt { + pub schema: &'static str, + pub m: usize, + pub point: Vec, + pub verdict: String, + pub reason: String, + pub note: String, +} + +/// Default multi-prime set: the three largest primes below 2Β³ΒΉ. Multi-prime +/// makes a modular pole (one prime dividing a nonzero-over-β„š quantity) a +/// non-event β€” another prime almost surely resolves it β€” and guards a +/// single-prime false-zero from hiding a PROPER certificate. +pub const DEFAULT_PRIMES: [u64; 3] = [2_147_483_647, 2_147_483_629, 2_147_483_587]; + +fn mod_mul(a: u64, b: u64, m: u64) -> u64 { + ((a as u128 * b as u128) % m as u128) as u64 +} +fn mod_pow(mut a: u64, mut e: u64, m: u64) -> u64 { + let mut r = 1u64 % m; + while e > 0 { + if e & 1 == 1 { + r = mod_mul(r, a, m); + } + a = mod_mul(a, a, m); + e >>= 1; + } + r +} + +/// Deterministic Miller–Rabin; bases {2,3,5,7} are a proven witness set for all +/// n < 3.2Β·10⁹ βŠƒ our p < 2Β³ΒΉ. Guards against a composite modulus, which would +/// make the Fermat inverse (and every "proof") silently unsound. +pub fn is_prime(n: u64) -> bool { + if n < 2 { + return false; + } + for &sp in &[2u64, 3, 5, 7, 11, 13, 17, 19, 23] { + if n % sp == 0 { + return n == sp; + } + } + let mut d = n - 1; + let mut r = 0u32; + while d % 2 == 0 { + d /= 2; + r += 1; + } + 'witness: for &a in &[2u64, 3, 5, 7] { + let mut x = mod_pow(a % n, d, n); + if x == 1 || x == n - 1 { + continue; + } + for _ in 0..r.saturating_sub(1) { + x = mod_mul(x, x, n); + if x == n - 1 { + continue 'witness; + } + } + return false; + } + true +} + +fn edge(m: usize, point: &[Q], reason: &str, note: &str) -> (String, String) { + let e = EdgeReceipt { + schema: "semisym_receipt_edge_v1", + m, + point: point.iter().map(|x| x.to_string()).collect(), + verdict: format!("EDGE_{}", reason.to_uppercase()), + reason: reason.into(), + note: note.into(), + }; + let j = serde_json::to_string_pretty(&e).unwrap(); + (e.verdict, j) +} + +/// Robust per-point resolution used by surveys. Returns (verdict_for_tally, +/// pretty_json). See the module GUARANTEE above. +pub fn probe_survey(m: usize, point: &[Q], primes: &[u64]) -> (String, String) { + let mut saw_candidate = false; + let mut n_poles = 0usize; + for &p in primes { + match probe_modp(m, point, p) { + // prime-independent geometric edge β€” record once, no retry + Err(reason) => { + return edge(m, point, reason, "no valid drift-flip geometry at this point"); + } + Ok(r) if r.modular_pole => { + n_poles += 1; + continue; // this prime divides something; try the next + } + Ok(r) if r.sound => { + // PROPER proven over β„š by this prime β€” fast path, done + let j = serde_json::to_string_pretty(&r).unwrap(); + return (r.verdict, j); + } + Ok(_) => { + // candidate all-zero on this prime; another prime may still + // expose a PROPER certificate hidden by a false-zero + saw_candidate = true; + } + } + } + // No prime proved PROPER β‡’ escalate to exact β„š for a sound verdict. Covers + // genuine candidate verdicts, single-prime false-zeros, and all-poled. + // (Reached only for interior, non-null-drift points, so crate::probe's + // interior/non-degenerate preconditions hold; the survey also wraps this + // in catch_unwind as a final backstop.) + let ex = crate::probe(m, point); + let why = if saw_candidate { + "no_modular_PROPER_proof(candidate all-zero)" + } else { + "all_primes_poled" + }; + let mut v: serde_json::Value = serde_json::to_value(&ex).unwrap(); + v["_escalated_from_modular"] = serde_json::json!({ + "reason": why, + "primes_tried": primes, + "poles": n_poles, + }); + (ex.verdict.clone(), serde_json::to_string_pretty(&v).unwrap()) +} diff --git a/rust/src/bin/ingest_semisym_scar.rs b/rust/src/bin/ingest_semisym_scar.rs new file mode 100644 index 00000000..e73c9f3d --- /dev/null +++ b/rust/src/bin/ingest_semisym_scar.rs @@ -0,0 +1,246 @@ +//! ingest_semisym_scar β€” map Β§0 covariant semi-symmetry receipts β†’ ENE rows. +//! +//! SKELETON (dry-run first). Reads the per-point receipt JSONs from +//! `semisym survey` (schemas `semisym_receipt_modp_v1`, `semisym_receipt_v1`, +//! `semisym_receipt_edge_v1`) and maps each lattice point to an +//! `ene.rrc_classifications` row for later data mining of the scar map. +//! +//! NO FLOATS (OTOM doctrine β€” the whole stack is integer / exact / Q16_16): +//! every quantity is stored EXACTLY. Rationals (Ξ²-norm, obstruction) are kept +//! as exact strings ("βˆ’628/9"); counts/signature are exact integers; the whole +//! bundle lives in a JSONB `metrics` column. Ordering, when needed, is exact +//! integer cross-multiplication (a/b < c/d ⇔ aΒ·d < cΒ·b), never IEEE float. +//! FUTURE revision (per "everything is logarithms", the prime-basis log): add a +//! prime-exponent vector Ξ½β‚š per rational β€” exact integer log-coordinates, +//! multiplicatively additive, using the precomputed prime LUT as the basis. +//! +//! SAFETY (repo dry-run-by-default rule): +//! * default β€” parse + map, PRINT a summary + WRITE SQL to a file. +//! NO database connection. +//! * --apply --dsn <..> β€” execute the SQL via `psql`. Requires an EXPLICIT +//! dsn; a bare --apply refuses (no baked endpoint). +//! RDS / rds_connect.py are never used; ENE is the +//! neon `research_stack`/`ene` schema. +//! +//! Ids are sha256(equation_id) so re-ingest is idempotent (ON CONFLICT UPDATE). +//! The legacy FLOAT columns (spectral_radius, score) are deliberately NOT +//! populated β€” they predate the float ban and should be migrated out. +//! +//! Usage: +//! ingest_semisym_scar [--out scar.sql] [--limit N] # dry-run +//! ingest_semisym_scar --apply --dsn "postgresql://…/research_stack" + +use std::collections::BTreeMap; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +use serde::Deserialize; +use serde_json::json; +use sha2::{Digest, Sha256}; + +/// Union of the three receipt schemas; every field optional so one struct +/// deserializes modp / exact-escalated / edge receipts alike. +#[derive(Deserialize, Default)] +struct Receipt { + schema: Option, + m: Option, + prime: Option, + geometrization: Option, + point: Option>, + gprime_signature: Option<(u32, u32)>, // JSON [pos, neg] + gprime_beta_norm: Option, // exact rational as string + verdict: Option, + obstruction_along_drift: Option, // exact rational (v1 only) + nabla_r_certified_nonzero: Option, + nabla_r_nonzero: Option, + nabla_r_total: Option, + rr_nonzero_modp: Option, + rr_nonzero: Option, + baseline_constant_curvature_quarter: Option, + reason: Option, // edge receipts + sound: Option, + modular_pole: Option, +} + +/// One `ene.rrc_classifications` row. Typed columns are exact-integer/text only; +/// all numeric detail (exact) is carried in `metrics` (JSONB). +struct RrcRow { + id: String, + equation_id: String, + shape: String, // verdict + pist_label: String, // geometrization / edge reason + weak_axes: Option, // negative signature count (exact integer) + metrics: String, // exact JSONB bundle (no floats) +} + +/// Deterministic UUID-shaped id from a key (sha256 β†’ 8-4-4-4-12 hex). +fn stable_uuid(key: &str) -> String { + let d = Sha256::digest(key.as_bytes()); + let h: String = d[..16].iter().map(|b| format!("{b:02x}")).collect(); + format!("{}-{}-{}-{}-{}", &h[0..8], &h[8..12], &h[12..16], &h[16..20], &h[20..32]) +} + +fn sql_str(s: &str) -> String { + format!("'{}'", s.replace('\'', "''")) +} +fn sql_u32(x: Option) -> String { + x.map(|v| v.to_string()).unwrap_or_else(|| "NULL".into()) +} + +fn map_receipt(r: &Receipt) -> RrcRow { + let point = r.point.clone().unwrap_or_default().join("_"); + let equation_id = format!("semisym:m{}:{}", r.m.unwrap_or(0), point); + let id = stable_uuid(&equation_id); + let shape = r.verdict.clone().unwrap_or_else(|| "UNKNOWN".into()); + let pist_label = r + .geometrization + .clone() + .or_else(|| r.reason.clone()) + .unwrap_or_default(); + let weak_axes = r.gprime_signature.map(|(_, neg)| neg); + + // exact JSONB bundle β€” rationals as strings, counts/signature as integers, + // NO floats anywhere. This is the "combine both" richer field. + let signature = r.gprime_signature.map(|(p, n)| json!([p, n])); + let metrics = json!({ + "schema": r.schema, + "verdict": r.verdict, + "geometrization": r.geometrization, + "prime": r.prime, + "signature": signature, + "beta_norm": r.gprime_beta_norm, // EXACT rational string + "obstruction_exact": r.obstruction_along_drift, // exact string or null (modp: null) + "nabla_certified": r.nabla_r_certified_nonzero.or(r.nabla_r_nonzero), + "nabla_total": r.nabla_r_total, + "rr_nonzero": r.rr_nonzero_modp.or(r.rr_nonzero), + "baseline_constant_curvature_quarter": r.baseline_constant_curvature_quarter, + "sound": r.sound, + "point": r.point, + "reason": r.reason, + }); + RrcRow { + id, + equation_id, + shape, + pist_label, + weak_axes, + metrics: serde_json::to_string(&metrics).unwrap(), + } +} + +fn row_sql(row: &RrcRow) -> String { + format!( + "INSERT INTO ene.rrc_classifications \ + (id, equation_id, shape, pist_label, weak_axes, metrics) VALUES \ + ({}, {}, {}, {}, {}, {}::jsonb) \ + ON CONFLICT (id) DO UPDATE SET shape=EXCLUDED.shape, pist_label=EXCLUDED.pist_label, \ + weak_axes=EXCLUDED.weak_axes, metrics=EXCLUDED.metrics;", + sql_str(&row.id), + sql_str(&row.equation_id), + sql_str(&row.shape), + sql_str(&row.pist_label), + sql_u32(row.weak_axes), + sql_str(&row.metrics), + ) +} + +fn main() { + let args: Vec = std::env::args().collect(); + if args.len() < 2 { + eprintln!("usage: ingest_semisym_scar [--out f.sql] [--limit N] [--apply --dsn ]"); + std::process::exit(2); + } + let dir = PathBuf::from(&args[1]); + let mut out = PathBuf::from("semisym_scar.sql"); + let mut limit: Option = None; + let mut apply = false; + let mut dsn: Option = None; + let mut i = 2; + while i < args.len() { + match args[i].as_str() { + "--out" => { out = PathBuf::from(&args[i + 1]); i += 2; } + "--limit" => { limit = args[i + 1].parse().ok(); i += 2; } + "--apply" => { apply = true; i += 1; } + "--dsn" => { dsn = Some(args[i + 1].clone()); i += 2; } + other => { eprintln!("unknown arg {other}"); std::process::exit(2); } + } + } + + let mut files: Vec = fs::read_dir(&dir) + .unwrap_or_else(|_| panic!("cannot read checkpoint dir {}", dir.display())) + .filter_map(|e| e.ok().map(|e| e.path())) + .filter(|p| p.extension().is_some_and(|x| x == "json")) + .collect(); + files.sort(); + if let Some(n) = limit { + files.truncate(n); + } + + let mut rows: Vec = Vec::with_capacity(files.len()); + let mut by_verdict: BTreeMap = BTreeMap::new(); + let mut parse_errors = 0usize; + let mut edges = 0usize; + for f in &files { + let txt = match fs::read_to_string(f) { Ok(t) => t, Err(_) => { parse_errors += 1; continue; } }; + let r: Receipt = match serde_json::from_str(&txt) { Ok(r) => r, Err(_) => { parse_errors += 1; continue; } }; + if r.schema.as_deref() == Some("semisym_receipt_edge_v1") || r.modular_pole == Some(true) { + edges += 1; + } + let row = map_receipt(&r); + *by_verdict.entry(row.shape.clone()).or_insert(0) += 1; + rows.push(row); + } + + // generation only β€” no DB touch. Migration adds the JSONB metrics column. + let mut sql = String::new(); + sql.push_str("BEGIN;\n"); + sql.push_str("ALTER TABLE ene.rrc_classifications ADD COLUMN IF NOT EXISTS metrics JSONB DEFAULT '{}'::jsonb;\n"); + for row in &rows { + sql.push_str(&row_sql(row)); + sql.push('\n'); + } + let audit_id = stable_uuid(&format!("semisym_scar_ingest:{}", dir.display())); + sql.push_str(&format!( + "INSERT INTO ene.ingest_events (id, session_id, event_type, payload) VALUES \ + ({}, NULL, 'semisym_scar_ingest', {}) ON CONFLICT (id) DO NOTHING;\n", + sql_str(&audit_id), + sql_str(&format!("{{\"rows\":{},\"source\":\"{}\"}}", rows.len(), dir.display())), + )); + sql.push_str("COMMIT;\n"); + fs::write(&out, &sql).expect("write sql"); + + eprintln!("── semisym scar ingest (DRY-RUN, float-free) ──"); + eprintln!("checkpoint : {}", dir.display()); + eprintln!("receipts : {} ({} parse errors, {} edge/pole)", files.len(), parse_errors, edges); + eprintln!("rows : {} β†’ ene.rrc_classifications (+ metrics JSONB, +1 ingest_events)", rows.len()); + eprintln!("verdicts : {by_verdict:?}"); + eprintln!("sql written : {}", out.display()); + eprintln!("preview (first 2):"); + for row in rows.iter().take(2) { + eprintln!(" {} shape={} weak_axes={:?}", row.equation_id, row.shape, row.weak_axes); + eprintln!(" metrics={}", row.metrics); + } + + if !apply { + eprintln!("\n[dry-run] no database touched. To apply: --apply --dsn "); + return; + } + let Some(dsn) = dsn else { + eprintln!("\nERROR: --apply requires --dsn (no default endpoint; confirm live ENE first)"); + std::process::exit(2); + }; + eprintln!("\n[apply] executing {} via psql …", out.display()); + let status = Command::new("psql") + .arg(&dsn) + .arg("-v").arg("ON_ERROR_STOP=1") + .arg("-f").arg(&out) + .status() + .expect("failed to spawn psql"); + if status.success() { + eprintln!("[apply] OK β€” {} rows upserted into ene.rrc_classifications", rows.len()); + } else { + eprintln!("[apply] psql failed ({status})"); + std::process::exit(1); + } +}