mirror of
https://github.com/allaunthefox/SilverSight.git
synced 2026-07-31 01:25:21 +00:00
feat(semisym): modular fast path + scar-map ingest skeleton
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
6c58feaf53
commit
e128aa50aa
5 changed files with 1462 additions and 21 deletions
|
|
@ -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"
|
||||
|
|
|
|||
207
rust/semisym/Cargo.lock
generated
Normal file
207
rust/semisym/Cargo.lock
generated
Normal file
|
|
@ -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"
|
||||
|
|
@ -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<Vec<Vec<usize>>>) {
|
||||
pub(crate) fn i3_table(n: usize) -> (Vec<[usize; 3]>, Vec<Vec<Vec<usize>>>) {
|
||||
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<Q> {
|
||||
pub(crate) fn drift_beta(m: usize) -> Vec<Q> {
|
||||
// BraidStateN.lean rossbyDriftFromChirality weights, cycled
|
||||
let base = [q(1, 1), q(-1, 1), q(1, 2), q(0, 1)];
|
||||
let w: Vec<Q> = (0..m).map(|i| base[i % 4].clone()).collect();
|
||||
|
|
@ -258,7 +260,7 @@ fn drift_beta(m: usize) -> Vec<Q> {
|
|||
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<Jet> = (0..n).map(|i| cx.coord(i, point[i].clone())).collect();
|
||||
|
|
@ -572,10 +574,19 @@ fn mat_inv(cx: &JetCtx, a: &[Vec<Jet>], n: usize) -> Vec<Vec<Jet>> {
|
|||
inv
|
||||
}
|
||||
|
||||
fn signature(gm: &[Vec<Q>]) -> (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<Q>]) -> (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<Q>]) -> Option<(usize, usize)> {
|
||||
let n = gm.len();
|
||||
let mut a: Vec<Vec<Q>> = gm.to_vec();
|
||||
let (mut pos, mut neg) = (0usize, 0usize);
|
||||
|
|
@ -598,7 +609,7 @@ fn signature(gm: &[Vec<Q>]) -> (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<Q>]) -> (usize, usize) {
|
|||
a[k][i] = Q::zero();
|
||||
}
|
||||
}
|
||||
(pos, neg)
|
||||
Some((pos, neg))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn det(a: &[Vec<Q>]) -> 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<Vec<u64>> = 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::<u64>()).collect::<Result<Vec<u64>, _>>().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<String> = 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<PathBuf> = 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<Q> = 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}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
832
rust/semisym/src/modp.rs
Normal file
832
rust/semisym/src/modp.rs
Normal file
|
|
@ -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<Fp> {
|
||||
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<Fp> {
|
||||
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<Fp>,
|
||||
d2: Vec<Fp>,
|
||||
d3: Vec<Fp>,
|
||||
}
|
||||
|
||||
struct CtxF {
|
||||
n: usize,
|
||||
n2: usize,
|
||||
n3: usize,
|
||||
p: u64,
|
||||
t3: Vec<[usize; 3]>,
|
||||
t3idx: Vec<Vec<Vec<usize>>>,
|
||||
}
|
||||
|
||||
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<JetF> {
|
||||
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<JetF> {
|
||||
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<String>,
|
||||
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<usize>, 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<Vec<Q>>, 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<Q> = 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<JetF>],
|
||||
n: usize,
|
||||
) -> Option<(Vec<Vec<Vec<Vec<JetF>>>>, Vec<Vec<Vec<Vec<JetF>>>>, Vec<Vec<Vec<JetF>>>)> {
|
||||
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<JetF>], n: usize) -> Option<Vec<Vec<JetF>>> {
|
||||
let mut m: Vec<Vec<JetF>> = a.to_vec();
|
||||
let mut inv: Vec<Vec<JetF>> = (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<JetF>], 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<ReceiptModp, &'static str> {
|
||||
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<JetF> = match (0..n)
|
||||
.map(|i| Fp::from_q(&point[i], p).map(|c| cx.coord(i, c)))
|
||||
.collect::<Option<Vec<_>>>()
|
||||
{
|
||||
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<JetF> = match (0..n)
|
||||
.map(|i| Fp::from_q(&beta_full[i], p).map(|b| cx.constant(b)))
|
||||
.collect::<Option<Vec<_>>>()
|
||||
{
|
||||
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<usize>)> = 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<usize>, 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<String>,
|
||||
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())
|
||||
}
|
||||
246
rust/src/bin/ingest_semisym_scar.rs
Normal file
246
rust/src/bin/ingest_semisym_scar.rs
Normal file
|
|
@ -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 <ckpt_dir> [--out scar.sql] [--limit N] # dry-run
|
||||
//! ingest_semisym_scar <ckpt_dir> --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<String>,
|
||||
m: Option<u32>,
|
||||
prime: Option<u64>,
|
||||
geometrization: Option<String>,
|
||||
point: Option<Vec<String>>,
|
||||
gprime_signature: Option<(u32, u32)>, // JSON [pos, neg]
|
||||
gprime_beta_norm: Option<String>, // exact rational as string
|
||||
verdict: Option<String>,
|
||||
obstruction_along_drift: Option<String>, // exact rational (v1 only)
|
||||
nabla_r_certified_nonzero: Option<u64>,
|
||||
nabla_r_nonzero: Option<u64>,
|
||||
nabla_r_total: Option<u64>,
|
||||
rr_nonzero_modp: Option<u64>,
|
||||
rr_nonzero: Option<u64>,
|
||||
baseline_constant_curvature_quarter: Option<bool>,
|
||||
reason: Option<String>, // edge receipts
|
||||
sound: Option<bool>,
|
||||
modular_pole: Option<bool>,
|
||||
}
|
||||
|
||||
/// 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<u32>, // 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<u32>) -> 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<String> = std::env::args().collect();
|
||||
if args.len() < 2 {
|
||||
eprintln!("usage: ingest_semisym_scar <ckpt_dir> [--out f.sql] [--limit N] [--apply --dsn <conn>]");
|
||||
std::process::exit(2);
|
||||
}
|
||||
let dir = PathBuf::from(&args[1]);
|
||||
let mut out = PathBuf::from("semisym_scar.sql");
|
||||
let mut limit: Option<usize> = None;
|
||||
let mut apply = false;
|
||||
let mut dsn: Option<String> = 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<PathBuf> = 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<RrcRow> = Vec::with_capacity(files.len());
|
||||
let mut by_verdict: BTreeMap<String, usize> = 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 <postgres-conn>");
|
||||
return;
|
||||
}
|
||||
let Some(dsn) = dsn else {
|
||||
eprintln!("\nERROR: --apply requires --dsn <postgres-conn> (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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue