mirror of
https://github.com/allaunthefox/SilverSight.git
synced 2026-07-31 01:25:21 +00:00
feat(semisym): exact-rational Rust discriminator — jet AD, checkpointed survey, Δ₇ verdict
Order-3 BigRational jet AD (no symbolic algebra): Fisher–Rao + drift-flip metric, Γ/R/∇R/R·R/Q(g,R) at rational points. Validated against the sympy Δ₃ ground truth (exact match incl. (∇R)_(0,0,1,0,1)=1987584/1500625 and the drift obstruction scalar). Signature via congruence elimination (Sylvester's law of inertia) — fixes a row-aliasing bug that reported (4,3) at the Δ₇ centroid; truth is (6,1), numpy-confirmed. Δ₇ centroid results (31s single-core): baseline constant curvature 1/4 holds; g' signature (6,1) with drift time-like (g'(β,β)=−35); verdict PROPER (11076/16807 nonzero ∇R, 591 inconsistent L ratios) — matches Δ₃. survey subcommand = resumable DAG: one receipt JSON per lattice point (skip-if-exists), rayon-parallel, --max-points deterministic stride cap; receipts are ingestable scar-map events (obstruction ⇒ scar candidate). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
be5d4e9de5
commit
6c58feaf53
2 changed files with 772 additions and 0 deletions
15
rust/semisym/Cargo.toml
Normal file
15
rust/semisym/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "semisym"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
num-bigint = "0.4"
|
||||
num-rational = "0.4"
|
||||
num-traits = "0.2"
|
||||
rayon = "1.10"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
757
rust/semisym/src/main.rs
Normal file
757
rust/semisym/src/main.rs
Normal file
|
|
@ -0,0 +1,757 @@
|
|||
//! semisym — Milestone §0 covariant semi-symmetry discriminator (exact ℚ).
|
||||
//!
|
||||
//! Rust port of python/semisymmetry_discriminator.py, validated against its
|
||||
//! Δ₃ ground truth (docs/reviews/SEMISYMMETRY_DISCRIMINATOR_DELTA3_LOG.txt).
|
||||
//! No floats anywhere: BigRational jet (Taylor) automatic differentiation to
|
||||
//! third order at a rational point — no symbolic algebra, no expression swell.
|
||||
//!
|
||||
//! Geometry: Fisher–Rao on Δ_{m−1}, reduced coords p₁..p_{m−1};
|
||||
//! drift-flip metric g' = g − 2 β♭⊗β♭ / g(β,β) with β from the
|
||||
//! rossbyDriftFromChirality weights (left +1, right −1, scarred +1/2,
|
||||
//! achiral 0), mean-centered (BraidStateN.lean).
|
||||
//!
|
||||
//! Derivative-order bookkeeping (why order-3 jets suffice):
|
||||
//! g' jets carry exact ∂⁰..∂³ → ∂g' order 2 → Γ order 2
|
||||
//! → ∂Γ order 1 → R order 1 → ∂R order 0 → ∇R exact values.
|
||||
//!
|
||||
//! Conventions (Kobayashi–Nomizu, identical to the Python reference):
|
||||
//! R^ρ_{σμν} = ∂_μ Γ^ρ_{νσ} − ∂_ν Γ^ρ_{μσ} + Γ^ρ_{μλ}Γ^λ_{νσ} − Γ^ρ_{νλ}Γ^λ_{μσ}
|
||||
//! (A·T)_{ijkl} = −A^m_i T_{mjkl} − A^m_j T_{imkl} − A^m_k T_{ijml} − A^m_l T_{ijkm}
|
||||
//!
|
||||
//! Usage:
|
||||
//! semisym probe --m 8 --point 1/8,1/8,1/8,1/8,1/8,1/8,1/8
|
||||
//! semisym survey --m 8 --grid 3 --checkpoint ckpt/ (resumable DAG:
|
||||
//! one receipt JSON per lattice point; existing receipts are skipped)
|
||||
//!
|
||||
//! Each receipt is an ingestable DAG-node event for the ENE scar map:
|
||||
//! obstruction present ⇒ scar candidate; see receipt fields.
|
||||
|
||||
use num_bigint::BigInt;
|
||||
use num_rational::BigRational;
|
||||
use num_traits::{One, Zero};
|
||||
use rayon::prelude::*;
|
||||
use serde::Serialize;
|
||||
use std::fmt::Write as _;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
type Q = BigRational;
|
||||
|
||||
fn q(n: i64, d: i64) -> Q {
|
||||
Q::new(BigInt::from(n), BigInt::from(d))
|
||||
}
|
||||
|
||||
fn parse_q(s: &str) -> Q {
|
||||
let s = s.trim();
|
||||
match s.split_once('/') {
|
||||
Some((a, b)) => Q::new(
|
||||
BigInt::from_str(a.trim()).expect("numerator"),
|
||||
BigInt::from_str(b.trim()).expect("denominator"),
|
||||
),
|
||||
None => Q::new(BigInt::from_str(s).expect("integer"), BigInt::one()),
|
||||
}
|
||||
}
|
||||
|
||||
// ── symmetric index helpers (n vars) ─────────────────────────────────
|
||||
|
||||
#[inline]
|
||||
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>>>) {
|
||||
let mut list = Vec::new();
|
||||
let mut idx = vec![vec![vec![0usize; n]; n]; n];
|
||||
for i in 0..n {
|
||||
for j in i..n {
|
||||
for k in j..n {
|
||||
let id = list.len();
|
||||
list.push([i, j, k]);
|
||||
// all permutations map to id
|
||||
let perms = [
|
||||
[i, j, k], [i, k, j], [j, i, k], [j, k, i], [k, i, j], [k, j, i],
|
||||
];
|
||||
for p in perms {
|
||||
idx[p[0]][p[1]][p[2]] = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(list, idx)
|
||||
}
|
||||
|
||||
// ── order-3 jets over ℚ ──────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Jet {
|
||||
n: usize,
|
||||
v: Q,
|
||||
d1: Vec<Q>, // n
|
||||
d2: Vec<Q>, // n(n+1)/2, raw ∂²
|
||||
d3: Vec<Q>, // C(n+2,3), raw ∂³
|
||||
}
|
||||
|
||||
struct JetCtx {
|
||||
n: usize,
|
||||
n2: usize,
|
||||
n3: usize,
|
||||
t3: Vec<[usize; 3]>,
|
||||
t3idx: Vec<Vec<Vec<usize>>>,
|
||||
}
|
||||
|
||||
impl JetCtx {
|
||||
fn new(n: usize) -> Self {
|
||||
let (t3, t3idx) = i3_table(n);
|
||||
JetCtx { n, n2: n * (n + 1) / 2, n3: t3.len(), t3, t3idx }
|
||||
}
|
||||
fn zero(&self) -> Jet {
|
||||
Jet {
|
||||
n: self.n,
|
||||
v: Q::zero(),
|
||||
d1: vec![Q::zero(); self.n],
|
||||
d2: vec![Q::zero(); self.n2],
|
||||
d3: vec![Q::zero(); self.n3],
|
||||
}
|
||||
}
|
||||
fn constant(&self, c: Q) -> Jet {
|
||||
let mut j = self.zero();
|
||||
j.v = c;
|
||||
j
|
||||
}
|
||||
fn coord(&self, i: usize, val: Q) -> Jet {
|
||||
let mut j = self.zero();
|
||||
j.v = val;
|
||||
j.d1[i] = Q::one();
|
||||
j
|
||||
}
|
||||
fn add(&self, a: &Jet, b: &Jet) -> Jet {
|
||||
let mut c = a.clone();
|
||||
c.v += &b.v;
|
||||
for i in 0..self.n { c.d1[i] += &b.d1[i]; }
|
||||
for i in 0..self.n2 { c.d2[i] += &b.d2[i]; }
|
||||
for i in 0..self.n3 { c.d3[i] += &b.d3[i]; }
|
||||
c
|
||||
}
|
||||
fn sub(&self, a: &Jet, b: &Jet) -> Jet {
|
||||
let mut c = a.clone();
|
||||
c.v -= &b.v;
|
||||
for i in 0..self.n { c.d1[i] -= &b.d1[i]; }
|
||||
for i in 0..self.n2 { c.d2[i] -= &b.d2[i]; }
|
||||
for i in 0..self.n3 { c.d3[i] -= &b.d3[i]; }
|
||||
c
|
||||
}
|
||||
fn neg(&self, a: &Jet) -> Jet {
|
||||
self.sub(&self.zero(), a)
|
||||
}
|
||||
fn mul(&self, a: &Jet, b: &Jet) -> Jet {
|
||||
let n = self.n;
|
||||
let mut c = self.zero();
|
||||
c.v = &a.v * &b.v;
|
||||
for i in 0..n {
|
||||
c.d1[i] = &a.d1[i] * &b.v + &a.v * &b.d1[i];
|
||||
}
|
||||
for i in 0..n {
|
||||
for j in i..n {
|
||||
c.d2[i2(n, i, j)] = &a.d2[i2(n, i, j)] * &b.v
|
||||
+ &a.d1[i] * &b.d1[j]
|
||||
+ &a.d1[j] * &b.d1[i]
|
||||
+ &a.v * &b.d2[i2(n, i, j)];
|
||||
}
|
||||
}
|
||||
for (id, t) in self.t3.iter().enumerate() {
|
||||
let [i, j, k] = *t;
|
||||
c.d3[id] = &a.d3[id] * &b.v
|
||||
+ &a.d2[i2(n, i, j)] * &b.d1[k]
|
||||
+ &a.d2[i2(n, i, k)] * &b.d1[j]
|
||||
+ &a.d2[i2(n, j, k)] * &b.d1[i]
|
||||
+ &a.d1[i] * &b.d2[i2(n, j, k)]
|
||||
+ &a.d1[j] * &b.d2[i2(n, i, k)]
|
||||
+ &a.d1[k] * &b.d2[i2(n, i, j)]
|
||||
+ &a.v * &b.d3[id];
|
||||
}
|
||||
c
|
||||
}
|
||||
/// reciprocal 1/b, solving b·r = 1 order by order
|
||||
fn inv(&self, b: &Jet) -> Jet {
|
||||
let n = self.n;
|
||||
assert!(!b.v.is_zero(), "jet reciprocal at pole");
|
||||
let mut r = self.zero();
|
||||
r.v = Q::one() / &b.v;
|
||||
for i in 0..n {
|
||||
r.d1[i] = -(&b.d1[i] * &r.v) / &b.v;
|
||||
}
|
||||
for i in 0..n {
|
||||
for j in i..n {
|
||||
let s = &b.d2[i2(n, i, j)] * &r.v
|
||||
+ &b.d1[i] * &r.d1[j]
|
||||
+ &b.d1[j] * &r.d1[i];
|
||||
r.d2[i2(n, i, j)] = -s / &b.v;
|
||||
}
|
||||
}
|
||||
for (id, t) in self.t3.iter().enumerate() {
|
||||
let [i, j, k] = *t;
|
||||
let s = &b.d3[id] * &r.v
|
||||
+ &b.d2[i2(n, i, j)] * &r.d1[k]
|
||||
+ &b.d2[i2(n, i, k)] * &r.d1[j]
|
||||
+ &b.d2[i2(n, j, k)] * &r.d1[i]
|
||||
+ &b.d1[i] * &r.d2[i2(n, j, k)]
|
||||
+ &b.d1[j] * &r.d2[i2(n, i, k)]
|
||||
+ &b.d1[k] * &r.d2[i2(n, i, j)];
|
||||
r.d3[id] = -s / &b.v;
|
||||
}
|
||||
r
|
||||
}
|
||||
fn div(&self, a: &Jet, b: &Jet) -> Jet {
|
||||
self.mul(a, &self.inv(b))
|
||||
}
|
||||
/// ∂_dir shift: returns jet valid one order lower (d3 of result zeroed)
|
||||
fn shift(&self, a: &Jet, dir: usize) -> Jet {
|
||||
let n = self.n;
|
||||
let mut s = self.zero();
|
||||
s.v = a.d1[dir].clone();
|
||||
for i in 0..n {
|
||||
s.d1[i] = a.d2[i2(n, dir, i)].clone();
|
||||
}
|
||||
for i in 0..n {
|
||||
for j in i..n {
|
||||
s.d2[i2(n, i, j)] = a.d3[self.t3idx[dir][i][j]].clone();
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
// ── receipts ─────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Receipt {
|
||||
schema: &'static str,
|
||||
m: usize,
|
||||
geometrization: String,
|
||||
point: Vec<String>,
|
||||
beta_reduced: Vec<String>,
|
||||
baseline_constant_curvature_quarter: bool,
|
||||
gprime_signature: (usize, usize),
|
||||
gprime_beta_norm: String,
|
||||
nabla_r_nonzero: usize,
|
||||
nabla_r_total: usize,
|
||||
nabla_r_sample: Option<(Vec<usize>, String)>,
|
||||
rr_nonzero: usize,
|
||||
pseudo_l_candidates: Vec<String>,
|
||||
verdict: String,
|
||||
obstruction_along_drift: String,
|
||||
}
|
||||
|
||||
fn qs(x: &Q) -> String {
|
||||
x.to_string()
|
||||
}
|
||||
|
||||
// ── geometry pipeline at a rational point ────────────────────────────
|
||||
|
||||
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();
|
||||
let mean: Q = w.iter().fold(Q::zero(), |a, b| a + b) / Q::from(BigInt::from(m as i64));
|
||||
w.iter().map(|wi| wi - &mean).collect()
|
||||
}
|
||||
|
||||
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();
|
||||
let mut pm = cx.constant(Q::one());
|
||||
for p in &ps {
|
||||
pm = cx.sub(&pm, p);
|
||||
}
|
||||
assert!(pm.v > Q::zero(), "point outside simplex");
|
||||
|
||||
// Fisher–Rao g
|
||||
let inv_pm = cx.inv(&pm);
|
||||
let mut g: Vec<Vec<Jet>> = vec![vec![cx.zero(); n]; n];
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
g[i][j] = if i == j {
|
||||
cx.add(&cx.inv(&ps[i]), &inv_pm)
|
||||
} else {
|
||||
inv_pm.clone()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// baseline check at this point: R = 1/4 (g∧g) — via full pipeline on g
|
||||
let base_ok = check_constant_curvature(&cx, &g, n);
|
||||
|
||||
// drift-flip g'
|
||||
let beta_full = drift_beta(m);
|
||||
let beta: Vec<Q> = beta_full[..n].to_vec();
|
||||
let bjets: Vec<Jet> = beta.iter().map(|b| cx.constant(b.clone())).collect();
|
||||
let mut beta_flat: Vec<Jet> = 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(q(2, 1));
|
||||
let mut gp: Vec<Vec<Jet>> = vec![vec![cx.zero(); n]; n];
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
let corr = cx.div(
|
||||
&cx.mul(&two, &cx.mul(&beta_flat[i], &beta_flat[j])),
|
||||
&bnorm,
|
||||
);
|
||||
gp[i][j] = cx.sub(&g[i][j], &corr);
|
||||
}
|
||||
}
|
||||
|
||||
// signature via leading principal minors (values only)
|
||||
let gm: Vec<Vec<Q>> = (0..n)
|
||||
.map(|i| (0..n).map(|j| gp[i][j].v.clone()).collect())
|
||||
.collect();
|
||||
let sig = signature(&gm);
|
||||
|
||||
// g'(β,β)
|
||||
let mut bb = Q::zero();
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
bb += &gm[i][j] * &beta[i] * &beta[j];
|
||||
}
|
||||
}
|
||||
|
||||
// curvature pipeline on g'
|
||||
let (rup, rdn, gamma) = curvature(&cx, &gp, n);
|
||||
|
||||
// ∇R (values, exact)
|
||||
let mut nabla_nz = 0usize;
|
||||
let mut nabla_sample: Option<(Vec<usize>, String)> = None;
|
||||
let mut drift_contract = Q::zero();
|
||||
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 {
|
||||
// ∂_a R_ijkl from R jets (order ≥1) minus Γ corrections
|
||||
let mut v = rdn[i][jj][k][l].d1[a].clone();
|
||||
for mm in 0..n {
|
||||
v -= &gamma[mm][a][i].v * &rdn[mm][jj][k][l].v;
|
||||
v -= &gamma[mm][a][jj].v * &rdn[i][mm][k][l].v;
|
||||
v -= &gamma[mm][a][k].v * &rdn[i][jj][mm][l].v;
|
||||
v -= &gamma[mm][a][l].v * &rdn[i][jj][k][mm].v;
|
||||
}
|
||||
if !v.is_zero() {
|
||||
nabla_nz += 1;
|
||||
if nabla_sample.is_none() {
|
||||
nabla_sample =
|
||||
Some((vec![a, i, jj, k, l], qs(&v)));
|
||||
}
|
||||
}
|
||||
let term = &beta[a] * &v;
|
||||
drift_contract += &term * &term;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// R·R and Q(g,R), pseudosymmetry ratios
|
||||
let mut rr_nz = 0usize;
|
||||
let mut ratios: Vec<Q> = Vec::new();
|
||||
let mut proper = false;
|
||||
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 = Q::zero();
|
||||
let mut qq = Q::zero();
|
||||
for mm in 0..n {
|
||||
let a_r = &rup[mm][i][a][b].v;
|
||||
rr -= a_r * &rdn[mm][jj][k][l].v;
|
||||
let a_r = &rup[mm][jj][a][b].v;
|
||||
rr -= a_r * &rdn[i][mm][k][l].v;
|
||||
let a_r = &rup[mm][k][a][b].v;
|
||||
rr -= a_r * &rdn[i][jj][mm][l].v;
|
||||
let a_r = &rup[mm][l][a][b].v;
|
||||
rr -= a_r * &rdn[i][jj][k][mm].v;
|
||||
}
|
||||
let wedge = |mm: usize, idx: usize| -> Q {
|
||||
let mut w = Q::zero();
|
||||
if mm == a { w += &gm[b][idx]; }
|
||||
if mm == b { w -= &gm[a][idx]; }
|
||||
w
|
||||
};
|
||||
for mm in 0..n {
|
||||
qq -= wedge(mm, i) * &rdn[mm][jj][k][l].v;
|
||||
qq -= wedge(mm, jj) * &rdn[i][mm][k][l].v;
|
||||
qq -= wedge(mm, k) * &rdn[i][jj][mm][l].v;
|
||||
qq -= wedge(mm, l) * &rdn[i][jj][k][mm].v;
|
||||
}
|
||||
if !rr.is_zero() {
|
||||
rr_nz += 1;
|
||||
if qq.is_zero() {
|
||||
proper = true;
|
||||
} else {
|
||||
let r = rr / qq;
|
||||
if !ratios.contains(&r) {
|
||||
ratios.push(r);
|
||||
}
|
||||
}
|
||||
} else if !qq.is_zero() {
|
||||
let z = Q::zero();
|
||||
if !ratios.contains(&z) {
|
||||
ratios.push(z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let verdict = if nabla_nz == 0 {
|
||||
"LOCALLY_SYMMETRIC".to_string()
|
||||
} else if rr_nz == 0 {
|
||||
"SEMISYMMETRIC_AT_POINT".to_string()
|
||||
} else if !proper && ratios.len() == 1 {
|
||||
format!("PSEUDOSYMMETRIC_AT_POINT L={}", qs(&ratios[0]))
|
||||
} else {
|
||||
"PROPER".to_string()
|
||||
};
|
||||
|
||||
Receipt {
|
||||
schema: "semisym_receipt_v1",
|
||||
m,
|
||||
geometrization: "drift_flip".into(),
|
||||
point: point.iter().map(qs).collect(),
|
||||
beta_reduced: beta.iter().map(qs).collect(),
|
||||
baseline_constant_curvature_quarter: base_ok,
|
||||
gprime_signature: sig,
|
||||
gprime_beta_norm: qs(&bb),
|
||||
nabla_r_nonzero: nabla_nz,
|
||||
nabla_r_total: n.pow(5),
|
||||
nabla_r_sample: nabla_sample,
|
||||
rr_nonzero: rr_nz,
|
||||
pseudo_l_candidates: ratios.iter().map(qs).collect(),
|
||||
verdict,
|
||||
obstruction_along_drift: qs(&drift_contract),
|
||||
}
|
||||
}
|
||||
|
||||
/// Γ (order-2 jets), R^ρ_σμν and R_ρσμν (order-1 jets)
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn curvature(
|
||||
cx: &JetCtx,
|
||||
g: &[Vec<Jet>],
|
||||
n: usize,
|
||||
) -> (Vec<Vec<Vec<Vec<Jet>>>>, Vec<Vec<Vec<Vec<Jet>>>>, Vec<Vec<Vec<Jet>>>) {
|
||||
let ginv = mat_inv(cx, g, n);
|
||||
let half = cx.constant(q(1, 2));
|
||||
// Γ^l_ij
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// R^ρ_σμν
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// lower first index
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(rup, rdn, gamma)
|
||||
}
|
||||
|
||||
fn check_constant_curvature(cx: &JetCtx, g: &[Vec<Jet>], n: usize) -> bool {
|
||||
let (_, rdn, _) = curvature(cx, g, n);
|
||||
let quarter = q(1, 4);
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
for k in 0..n {
|
||||
for l in 0..n {
|
||||
let expect =
|
||||
&quarter * (&g[i][k].v * &g[j][l].v - &g[i][l].v * &g[j][k].v);
|
||||
if rdn[i][j][k][l].v != expect {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn mat_inv(cx: &JetCtx, a: &[Vec<Jet>], n: usize) -> Vec<Vec<Jet>> {
|
||||
// Gauss-Jordan with jet arithmetic; exact, pivots nonzero for metrics
|
||||
let mut m: Vec<Vec<Jet>> = a.to_vec();
|
||||
let mut inv: Vec<Vec<Jet>> = (0..n)
|
||||
.map(|i| {
|
||||
(0..n)
|
||||
.map(|j| if i == j { cx.constant(Q::one()) } else { cx.zero() })
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
for col in 0..n {
|
||||
// pivot: find row with nonzero value
|
||||
let piv = (col..n)
|
||||
.find(|&r| !m[r][col].v.is_zero())
|
||||
.expect("singular jet matrix");
|
||||
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 {
|
||||
if r != col && !(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()))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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).
|
||||
let n = gm.len();
|
||||
let mut a: Vec<Vec<Q>> = gm.to_vec();
|
||||
let (mut pos, mut neg) = (0usize, 0usize);
|
||||
for k in 0..n {
|
||||
if a[k][k].is_zero() {
|
||||
// symmetric pivot repair: prefer swapping in a nonzero diagonal
|
||||
if let Some(j) = ((k + 1)..n).find(|&j| !a[j][j].is_zero()) {
|
||||
a.swap(k, j);
|
||||
for row in a.iter_mut() {
|
||||
row.swap(k, j);
|
||||
}
|
||||
} else if let Some(j) = ((k + 1)..n).find(|&j| !a[k][j].is_zero()) {
|
||||
// row/col addition: (k) += (j) symmetrically ⇒ a[k][k] = 2a[k][j] ≠ 0
|
||||
for t in k..n {
|
||||
let v = a[j][t].clone();
|
||||
a[k][t] += v;
|
||||
}
|
||||
for t in k..n {
|
||||
let v = a[t][j].clone();
|
||||
a[t][k] += v;
|
||||
}
|
||||
} else {
|
||||
panic!("degenerate metric block (rank deficiency)");
|
||||
}
|
||||
}
|
||||
if a[k][k] > Q::zero() {
|
||||
pos += 1;
|
||||
} else {
|
||||
neg += 1;
|
||||
}
|
||||
let pv = a[k][k].clone();
|
||||
// snapshot row k: zeroing a[k][i] mid-loop must not corrupt the
|
||||
// updates for later rows (this exact aliasing bug produced a wrong
|
||||
// (4,3) signature at the Δ₇ centroid; truth is (6,1))
|
||||
let row_k: Vec<Q> = a[k].clone();
|
||||
for i in (k + 1)..n {
|
||||
let f = &a[i][k] / &pv;
|
||||
for j in (k + 1)..n {
|
||||
let t = &f * &row_k[j];
|
||||
a[i][j] -= t;
|
||||
}
|
||||
a[i][k] = Q::zero();
|
||||
a[k][i] = Q::zero();
|
||||
}
|
||||
}
|
||||
(pos, neg)
|
||||
}
|
||||
|
||||
fn det(a: &[Vec<Q>]) -> Q {
|
||||
let n = a.len();
|
||||
let mut m = a.to_vec();
|
||||
let mut d = Q::one();
|
||||
for col in 0..n {
|
||||
let piv = match (col..n).find(|&r| !m[r][col].is_zero()) {
|
||||
Some(p) => p,
|
||||
None => return Q::zero(),
|
||||
};
|
||||
if piv != col {
|
||||
m.swap(col, piv);
|
||||
d = -d;
|
||||
}
|
||||
d *= m[col][col].clone();
|
||||
let pv = m[col][col].clone();
|
||||
for r in (col + 1)..n {
|
||||
let f = &m[r][col] / &pv;
|
||||
for j in col..n {
|
||||
let t = &f * &m[col][j];
|
||||
m[r][j] -= t;
|
||||
}
|
||||
}
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
// ── lattice survey with checkpoint/resume ────────────────────────────
|
||||
|
||||
fn lattice_points(m: usize, grid: u64) -> Vec<Vec<Q>> {
|
||||
// interior compositions: p_i = c_i / (grid*m) with c_i ≥ 1, Σ c = grid*m
|
||||
// sampled coarsely: c_i ∈ {1..} via stars-and-bars over a small grid
|
||||
let n = m - 1;
|
||||
let total = grid * m as u64;
|
||||
let mut pts = Vec::new();
|
||||
fn rec(n: usize, left: u64, cur: &mut Vec<u64>, out: &mut Vec<Vec<u64>>) {
|
||||
if n == 0 {
|
||||
if left >= 1 {
|
||||
let mut c = cur.clone();
|
||||
c.push(left);
|
||||
out.push(c);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for c in 1..=(left.saturating_sub(n as u64)) {
|
||||
cur.push(c);
|
||||
rec(n - 1, left - c, cur, out);
|
||||
cur.pop();
|
||||
}
|
||||
}
|
||||
let mut combos = Vec::new();
|
||||
rec(n, total, &mut Vec::new(), &mut combos);
|
||||
for c in combos {
|
||||
pts.push(
|
||||
c[..n]
|
||||
.iter()
|
||||
.map(|&ci| Q::new(BigInt::from(ci), BigInt::from(total)))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
pts
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let cmd = args.get(1).map(String::as_str).unwrap_or("probe");
|
||||
let mut m = 4usize;
|
||||
let mut point: Option<Vec<Q>> = None;
|
||||
let mut grid = 2u64;
|
||||
let mut ckpt = PathBuf::from("semisym_ckpt");
|
||||
let mut max_points = 200usize;
|
||||
let mut i = 2;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"--m" => { m = args[i + 1].parse().unwrap(); i += 2; }
|
||||
"--point" => {
|
||||
point = Some(args[i + 1].split(',').map(parse_q).collect());
|
||||
i += 2;
|
||||
}
|
||||
"--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; }
|
||||
other => panic!("unknown arg {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
"survey" => {
|
||||
fs::create_dir_all(&ckpt).unwrap();
|
||||
let mut pts = lattice_points(m, grid);
|
||||
if pts.len() > max_points {
|
||||
// deterministic stride subsample — same DAG nodes every run,
|
||||
// so resume-by-receipt stays stable
|
||||
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 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());
|
||||
}
|
||||
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();
|
||||
(idx, v)
|
||||
})
|
||||
.collect();
|
||||
let mut counts = std::collections::BTreeMap::new();
|
||||
for (_, v) in &done {
|
||||
*counts.entry(v.clone()).or_insert(0usize) += 1;
|
||||
}
|
||||
println!("{counts:?}");
|
||||
}
|
||||
other => panic!("unknown command {other}"),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue