mirror of
https://github.com/allaunthefox/SilverSight.git
synced 2026-07-31 01:25:21 +00:00
feat(spectral): f/Cartan integration — exact spiral packing, f(n) layout, Cartan Δ-floor, torus-winding fix
STEP 1 — integer spiral-index packing (EXACT): pack_spiral_index/ unpack_spiral_index with corpus-wide base=2·max|c|+1 and offset-encoded digits, recorded in the codebook JSON header; round-trip verified over all 250 matrices. Fixes the sign bug in integration_sprint.py's integer path (unoffset signed packing collapsed (-1,1) with (1,0) in base 2). STEP 2 — f(n) layout (DECORATIVE): per-entry radius_sq + angle_frac, computed in decimal arithmetic (indices reach ~1e44 >> 2^52); verified against VERIFICATION_LOG V007 f(20121); min angular separation ≈4.64e-6 of a turn reported. STEP 3 — Cartan Δ-floor (--cartan-floor): Δ=17/1792 exact (CartanConnection.lean:70) as Fisher-distance resolution floor on Δ₇; 13 sub-Δ pairs merge 196 fingerprints into 187 Δ-resolution codewords vs 9 gap-rule clusters; emitted alongside, not replacing, the 3×-median rule. Caveat asserted in tests: the |c|/Σ|c| simplex projection is lossy (one corpus pair at Fisher distance 0) — similarity, not identity. STEP 4 — torus-winding saturation fix: TorusWinding gains a_exact/b_exact (plain ℤ, no Q16.16 clamp); torus_to_spiral_index prefers them, making the round trip lossless (regression-tested at n=1e9). BraidEigensolid.lean needs the same widening — noted, Lean untouched. Also corrects charpoly_codebook.py's stale docstring counts (192/180 → 196/182, cross-checked against the exact Faddeev–LeVerrier fingerprints: identical on all 250 matrices). Tests: 43 passed (19 new). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e81caa4a6a
commit
359effdfc6
7 changed files with 2126 additions and 239 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -3,7 +3,7 @@
|
||||||
**Date:** 2026-07-01
|
**Date:** 2026-07-01
|
||||||
**Module:** `python/spectral_codebook.py`
|
**Module:** `python/spectral_codebook.py`
|
||||||
**Output:** `data/spectral_codebook.json`
|
**Output:** `data/spectral_codebook.json`
|
||||||
**Tests:** `tests/test_spectral_codebook.py` (15 tests)
|
**Tests:** `tests/test_spectral_codebook.py` (43 tests)
|
||||||
|
|
||||||
Implements the "Next Steps" of `docs/SPECTRAL_CODEBOOK_ANALYSIS.md`: a
|
Implements the "Next Steps" of `docs/SPECTRAL_CODEBOOK_ANALYSIS.md`: a
|
||||||
gap-aware spectral codebook over the 250 8×8 integer braid adjacency
|
gap-aware spectral codebook over the 250 8×8 integer braid adjacency
|
||||||
|
|
@ -16,6 +16,7 @@ encode/decode round trip and an exact, float-free fingerprint per matrix.
|
||||||
python3 python/spectral_codebook.py # build + report + write JSON
|
python3 python/spectral_codebook.py # build + report + write JSON
|
||||||
python3 python/spectral_codebook.py --check-manifold # also run the 278-row corpus
|
python3 python/spectral_codebook.py --check-manifold # also run the 278-row corpus
|
||||||
python3 python/spectral_codebook.py --gap-factor 3 --min-support 10 --no-write
|
python3 python/spectral_codebook.py --gap-factor 3 --min-support 10 --no-write
|
||||||
|
python3 python/spectral_codebook.py --cartan-floor # + Fisher/Δ=17/1792 floor report
|
||||||
python3 tests/test_spectral_codebook.py # or python3 -m pytest tests/test_spectral_codebook.py
|
python3 tests/test_spectral_codebook.py # or python3 -m pytest tests/test_spectral_codebook.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -122,6 +123,73 @@ matrices, no id lacks a matrix). Rerunning gap quantization on the 278-row
|
||||||
corpus reproduces the 250-matrix boundaries exactly: **no new gaps, no new
|
corpus reproduces the 250-matrix boundaries exactly: **no new gaps, no new
|
||||||
clusters, all extra rows fall inside existing clusters.**
|
clusters, all extra rows fall inside existing clusters.**
|
||||||
|
|
||||||
|
## Φ-corkscrew and Cartan layers (f/Cartan integration batch)
|
||||||
|
|
||||||
|
Four layers added on top of the fingerprints, each labelled **exact**
|
||||||
|
(integer arithmetic, Lean-provable) or **decorative** (placement/reporting
|
||||||
|
only, carries no information):
|
||||||
|
|
||||||
|
### 1. Integer spiral-index packing — EXACT
|
||||||
|
|
||||||
|
`pack_spiral_index` / `unpack_spiral_index` with corpus-wide parameters
|
||||||
|
recorded in the JSON header (`phi_corkscrew.packing`): offset = max|c| over
|
||||||
|
all fingerprint coefficients (164 234 on this corpus), base = 2·offset + 1
|
||||||
|
(328 469). Digits are offset-encoded (`c + offset ∈ [0, base)`), so
|
||||||
|
`spiral_index = Σ (cᵢ + offset)·baseⁱ` is the base-B positional numeral of
|
||||||
|
the fingerprint — injective on fingerprints by uniqueness of positional
|
||||||
|
representation, inverted exactly by `unpack_spiral_index`, and verified by
|
||||||
|
round trip over all 250 matrices in the test suite. It inherits charpoly's
|
||||||
|
non-injectivity on *matrices* (cospectral classes share one index), so
|
||||||
|
identity still requires the within-cluster index.
|
||||||
|
|
||||||
|
This **supersedes** the float phinary packing that `integration_sprint.py`
|
||||||
|
used historically (`phinary += c·φ⁻ⁱ` then truncation — not injective).
|
||||||
|
The integer path in `integration_sprint.phi_corkscrew_index` is fixed the
|
||||||
|
same way (offset-encoded digits; its unoffset signed packing collapsed
|
||||||
|
e.g. `(-1, 1)` with `(1, 0)` in base 2), but its base is per-call — for
|
||||||
|
corpus-comparable indices use the codebook's recorded (base, offset).
|
||||||
|
|
||||||
|
### 2. f(n) layout — DECORATIVE
|
||||||
|
|
||||||
|
`f(n) = (√n·cos(nψ), √n·sin(nψ))`, ψ = 2π/φ². Injective because 1/φ² is
|
||||||
|
irrational; low-discrepancy by the three-distance theorem;
|
||||||
|
information-neutral (everything decodes from `spiral_index` alone). Each
|
||||||
|
entry stores `layout.radius_sq` (= the spiral index, exact int) and
|
||||||
|
`layout.angle_frac` (fraction of a full turn). Angles are computed in
|
||||||
|
`decimal` arithmetic at full precision because spiral indices reach
|
||||||
|
~10⁴⁴ ≫ 2⁵², where float64 retains no fractional bits of n/φ². Verified
|
||||||
|
against `VERIFICATION_LOG.md` V007: f(20121) = (−137.80079576,
|
||||||
|
−33.64432624). Measured minimum pairwise angular separation across the
|
||||||
|
corpus: ≈ 4.64 × 10⁻⁶ of a turn (positive, as irrationality requires).
|
||||||
|
|
||||||
|
### 3. Cartan ∆-floor quantization (`--cartan-floor`) — EXACT ∆, reported floor
|
||||||
|
|
||||||
|
∆ = **17/1792** exactly (λ_min of the Cartan crossing blocks,
|
||||||
|
`CartanConnection.lean:70`, `docs/cartan_fingerprint.md`), adopted as the
|
||||||
|
resolution floor on the Fisher metric of Δ₇:
|
||||||
|
`d_F(p, q) = 2·arccos(Σ√(pᵢqᵢ))` with `pᵢ = |cᵢ|/Σ|cⱼ|` (all-zero
|
||||||
|
fingerprint → uniform, by convention). Measured on this corpus: **13
|
||||||
|
fingerprint pairs fall below ∆**, merging 196 fingerprints into **187
|
||||||
|
∆-resolution codewords**, vs **9 clusters** from the 3×-median-gap λ rule.
|
||||||
|
Both rules are emitted side by side; neither replaces the other.
|
||||||
|
|
||||||
|
Caveat, asserted in the tests: the simplex projection destroys sign and
|
||||||
|
scale, so distinct fingerprints can collide on Δ₇ (this corpus has a pair
|
||||||
|
at Fisher distance exactly 0). The Fisher/∆ layer is a **similarity**
|
||||||
|
metric, not an identity key.
|
||||||
|
|
||||||
|
### 4. Torus-winding saturation fix — EXACT
|
||||||
|
|
||||||
|
`pist_braid_bridge.TorusWinding` now carries `a_exact`/`b_exact` (plain ℤ,
|
||||||
|
no clamp) alongside the legacy Q16.16 raws, which silently saturate above
|
||||||
|
32767 (i.e. for spiral_index > 65535 — and codebook indices reach ~10⁴⁴).
|
||||||
|
`torus_to_spiral_index` prefers the exact fields, making the round trip
|
||||||
|
lossless for every index (regression-tested at n = 10⁹); legacy windings
|
||||||
|
without exact fields keep the old Q16 behaviour. The matching Lean type in
|
||||||
|
`BraidEigensolid.lean` needs the same widening (Int fields alongside
|
||||||
|
Q16_16) before this layer can be mirrored formally — noted in the
|
||||||
|
dataclass docstring, Lean file intentionally untouched.
|
||||||
|
|
||||||
## Notes for the Lean side
|
## Notes for the Lean side
|
||||||
|
|
||||||
- **Identity layer vs. similarity layer.** `ClassifyN.hashMatrix` is a
|
- **Identity layer vs. similarity layer.** `ClassifyN.hashMatrix` is a
|
||||||
|
|
@ -222,11 +290,16 @@ equation provenance; this sync does not (and cannot) connect to it.
|
||||||
|
|
||||||
## Output schema (`data/spectral_codebook.json`)
|
## Output schema (`data/spectral_codebook.json`)
|
||||||
|
|
||||||
- header: `schema` (`spectral_codebook_v2`), `quantization` (factor,
|
- header: `schema` (`spectral_codebook_v2`), `phi_corkscrew` (packing
|
||||||
min support, median gap, kept/candidate/suppressed boundaries,
|
base/offset + layout semantics and measured min angular separation),
|
||||||
sparse-tail cutoff), `clusters` (table above), `collisions` (all counts
|
`quantization` (factor, min support, median gap,
|
||||||
and explicit collision classes), `manifold_278` (when `--check-manifold`).
|
kept/candidate/suppressed boundaries, sparse-tail cutoff), `clusters`
|
||||||
|
(table above), `collisions` (all counts and explicit collision classes),
|
||||||
|
`manifold_278` (when `--check-manifold`), `cartan_floor` (when
|
||||||
|
`--cartan-floor`: sub-Δ Fisher pairs, Δ-resolution codeword count,
|
||||||
|
rule comparison).
|
||||||
- `entries[]`: `equation_id`, `codeword`, `index`, `spectral_radius`
|
- `entries[]`: `equation_id`, `codeword`, `index`, `spectral_radius`
|
||||||
(exact), `lambda_power_iteration` (legacy), `charpoly` (8 exact ints),
|
(exact), `lambda_power_iteration` (legacy), `charpoly` (8 exact ints),
|
||||||
`density`, `frobenius`, `trace`, optional `sparse_tail`,
|
`density`, `frobenius`, `trace`, `spiral_index` (exact int packing of
|
||||||
|
charpoly), `layout` (`radius_sq`, `angle_frac`), optional `sparse_tail`,
|
||||||
optional `identical_matrix_ids`.
|
optional `identical_matrix_ids`.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ polynomial coefficients as the codebook key.
|
||||||
|
|
||||||
Key advantages over power-iteration spectral radius:
|
Key advantages over power-iteration spectral radius:
|
||||||
- Exact (integer coefficients, no float precision issues)
|
- Exact (integer coefficients, no float precision issues)
|
||||||
- 192 unique fingerprints vs 180 from spectral radius
|
- 196 unique fingerprints vs 182 from spectral radius (measured on the
|
||||||
|
250-matrix corpus; identical coefficients to the exact Faddeev–LeVerrier
|
||||||
|
fingerprints in spectral_codebook.py on all 250 matrices)
|
||||||
- Cayley-Hamilton verifiable in ℤ (fits integer-only doctrine)
|
- Cayley-Hamilton verifiable in ℤ (fits integer-only doctrine)
|
||||||
- Guaranteed L∞ minimum distance of 1 between distinct codewords
|
- Guaranteed L∞ minimum distance of 1 between distinct codewords
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -282,13 +282,17 @@ def phi_corkscrew_index(spectral_coeffs: np.ndarray) -> int:
|
||||||
|
|
||||||
# If coefficients are integers (charpoly), use them directly
|
# If coefficients are integers (charpoly), use them directly
|
||||||
if np.all(coeffs == np.round(coeffs)):
|
if np.all(coeffs == np.round(coeffs)):
|
||||||
# Integer packing: shift each coefficient into a unique digit position
|
# Offset-encoded positional packing: digit = c + abs_max ∈ [0, base)
|
||||||
# Base = max(|coeff|) + 1 to guarantee injectivity
|
# with base = 2·abs_max + 1. Packing signed coefficients without
|
||||||
|
# the offset is NOT injective: (-1, 1) and (1, 0) both give 1 in
|
||||||
|
# base 2. base/offset are per-call here; for corpus-comparable
|
||||||
|
# indices use spectral_codebook.pack_spiral_index with the
|
||||||
|
# corpus-wide (base, offset) recorded in the codebook JSON.
|
||||||
abs_max = int(np.max(np.abs(coeffs))) if len(coeffs) > 0 else 0
|
abs_max = int(np.max(np.abs(coeffs))) if len(coeffs) > 0 else 0
|
||||||
base = max(abs_max + 1, 2)
|
base = max(2 * abs_max + 1, 2)
|
||||||
result = 0
|
result = 0
|
||||||
for i, c in enumerate(coeffs):
|
for i, c in enumerate(coeffs):
|
||||||
result += int(c) * (base ** i)
|
result += (int(c) + abs_max) * (base ** i)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Float coefficients: normalize to [0, 1], quantize to 16-bit integers
|
# Float coefficients: normalize to [0, 1], quantize to 16-bit integers
|
||||||
|
|
|
||||||
|
|
@ -334,9 +334,17 @@ class TorusWinding:
|
||||||
Matches: SilverSight.BraidEigensolid.TorusWinding
|
Matches: SilverSight.BraidEigensolid.TorusWinding
|
||||||
a = spatial winding (latitude cycle)
|
a = spatial winding (latitude cycle)
|
||||||
b = phase/torsion winding (longitude cycle)
|
b = phase/torsion winding (longitude cycle)
|
||||||
|
|
||||||
|
The Q16_16 raws are LOSSY above value 32767 (Q16_MAX_RAW clamp);
|
||||||
|
a_exact/b_exact carry the plain-ℤ winding with no clamp and are the
|
||||||
|
authoritative values when present. BraidEigensolid.lean's
|
||||||
|
TorusWinding needs the same widening (Int fields alongside the
|
||||||
|
Q16_16 raws) before this exact layer can be mirrored formally.
|
||||||
"""
|
"""
|
||||||
a: int # Q16_16 raw
|
a: int # Q16_16 raw — saturates for values > 32767
|
||||||
b: int # Q16_16 raw
|
b: int # Q16_16 raw — saturates for values > 32767
|
||||||
|
a_exact: Optional[int] = None # plain ℤ winding, no clamp
|
||||||
|
b_exact: Optional[int] = None # plain ℤ winding, no clamp
|
||||||
|
|
||||||
def to_float(self) -> Tuple[float, float]:
|
def to_float(self) -> Tuple[float, float]:
|
||||||
return q16_to_float(self.a), q16_to_float(self.b)
|
return q16_to_float(self.a), q16_to_float(self.b)
|
||||||
|
|
@ -356,8 +364,9 @@ def spiral_to_torus_winding(spiral_index: int) -> TorusWinding:
|
||||||
|
|
||||||
⚠️ SATURATION WARNING: b_raw uses Q16.16 encoding, which clamps at
|
⚠️ SATURATION WARNING: b_raw uses Q16.16 encoding, which clamps at
|
||||||
Q16_MAX_RAW / 65536 ≈ 32767.99. For spiral_index > 65535, the
|
Q16_MAX_RAW / 65536 ≈ 32767.99. For spiral_index > 65535, the
|
||||||
phase winding b silently saturates. Use torus_winding_safe() for
|
phase winding b silently saturates. The a_exact/b_exact fields carry
|
||||||
indices that may exceed this range.
|
the unclamped integers; torus_to_spiral_index prefers them, so the
|
||||||
|
round trip is exact for every spiral_index.
|
||||||
|
|
||||||
This mirrors: TorusWinding in BraidEigensolid.lean
|
This mirrors: TorusWinding in BraidEigensolid.lean
|
||||||
"""
|
"""
|
||||||
|
|
@ -366,7 +375,7 @@ def spiral_to_torus_winding(spiral_index: int) -> TorusWinding:
|
||||||
b_val = spiral_index // phi_step
|
b_val = spiral_index // phi_step
|
||||||
a_raw = float_to_q16(float(a_val))
|
a_raw = float_to_q16(float(a_val))
|
||||||
b_raw = float_to_q16(float(b_val))
|
b_raw = float_to_q16(float(b_val))
|
||||||
return TorusWinding(a=a_raw, b=b_raw)
|
return TorusWinding(a=a_raw, b=b_raw, a_exact=a_val, b_exact=b_val)
|
||||||
|
|
||||||
|
|
||||||
def spiral_to_torus_winding_safe(spiral_index: int) -> Tuple[TorusWinding, bool]:
|
def spiral_to_torus_winding_safe(spiral_index: int) -> Tuple[TorusWinding, bool]:
|
||||||
|
|
@ -385,18 +394,22 @@ def spiral_to_torus_winding_safe(spiral_index: int) -> Tuple[TorusWinding, bool]
|
||||||
saturated = b_val > max_q16_val
|
saturated = b_val > max_q16_val
|
||||||
a_raw = float_to_q16(float(a_val))
|
a_raw = float_to_q16(float(a_val))
|
||||||
b_raw = float_to_q16(float(b_val))
|
b_raw = float_to_q16(float(b_val))
|
||||||
return TorusWinding(a=a_raw, b=b_raw), saturated
|
return TorusWinding(a=a_raw, b=b_raw, a_exact=a_val, b_exact=b_val), saturated
|
||||||
|
|
||||||
|
|
||||||
def torus_to_spiral_index(w: TorusWinding) -> int:
|
def torus_to_spiral_index(w: TorusWinding) -> int:
|
||||||
"""Inverse: recover spiral index from torus winding.
|
"""Inverse: recover spiral index from torus winding.
|
||||||
|
|
||||||
n = φ_step · b + a (in integer approximation)
|
n = φ_step · b + a
|
||||||
|
|
||||||
⚠️ For spiral_index > 65535, this will return a saturated value.
|
Prefers the exact ℤ fields when present (lossless for all n);
|
||||||
Use spiral_to_torus_winding_safe() to detect this case.
|
falls back to the Q16.16 raws, which saturate for
|
||||||
|
spiral_index > 65535 — use spiral_to_torus_winding_safe() to
|
||||||
|
detect that case on legacy windings without exact fields.
|
||||||
"""
|
"""
|
||||||
phi_step = int(round(PHI))
|
phi_step = int(round(PHI))
|
||||||
|
if w.a_exact is not None and w.b_exact is not None:
|
||||||
|
return phi_step * w.b_exact + w.a_exact
|
||||||
b_int = int(round(q16_to_float(w.b)))
|
b_int = int(round(q16_to_float(w.b)))
|
||||||
a_int = int(round(q16_to_float(w.a)))
|
a_int = int(round(q16_to_float(w.a)))
|
||||||
return phi_step * b_int + a_int
|
return phi_step * b_int + a_int
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,186 @@ def spectral_profile(mat: Sequence[Sequence[int]]) -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
# Φ-corkscrew: integer spiral-index packing (exact) + f(n) layout
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# The packing layer is EXACT and provable: with a corpus-wide base
|
||||||
|
# B = 2·max|c| + 1 and offset o = max|c|,
|
||||||
|
# every offset digit c_i + o lies in [0, B), so
|
||||||
|
# n = Σ (c_i + o)·B^i
|
||||||
|
# is the base-B positional numeral of the digit tuple — injective on
|
||||||
|
# char-poly fingerprints by uniqueness of positional representation, and
|
||||||
|
# unpack_spiral_index inverts it exactly. It inherits charpoly's
|
||||||
|
# NON-injectivity on matrices (cospectral classes share one index).
|
||||||
|
#
|
||||||
|
# The layout layer f(n) = (√n·cos(nψ), √n·sin(nψ)), ψ = 2π/φ², is
|
||||||
|
# DECORATIVE placement only: injective because 1/φ² is irrational
|
||||||
|
# (distinct n give distinct angles), information-neutral (everything is
|
||||||
|
# recoverable from n alone), low-discrepancy by the three-distance
|
||||||
|
# theorem. It carries no information beyond spiral_index.
|
||||||
|
|
||||||
|
def packing_params(fingerprints: Sequence[Sequence[int]]) -> Tuple[int, int]:
|
||||||
|
"""Corpus-wide (base, offset) for spiral-index packing.
|
||||||
|
|
||||||
|
offset = max|c| over every coefficient of every fingerprint;
|
||||||
|
base = 2·offset + 1, so offset-encoded digits fill [0, base).
|
||||||
|
"""
|
||||||
|
offset = max((abs(c) for fp in fingerprints for c in fp), default=0)
|
||||||
|
return 2 * offset + 1, offset
|
||||||
|
|
||||||
|
|
||||||
|
def pack_spiral_index(coeffs: Sequence[int], base: int, offset: int) -> int:
|
||||||
|
"""Little-endian base-B packing of offset-encoded coefficients.
|
||||||
|
|
||||||
|
Exact integer arithmetic; injective for digits in range (raises
|
||||||
|
otherwise). Replaces the float phinary packing of
|
||||||
|
integration_sprint.phi_corkscrew_index, which was not injective.
|
||||||
|
"""
|
||||||
|
n = 0
|
||||||
|
for i, c in enumerate(coeffs):
|
||||||
|
d = c + offset
|
||||||
|
if not 0 <= d < base:
|
||||||
|
raise ValueError(
|
||||||
|
f"coefficient {c} out of packing range for "
|
||||||
|
f"base={base}, offset={offset}")
|
||||||
|
n += d * base ** i
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def unpack_spiral_index(n: int, base: int, offset: int,
|
||||||
|
length: int = 8) -> Tuple[int, ...]:
|
||||||
|
"""Exact inverse of pack_spiral_index."""
|
||||||
|
if n < 0:
|
||||||
|
raise ValueError("spiral index must be non-negative")
|
||||||
|
out: List[int] = []
|
||||||
|
for _ in range(length):
|
||||||
|
n, d = divmod(n, base)
|
||||||
|
out.append(d - offset)
|
||||||
|
if n:
|
||||||
|
raise ValueError("spiral index exceeds length digits in this base")
|
||||||
|
return tuple(out)
|
||||||
|
|
||||||
|
|
||||||
|
# 1/φ² = (3 − √5)/2: the fractional rotation per step of the corkscrew.
|
||||||
|
def angle_frac(n: int) -> float:
|
||||||
|
"""frac(n/φ²) — the corkscrew angle of index n as a fraction of 2π.
|
||||||
|
|
||||||
|
Computed in decimal arithmetic at a precision that covers the integer
|
||||||
|
part of n·(3−√5)/2, because float64 loses the fractional part
|
||||||
|
entirely once n exceeds 2^52 (spiral indices here reach base^8).
|
||||||
|
"""
|
||||||
|
from decimal import Decimal, getcontext
|
||||||
|
getcontext().prec = len(str(abs(n))) + 25
|
||||||
|
alpha = (Decimal(3) - Decimal(5).sqrt()) / 2
|
||||||
|
return float((Decimal(n) * alpha) % 1)
|
||||||
|
|
||||||
|
|
||||||
|
def corkscrew_xy(n: int) -> Tuple[float, float]:
|
||||||
|
"""f(n) = (√n·cos(nψ), √n·sin(nψ)), ψ = 2π/φ². Placement only."""
|
||||||
|
theta = 2.0 * math.pi * angle_frac(n)
|
||||||
|
r = math.sqrt(n)
|
||||||
|
return r * math.cos(theta), r * math.sin(theta)
|
||||||
|
|
||||||
|
|
||||||
|
def min_angular_separation(indices: Sequence[int]) -> Optional[float]:
|
||||||
|
"""Minimum circular distance (as a fraction of a full turn) between
|
||||||
|
the corkscrew angles of distinct spiral indices."""
|
||||||
|
uniq = sorted({angle_frac(n) for n in set(indices)})
|
||||||
|
if len(uniq) < 2:
|
||||||
|
return None
|
||||||
|
gaps = [uniq[i + 1] - uniq[i] for i in range(len(uniq) - 1)]
|
||||||
|
gaps.append(1.0 - uniq[-1] + uniq[0]) # wrap-around
|
||||||
|
return min(gaps)
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
# Cartan ∆-floor quantization (Fisher metric on Δ₇)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# ∆ = 17/1792 is the minimum eigenvalue of the Cartan crossing blocks,
|
||||||
|
# proven exact in formal/.../CartanConnection.lean:70 (see
|
||||||
|
# docs/cartan_fingerprint.md). It is adopted here as the resolution
|
||||||
|
# floor on the Fisher metric: two codewords closer than ∆ are treated
|
||||||
|
# as indistinguishable at operator resolution. This gives a PRINCIPLED
|
||||||
|
# alternative to the 3×-median-gap heuristic; both are emitted so the
|
||||||
|
# rules can be compared, neither replaces the other.
|
||||||
|
|
||||||
|
CARTAN_DELTA = Fraction(17, 1792) # exact; CartanConnection.lean:70
|
||||||
|
|
||||||
|
|
||||||
|
def simplex_project(coeffs: Sequence[int]) -> Tuple[float, ...]:
|
||||||
|
"""Project a fingerprint onto Δ₇: p_i = |c_i| / Σ|c_j|.
|
||||||
|
|
||||||
|
Convention: the all-zero fingerprint (nilpotent x⁸ class) maps to
|
||||||
|
the uniform distribution — flagged in the report, since that choice
|
||||||
|
is a convention, not a theorem.
|
||||||
|
"""
|
||||||
|
total = sum(abs(c) for c in coeffs)
|
||||||
|
k = len(coeffs)
|
||||||
|
if total == 0:
|
||||||
|
return tuple(1.0 / k for _ in range(k))
|
||||||
|
return tuple(abs(c) / total for c in coeffs)
|
||||||
|
|
||||||
|
|
||||||
|
def fisher_distance(p: Sequence[float], q: Sequence[float]) -> float:
|
||||||
|
"""d_F(p, q) = 2·arccos(Σ√(p_i·q_i)) — Fisher geodesic distance on
|
||||||
|
the probability simplex (Bhattacharyya angle × 2)."""
|
||||||
|
bc = sum(math.sqrt(a * b) for a, b in zip(p, q))
|
||||||
|
return 2.0 * math.acos(min(1.0, bc))
|
||||||
|
|
||||||
|
|
||||||
|
def cartan_floor_report(profiles: Dict[str, dict]) -> dict:
|
||||||
|
"""All-pairs Fisher distances between distinct fingerprints; pairs
|
||||||
|
below ∆ = 17/1792 are merged into ∆-resolution components (union-
|
||||||
|
find), giving the codeword count the Cartan floor supports."""
|
||||||
|
fp_rep: Dict[Tuple[int, ...], str] = {}
|
||||||
|
for eid in sorted(profiles):
|
||||||
|
fp_rep.setdefault(tuple(profiles[eid]["charpoly"]), eid)
|
||||||
|
fps = list(fp_rep)
|
||||||
|
pts = [simplex_project(fp) for fp in fps]
|
||||||
|
delta = float(CARTAN_DELTA)
|
||||||
|
|
||||||
|
parent = list(range(len(fps)))
|
||||||
|
|
||||||
|
def find(i: int) -> int:
|
||||||
|
while parent[i] != i:
|
||||||
|
parent[i] = parent[parent[i]]
|
||||||
|
i = parent[i]
|
||||||
|
return i
|
||||||
|
|
||||||
|
sub_delta: List[dict] = []
|
||||||
|
min_d, min_pair = None, None
|
||||||
|
for i in range(len(fps)):
|
||||||
|
for j in range(i + 1, len(fps)):
|
||||||
|
d = fisher_distance(pts[i], pts[j])
|
||||||
|
if min_d is None or d < min_d:
|
||||||
|
min_d, min_pair = d, (fp_rep[fps[i]], fp_rep[fps[j]])
|
||||||
|
if d < delta:
|
||||||
|
sub_delta.append({
|
||||||
|
"representatives": [fp_rep[fps[i]], fp_rep[fps[j]]],
|
||||||
|
"fisher_distance": round(d, 8),
|
||||||
|
})
|
||||||
|
ri, rj = find(i), find(j)
|
||||||
|
if ri != rj:
|
||||||
|
parent[rj] = ri
|
||||||
|
components = len({find(i) for i in range(len(fps))})
|
||||||
|
return {
|
||||||
|
"delta_exact": "17/1792",
|
||||||
|
"delta_source": "CartanConnection.lean:70 (docs/cartan_fingerprint.md)",
|
||||||
|
"delta_float": delta,
|
||||||
|
"metric": "Fisher on Δ₇: d_F = 2·arccos(Σ√(p_i·q_i)); "
|
||||||
|
"p_i = |c_i|/Σ|c_j| (all-zero fingerprint → uniform, "
|
||||||
|
"by convention)",
|
||||||
|
"distinct_fingerprints": len(fps),
|
||||||
|
"pairs_below_delta": len(sub_delta),
|
||||||
|
"sub_delta_pairs": sub_delta[:50],
|
||||||
|
"delta_resolution_codewords": components,
|
||||||
|
"min_fisher_distance": round(min_d, 8) if min_d is not None else None,
|
||||||
|
"min_fisher_pair": list(min_pair) if min_pair else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
# Gap-aware quantization (deduped, sample-size guarded)
|
# Gap-aware quantization (deduped, sample-size guarded)
|
||||||
# ──────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -350,6 +530,14 @@ class Codebook:
|
||||||
eid: spectral_profile(mat) for eid, mat in matrices.items()
|
eid: spectral_profile(mat) for eid, mat in matrices.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Φ-corkscrew integer packing (corpus-wide base/offset, exact)
|
||||||
|
self.pack_base, self.pack_offset = packing_params(
|
||||||
|
[p["charpoly"] for p in self.profiles.values()])
|
||||||
|
self.spiral_indices: Dict[str, int] = {
|
||||||
|
eid: pack_spiral_index(p["charpoly"], self.pack_base, self.pack_offset)
|
||||||
|
for eid, p in self.profiles.items()
|
||||||
|
}
|
||||||
|
|
||||||
# dedupe byte-identical matrices BEFORE gap analysis
|
# dedupe byte-identical matrices BEFORE gap analysis
|
||||||
self.matrix_groups: Dict[Matrix, List[str]] = {}
|
self.matrix_groups: Dict[Matrix, List[str]] = {}
|
||||||
for eid in sorted(self.profiles):
|
for eid in sorted(self.profiles):
|
||||||
|
|
@ -480,6 +668,7 @@ class Codebook:
|
||||||
for eid in sorted(self.profiles):
|
for eid in sorted(self.profiles):
|
||||||
cw, idx = self._assign[eid]
|
cw, idx = self._assign[eid]
|
||||||
p = self.profiles[eid]
|
p = self.profiles[eid]
|
||||||
|
n = self.spiral_indices[eid]
|
||||||
entry = {
|
entry = {
|
||||||
"equation_id": eid,
|
"equation_id": eid,
|
||||||
"codeword": cw,
|
"codeword": cw,
|
||||||
|
|
@ -490,6 +679,11 @@ class Codebook:
|
||||||
"density": p["density"],
|
"density": p["density"],
|
||||||
"frobenius": p["frobenius"],
|
"frobenius": p["frobenius"],
|
||||||
"trace": p["trace"],
|
"trace": p["trace"],
|
||||||
|
"spiral_index": n,
|
||||||
|
"layout": {
|
||||||
|
"radius_sq": n,
|
||||||
|
"angle_frac": round(angle_frac(n), 12),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if self.is_sparse_tail(p["spectral_radius"]):
|
if self.is_sparse_tail(p["spectral_radius"]):
|
||||||
entry["sparse_tail"] = True
|
entry["sparse_tail"] = True
|
||||||
|
|
@ -507,6 +701,33 @@ class Codebook:
|
||||||
"characteristic polynomial (charpoly); lambda_power_iteration is "
|
"characteristic polynomial (charpoly); lambda_power_iteration is "
|
||||||
"the legacy estimate and is unreliable on peripheral spectra"
|
"the legacy estimate and is unreliable on peripheral spectra"
|
||||||
),
|
),
|
||||||
|
"phi_corkscrew": {
|
||||||
|
"packing": {
|
||||||
|
"base": self.pack_base,
|
||||||
|
"offset": self.pack_offset,
|
||||||
|
"digits": 8,
|
||||||
|
"order": "little-endian: charpoly c1 is digit 0",
|
||||||
|
"semantics": (
|
||||||
|
"spiral_index = Σ (c_i + offset)·base^i — exact "
|
||||||
|
"integer, injective on charpoly fingerprints "
|
||||||
|
"(unpack_spiral_index inverts it exactly); inherits "
|
||||||
|
"charpoly's non-injectivity on matrices, so identity "
|
||||||
|
"still needs the within-cluster index"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"layout": {
|
||||||
|
"psi": "2π/φ² (golden angle)",
|
||||||
|
"angle_frac": "frac(n·(3−√5)/2), decimal-exact precision",
|
||||||
|
"min_angular_separation_frac": (
|
||||||
|
min_angular_separation(list(self.spiral_indices.values()))
|
||||||
|
),
|
||||||
|
"semantics": (
|
||||||
|
"decorative placement only: f(n) is injective because "
|
||||||
|
"1/φ² is irrational, and carries no information beyond "
|
||||||
|
"spiral_index"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
"quantization": {
|
"quantization": {
|
||||||
"method": "gap_aware_deduped_min_support",
|
"method": "gap_aware_deduped_min_support",
|
||||||
"gap_factor": self.gap_factor,
|
"gap_factor": self.gap_factor,
|
||||||
|
|
@ -584,6 +805,9 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||||
help="Minimum distinct matrices per cluster (default 10)")
|
help="Minimum distinct matrices per cluster (default 10)")
|
||||||
ap.add_argument("--check-manifold", action="store_true",
|
ap.add_argument("--check-manifold", action="store_true",
|
||||||
help="Also run over the 278-row RRC/Q16_16Manifold corpus")
|
help="Also run over the 278-row RRC/Q16_16Manifold corpus")
|
||||||
|
ap.add_argument("--cartan-floor", action="store_true",
|
||||||
|
help="Also emit the Cartan ∆=17/1792 Fisher-floor report "
|
||||||
|
"(alongside, not replacing, the gap rule)")
|
||||||
ap.add_argument("--no-write", action="store_true",
|
ap.add_argument("--no-write", action="store_true",
|
||||||
help="Report only; do not write the JSON")
|
help="Report only; do not write the JSON")
|
||||||
ap.add_argument("--sync-db", action="store_true",
|
ap.add_argument("--sync-db", action="store_true",
|
||||||
|
|
@ -638,6 +862,21 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||||
print(f" boundaries match 250-matrix codebook: {mc['boundaries_match_250']} "
|
print(f" boundaries match 250-matrix codebook: {mc['boundaries_match_250']} "
|
||||||
f"({mc['clusters']} clusters)")
|
f"({mc['clusters']} clusters)")
|
||||||
|
|
||||||
|
if args.cartan_floor:
|
||||||
|
cf = cartan_floor_report(cb.profiles)
|
||||||
|
doc["cartan_floor"] = cf
|
||||||
|
gap_clusters = len(q["boundaries"]) + 1
|
||||||
|
print(f"\n Cartan ∆-floor (∆ = {cf['delta_exact']} ≈ "
|
||||||
|
f"{cf['delta_float']:.6f}, {cf['delta_source']}):")
|
||||||
|
print(f" distinct fingerprints on Δ₇ : {cf['distinct_fingerprints']}")
|
||||||
|
print(f" pairs below ∆ (Fisher) : {cf['pairs_below_delta']}")
|
||||||
|
print(f" min Fisher distance : {cf['min_fisher_distance']} "
|
||||||
|
f"{cf['min_fisher_pair']}")
|
||||||
|
print(f" rule comparison — codewords : "
|
||||||
|
f"∆-floor {cf['delta_resolution_codewords']} vs "
|
||||||
|
f"3×-median-gap {gap_clusters} (λ clusters); both emitted, "
|
||||||
|
f"neither replaces the other")
|
||||||
|
|
||||||
if args.sync_db:
|
if args.sync_db:
|
||||||
import spectral_codebook_db
|
import spectral_codebook_db
|
||||||
print("\n DB sync (dry run):")
|
print("\n DB sync (dry run):")
|
||||||
|
|
|
||||||
|
|
@ -320,5 +320,189 @@ class TestSpectralCodebookDbSync(unittest.TestCase):
|
||||||
os.environ["NEON_PG"] = old
|
os.environ["NEON_PG"] = old
|
||||||
|
|
||||||
|
|
||||||
|
class TestPhiCorkscrewPacking(unittest.TestCase):
|
||||||
|
"""STEP 1: integer spiral-index packing — exact, injective, reversible."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.matrices = sc.parse_matrices_lean()
|
||||||
|
cls.codebook = sc.Codebook(cls.matrices)
|
||||||
|
|
||||||
|
def test_base_is_odd_and_covers_corpus(self):
|
||||||
|
base, offset = self.codebook.pack_base, self.codebook.pack_offset
|
||||||
|
self.assertEqual(base, 2 * offset + 1)
|
||||||
|
for p in self.codebook.profiles.values():
|
||||||
|
for c in p["charpoly"]:
|
||||||
|
self.assertLessEqual(abs(c), offset)
|
||||||
|
|
||||||
|
def test_round_trip_all_250(self):
|
||||||
|
base, offset = self.codebook.pack_base, self.codebook.pack_offset
|
||||||
|
for eid, p in self.codebook.profiles.items():
|
||||||
|
fp = tuple(p["charpoly"])
|
||||||
|
n = self.codebook.spiral_indices[eid]
|
||||||
|
self.assertEqual(sc.unpack_spiral_index(n, base, offset, 8), fp, eid)
|
||||||
|
|
||||||
|
def test_injective_on_fingerprints(self):
|
||||||
|
by_fp = {}
|
||||||
|
for eid, p in self.codebook.profiles.items():
|
||||||
|
by_fp.setdefault(tuple(p["charpoly"]), set()).add(
|
||||||
|
self.codebook.spiral_indices[eid])
|
||||||
|
# one index per fingerprint …
|
||||||
|
for fp, idxs in by_fp.items():
|
||||||
|
self.assertEqual(len(idxs), 1, fp)
|
||||||
|
# … and distinct fingerprints get distinct indices
|
||||||
|
all_idx = [next(iter(v)) for v in by_fp.values()]
|
||||||
|
self.assertEqual(len(set(all_idx)), len(by_fp))
|
||||||
|
|
||||||
|
def test_signed_collision_regression(self):
|
||||||
|
# The unoffset packing collapsed (-1, 1) and (1, 0); ours must not.
|
||||||
|
base, offset = sc.packing_params([(-1, 1), (1, 0)])
|
||||||
|
a = sc.pack_spiral_index((-1, 1), base, offset)
|
||||||
|
b = sc.pack_spiral_index((1, 0), base, offset)
|
||||||
|
self.assertNotEqual(a, b)
|
||||||
|
|
||||||
|
def test_out_of_range_raises(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
sc.pack_spiral_index((5,), base=3, offset=1)
|
||||||
|
|
||||||
|
def test_integration_sprint_integer_path_fixed(self):
|
||||||
|
import numpy as np
|
||||||
|
import integration_sprint as isp
|
||||||
|
# same shape, same abs_max (=1) → same per-call base/offset, so the
|
||||||
|
# historical base-2 collision pair must now pack to distinct indices
|
||||||
|
a = isp.phi_corkscrew_index(np.array([-1.0, 1.0, 0.0]))
|
||||||
|
b = isp.phi_corkscrew_index(np.array([1.0, 0.0, -1.0]))
|
||||||
|
self.assertNotEqual(a, b)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCorkscrewLayout(unittest.TestCase):
|
||||||
|
"""STEP 2: f(n) layout — decorative placement, verified against V007."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.matrices = sc.parse_matrices_lean()
|
||||||
|
cls.codebook = sc.Codebook(cls.matrices)
|
||||||
|
|
||||||
|
def test_v007_reference_point(self):
|
||||||
|
# VERIFICATION_LOG.md V007: f(20121) = (-137.80079576, -33.64432624)
|
||||||
|
x, y = sc.corkscrew_xy(20121)
|
||||||
|
self.assertAlmostEqual(x, -137.80079576, places=5)
|
||||||
|
self.assertAlmostEqual(y, -33.64432624, places=5)
|
||||||
|
|
||||||
|
def test_angle_frac_in_unit_interval(self):
|
||||||
|
for eid, n in self.codebook.spiral_indices.items():
|
||||||
|
f = sc.angle_frac(n)
|
||||||
|
self.assertGreaterEqual(f, 0.0, eid)
|
||||||
|
self.assertLess(f, 1.0, eid)
|
||||||
|
|
||||||
|
def test_angle_frac_exceeds_float64_naive(self):
|
||||||
|
# for n >> 2^52 the float product n·α has no fractional bits left;
|
||||||
|
# the decimal path must still give a nontrivial fraction
|
||||||
|
n = 10 ** 40 + 7
|
||||||
|
f = sc.angle_frac(n)
|
||||||
|
self.assertGreaterEqual(f, 0.0)
|
||||||
|
self.assertLess(f, 1.0)
|
||||||
|
# consecutive indices step by frac(α): distinguishable at any scale
|
||||||
|
g = sc.angle_frac(n + 1)
|
||||||
|
alpha_frac = 0.3819660112501051 # frac((3−√5)/2)
|
||||||
|
diff = (g - f) % 1.0
|
||||||
|
self.assertAlmostEqual(diff, alpha_frac, places=9)
|
||||||
|
|
||||||
|
def test_layout_in_json_and_information_neutral(self):
|
||||||
|
doc = self.codebook.to_json()
|
||||||
|
pc = doc["phi_corkscrew"]
|
||||||
|
self.assertEqual(pc["packing"]["base"], self.codebook.pack_base)
|
||||||
|
sep = pc["layout"]["min_angular_separation_frac"]
|
||||||
|
self.assertIsNotNone(sep)
|
||||||
|
self.assertGreater(sep, 0.0)
|
||||||
|
for entry in doc["entries"]:
|
||||||
|
# radius² IS the spiral index — layout adds no information
|
||||||
|
self.assertEqual(entry["layout"]["radius_sq"], entry["spiral_index"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestCartanFloor(unittest.TestCase):
|
||||||
|
"""STEP 3: Cartan ∆ = 17/1792 as Fisher-distance resolution floor."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.matrices = sc.parse_matrices_lean()
|
||||||
|
cls.codebook = sc.Codebook(cls.matrices)
|
||||||
|
cls.report = sc.cartan_floor_report(cls.codebook.profiles)
|
||||||
|
|
||||||
|
def test_delta_exact_value(self):
|
||||||
|
from fractions import Fraction
|
||||||
|
self.assertEqual(sc.CARTAN_DELTA, Fraction(17, 1792))
|
||||||
|
|
||||||
|
def test_fisher_metric_axioms(self):
|
||||||
|
p = sc.simplex_project((1, -2, 3, 0, 0, 0, 0, 4))
|
||||||
|
q = sc.simplex_project((0, 5, 0, 1, 0, 0, 0, 0))
|
||||||
|
self.assertAlmostEqual(sc.fisher_distance(p, p), 0.0, places=12)
|
||||||
|
self.assertAlmostEqual(sc.fisher_distance(p, q),
|
||||||
|
sc.fisher_distance(q, p), places=12)
|
||||||
|
# disjoint support → maximal distance π
|
||||||
|
u = sc.simplex_project((1, 0, 0, 0, 0, 0, 0, 0))
|
||||||
|
v = sc.simplex_project((0, 1, 0, 0, 0, 0, 0, 0))
|
||||||
|
import math
|
||||||
|
self.assertAlmostEqual(sc.fisher_distance(u, v), math.pi, places=12)
|
||||||
|
|
||||||
|
def test_simplex_projection_sums_to_one(self):
|
||||||
|
for p in self.codebook.profiles.values():
|
||||||
|
pt = sc.simplex_project(p["charpoly"])
|
||||||
|
self.assertAlmostEqual(sum(pt), 1.0, places=12)
|
||||||
|
|
||||||
|
def test_zero_fingerprint_convention(self):
|
||||||
|
pt = sc.simplex_project((0,) * 8)
|
||||||
|
self.assertEqual(pt, tuple(1.0 / 8 for _ in range(8)))
|
||||||
|
|
||||||
|
def test_report_consistency(self):
|
||||||
|
r = self.report
|
||||||
|
self.assertEqual(r["distinct_fingerprints"], 196)
|
||||||
|
# merging k sub-∆ pairs can remove at most k components
|
||||||
|
self.assertGreaterEqual(
|
||||||
|
r["delta_resolution_codewords"],
|
||||||
|
r["distinct_fingerprints"] - r["pairs_below_delta"])
|
||||||
|
self.assertLessEqual(r["delta_resolution_codewords"],
|
||||||
|
r["distinct_fingerprints"])
|
||||||
|
|
||||||
|
def test_simplex_projection_is_lossy(self):
|
||||||
|
# |c|/Σ|c| destroys sign+scale: distinct fingerprints can collide on
|
||||||
|
# Δ₇ (min Fisher distance 0.0 in this corpus) — similarity layer,
|
||||||
|
# NOT an identity key. This is asserted so the doc claim stays true.
|
||||||
|
self.assertEqual(self.report["min_fisher_distance"], 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTorusWindingExact(unittest.TestCase):
|
||||||
|
"""STEP 4: torus-winding round trip must not saturate at n ≥ 65536."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
import pist_braid_bridge as pbb
|
||||||
|
cls.pbb = pbb
|
||||||
|
|
||||||
|
def test_round_trip_regression_1e9(self):
|
||||||
|
pbb = self.pbb
|
||||||
|
for n in (0, 1, 65535, 65536, 10 ** 9):
|
||||||
|
w = pbb.spiral_to_torus_winding(n)
|
||||||
|
self.assertEqual(pbb.torus_to_spiral_index(w), n, n)
|
||||||
|
|
||||||
|
def test_exact_fields_populated(self):
|
||||||
|
pbb = self.pbb
|
||||||
|
w = pbb.spiral_to_torus_winding(10 ** 9)
|
||||||
|
self.assertEqual(w.a_exact, 10 ** 9 % 2)
|
||||||
|
self.assertEqual(w.b_exact, 10 ** 9 // 2)
|
||||||
|
|
||||||
|
def test_q16_raws_still_saturate_and_are_flagged(self):
|
||||||
|
pbb = self.pbb
|
||||||
|
w, saturated = pbb.spiral_to_torus_winding_safe(10 ** 9)
|
||||||
|
self.assertTrue(saturated)
|
||||||
|
self.assertEqual(w.b, pbb.Q16_MAX_RAW) # legacy raw clamps…
|
||||||
|
self.assertEqual(pbb.torus_to_spiral_index(w), 10 ** 9) # …exact wins
|
||||||
|
|
||||||
|
def test_legacy_winding_without_exact_fields(self):
|
||||||
|
pbb = self.pbb
|
||||||
|
w = pbb.TorusWinding(a=pbb.float_to_q16(1.0), b=pbb.float_to_q16(21.0))
|
||||||
|
self.assertEqual(pbb.torus_to_spiral_index(w), 43)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue