chess_spectral (Python)¶
Python reference implementations of the 640-dim 2D and 45 056-dim
4D spectral chess encoders, plus the quantum-mechanical front-end
(2D + 4D kinematics; 4D dynamics shipped in v1.5, 2D dynamics in v1.6.x),
the v1.6 §16 search + tournament + sweep engine surface, the
v5 unified wire format (three encoding modes — dense / per-channel
replacement / XOR-stream — with empirical 7.23× compression on 4D
fixtures vs dense gzipped), the v1.7 native bitboard fast-path
+ time-budget mid-iteration honoring (cumulative ~125× speedup on
dense legal_moves() calls vs v1.6), and the v1.8 GameState4D
push/pop + board accessors + check predicates consumer surface
(chess4D-OC visualizer M11.40 unblocker).
Sibling of the C17 port in ../src/. Use the Python package for REPL /
LLM / notebook analysis, Pyodide-bridge consumers, and the §16
ship-gate matrix runner; use the C binaries for batch encoding
throughput.
The pieces ship under two top-level packages:
chess_spectral— 2D encoder + 4D encoder math + 4D phase operators + QM extension. Everything that's pure spectral / B_4 representation theory lives here.chess_spectral_4d— 4D game-state surface (move history, side-to-move, draw status, FEN4 round-trip, the Pyodidechess_spectral_4d.bridgemodule). Splits cleanly from the encoder so the 4D-rules concerns don't bleed into the spectral math.
What's new in v1.19 (May 2026)¶
srmech profile pattern — chess-spectral is now discoverable via
the srmech.profiles entry-point group (ADR-0001
§7 Step 1, Task #211). Two paths to the same encoder:
# Direct (unchanged; works as it always has):
from chess_spectral import encode_2d, fen_to_pos
enc = encode_2d(fen_to_pos(fen))
# Via srmech (new in 1.19.0):
import srmech
chess = srmech.profile("chess")
enc = chess.encode_2d(chess.fen_to_pos(fen))
Simple-tier profile¶
chess-spectral keeps its own ctypes binding to its C library internally — srmech only exposes the Python bridge surfaces. This is the canonical simple-tier pattern from ADR-0001 §5: the native binary belongs to chess-spectral, the discovery / introspection surface belongs to srmech.
Eight bridge surfaces declared¶
encode_2d,encode_4d— canonical 640-dim / 45 056-dim encoders.fen_to_pos— FEN parser.channel_energies— per-channel L² introspection.encode_2d_pure_phase— integer-arithmetic encoder.phase_only_pseudo_legal_moves— ALU-native move enumeration.encode_2d_bip_hybrid/decode_2d_bip_hybrid— sign × magnitude compression (~3.4× at 8-bit).
The same eight functions register as srmech.amsc.tool_schema
entries (owner=chess) for LLM-agent discovery.
New runtime dep: srmech>=0.3.1,<0.4¶
srmech v0.3.1 added the loader's package-only entry-point support and
[profile.tool_schema] extension loading. v0.3.0 would silently
enumerate the chess profile as invalid; v0.3.1 makes it work
end-to-end.
Workflow: TestPyPI auto-routing for rc tags¶
chess-spectral-publish.yml now matches the pattern srmech and
ephemerides-spectral use:
- Tag
chess-spectral-vX.Y.ZrcN→ TestPyPI (auto) - Tag
chess-spectral-vX.Y.Z(no rc) → PyPI (auto) workflow_dispatchw/targetinput remains as a manual override.
What's new in v1.18 (May 2026)¶
C port of encode_4d_pure_phase — the last deferred item from
the 1.14.0/1.15.0/1.17.0 amendment notes. Substantial multi-file
native shared library compiled with JPL Coding Standards (Power
of Ten) discipline throughout.
Empirical bench — C is 13-47× faster than Python¶
| n_pieces | Python pure-phase | C pure-phase | speedup |
|---|---|---|---|
| 4 | 1.5 ms | 118 µs | 13× |
| 24 | 2.9 ms | 162 µs | 18× |
| 64 | 5.4 ms | 175 µs | 31× |
| 128 | 10.2 ms | 215 µs | 47× |
vs the Python encode_4d float baseline at n=128 (~27 ms):
~125× faster end-to-end.
Bit-exact parity¶
500-position random 4D stress corpus: 0/500 mismatches between Python and C. Both sides do integer arithmetic only; tolerance is zero.
JPL discipline applied throughout¶
- No goto, no recursion, no setjmp/longjmp
- All loops compile-time-bounded
- No dynamic allocation (caller-provided buffers + static tables)
- Functions ≤ 60 lines (split where needed)
- ≥ 2 assertions per function
- Restricted variable scope; const where possible
- No function pointers (switch dispatch + data pointer arrays)
- Builds clean under MSVC
/W4and GCC-Wall -Wextra
API¶
include/cs_encoder_pure_phase_4d.h— C public APIchess_spectral._native_pure_phase_4d— Python ctypes wrapper withHAS_NATIVE_PURE_PHASEguard- Falls back gracefully to Python when shared library isn't loadable (sdist install, Pyodide WASM)
8 new C source files mirror the Python module's per-channel structure; codegen extension emits the integer tables alongside the existing float tables.
What's new in v1.17 (May 2026)¶
Vectorize the per-piece Python loop in the pure-phase encoders — the deferred item from 1.14.0/1.15.0 amendment notes. Result is much stronger than the original 30-50× estimate suggested for the fiber loop alone, but the integrated encoder speedup is dramatic: 2D pure-phase goes from 1.64× SLOWER on opening (1.14.0's mixed result) to 1.88× FASTER; 4D pure-phase wins 1.73-2.47× across all piece-count regimes vs the float baseline.
The 1.14.0 §20.21 framing of pure-phase as "mixed/positional, not a clean win" was an artifact of per-piece Python overhead masking the int-arithmetic architectural advantage. Vectorize lifts that mask.
2D pure-phase reversal¶
| Position | 1.14.0 (loop) | 1.17.0 (vectorized) |
|---|---|---|
| opening | 1.64× SLOWER | 1.88× faster |
| midgame | parity | 1.13× faster |
| endgame | 1.50× faster | 1.45× faster |
4D pure-phase across piece counts¶
| n_pieces | float | pure-phase | speedup |
|---|---|---|---|
| 4 | 2.8 ms | 1.6 ms | 1.73× |
| 24 | 7.1 ms | 2.9 ms | 2.47× |
| 64 | 14.4 ms | 6.0 ms | 2.40× |
| 128 | 27.4 ms | 12.1 ms | 2.25× |
Three vectorization patterns:
- Loop-swap + einsum batching (2D fiber-sym): group occupied squares by FIBER piece type (5 types: N/B/R/Q/K), batch all 3 d-channels via einsum.
- Loop-swap + broadcast (4D fiber-sym): per-piece outer + 3-d broadcast inner, eliminating the 3× redundant sparse-row gather.
- Group-by-piece-type aggregation (4D FD_DIAG): 6 piece-row buckets vs N pieces — 10× fewer 4096-vector adds.
Plus a minor STD4 axis-broadcast vectorize on the 4D side.
87 tests (immolation + stress) pass after the vectorize — bit-exact preserved on every position in the 1500-position random stress corpus.
This is a closed empirical chapter: the §20.15 5-phase BSHDC roadmap shipped 1.10-1.14, the 1.15 4D parity restoration closed the 2D/4D drift, and 1.17 surfaces the architectural win that 1.14's bench had hidden.
What's new in v1.16 (May 2026) — research tooling¶
Three diagnostic + analysis instruments addressing deferred items from 1.14.0's stress-test note. Not core encoder API; infrastructure for asking empirical questions and locking in answers.
- Tournament runner (
tests/run_evaluator_tournament.py) — round-robin between evaluator variants (material, spectral_float64, spectral_hybrid_8bit_lru) at configurable depth. Reports ELO + per-pair record + termination histogram as JSON. - Search-tree bench (
tests/bench_search_tree.py) — measures nodes/sec at depth, combining move-gen + eval + TT + ordering + quiescence. Complements the existing static-eval-only bench. - PGN-sourced phase classifier (
chess_spectral.phase_classifier) — k-means clustering of log-channel-energy fingerprints from real games. Replaces the hand-picked open/mid/end FEN corpus with data-driven phase labels.
33 immolation tests across the three modules; baseline JSON for
bench_search_tree.py at depth=4 captured in tests/bench_baselines/.
What's new in v1.15 (May 2026)¶
4D pure-phase encoder ships — completing the chess2d/chess4d parity restoration begun by 1.14.0's amendment 1. The 1.14.0 ship of B-spike-4 was 2D-only; the §20.15 4D pure-phase encoder was deferred to "1.15.0+" with the open question of how to integerize the B₄-driven 4D structure (no integer character formula analog to D₄'s ±1, ±2, 0 character table). 1.15.0 closes the gap.
-
encode_4d_pure_phase(pos4)— int32 output, integer arithmetic throughout.encode_4d_pure_phase_to_float(pos4)— same encoder with per-channel dequantization; drop-in forencode_4dcomparison. -
A_1 channel uses the scale-by-LCM integerization trick: every B_4 orbit size divides 384, so
P_A1 × 384has all-integer entries (specifically, divisors of 384). Lossless — bit-exact relative to the float baseline. -
STD4 channels (X/Y/Z/W) use scale-by-4 for the quarter-integer
coord_residtable (multiples of 0.25, range [-5.25, +5.25]). Lossless — bit-exact. -
Fiber / pawn / diag channels use the same int16-quantized approach as 2D, with per-table max-abs scale factors. Lossy at the int16 quantization level (≥ 99.999% cosine-sim).
-
Empirical speed verdict — uniformly equal or faster (the OPPOSITE of 2D's mixed result): bench at iters=100 on random positions, sparse / midgame / dense:
| Position | float (µs) | pure_int (µs) | pure/float |
|---|---|---|---|
| sparse n=4 | 10 707 | 10 549 | 0.99× |
| midgame n=24 | 21 805 | 21 095 | 0.97× |
| dense n=128 | 88 996 | 70 283 | 0.79× |
The dense-position win comes from sparse matvec being integer-ALU friendly + STD4 elementwise being SIMD-friendly. Real chess4D-OC visualizer benefit at 28-king Oana-Chiru initial.
- 29 new tests lock the surface: 21 immolation tests (cosine-sim, bit-exactness for A_1 + STD4, channel-energy Spearman, edge cases) + 8 stress tests on 500 random 4D positions (per-position cosine ≥ 0.99, median ≥ 0.999, A_1 + STD4 bit-exact at scale, no NaN, int32 bounded, deterministic).
This closes the 2D/4D pure-phase parity gap. The remaining
encode_4d_pure_phase deferral from 1.14.0 amendment 1 is now
discharged.
What's new in v1.14 (May 2026)¶
The §20.15 fifth and final phase: pure-phase encoder rewrite
(B-spike-4). Integer arithmetic throughout the encoder hot path
— D₄ irrep projection is integer at the core, fiber tables are
int16-quantized at module load, dequantization happens at the
channel-output boundary. New encoder_pure_phase module.
Acceptance gate met (cosine-sim ≥ 99.998% vs float baseline; D₄
channels bit-exact). Speed result is mixed/positional, not a
clean win. All five §20.15 phases now shipped.
-
encode_2d_pure_phase(pos)— int32 output, integer arithmetic throughout.encode_2d_pure_phase_to_float(pos)— same encoder with per-channel dequantization; drop-in forencode_640comparison. -
D₄ channels (A1/A2/B1/B2/E) are bit-exact vs the float baseline — the character formula is integer at the core.
-
Fiber channels (F1/F2/F3/FA/FD) use int16-quantized tables with per-table scale factors; cosine-sim ≥ 99.998% agreement.
-
Empirical speed verdict — mixed: 1.64× SLOWER at opening (dense), parity at midgame, 1.50× FASTER at endgame (sparse). The runtime winner remains
spectral_hybrid_8bit_lru(1.13.0+) at ~50 µs across all corpora. Honest finding documented in §20.21. -
Half-integer bishop fix: VALS has
B=3.5; pure-phase scales the signal by 2 and absorbs the factor in the dequant scale. Documented as_VALS_INT_SCALE = 2. -
21 new immolation tests lock cosine-sim acceptance, D₄ bit-exactness, Spearman ρ ≥ 0.99, quantized-table contracts.
B-spike-4 in the §20.15 phasing — the fifth and final phase. 1a → 1b → 2 → 3 → 4, all shipped within ~5 days.
1.14.0 amendment (May 2026) — 4D parity restored + pedantic stress tests added.
The original 1.14.0 ship was 2D-only and 1.13.0's
spectral_hybrid evaluator family was 2D-only too — both
flagged as a parity regression. Amendment:
- 4D evaluator parity restored:
evaluate_from_hybrid_4d,channel_energies_from_hybrid_4d,evaluate_4d,make_cached_evaluator_4d— full parity with the 2D versions, using the 1.12.0SpectralBIPHybrid4Dstorage. - Pedantic stress testing: 1000 random 2D positions for the pure-phase encoder, 500 random 4D positions for the hybrid evaluator family. All 17 stress tests pass. Acceptance gates (cosine-sim ≥ 0.99 every position; D₄ bit- exactness; channel-energy Spearman ρ ≥ 0.99; no NaN; LRU semantics) hold at scale.
encode_4d_pure_phasedeferred to 1.15.0+. 2D's pure-phase encoder works because D₄ has an integer character formula; 4D's B₄ structure uses sparse-matrix arithmetic without the same convenience. The 4D hybrid encoder (1.12.0) already provides integer storage; this amendment closes the eval-side parity gap.
1.14.0 amendment 2 (May 2026) — chess4D-OC consumer wishlist surface. Late-cycle additions driven by the chess4D-OC pre-publish wishlist; all ship in 1.14.0 (no minor bump — public surface only grows):
- Tier 1: M14.4c entanglement-viz unblockers
qm_4d_bridge.get_qm_density_from_psi(psi)andqm_4d_bridge.get_probability_current_from_psi(psi)— ψ-direct variants of the existing state-driven functions, for the post-collapse render path. The current-from-psi variant returnsjflattened to(16384,)(cell-major) so consumers don't re-flatten in the worker.qm_4d_bridge.get_density_matrix_of(state, piece_id, *, neighborhood_radius=1)— partial implementation that replaces the previous unconditionalNotImplementedError. Computes a Manhattan-neighborhood channel reduced density giving per-piece purity for the M14.3 entanglement-halo viz to light up. Carries anisPartial: Trueflag; the full η-metric construction (ADR-005) ships later with the same signature.
- Tier 2: consumer ergonomics
HybridCache.clear()— drops cached entries + resets counters for the chess4D-OC reset path.qm_4d_bridge.channel_energies_2d/qm_4d_bridge.channel_energies_4d— Pyodide-friendly entry points (encode + channel-energy in one shot, JS-serializable dict). Saves consumers from importing the engine sub-package.
- 33 immolation tests lock the wishlist surface contracts; the existing 95-test bridge / hybrid-eval surface continues to pass.
What's new in v1.13 (May 2026)¶
The §20.15 fourth-tier ship (partial): encoder-eval speedup work.
A new spectral_hybrid evaluator computes channel energy directly
from a SpectralBIPHybrid2D (skipping the float decode step), an
LRU-cached wrapper makes that practical for the §16 search engine,
and a benchmark harness records before/after numbers for each path.
~15× speedup at the warm-LRU steady state vs spectral_float64.
-
spectral_hybridmodule —evaluate,evaluate_from_hybrid,channel_energies_from_hybrid. The mathematical identity:‖v_c‖² = Σ (sign × mag)² = Σ mag²(sign cancels in squared sum), so use uint8² sum + per-channel scale² multiply. Skips the float decode step entirely on the cache-hit path. -
spectral_hybrid_cachemodule —HybridCacheLRU plusmake_cached_evaluator(magnitude_bits=8, cache_size=10000)factory returning(evaluator_fn, cache). Drop the callable intoSearchOptions.evaluatorand inspectcache.stats()for hit/miss diagnostics. Models the §16 TT-cache-hit pattern. -
spectral_float32module — float32 downstream sibling mirroring the ephemerides two-stage architecture (their complex128 → complex64 is our float64 → float32). Shipped despite a null bench result on the standalone path; a build- block for future float32-native encoder work. -
tests/bench_spectral_eval.py— diagnostic benchmark harness with three corpora (opening / midgame / endgame). Variants registered as plug-ins; the discipline: no speedup claim ships in the CHANGELOG without measured numbers from this harness on a fixed corpus. Baselines saved attests/bench_baselines/before_1.13.0.json. -
§16.7 amendment — the Othello prior (Edax-spectral, +243 Elo at L6 / 0 Elo at L10+) is now flagged as ML-fork-contaminated; B-spike-3 is no longer gated on the depth-decay claim. The §16.5 design discipline (test multiple depths, audit training target, don't trust eval-task RMSE as Elo proxy) remains sound.
-
33 new immolation tests lock the algebraic identity (channel- energy from hybrid agrees with float64 within 5% relative on the corpus; sign agreement exact at 8-bit and 4-bit), the LRU cache semantics, and the float32 sign-agreement-within-1e-4 property.
This is B-spike-3 in the §20.15 phasing — 4 of 5 phases now shipped (1.10.0-1.13.0). B-spike-4 (pure-phase rewrite) shipped across the 1.14.0-1.18.0 sequence: 2D in 1.14.0, 4D in 1.15.0, vectorized in 1.17.0, C-ported in 1.18.0.
What's new in v1.12 (May 2026)¶
The §20.15 third-tier ship: encoder BIP-hybrid — sign × magnitude
factoring for the spectral encoder. Sign packs as 1 bit per dim
(algebraically exact); magnitude quantizes to 4 or 8 bits per dim
with per-channel scaling. Cosine-sim ≥ 99.99% at 8-bit on both 2D
and 4D acceptance corpora. No breaking changes vs 1.11.x; encode_640
/ encode_4d defaults unchanged.
-
Storage: 760 bytes (2D) / 50 KB (4D) at 8-bit, vs 2,560 / 180 KB float32 — about 3.4-3.6× compression with negligible cosine-sim loss. At 4-bit: 440 / 28 KB → 5.8-6.4× compression; passes acceptance for 2D, lands just under 99.5% for 4D (research finding documented in §20.18).
-
encode_2d_bip_hybrid/encode_4d_bip_hybrid— new public API. ReturnsSpectralBIPHybrid2D/SpectralBIPHybrid4Ddataclasses with three integer fields:sign_packed(bytes),magnitude_scales(per-channel float32 norms),magnitudes(uint8 — packed nibbles for 4-bit, plain bytes for 8-bit). -
decode_2d_bip_hybrid/decode_4d_bip_hybrid— reconstruct float64 vector. Lossy at the magnitude-quantization level; sign storage is exact. -
cosine_similarity_hybrid_*— pair-wise distance metric for hybrid vectors. Approximates the float32 baseline within 1% absolute deviation at 8-bit. -
Bridge surface —
chess_spectral_4d.bridge.{encode_position_2d_bip_hybrid, encode_position_4d_bip_hybrid}for chess4D-OC / Pyodide consumers. Returns plain Python int/list across the WASM boundary. -
62 immolation tests lock the §20.15 acceptance gate (8-bit on 2D + 4D), the sign-bit storage exactness, the per-channel magnitude bounds, the 4D 4-bit research finding (sim ∈ [0.99, 0.999]), the pair-wise cosine-sim approximation, and the 4-bit nibble packing round trip.
This is B-spike-2 in the §20.15 three-tier phasing. B-spike-1a (sheet-block BIP) shipped in 1.10.0; B-spike-1b (ALU-native phase engine) shipped in 1.11.0; B-spike-3 (search-engine integration) shipped in 1.13.0; B-spike-4 (pure-phase rewrite) shipped across 1.14.0-1.18.0 (2D → 4D → vectorize → C port).
What's new in v1.11 (May 2026)¶
The §20.15 second-tier ship: ALU-native phase-operator move
generator. The §11 phase-operator engine — already integer-
arithmetic at the core, already BIP-isomorphic at the group-
theoretic level over Z_640 — gains a public integration entry
point that doesn't require a python-chess.Board argument. Pure
ALU-native pseudo-legal move generation for Pyodide / WASM /
non-Python downstream consumers. No breaking changes vs 1.10.x.
-
phase_only_pseudo_legal_moves(pos, side_to_move_white, ep_file=...)— new integration entry point. Iterates the side-to-move's pieces, computes per-piece destination sets via Solution B's phase-arithmetic, returns a flat list of(from_sq, to_sq, promotion_char)tuples in encoder sq convention. Pseudo-legal in the python-chess sense (respects piece geometry, occupation, double-push, en passant, promotion expansion); caller filters check. -
occupation_field_from_pos_dict+ep_phase_from_ep_file— pure-phase adapters that skip python-chess entirely. Counterparts of the existingoccupation_field_from_boardandep_phase_from_board; consumers that have a position dict + side-to-move + ep_file (e.g., from a 1.10.0 BIP-encoded sheet block) can build the occupation field directly without reconstructing a Board. -
Acceptance gate: parity vs
board.pseudo_legal_moveson a representative 8-FEN corpus (opening, middlegame, endgame, EP-active, promotion-imminent). All move sets match exactly modulo castles (deferred to 1.12.0+). 26 tests, pass on first implementation. -
Documented
Z_640wire contract (notebook §20.17). The constantsMODULUS,ROW_GEN,COL_GEN,DIAG_NE_SW_GEN,DIAG_NW_SE_GEN,KNIGHT_SHIFTS,KING_SHIFTSare part of the public API and don't move without a major version bump. -
Diagnostic benchmark (
tests/bench_phase_operator_movegen.py) — ~190 µs at startpos for ALU-native vs ~73 µs for python-chess Cython (~2.5× ratio, expected). Structurally faster than Solution-B-per-piece-loop because the occupation field is built once per position instead of per-piece. Value is portability, not raw speed. -
Deferred to a 1.12.0+ follow-up: castling generation in the ALU-native path (needs attack map), check-filter refactor of
phasecast_is_check(currently takes a Board input). Consumers wanting check-filtered legal moves should pair the 1.11.0 pseudo-legal output with the existingphasecast_is_check.
This is B-spike-1b in the notebook §20.15 three-tier phasing. B-spike-1a (sheet-block BIP) shipped in 1.10.0; B-spike-2 (encoder BIP-hybrid) is the next ship and depends on the bit-packing patterns 1.10.0 established.
What's new in v1.10 (May 2026)¶
The §20 BSHDC spike's first ship: BIP-encoded sheet block. Integer-native form of the 1.9.0 non-Markovian aux block that packs the same content into 3 bytes (vs 88 bytes float64) with a bit-exact round trip on the legal state space. ALU-native operator fast paths for Pyodide / chess4D-OC / batch retrieval consumers. No breaking changes vs 1.9.x; both representations ride side-by-side.
-
SheetStateBIPdataclass —categorical: uint16+halfmove_clock: uint8. ~29× smaller than the 1.9.0 float64 path. Round-trip exact for all 87,264 legal sheet states (864 categorical × 101 half-move) — verified exhaustively intests/test_sheets_bip.py. The Z₁₀₁ half-move stays in its ownuint8slot per §19.4's "structural split" finding (no embedding into Z₁₂₈ that would introduce a wrap discontinuity). -
Operator fast paths — single-integer-op queries against the categorical portion:
castling_alive,kingside_castling_alive,ep_target_active,fifty_move_rule_triggered,threefold_claimable, etc. Each is a bit-mask + comparison, no FPU. -
Distance metrics —
hamming_distance_categorical(a, b)for corpus-similarity retrieval where binary state distinctions matter (sharper than float cosine-sim for the categorical portion).halfmove_distance(a, b)for the integer Z₁₀₁ slot. -
Bridge surface —
chess_spectral_4d.bridge.get_sheet_state_bip/encode_sheet_aux_bip/decode_sheet_state_from_bip. All numpy-free across the WASM boundary; integers cross the Pyodide bridge directly. -
Where BIP wins: Pyodide / WASM consumers (50-100×), batch retrieval over saved corpora (~3 orders of magnitude), Hamming-distance corpus filtering, future v5 mode-2 XOR-stream wire format compression. Where it doesn't: single-position depth-1 queries (per §19.10's bitboard floor — bit-shift + AND can't undercut python-chess's
int & maskeither). -
35 new immolation tests (
tests/test_sheets_bip.py) lock the 87,264-case exhaustive round trip, the 8-operator parity vs the 1.9.0 SheetState path, the Hamming-distance behavior (including the subtle case that rep=1 vs rep=2 differ in 2 bits, not 1, due to binary representation), the bridge round-trip, and the python-chess + GameState4D factory parities.
This is B-spike-1a in the notebook §20.15 three-tier phasing. B-spike-1b (promote §11 phase-operator engine to public API) remains independent and can ship separately. B-spike-2 (encoder BIP-hybrid) depends on the bit-packing patterns this 1.10.0 establishes.
What's new in v1.9 (May 2026)¶
The §19 spike's Phase-1 sheet block ships as a
representation-completeness feature alongside a new
legal-moves CLI command for both 2D and 4D. No API breaks vs.
1.8.x; every addition is opt-in surface.
-
Non-Markovian sheet aux block (
from chess_spectral import SheetState, encode_aux_block) — the 11-dim aux block carries castling rights, en-passant target, side-to-move, half-move clock, and repetition count alongside the base encoder vector. Opt in via the newsheets=kwarg onencode_640(pos, sheets=...)andencode_4d(pos4, sheets=...); output dimension grows from 640 → 651 (2D) and 45056 → 45067 (4D), with the base dims byte-identical to legacy / C encoder output. Lift state frompython-chessviaSheetState.from_chess_board(board)or from a 4DGameState4DviaSheetState.from_game_state_4d(state). Half-move clock uses aZ_101Fourier carrier preserving clock-distance fidelity; round-trip exact for every integer[0, 100]. -
Representation only — no speed claim. Notebook §19.10 documents the depth-1 bitboard floor:
python-chess'shas_castling_rightsis one int-AND, and float→int conversion from a numpy slice can't undercut it. The same logic holds forBoard4D.halfmove_clock >= 100and the hash-table threefold- repetition lookup. The sheet block's value is making encoder vectors self-sufficient for downstream consumers of saved / transmitted vectors (chess4D-OC, Pyodide pipelines, batch retrieval over saved corpora) — not raw runtime speed. -
legal-movesCLI command (2D) —chess-spectral legal-moves --fen "<fen>" [--format uci|san|json] [--with-sheets]enumerates the side-to-move's legal moves. Default UCI output, one move per line; JSON format includes per-move flags (capture / castling / en-passant / promotion) plus optional sheet block. Stdin support via--fen -. Usespython-chessfor legal-move generation. -
legal-movesCLI command (4D) —chess-spectral-4d legal-moves --fen4 "<fen4>" [--format compact|json] [--with-sheets] [--side-to-move white|black] [--halfmove-clock N] [--fullmove-number N]enumerates legal moves at a 4D-OC position. Default compact formatx,y,z,w->x,y,z,wwithDP(double push),EP(en-passant capture), and=Q(promotion) tags; JSON format structured. UsesBoard4D.legal_moves()(the native 4D move-gen via graph-Laplacian-derived primitives). -
Pyodide-bridge surface for sheets —
chess_spectral_4d.bridgegainsget_sheet_state,encode_sheet_aux, anddecode_sheet_aux_from_vector. The bridge contract holds ("plain dict, no numpy across the WASM boundary"); aux blocks cross aslist[float].chess4D-OCand any other browser / Pyodide consumer can now reason about castling rights / EP / halfmove / repetition without reconstructing apython-chess.Boardor aGameState4Dworker-side. -
encode_2dfuture-proof alias —from chess_spectral import encode_2d(1.9.0+) is the recommended name going forward, mirroring the 4D path'sencode_4d.encode_640remains as a permanent alias, but the dim count is not a stable contract — sheets already bumped 640 → 651, and future channel enrichment (per the §20 BSHDC spike's antikythera-spectral reference) or further non-Markov state extensions may move it again. QueryENCODING_DIM(or checkenc.shape) and iterate channels viaCHANNELSrather than hardcoding 640. See notebook §19.11 for the full future-work note. -
53 new immolation tests lock the sheet round-trip (every legal halfmove value, every castling combination, every EP file), the encoder integration (base preservation + aux at correct offset), the factory lifts (
from_chess_board,from_game_state_4d), the CLI smoke surface for both 2D and 4D legal-move enumeration, and the bridge round-trip (state → get → encode → decode) for both dimensions.
1.9.1 update — README polish + v5 wire format design decision.
Quick-start example switched from encode_640 to the
future-proof encode_2d alias (the 640-dim count is no longer a
stable contract — see §19.11). PyPI project links gain a
Roadmap entry pointing at
ROADMAP.md.
v5 wire format officially documented as not carrying the sheet
aux block — sheets ride alongside in-memory vectors, and on-disk
the source PGN / NDJSON / FEN sidecar already carries the
non-Markov state. See frame_v5.py
header note + notebook §19.12 for full rationale and the future-
extension path (223 reserved bytes in the v5 header give plenty
of room when a consumer eventually needs persisted sheet frames).
What's new in v1.8 (May 2026)¶
The chess4D-OC visualizer's M11.40 unblocker release. Tier-1
of the upstream wishlist ships in 1.8.0 — GameState4D graduates
from a position+history snapshot to a persistent mutation type,
mirroring python-chess.Board's push/pop ergonomics so the
chess4D-OC worker can drop the python-chess4d-oana-chiru
runtime dep. No API breaks vs. 1.7.x — every addition is opt-in
surface.
-
GameState4D.push(move)/GameState4D.pop()— apply / undo a ply, mutating in place.pushaccepts aMove4Dor a((from_xyzw), (to_xyzw)[, promote_to])tuple.popraisesIndexErroron an empty history (parallel tochess.Board.pop()'s contract). Returns the recorded / poppedMove4Dso callers can recover capture / promotion metadata. -
GameState4D.boardview — read-only proxy over the live position dict. Exposesoccupant(sq)andpieces_of(side)accessors plus__contains__and__len__.sqaccepts both the linearintand the(x,y,z,w)Coord4Dtuple. The view does not copy — push / pop mutations are reflected immediately. -
GameState4D.to_fen()/GameState4D.from_fen(fen4)— symmetric aliases forto_fen4/from_fen4(the 1.7.1 slash-tolerant FEN4 form is accepted on both names). -
GameState4D.iter_pieces()— yields(sq_idx, piece_value)tuples in the formatchess_spectral.encoder_4d.encode_4dconsumes directly.dict(state.iter_pieces())is a one-liner replacement for the chess4D-OC worker's previous_state_to_pos4helper. -
chess_spectral_4d.engine.search.search()acceptsGameState4D— sameSearchResultshape as before; the engine constructs a transientBoard4Dinternally. Drops theBoard4D.from_fen(state.to_fen4())hop chess4D-OC was previously paying per search call. -
GameState4D.is_check()/is_checkmate()/is_stalemate()— wrapsBoard4D.is_check()plus a short-circuit legal-moves probe for mate / stalemate.is_checkmate/is_stalematebail on the first legal move vianext(iter(...), None), so the dense-position cost isO(legal-move generation)worst case — about 2s at the dense 28-king start with the 1.7.0 native bitboard fast-path active. -
16 new immolation tests in
tests/test_gamestate4d_consumer_surface.pylock down the contract shape and at least one concrete behaviour per wishlist item. Hard gate — chess4D-OC's M11.40 PR will fail loudly if any of these regress.
1.8.1 update — canonical 4D initial position landed.
chess_spectral_4d.initial_position() returns a fresh
GameState4D at the Oana-Chiru §3.3 4D starting layout (896
pieces total: 448 white + 448 black, 28 kings per side).
STARTING_FEN4 exposes the same layout as a FEN4 v1 string for
consumers that want a literal. Slice helpers
(central_slices(), white_only_slices(), black_only_slices(),
empty_slices()) expose the §3.3 (z, w) classification with
|C| = 4, |W_only| = 24, |B_only| = 24, |E| = 12. 24 new
immolation tests including a SHA-256 hash lock on the canonical
FEN4 string.
Tier-2 (ψ-driven density / current, partial-trace density matrices) is still deferred — needs the η-metric machinery from ADR-005.
What's new in v1.7 (May 2026)¶
The chess4D-OC visualizer wishlist release. Headline pieces:
-
SearchOptions.time_budget_mshonored mid-iteration (chess_spectral_4d.engine.search). Previously the deadline was checked only between iterative-deepening iterations — a 5-second budget on the dense 28-king starting position could overrun by ~100× before depth-1 alone completed. v1.7 threads the deadline into the alpha-beta inner loop and returns the deepest-completed- so-far best move on deadline exit. NewSearchResult.timed_out: boolfield distinguishes deadline-exit from natural completion. -
Native bitboard fast-path (
chess_spectral.spatial_4d,cs_bitboard4dshared library). Pure-C primitives for the 4096-bitBitboard4D— popcount, bitwise AND/OR/XOR/NOT/sub, per-square set/clear/toggle/test, predicates, and the load-bearingcs_bb4_to_squaresiteration helper (per-bit LSB extraction in C plusb &= b - 1clear, all without crossing the ctypes boundary per square). Ships in the wheel underchess_spectral/_native/; loaded via ctypes at import. Pure-PythonBitboard4Dcontinues to work as fallback (sdist install, Pyodide / micropip) — verified by a dedicatedfallback-testCI job.Bitboard4D.to_squares()/.squares()route through the native helper whenchess_spectral.HAS_NATIVE_BITBOARDis True; ~16× faster iteration on dense bitboards. -
Board4D.legal_moves()algorithmic refactor. The legal-move filter at the dense 28-king start position previously called_is_attackedonce per own king (K=28 calls, each iterating all N opponent attackers from scratch). v1.7 iterates attackers once and testsking_bb.intersects(attack_set)per attacker, short- circuiting on the first hit — same per-op cost (O(1) bitboard test) but one call replaces K. ~12.7× faster on the representative non-in-check 264-piece position. -
Cumulative wishlist outcome. The chess4D-OC visualizer's reported
legal_moves()~250s pain point at the standard 28-king start is now ~2s on the same hardware (~125× faster) with no API changes. Time-budget-checked search now respects user budgets within ~0.5s grace regardless of position density. -
Downstream consumer flag.
HAS_NATIVE_BITBOARDis exposed at the top-levelchess_spectralpackage — consumers can dofrom chess_spectral import HAS_NATIVE_BITBOARDto badge "native fast-path active" or fall back cleanly when the native lib isn't present.
For the full release history see CHANGELOG.md.
What's new in v1.6 (April 2026)¶
The §16 ship-gate release. Headline pieces:
- Search core, tournament harness, and sweep ship-gate runner ship as CLI commands at both 2D and 4D. Per-side symmetric agent specs let white and black be configured independently in the same single-process tournament loop:
spectral_py sweep \
--evaluators material,spectral,qm \
--depths 1,2,3,4 \
--n-games-per-pair 10 \
--time-budget-ms 5000 \
-o sweep.json
-
Three §16.1 evaluator families (
material,spectral,qm) ship at both 2D and 4D with a uniformevaluate(position, side_to_move) -> floatcontract — drop in any of them as the search heuristic. -
Three independent in-house move-rule oracles — each encodes the same legality predicate ("is move m legal in position p?") through a different mathematical lens, validated head-to-head against the
python-chess[4d]reference and against each other. Not because one is more correct — they all agree on the same legal set — but because each lens is a standalone artifact for studying how spatial motion can be encoded: -
Bitboard / attack-tables (
chess_spectral.spatial_4d) — the engineering lens. Bitboard4D primitive, per-piece attack tables (knight, king, rook, bishop, queen via magic-bitboard- style ray casting), axis-typed pawn moves (Pw/Py per Oana-Chiru §3 Def 11), Board4D game state, and draw rules. Same idea as a classic chess engine, lifted to Z_8^4. - Phase-space operators (
chess_spectral.phase_operators,chess_spectral.phase_operators_4d) — the algebraic lens. Per-piece move generation as group actions on a phase-space representation; legality is "the candidate move is in the orbit of the piece operator, intersected with the occupation oracle." 2D ships under §11; 4D under §13. -
Discrete-Laplacian eigenbasis oracle (2D + 4D) — the spectral lens. The lattice's discrete Laplacian (Kron-sum of P_8 path-graph Laplacians; eigenvectors form a DCT-style basis) doubles as a structural lookup table for move legality — the same eigenbasis the spectral encoder uses to embed positions also tells you which moves are reachable. Concrete demonstration that "encode the geometry" and "encode the rules" share one foundation.
-
v5 unified
.spectral[z]/.spectralz4wire format (chess_spectral.frame_v5) — single 256-byte header serves both 2D and 4D via explicitn_dimensionsfield. Three encoding modes selected byencoding_mode: dense (= legacy v2/v4 frame body), per-channel replacement (variable-size, ~2.84× compression on 4D stable workloads), and XOR-stream (fixed-size, 7.23× compression on 4D vs dense gzipped). Seedocs/WIRE_FORMAT.mdfor the byte-level spec covering all four shipped versions (v2/v3/v4/v5) and the reader-dispatch convention. -
CI gate: the 15-cell
verify-wheelsmatrix is now opt-in via thewheel-checkPR label (was running on every PR; saves ~150 runner-min/PR while keeping the publish-time matrix as the load-bearing wheel-correctness gate).
For the full release history see
CHANGELOG.md.
Both packages share a single dist version derived from
importlib.metadata; see Install below.
Install¶
From PyPI (recommended):
The base install pulls only numpy and scipy — sufficient for
encoding, the 4D phase operators, the kinematic QM layer, and the full
§17.1 bridge surface.
Optional extras:
# PGN ingest via chess_spectral.corpus (adds python-chess)
pip install "chess-spectral[corpus]"
# 4D phase-operator validation gates against the Oana & Chiru oracle
pip install "chess-spectral[test]" # adds python-chess4d-oana-chiru
Package page: https://pypi.org/project/chess-spectral/
From source¶
Editable install from a local checkout:
From a git URL (pin a commit in production):
pip install "git+https://github.com/lemonforest/mlehaptics.git@COMMIT#subdirectory=docs/chess-maths/chess-spectral/python"
After install, two console scripts are on your $PATH:
Both packages also expose __version__, derived dynamically from the
installed dist:
>>> import chess_spectral, chess_spectral_4d
>>> chess_spectral.__version__ == chess_spectral_4d.__version__
True # both derive from importlib.metadata.version("chess-spectral");
# they cannot drift from each other or from the wheel.
In-place (no install)¶
The legacy workflow still works: every test and analysis script uses
sys.path.insert to bootstrap off the python/ directory, so
pytest docs/chess-maths/chess-spectral/python/tests/ runs without any
install.
Quick start (2D)¶
>>> from chess_spectral import (
... encode_2d, channel_energies, read_encodings, fen_to_pos,
... ENCODING_DIM, CHANNELS,
... )
>>> pos = fen_to_pos("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
>>> enc = encode_2d(pos)
>>> enc.shape
(640,)
>>> ENCODING_DIM # don't hardcode 640; the dim count may grow (see §19.11)
640
>>> channel_energies(enc)
{'A1': 0.0, 'A2': 19.845, 'B1': 45.2825, 'B2': 45.2825,
'E': 322.57, 'F1': 88.77, 'F2': 1851.01, 'F3': 1507.65,
'FA': 19.92, 'FD': 0.0}
# Read a whole game that was encoded by either C or Python
>>> hdr, arr = read_encodings("game.spectralz") # transparent gzip
>>> arr.shape
(161, 640)
Quick start (4D, 45 056-dim)¶
The 4D encoder runs on the Z_8^4 hypercubic lattice with B_4
hyperoctahedral symmetry adaptation (per Oana & Chiru, AppliedMath
6(3):48, 2026). Output is a 45 056-dim float32 vector partitioned into
11 channels of 4096 modes each: A1, STD4_X/Y/Z/W,
FIB_SYM_1/2/3, FA_PAWN_W, FA_PAWN_Y, FD_DIAG.
>>> from chess_spectral.fen_4d import parse
>>> from chess_spectral.encoder_4d import (
... encode_4d, channel_energies_4d, CHANNELS_4D,
... )
>>> pos = parse("4d-fen v1: K@4,0,0,0; k@4,7,7,7")
>>> v = encode_4d(pos)
>>> v.shape, v.dtype
((45056,), dtype('float32'))
>>> energies = channel_energies_4d(v) # per-channel L2 energies
>>> sorted(energies)
['A1', 'FA_PAWN_W', 'FA_PAWN_Y', 'FD_DIAG',
'FIB_SYM_1', 'FIB_SYM_2', 'FIB_SYM_3',
'STD4_W', 'STD4_X', 'STD4_Y', 'STD4_Z']
The 4D game-state surface (move history, draw detection, FEN4
round-trip, promotion-piece argument) lives in chess_spectral_4d:
>>> from chess_spectral_4d import GameState4D, apply_move, MoveHistory4D
>>> from chess_spectral_4d import bridge
# Load a placement from a FEN4 string (white-to-move by default)
>>> result = bridge.load_state("4d-fen v1: K@4,0,0,0; k@4,7,7,7")
>>> state = result['state']
>>> isinstance(state, GameState4D)
True
# apply_move(state, from_sq, to_sq, *, promote_to='Q') is the v1.4 API
>>> # state2 = apply_move(state, from_sq=4, to_sq=5, promote_to='Q')
# Draw status: priority threefold > 50-move > insufficient > stalemate
>>> bridge.get_draw_status(state, has_legal_moves=True)
{'ok': True, 'status': 'none'}
For raw move-rule logic (legal-move generation, check detection) on
Z_8^4, see chess_spectral.phase_operators_4d below.
Quick start (QM extension, v1.5+)¶
The QM extension ships in two layers, mirroring the standard physics split:
- Kinematics —
chess_spectral.qm_4d. What states and operators look like: state space, observables, measurement structure, theB_4group action. Lifts encoder output toψ ∈ ℂ^{45056}, exposes the 11-channel decomposition as a built-in projection-valued measure (PVM), builds five Hermitian piece-reach observables (rook / bishop / queen / king / knight) on the per-channelℂ^{4096}factor, and ships the 384-elementB_4unitary representation as a cached LUT. - Dynamics —
chess_spectral.qm_4d_dynamics. How states change: the 11 per-channelu_move_*builders for move-as-unitary transitions (discrete) plusevolve_under_h0for Zeno-style continuous time evolution between move boundaries.
Consumers that want a Pyodide-JSON-shaped surface use the
chess_spectral.qm_4d_bridge dispatch layer described in the next
section.
>>> from chess_spectral.fen_4d import parse
>>> from chess_spectral.qm_4d import (
... state_to_psi,
... prob_channel, measure_channel_distribution,
... channel_projector,
... H_rook_4, H_bishop_4, H_queen_4, H_king_4, H_knight_4,
... measure_observable_distribution,
... b4_unitary_rep_4096, b4_unitary_rep_full,
... expectation, is_normalized, is_hermitian, is_unitary,
... )
>>> pos = parse("4d-fen v1: K@4,0,0,0; k@4,7,7,7; R@0,0,0,0")
>>> psi = state_to_psi(pos, side_to_move=True)
>>> psi.shape, psi.dtype
((45056,), dtype('complex128'))
>>> is_normalized(psi)
True
# Born-rule channel measurement: probability mass per channel
>>> prob_channel(psi, c=0) # A1 channel
3.3e-08
>>> probs = measure_channel_distribution(psi) # all 11 channels
>>> abs(probs.sum() - 1.0) < 1e-10
True
# ⟨ψ|H_rook|ψ⟩ on the rook channel block
>>> # expectation(H_rook_4, psi[0:4096]) # H_piece_4 is sparse 4096x4096
# Born-rule eigenbasis distribution: |⟨φ_k|ψ⟩|² grouped by eigenvalue
>>> # eigvals, probs = measure_observable_distribution(H_rook_4, psi[0:4096])
# B_4 group action (384 elements). 4096-dim per-channel block, or the
# I_11 ⊗ U_4096(g) Kronecker extension to the full 45 056-dim space.
>>> # U = b4_unitary_rep_full(g) # cached; sparse 45056x45056
The Hermitian piece-reach observables H_rook_4, H_bishop_4,
H_queen_4, H_king_4, H_knight_4 are built on demand and cached.
They are real-symmetric on ℂ^{4096} with integer / near-integer
spectra (Hermiticity verified at floating residual ~5e-15; Pre-flight
2 in qm_4d.py). Pawn observables break
Hermiticity under the standard inner product (directed push) — the
pseudo-Hermitian η-metric construction is deferred to v1.8+ per
ADR-005.
b4_unitary_rep_4096(g) and b4_unitary_rep_full(g) realize the
order-384 hyperoctahedral group as sparse unitaries on ℂ^{4096} and
ℂ^{45056} respectively (the latter is I_{11} ⊗ U_{4096}(g) — same
B_4 action applied independently to each of the 11 channels). Both are
cached per group element. measure_observable_distribution(H, ψ)
diagonalizes any Hermitian observable on ℂ^{4096} and returns the
Born-rule probability distribution over distinct eigenvalues.
Move-as-unitary dynamics (Phase 4, Track B) live in
chess_spectral.qm_4d_dynamics. The module ships per-channel builders
for all 11 channels and both non-capture and capture moves:
u_move_a1— A_1 channel via projector-sandwich (B1).u_move_std4— STD4_X/Y/Z/W via similarity-transform; same-orbit is strict-unitary, cross-orbit returns a measurement-only marker (B3a, ADR-003 amendment).u_move_fa_pawn— FA_PAWN_W/Y via axis-parity-odd projector sandwich (B3b).u_move_fib_meas— FIB_SYM_½/3 via measurement-only re-encode (B3c, per the Phase 3.5 amendment to ADR-003 §3.3).u_move_fd_diag— FD_DIAG via rank-1 update + renormalization (B3d/e).evolve_under_h0+H_FREE_4D— Zeno-style continuous evolution between move boundaries, whereH_0 = -Δis the lattice Laplacian (B2, ADR-002).
§17.1 Pyodide bridge surface (v1.5)¶
chess_spectral.qm_4d_bridge is the consumer-facing bridge —
the 7 §17.1 QM-extension methods plus 6 §17.5 dev/debug methods —
designed for Pyodide consumers (e.g., the chess4D-OC visualizer)
that need Pyodide-JSON-serializable returns and Float32 ψ-amplitudes
ready for Float32Array shader uploads.
>>> from chess_spectral.qm_4d_bridge import (
... # §17.1 (QM-extension surface)
... get_qm_state, get_qm_density, apply_move_qm, apply_move_qm_full,
... measure_at, get_density_matrix_of, get_probability_current,
... get_qm_expectation,
... # §17.5 (dev / debug surface)
... get_version, get_encoder_shape, get_fen4_state, load_fen4,
... load_jsonl_fixture, has_legal_moves,
... )
# Round-trip: load a FEN4, get ψ as Float32 interleaved, apply a move,
# get the updated ψ.
>>> r = load_fen4("4d-fen v1: K@0,0,0,0; k@7,7,7,7; R@1,0,0,0")
>>> state = r['state']
>>> r = get_qm_state(state, side_to_move=True)
>>> r['basisDim'], r['psi'].dtype, r['psi'].shape
(45056, dtype('float32'), (90112,)) # 2 × 45 056 — real+imag interleaved
>>> abs(r['normSq'] - 1.0) < 1e-6
True
# Per-cell density: |ψ|² summed across the 11 channels per cell
>>> r = get_qm_density(state)
>>> r['density'].shape, abs(r['density'].sum() - 1.0) < 1e-6
((4096,), True)
# Apply a move and get the assembled ψ_post (Float32 interleaved).
# move format: (from_sq, to_sq) as ints OR ((x,y,z,w), (x,y,z,w))
>>> # r = apply_move_qm_full(state, move=(1, 2))
>>> # r['ok'], r['psi'].shape
>>> # (True, (90112,))
# §17.5 debug surface
>>> get_version()['version'] # e.g., '1.5.0'
>>> get_encoder_shape()['totalDim'] # 45056
>>> get_encoder_shape()['channels'] # [{'name': 'A1', 'offset': 0, 'dim': 4096}, ...]
Wire format (ComplexArray, Float32Array-friendly): every ψ
return is a 1-D Float32 array of length 2 * 45056 = 90112, where
psi[2k] is Re(ψ_k) and psi[2k+1] is Im(ψ_k). This matches the
§17.1 contract documented in the
research notebook §17.1.
apply_move_qm vs apply_move_qm_full. The low-level
apply_move_qm returns a per-channel dispatch dict (mixed csr_matrix
+ marker dict values) for consumers that want to reason about per-
channel structure. The high-level apply_move_qm_full does the
block-by-block assembly (csr_matrix → U_chan @ ψ_pre[block];
marker dict → psi_post_block splice) and returns the assembled
ψ_post as Float32 interleaved. Most consumers want the _full
variant.
The B5 milestone (April 2026) closed the last unshipped channels, so
the bridge no longer raises for any move type — non-captures and
captures both succeed via the channels' B5 capture-path branches. See
qm_4d_bridge.py for per-method
docstrings and qm_4d_dynamics.py
for the per-channel construction details.
Deferred to v1.8+:
- get_density_matrix_of (reduced density matrix; needs partial-
trace machinery on channel labels).
- get_qm_density(piece_id=...) (per-piece marginal; same blocker).
Both raise NotImplementedError with a pointer to the v1.8+ milestone.
(v1.7.0 shipped the D1 / D2 native fast-path workstream — see the
"What's new in v1.7" section below — and rolled the partial-trace
work to a follow-up.)
CLI¶
The 2D CLI (chess-spectral, entry point chess_spectral.cli:main)
mirrors the C spectral CLI subcommand-for-subcommand. Output is
byte-identical to the C binary on the same input — the
spectral csv command produces the same bytes on either side.
chess-spectral csv game.spectralz -o game.csv
chess-spectral encode -i game.ndjson -o game.spectralz -z
chess-spectral encode-fen --fen "..." -o single.spectral
chess-spectral compare a.spectralz b.spectralz
chess-spectral query game.spectralz --ply 30
chess-spectral heatmap game.spectralz --ply 30 --channel A1
chess-spectral analyze game.spectralz
chess-spectral export game.spectralz -o game.json
chess-spectral version
The 4D CLI (chess-spectral-4d):
chess-spectral-4d tables-verify --phase all
chess-spectral-4d encode-fen4 --fen4 "4d-fen v1: K@0,0,0,0; ..."
chess-spectral-4d encode-moves4 --moves game.ndjson4 -o game.spectralz4 -z
chess-spectral-4d corpus-gen --games game1.ndjson4 game2.ndjson4 ...
chess-spectral-4d version
Both CLIs follow the --help discipline: every subcommand and every
argument has non-empty help text. Run <cmd> --help (or
<cmd> <subcommand> --help) before invoking; the immolation suite
gates this in CI.
Layout¶
chess_spectral/ # 2D + 4D encoder math + QM extension
__init__.py # __version__ via importlib.metadata
encoder.py # encode_2d(pos) → np.ndarray (encode_640 = legacy alias)
frame.py # v2 .spectral[z] binary I/O + transparent gzip
csv_export.py # dist_prev / cos_prev / energies CSV
cli.py # `chess-spectral` (2D CLI)
phase_operators/ # 2D §11 phase-space move generator (1.2.0+)
encoder_4d.py # encode_4d(pos4) → float32(45056,)
frame_4d.py # v3/v4 .spectralz4 binary I/O (legacy reader)
frame_v5.py # v5 unified wire format (2D + 4D, 3 encoding modes; default for new writes from v1.6)
tables_4d.py # B_4 group, lattice tables, eigenmodes
fen_4d.py # FEN4 v1 placement-literal parser + serialize
phase_operators_4d/ # 4D §13 phase-operator move engine (1.3.0+)
qm_4d.py # Track A kinematic QM front-end (1.5.0+)
qm_4d_dynamics.py # Track B per-channel U_move builders (1.5.0+)
qm_4d_bridge.py # §17.1 + §17.5 Pyodide bridge surface (1.5.0+)
chess_spectral_4d/ # 4D game-state surface (1.4.0+)
__init__.py # GameState4D, Move4D, MoveHistory4D, apply_move, bridge
move_history.py # ply log, side-to-move, 50-move clock, repetition hash
apply_move.py # apply_move(state, from_sq, to_sq, *, promote_to='Q')
bridge.py # load_state, get_draw_status, get_move_history,
# is_insufficient_material_2d
cli.py # `chess-spectral-4d` (4D CLI)
pyproject.toml # PEP 621 packaging metadata
tests/ # pytest suite (see test count below)
Test count (post-v1.5): 45 895 tests collected. Breakdown:
~44 876 parametric 4D phase-operator tests (the bulk), 81-test
end-to-end immolation suite (test_smoke_e2e.py, expanded from 41
for v1.5 surface coverage), 272 v1.5 QM tests across the kinematic
front-end (test_qm_4d.py, test_qm_4d_z2_grading.py), the Track B
B1..B5 dynamics gates (test_qm_4d_dynamics_b{1,2,3a,3b,3c,3d,5}.py),
and the §17.1/§17.5 bridge surface (test_qm_4d_bridge_v15.py),
plus 102 fast tests, 260 pawn-axis / phase-4d-check / phase-4d-
unobstructed tests, 92 2D phase_operators tests, and 210 v1.4
game-state tests. Run via
pytest docs/chess-maths/chess-spectral/python/tests/.
Phase operators (2D, §11)¶
chess_spectral.phase_operators ships a phase-space move generator
and check detector (added in 1.2.0). The primitives compute all moves
and check relationships as modular arithmetic on a single integer per
square — phi(r, c) = r·67 + c·7 mod 640 — rather than geometric
coordinates. They are a drop-in equivalent to python-chess's
pseudo_legal_moves + is_check, validated at 100% on the reference
corpus, and compose naturally with the spectral encoder's coprime
phase structure.
import chess
from chess_spectral.phase_operators import (
occupation_aware_moves_c, # pseudo-legal dests from a square
available_castles, # legal castles for side-to-move
phasecast_is_check, # is the mover's king attacked?
move_leaves_king_in_check, # would this move expose our king?
)
board = chess.Board()
dests = occupation_aware_moves_c(board, "N", 0, 1, +1)
# -> frozenset({(2, 0), (2, 2)}) (a3 and c3)
phasecast_is_check(board) # False on the starting position
Validation coverage and rationale: see
PHASE_OPERATOR_SUPPLEMENT.md.
Phase operators (4D, §13)¶
chess_spectral.phase_operators_4d (1.3.0+) is the 4D analogue —
mixed-radix tower with modulus 145451 and ladder coefficient 14
(vs the 2D framework's 8). Validated against
python-chess4d-oana-chiru
at 44 803 (state, origin, piece) cases for occupation-aware moves
and 232 cases for check detection.
from chess_spectral.phase_operators_4d import (
phi4,
P_rook4, P_bishop4, P_queen4, P_king4, P_knight4,
P_pawn4_white, P_pawn4_black,
occupation_aware_moves_a_4d, # phase-op candidates ∩ chess4d oracle
phasecast_is_check_4d,
move_leaves_king_in_check_4d,
)
Full design + experimental record:
PHASE_OPERATOR_SUPPLEMENT_4D.md.
When to use what¶
The 2D and 4D encoders are independent build targets that share table generation discipline. Pick by what you're encoding; the QM extension sits on top of the 4D encoder.
2D (chess_spectral) |
4D (chess_spectral + chess_spectral_4d) |
|
|---|---|---|
| Encoding dim | 640 | 45 056 |
| Lattice | Z_8 × Z_8 |
Z_8^4 |
| Symmetry group | D_4 (order 8) |
B_4 hyperoctahedral (order 384) |
| Game rules | python-chess (fen_to_pos) |
Oana & Chiru (python-chess4d-oana-chiru) |
| Channels | 10 (A1, A2, B1, B2, E, F1, F2, F3, FA, FD) |
11 (A1, STD4_X/Y/Z/W, FIB_SYM_1/2/3, FA_PAWN_W/Y, FD_DIAG) |
| Phase operators | phase_operators (1.2.0+) |
phase_operators_4d (1.3.0+) |
| QM extension | not yet shipped | qm_4d + qm_4d_dynamics + qm_4d_bridge (1.5.0+) |
Python vs C — same encoders on either side, byte-identical output:
C (../src/) |
Python (this package) | |
|---|---|---|
| Throughput | µs/encode | ms/encode |
| REPL / notebooks | ✗ | ✓ |
| LLM-pasteable | binary | code |
scipy.linalg exploration |
✗ | ✓ |
| Embeds in mobile / web (Pyodide) | ✓ | ✓ (Pyodide) |
| Exact numerical reference | tables baked at build | rebuilt from primitives |
Develop new channels in Python first (faster iteration, scipy.linalg
at hand, no rebuild loop). Once the math is frozen, port to C and
verify parity via the test suite — the critical test is
test_csv_matches_c_byte_for_byte (2D) /
test_e2e_spectralz4_parity.py (4D), which assert the C-produced
encoded bytes equal the Python-produced bytes.
See also¶
- Cross-disciplinary applications — research notebook §15
(
chess_spectral_research_notebook.md) for the framing of chess-spectral as aH(4, 8)Hamming-scheme toolkit withB_4-equivariant frozen featurizer and Born-rule loss hooks. - §17 bridge contracts — same notebook, §17.1 / §17.5 for the consumer-facing method specs that this package implements.
- 4D notebook —
chess_spectral_4d_notebook.mdfor the 4D-specific research record (encoder injectivity, B_4 spectral identity, qm_4d pre-flight findings). - Track B ADRs —
docs/adr/qm_4d/for the design record of the v1.5 QM extension: - ADR-001 phase convention for unitary moves
- ADR-002 time-evolution semantics (continuous H_0 between move
boundaries;
evolve_under_h0) - ADR-003 per-channel move transformation (+ Phase 3.5 orbit-restriction amendment for cross-orbit STD4 / FIB_SYM measurement-only re-encode)
- ADR-004 Z_2 superselection structure (side-to-move sign multiplier, resolves the 8-collision encoder hash issue)
- ADR-005 pawn pseudo-Hermitian η-metric (deferred to v1.8+)
PHASE_3_5_PROBE_RESULTS.md— empirical probe record that drove the ADR-003 amendment.- Pawn-axis split (v1.1.1) — Oana & Chiru Definition 11; the
encoder splits the pawn antisymmetric channel into W-axis and
Y-axis sub-channels (
FA_PAWN_W,FA_PAWN_Y) and grew from 40 960-dim to 45 056-dim. Seeencoder_4d.pyheader for the rationale.