diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ef5d9e..e55daff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## Unreleased — D4-TIA-S3-EQUIV v0.1.0 harness + +- Added the exact six-element D4/F4 triality action generated by Sakai's + \(w_S,w_T\) involutions and pinned a deterministic six-point D4 orbit. +- Added six canonical \(SL_2(\mathbb Z)\) representatives for the matching + classes in \(SL_2(\mathbb Z)/\Gamma(2)\cong S_3\). +- Verified that the D4/F4 and modular quotient multiplication tables agree under + the published generator correspondence \(g_S\leftrightarrow w_S\), + \(g_T\leftrightarrow w_T\). +- Derived transformed quadratic/cubic coefficient actions from the binary-form + action itself and verified exact covariant equivariance for all 15 generators + under all six representatives: 90 symbolic identities. +- Explicitly documented that the six modular representatives are quotient-class + representatives, not a literal six-element subgroup of \(SL_2(\mathbb Z)\). +- Added a verifier, tests, roadmap integration, and no new sonification, + MIDI, tuning, tempo, timbre, or rendered-audio behavior. + ## Unreleased — D4-TIA-COV v0.1.0 algebra engine - Added an exact sparse-polynomial engine over reduced rational arithmetic for diff --git a/README.md b/README.md index c3ae748..e1f4d3e 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,33 @@ npm run verify:d4-tia-cov See [D4-TIA-COV v0.1.0](docs/D4_TRIALITY_COVARIANT_ENGINE.md). + +## D4-TIA-S3-EQUIV: exact triality orbit and equivariance harness + +The next layer is now executable as **D4-TIA-S3-EQUIV v0.1.0**. The harness +constructs the exact six-element D4/F4 triality quotient generated by +\(w_S,w_T\), matches it to six canonical representatives of +\(SL_2(\mathbb Z)/\Gamma(2)\), and verifies the binary-covariant law + +\[ +\Psi(\alpha';u,v)=\Psi(\alpha;u',v') +\] + +for all 15 exact covariants under all six representatives: **90 exact symbolic +checks**. + +The implementation deliberately distinguishes quotient classes from their +upstairs \(SL_2(\mathbb Z)\) representatives. No new MIDI, tuning, tempo, +timbre, or rendered-audio mapping is introduced. + +Verify it independently: + +```bash +npm run verify:d4-tia-s3 +``` + +See [D4-TIA-S3-EQUIV v0.1.0](docs/D4_TRIALITY_S3_EQUIVARIANCE.md). + ## Notation and Conventions Unless explicitly stated otherwise, scalars are in \(\mathbb C\), all vector @@ -229,7 +256,7 @@ npm run verify npm run build:v3 ``` -`npm run verify` validates D4-TIA-COV, D4-TIA-15, v3, and continues to validate the immutable v2 and v1 +`npm run verify` validates D4-TIA-COV, D4-TIA-S3-EQUIV, D4-TIA-15, v3, and continues to validate the immutable v2 and v1 contracts. The build command fails closed for unsafe, existing nonempty, or non-`dist/` output paths and never recursively deletes a caller-selected path. @@ -239,6 +266,7 @@ non-`dist/` output paths and never recursively deletes a caller-selected path. - [D4/F4 triality reference bridge](docs/D4_TRIALITY_REFERENCE_BRIDGE.md) - [D4-TIA-15 invariant-algebra sonification profile](docs/D4_TRIALITY_ALGEBRA_SONIFICATION.md) - [D4-TIA-COV exact covariant algebra engine](docs/D4_TRIALITY_COVARIANT_ENGINE.md) +- [D4-TIA-S3-EQUIV orbit and equivariance harness](docs/D4_TRIALITY_S3_EQUIVARIANCE.md) - [Claim boundaries](docs/ETQ_V3_CLAIM_BOUNDARIES.md) - [v2 to v3 migration](docs/ETQ_V2_TO_V3_MIGRATION.md) - [Formal v3.0.1 paper and build instructions](docs/etq-303/README.md) diff --git a/docs/D4_TRIALITY_S3_EQUIVARIANCE.md b/docs/D4_TRIALITY_S3_EQUIVARIANCE.md new file mode 100644 index 0000000..afd8caa --- /dev/null +++ b/docs/D4_TRIALITY_S3_EQUIVARIANCE.md @@ -0,0 +1,247 @@ +# D4-TIA-S3-EQUIV v0.1.0 — D4/F4 Orbit and Covariant Equivariance Harness + +**Status:** implemented research harness +**Harness ID:** `D4-TIA-S3-EQUIV` +**Version:** `0.1.0` +**Sonification impact:** none — this phase tests symmetry before receiver design + +## Purpose + +D4-TIA-COV made the 15 published covariants executable. This phase makes the +relevant symmetry action executable and checks that the covariant layer actually +commutes with the action it is supposed to respect. + +There are two related but distinct levels: + +1. the finite D4/F4 quotient + \[ + W(F_4)/W(D_4)\cong S_3, + \] + generated by the exact involutions \(w_S,w_T\); and +2. the modular quotient + \[ + SL_2(\mathbb Z)/\Gamma(2)\cong S_3, + \] + with the generator correspondence + \(g_S\leftrightarrow w_S\) and \(g_T\leftrightarrow w_T\). + +The harness preserves that distinction. The six matrices used on the modular +side are canonical representatives of quotient classes in \(SL_2(\mathbb Z)\). +They are **not** asserted to form a literal six-element subgroup upstairs. + +## Exact D4/F4 triality orbit + +The D4/F4 side uses the same published matrices already frozen by the reference +bridge: + +\[ +w_T=\operatorname{diag}(1,1,1,-1), +\] + +and + +\[ +w_S=\frac12 +\begin{pmatrix} +1&1&1&-1\\ +1&1&-1&1\\ +1&-1&1&1\\ +-1&1&1&1 +\end{pmatrix}. +\] + +The harness verifies exactly + +\[ +w_S^2=I,\qquad +w_T^2=I,\qquad +(w_Sw_T)^3=I, +\] + +constructs the six matrices + +\[ +e,\ S,\ T,\ ST,\ TS,\ STS, +\] + +and checks closure by exact rational matrix multiplication. + +The fixed seed + +\[ +z_0=(2,4,6,10) +\] + +has six distinct images: + +| class | orbit vector | +|---|---| +| \(e\) | \((2,4,6,10)\) | +| \(S\) | \((1,5,7,9)\) | +| \(T\) | \((2,4,6,-10)\) | +| \(ST\) | \((11,-5,-3,-1)\) | +| \(TS\) | \((1,5,7,-9)\) | +| \(STS\) | \((11,-5,-3,1)\) | + +The orbit is a deterministic test fixture, not a physical state trajectory. + +## Modular quotient representatives + +The modular generators are + +\[ +g_S= +\begin{pmatrix} +0&-1\\ +1&0 +\end{pmatrix}, +\qquad +g_T= +\begin{pmatrix} +1&1\\ +0&1 +\end{pmatrix}. +\] + +The harness uses the representatives + +\[ +e,\ S,\ T,\ ST,\ TS,\ STS +\] + +and reduces each matrix modulo 2 to identify its class in +\(SL_2(\mathbb Z)/\Gamma(2)\). + +All six mod-2 classes are distinct. Their quotient multiplication table is +computed from representative products modulo 2 and is checked to agree exactly +with the multiplication table of the six D4/F4 matrices. + +This is the finite fiber-product bridge we need: matching quotient labels and +multiplication, without conflating the upstairs representations. + +## Binary-form action + +For + +\[ +M= +\begin{pmatrix} +a&b\\ +c&d +\end{pmatrix} +\in SL_2(\mathbb Z), +\] + +the variable action is + +\[ +u'=au+bv,\qquad +v'=cu+dv. +\] + +The transformed coefficients are **derived** from the source definitions + +\[ +f'(u,v)=f(u',v'), +\qquad +g'(u,v)=g(u',v'), +\] + +rather than entered as a second independent table. + +The implementation expands those equations exactly in the sparse rational +polynomial engine and extracts the resulting transformed +\(\alpha_i'\) and \(\beta_j'\). + +## Exact covariant equivariance + +Sakai's covariant definition requires + +\[ +\Psi(\alpha';u,v)=\Psi(\alpha;u',v') +\] + +under the \(SL_2(\mathbb C)\) action. + +For each of the six canonical \(SL_2(\mathbb Z)\) representatives, the harness: + +1. derives \(\alpha',\beta'\) from transformed \(f,g\); +2. substitutes those transformed coefficients into every exact covariant; +3. independently substitutes \(u',v'\) into the original covariant; and +4. compares the resulting exact sparse polynomials term-for-term. + +With 15 covariants and six representatives, the harness performs + +\[ +15\times6=90 +\] + +exact symbolic equivariance checks. + +No numerical tolerance, floating-point comparison, sampling argument, or +listener-facing receiver is involved. + +## Why this comes before D4-TIA-15 v2 + +D4-TIA-15 v1 maps invariant-algebra grading metadata into symbolic MIDI fields. +The next version is intended to sonify **evaluated covariants and their orbit +behavior**. + +Before that can be done responsibly, the repository needs a tested statement of +what is allowed to move under triality and what must remain invariant after +projection. + +This harness supplies that boundary. + +The next phase may define a receiver \(\Phi\) only if it preserves the tested +equivariance structure before any invariant aggregation. In other words, the +design target is a commuting diagram, not an aesthetically chosen three-way +permutation. + +## Verify + +Run: + +```bash +npm test +npm run verify:d4-tia-s3 +``` + +The full root verifier also includes this harness. + +## Canonical fixture + +`examples/d4-tia-s3-equivariance.v0.1.canonical.json` freezes the exact +six-element D4 orbit, modular quotient representatives and multiplication +table, plus the coefficient-action and transformed-basis hashes for all six +representatives. The companion schema binds the complete canonical summary +with `const`, so a changed orbit, representative, quotient table, or +equivariance commitment fails verification rather than silently redefining +the harness. + +## Claim boundary + +Supported: + +- exact six-element D4/F4 triality quotient action generated by \(w_S,w_T\); +- exact agreement with the six mod-2 modular quotient classes; +- exact transformed coefficient actions derived from the binary forms; +- exact covariant equivariance for all 15 constructed generators under all six + representatives. + +Not supported: + +- that the six representatives form a literal finite subgroup of + \(SL_2(\mathbb Z)\); +- that a MIDI receiver has already been shown equivariant; +- that orbit positions are physical states or acoustic quantities; or +- any empirical or physical validation claim. + +## References + +- K. Sakai, *The ring of D4 triality invariants*, arXiv:2504.00546v2 (2026): + quotient maps and generator identification in Eqs. (2.10)–(2.16); + binary covariant transformation law in Definition 4.2. +- K. Sakai, *E-strings, F4, and D4 triality*, JHEP 07 (2023) 192, + arXiv:2304.04878: modular-triality fiber product and the same \(w_S,w_T\) + convention. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1d419af..d351a9e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -65,17 +65,35 @@ audio remain outside profile identity. ETQ-101 v2 and ETQ-303 v3 are unchanged. This phase changes no MIDI/event mapping. It turns the literature ledger into executable mathematics before any further auditory interpretation. +## Root D4-TIA-S3-EQUIV v0.1.0 — orbit + equivariance harness (implemented) + +- exact six-element \(W(F_4)/W(D_4)\cong S_3\) action generated by the + published \(w_S,w_T\) involutions; +- a deterministic six-point orbit of the fixed D4 seed \((2,4,6,10)\); +- six canonical \(SL_2(\mathbb Z)\) representatives for the corresponding + classes in \(SL_2(\mathbb Z)/\Gamma(2)\cong S_3\); +- exact agreement of the D4/F4 and modular quotient multiplication tables; +- coefficient actions derived directly from + \(f'(u,v)=f(u',v')\), \(g'(u,v)=g(u',v')\); +- exact verification of + \(\Psi(\alpha';u,v)=\Psi(\alpha;u',v')\) + for all 15 constructed covariants under all six representatives; and +- explicit separation between the finite quotient action and the upstairs + \(SL_2(\mathbb Z)\) representatives. + +The harness performs 90 exact symbolic equivariance checks and adds no new +receiver or sonification mapping. + ## Sequenced D4-TIA research follow-up The remaining D4-TIA sequence is intentionally ordered: -1. **D4/F4 \(S_3\) orbit + equivariance harness** — next. -2. **D4-TIA-15 v2** — sonify evaluated covariants/orbits while testing - equivariance before invariant projection. -3. **Freeze/tag/archive** the resulting research line and then update Zenodo. +1. **D4-TIA-15 v2** — sonify evaluated covariants/orbits while preserving the + tested equivariance boundary before invariant projection. +2. **Freeze/tag/archive** the resulting research line and then update Zenodo. -Do not skip directly to a new receiver mapping before the equivariance harness -exists and is tested. +The algebra and symmetry-action layers are now executable and tested; the next +phase may finally define the evaluated-covariant/orbit receiver contract. The remaining numbered phases below apply only to the independent `APP/` and `sonification/` audio laboratories. diff --git a/examples/d4-tia-s3-equivariance.v0.1.canonical.json b/examples/d4-tia-s3-equivariance.v0.1.canonical.json new file mode 100644 index 0000000..fdc09ee --- /dev/null +++ b/examples/d4-tia-s3-equivariance.v0.1.canonical.json @@ -0,0 +1,687 @@ +{ + "schema": "qsol.d4-tia-s3-equivariance/v0.1", + "harnessId": "D4-TIA-S3-EQUIV", + "harnessVersion": "0.1.0", + "quotientIdentification": { + "modular": "SL2(Z)/Gamma(2)~=S3", + "d4": "W(F4)/W(D4)~=S3", + "generatorCorrespondence": [ + "gS<->wS", + "gT<->wT" + ], + "warning": "The six modular matrices are canonical SL2(Z) representatives of quotient classes; they are not asserted to form a literal six-element subgroup upstairs." + }, + "d4Orbit": { + "generators": { + "S": [ + [ + "1/2", + "1/2", + "1/2", + "-1/2" + ], + [ + "1/2", + "1/2", + "-1/2", + "1/2" + ], + [ + "1/2", + "-1/2", + "1/2", + "1/2" + ], + [ + "-1/2", + "1/2", + "1/2", + "1/2" + ] + ], + "T": [ + [ + "1", + "0", + "0", + "0" + ], + [ + "0", + "1", + "0", + "0" + ], + [ + "0", + "0", + "1", + "0" + ], + [ + "0", + "0", + "0", + "-1" + ] + ] + }, + "elements": [ + { + "label": "e", + "matrix": [ + [ + "1", + "0", + "0", + "0" + ], + [ + "0", + "1", + "0", + "0" + ], + [ + "0", + "0", + "1", + "0" + ], + [ + "0", + "0", + "0", + "1" + ] + ] + }, + { + "label": "S", + "matrix": [ + [ + "1/2", + "1/2", + "1/2", + "-1/2" + ], + [ + "1/2", + "1/2", + "-1/2", + "1/2" + ], + [ + "1/2", + "-1/2", + "1/2", + "1/2" + ], + [ + "-1/2", + "1/2", + "1/2", + "1/2" + ] + ] + }, + { + "label": "T", + "matrix": [ + [ + "1", + "0", + "0", + "0" + ], + [ + "0", + "1", + "0", + "0" + ], + [ + "0", + "0", + "1", + "0" + ], + [ + "0", + "0", + "0", + "-1" + ] + ] + }, + { + "label": "ST", + "matrix": [ + [ + "1/2", + "1/2", + "1/2", + "1/2" + ], + [ + "1/2", + "1/2", + "-1/2", + "-1/2" + ], + [ + "1/2", + "-1/2", + "1/2", + "-1/2" + ], + [ + "-1/2", + "1/2", + "1/2", + "-1/2" + ] + ] + }, + { + "label": "TS", + "matrix": [ + [ + "1/2", + "1/2", + "1/2", + "-1/2" + ], + [ + "1/2", + "1/2", + "-1/2", + "1/2" + ], + [ + "1/2", + "-1/2", + "1/2", + "1/2" + ], + [ + "1/2", + "-1/2", + "-1/2", + "-1/2" + ] + ] + }, + { + "label": "STS", + "matrix": [ + [ + "1/2", + "1/2", + "1/2", + "1/2" + ], + [ + "1/2", + "1/2", + "-1/2", + "-1/2" + ], + [ + "1/2", + "-1/2", + "1/2", + "-1/2" + ], + [ + "1/2", + "-1/2", + "-1/2", + "1/2" + ] + ] + } + ], + "multiplicationTable": { + "e": { + "e": "e", + "S": "S", + "T": "T", + "ST": "ST", + "TS": "TS", + "STS": "STS" + }, + "S": { + "e": "S", + "S": "e", + "T": "ST", + "ST": "T", + "TS": "STS", + "STS": "TS" + }, + "T": { + "e": "T", + "S": "TS", + "T": "e", + "ST": "STS", + "TS": "S", + "STS": "ST" + }, + "ST": { + "e": "ST", + "S": "STS", + "T": "S", + "ST": "TS", + "TS": "e", + "STS": "T" + }, + "TS": { + "e": "TS", + "S": "T", + "T": "STS", + "ST": "e", + "TS": "ST", + "STS": "S" + }, + "STS": { + "e": "STS", + "S": "ST", + "T": "TS", + "ST": "S", + "TS": "T", + "STS": "e" + } + }, + "seed": [ + 2, + 4, + 6, + 10 + ], + "orbit": [ + { + "label": "e", + "vector": [ + "2", + "4", + "6", + "10" + ] + }, + { + "label": "S", + "vector": [ + "1", + "5", + "7", + "9" + ] + }, + { + "label": "T", + "vector": [ + "2", + "4", + "6", + "-10" + ] + }, + { + "label": "ST", + "vector": [ + "11", + "-5", + "-3", + "-1" + ] + }, + { + "label": "TS", + "vector": [ + "1", + "5", + "7", + "-9" + ] + }, + { + "label": "STS", + "vector": [ + "11", + "-5", + "-3", + "1" + ] + } + ] + }, + "modularQuotient": { + "generators": { + "S": [ + [ + 0, + -1 + ], + [ + 1, + 0 + ] + ], + "T": [ + [ + 1, + 1 + ], + [ + 0, + 1 + ] + ] + }, + "representatives": [ + { + "label": "e", + "matrix": [ + [ + 1, + 0 + ], + [ + 0, + 1 + ] + ], + "mod2Key": "1001" + }, + { + "label": "S", + "matrix": [ + [ + 0, + -1 + ], + [ + 1, + 0 + ] + ], + "mod2Key": "0110" + }, + { + "label": "T", + "matrix": [ + [ + 1, + 1 + ], + [ + 0, + 1 + ] + ], + "mod2Key": "1101" + }, + { + "label": "ST", + "matrix": [ + [ + 0, + -1 + ], + [ + 1, + 1 + ] + ], + "mod2Key": "0111" + }, + { + "label": "TS", + "matrix": [ + [ + 1, + -1 + ], + [ + 1, + 0 + ] + ], + "mod2Key": "1110" + }, + { + "label": "STS", + "matrix": [ + [ + -1, + 0 + ], + [ + 1, + -1 + ] + ], + "mod2Key": "1011" + } + ], + "multiplicationTable": { + "e": { + "e": "e", + "S": "S", + "T": "T", + "ST": "ST", + "TS": "TS", + "STS": "STS" + }, + "S": { + "e": "S", + "S": "e", + "T": "ST", + "ST": "T", + "TS": "STS", + "STS": "TS" + }, + "T": { + "e": "T", + "S": "TS", + "T": "e", + "ST": "STS", + "TS": "S", + "STS": "ST" + }, + "ST": { + "e": "ST", + "S": "STS", + "T": "S", + "ST": "TS", + "TS": "e", + "STS": "T" + }, + "TS": { + "e": "TS", + "S": "T", + "T": "STS", + "ST": "e", + "TS": "ST", + "STS": "S" + }, + "STS": { + "e": "STS", + "S": "ST", + "T": "TS", + "ST": "S", + "TS": "T", + "STS": "e" + } + } + }, + "multiplicationTablesMatch": true, + "equivariance": { + "law": "Psi(alpha';u,v)=Psi(alpha;u',v')", + "representatives": [ + { + "label": "e", + "modularMatrix": [ + [ + 1, + 0 + ], + [ + 0, + 1 + ] + ], + "mod2Key": "1001", + "coefficientActionSha256": "185115b9b28e422ee05212edca011c65df4c5ab0e793a4f9ace08dac293aa86b", + "transformedBasisSha256": "56b8b812cfcaafc7e7cbb0989699f17884b8d871351389df8fab693a5be06080", + "checkedGenerators": 15, + "allPassed": true + }, + { + "label": "S", + "modularMatrix": [ + [ + 0, + -1 + ], + [ + 1, + 0 + ] + ], + "mod2Key": "0110", + "coefficientActionSha256": "e1a3070fed3d7b36be835fa3880c883e0037888531ead9ddb0ec062613564b2c", + "transformedBasisSha256": "e91ccda5e80205b3d804577035fbf51c9dbfc27f6ca0ba79b6e8867260d0e490", + "checkedGenerators": 15, + "allPassed": true + }, + { + "label": "T", + "modularMatrix": [ + [ + 1, + 1 + ], + [ + 0, + 1 + ] + ], + "mod2Key": "1101", + "coefficientActionSha256": "e4caf286de06b999e592cb2c1788cd9de8729f3ea9d64736e6a22a889865c586", + "transformedBasisSha256": "a234378f666c9f1b6fac0aa49886c3c0eb7200829d8618002ef237f270444b2a", + "checkedGenerators": 15, + "allPassed": true + }, + { + "label": "ST", + "modularMatrix": [ + [ + 0, + -1 + ], + [ + 1, + 1 + ] + ], + "mod2Key": "0111", + "coefficientActionSha256": "d6463a68d274468a19686eb28784ca2e8934ee2e05adf4383fd161e5b0a54bcd", + "transformedBasisSha256": "f0ba61bed81f7920a849ab6e7c2ce4ec8df54c5000133acb1d9cfd1f10353158", + "checkedGenerators": 15, + "allPassed": true + }, + { + "label": "TS", + "modularMatrix": [ + [ + 1, + -1 + ], + [ + 1, + 0 + ] + ], + "mod2Key": "1110", + "coefficientActionSha256": "26878272d4f2110faad459ac1e4801a9754cdfe662a2abfaf4bc76cbfd41fae6", + "transformedBasisSha256": "2049f04675545c9184d3eaf566a26e23ff06da27400afc225a5fd07eb3b18227", + "checkedGenerators": 15, + "allPassed": true + }, + { + "label": "STS", + "modularMatrix": [ + [ + -1, + 0 + ], + [ + 1, + -1 + ] + ], + "mod2Key": "1011", + "coefficientActionSha256": "11b562c354f98be0661620204b85fd6d30c713e47accb2ec678cc5365e453e4d", + "transformedBasisSha256": "e9ab2b4c3a327aac1c024f56cc42470fecf9bab96e3b6723b7ac44eb9c1881d6", + "checkedGenerators": 15, + "allPassed": true + } + ], + "checkedRepresentatives": 6, + "checkedGenerators": 15, + "exactChecks": 90, + "allPassed": true, + "quotientMultiplicationTable": { + "e": { + "e": "e", + "S": "S", + "T": "T", + "ST": "ST", + "TS": "TS", + "STS": "STS" + }, + "S": { + "e": "S", + "S": "e", + "T": "ST", + "ST": "T", + "TS": "STS", + "STS": "TS" + }, + "T": { + "e": "T", + "S": "TS", + "T": "e", + "ST": "STS", + "TS": "S", + "STS": "ST" + }, + "ST": { + "e": "ST", + "S": "STS", + "T": "S", + "ST": "TS", + "TS": "e", + "STS": "T" + }, + "TS": { + "e": "TS", + "S": "T", + "T": "STS", + "ST": "e", + "TS": "ST", + "STS": "S" + }, + "STS": { + "e": "STS", + "S": "ST", + "T": "TS", + "ST": "S", + "TS": "T", + "STS": "e" + } + } + }, + "claimBoundary": "The harness verifies the exact six-element D4/F4 triality quotient orbit and exact SL2(C) covariant equivariance for the 15 constructed binary covariants under six canonical SL2(Z) representatives. It does not claim that the quotient classes themselves are a six-element subgroup of SL2(Z), and it introduces no sonification mapping." +} diff --git a/package.json b/package.json index 9684ac8..32c4a18 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,14 @@ "test:tuning": "node --test tests/tanner-tuning-fork.test.mjs", "build:v3": "node scripts/build-v3-artifacts.mjs", "build:paper": "bash scripts/build-etq303-paper.sh", - "verify": "node scripts/verify-d4-tia-cov.mjs && node scripts/verify-d4-tia.mjs && node scripts/verify-v3.mjs && node scripts/verify.mjs && node scripts/verify-legacy.mjs", + "verify": "node scripts/verify-d4-tia-cov.mjs && node scripts/verify-d4-tia-s3-equivariance.mjs && node scripts/verify-d4-tia.mjs && node scripts/verify-v3.mjs && node scripts/verify.mjs && node scripts/verify-legacy.mjs", "verify:v3": "node scripts/verify-v3.mjs", "verify:v2": "node scripts/verify.mjs", "verify:legacy": "node scripts/verify-legacy.mjs", "build:d4-tia": "node scripts/build-d4-tia-artifacts.mjs", "verify:d4-tia": "node scripts/verify-d4-tia.mjs", - "verify:d4-tia-cov": "node scripts/verify-d4-tia-cov.mjs" + "verify:d4-tia-cov": "node scripts/verify-d4-tia-cov.mjs", + "verify:d4-tia-s3": "node scripts/verify-d4-tia-s3-equivariance.mjs" }, "engines": { "node": ">=20" diff --git a/scripts/verify-d4-tia-s3-equivariance.mjs b/scripts/verify-d4-tia-s3-equivariance.mjs new file mode 100644 index 0000000..238c3d1 --- /dev/null +++ b/scripts/verify-d4-tia-s3-equivariance.mjs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MPL-2.0 + +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + HARNESS_ID, + HARNESS_SCHEMA_ID, + HARNESS_VERSION, + S3_LABELS, + buildS3EquivarianceHarnessSummary, +} from "../src/d4-triality-s3-equivariance.mjs"; + +const PROJECT_ROOT = resolve(fileURLToPath(new URL("..", import.meta.url))); +const fixture = JSON.parse( + readFileSync( + resolve(PROJECT_ROOT, "examples/d4-tia-s3-equivariance.v0.1.canonical.json"), + "utf8", + ), +); +const schema = JSON.parse( + readFileSync( + resolve(PROJECT_ROOT, "spec/d4-tia-s3-equivariance.v0.1.schema.json"), + "utf8", + ), +); + +const summary = buildS3EquivarianceHarnessSummary(); + +assert.equal(summary.schema, HARNESS_SCHEMA_ID); +assert.deepEqual(summary, fixture); +assert.equal(schema.$id, HARNESS_SCHEMA_ID); +assert.deepEqual(schema.const, fixture); +assert.equal(summary.harnessId, HARNESS_ID); +assert.equal(summary.harnessVersion, HARNESS_VERSION); +assert.equal(summary.d4Orbit.elements.length, 6); +assert.equal(summary.d4Orbit.orbit.length, 6); +assert.equal(summary.modularQuotient.representatives.length, 6); +assert.equal(summary.multiplicationTablesMatch, true); +assert.equal(summary.equivariance.checkedRepresentatives, 6); +assert.equal(summary.equivariance.checkedGenerators, 15); +assert.equal(summary.equivariance.exactChecks, 90); +assert.equal(summary.equivariance.allPassed, true); +assert.deepEqual( + summary.d4Orbit.multiplicationTable, + summary.modularQuotient.multiplicationTable, +); +assert.deepEqual( + summary.d4Orbit.elements.map((entry) => entry.label), + S3_LABELS, +); +assert.deepEqual( + summary.modularQuotient.representatives.map((entry) => entry.label), + S3_LABELS, +); +assert.ok( + summary.equivariance.representatives.every( + (entry) => + entry.checkedGenerators === 15 && + entry.allPassed === true && + /^[0-9a-f]{64}$/.test(entry.coefficientActionSha256) && + /^[0-9a-f]{64}$/.test(entry.transformedBasisSha256), + ), +); + +console.log(JSON.stringify({ status: "PASS", ...summary }, null, 2)); diff --git a/spec/d4-tia-s3-equivariance.v0.1.schema.json b/spec/d4-tia-s3-equivariance.v0.1.schema.json new file mode 100644 index 0000000..328c128 --- /dev/null +++ b/spec/d4-tia-s3-equivariance.v0.1.schema.json @@ -0,0 +1,692 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "qsol.d4-tia-s3-equivariance/v0.1", + "title": "D4-TIA-S3-EQUIV v0.1.0 canonical harness summary", + "const": { + "schema": "qsol.d4-tia-s3-equivariance/v0.1", + "harnessId": "D4-TIA-S3-EQUIV", + "harnessVersion": "0.1.0", + "quotientIdentification": { + "modular": "SL2(Z)/Gamma(2)~=S3", + "d4": "W(F4)/W(D4)~=S3", + "generatorCorrespondence": [ + "gS<->wS", + "gT<->wT" + ], + "warning": "The six modular matrices are canonical SL2(Z) representatives of quotient classes; they are not asserted to form a literal six-element subgroup upstairs." + }, + "d4Orbit": { + "generators": { + "S": [ + [ + "1/2", + "1/2", + "1/2", + "-1/2" + ], + [ + "1/2", + "1/2", + "-1/2", + "1/2" + ], + [ + "1/2", + "-1/2", + "1/2", + "1/2" + ], + [ + "-1/2", + "1/2", + "1/2", + "1/2" + ] + ], + "T": [ + [ + "1", + "0", + "0", + "0" + ], + [ + "0", + "1", + "0", + "0" + ], + [ + "0", + "0", + "1", + "0" + ], + [ + "0", + "0", + "0", + "-1" + ] + ] + }, + "elements": [ + { + "label": "e", + "matrix": [ + [ + "1", + "0", + "0", + "0" + ], + [ + "0", + "1", + "0", + "0" + ], + [ + "0", + "0", + "1", + "0" + ], + [ + "0", + "0", + "0", + "1" + ] + ] + }, + { + "label": "S", + "matrix": [ + [ + "1/2", + "1/2", + "1/2", + "-1/2" + ], + [ + "1/2", + "1/2", + "-1/2", + "1/2" + ], + [ + "1/2", + "-1/2", + "1/2", + "1/2" + ], + [ + "-1/2", + "1/2", + "1/2", + "1/2" + ] + ] + }, + { + "label": "T", + "matrix": [ + [ + "1", + "0", + "0", + "0" + ], + [ + "0", + "1", + "0", + "0" + ], + [ + "0", + "0", + "1", + "0" + ], + [ + "0", + "0", + "0", + "-1" + ] + ] + }, + { + "label": "ST", + "matrix": [ + [ + "1/2", + "1/2", + "1/2", + "1/2" + ], + [ + "1/2", + "1/2", + "-1/2", + "-1/2" + ], + [ + "1/2", + "-1/2", + "1/2", + "-1/2" + ], + [ + "-1/2", + "1/2", + "1/2", + "-1/2" + ] + ] + }, + { + "label": "TS", + "matrix": [ + [ + "1/2", + "1/2", + "1/2", + "-1/2" + ], + [ + "1/2", + "1/2", + "-1/2", + "1/2" + ], + [ + "1/2", + "-1/2", + "1/2", + "1/2" + ], + [ + "1/2", + "-1/2", + "-1/2", + "-1/2" + ] + ] + }, + { + "label": "STS", + "matrix": [ + [ + "1/2", + "1/2", + "1/2", + "1/2" + ], + [ + "1/2", + "1/2", + "-1/2", + "-1/2" + ], + [ + "1/2", + "-1/2", + "1/2", + "-1/2" + ], + [ + "1/2", + "-1/2", + "-1/2", + "1/2" + ] + ] + } + ], + "multiplicationTable": { + "e": { + "e": "e", + "S": "S", + "T": "T", + "ST": "ST", + "TS": "TS", + "STS": "STS" + }, + "S": { + "e": "S", + "S": "e", + "T": "ST", + "ST": "T", + "TS": "STS", + "STS": "TS" + }, + "T": { + "e": "T", + "S": "TS", + "T": "e", + "ST": "STS", + "TS": "S", + "STS": "ST" + }, + "ST": { + "e": "ST", + "S": "STS", + "T": "S", + "ST": "TS", + "TS": "e", + "STS": "T" + }, + "TS": { + "e": "TS", + "S": "T", + "T": "STS", + "ST": "e", + "TS": "ST", + "STS": "S" + }, + "STS": { + "e": "STS", + "S": "ST", + "T": "TS", + "ST": "S", + "TS": "T", + "STS": "e" + } + }, + "seed": [ + 2, + 4, + 6, + 10 + ], + "orbit": [ + { + "label": "e", + "vector": [ + "2", + "4", + "6", + "10" + ] + }, + { + "label": "S", + "vector": [ + "1", + "5", + "7", + "9" + ] + }, + { + "label": "T", + "vector": [ + "2", + "4", + "6", + "-10" + ] + }, + { + "label": "ST", + "vector": [ + "11", + "-5", + "-3", + "-1" + ] + }, + { + "label": "TS", + "vector": [ + "1", + "5", + "7", + "-9" + ] + }, + { + "label": "STS", + "vector": [ + "11", + "-5", + "-3", + "1" + ] + } + ] + }, + "modularQuotient": { + "generators": { + "S": [ + [ + 0, + -1 + ], + [ + 1, + 0 + ] + ], + "T": [ + [ + 1, + 1 + ], + [ + 0, + 1 + ] + ] + }, + "representatives": [ + { + "label": "e", + "matrix": [ + [ + 1, + 0 + ], + [ + 0, + 1 + ] + ], + "mod2Key": "1001" + }, + { + "label": "S", + "matrix": [ + [ + 0, + -1 + ], + [ + 1, + 0 + ] + ], + "mod2Key": "0110" + }, + { + "label": "T", + "matrix": [ + [ + 1, + 1 + ], + [ + 0, + 1 + ] + ], + "mod2Key": "1101" + }, + { + "label": "ST", + "matrix": [ + [ + 0, + -1 + ], + [ + 1, + 1 + ] + ], + "mod2Key": "0111" + }, + { + "label": "TS", + "matrix": [ + [ + 1, + -1 + ], + [ + 1, + 0 + ] + ], + "mod2Key": "1110" + }, + { + "label": "STS", + "matrix": [ + [ + -1, + 0 + ], + [ + 1, + -1 + ] + ], + "mod2Key": "1011" + } + ], + "multiplicationTable": { + "e": { + "e": "e", + "S": "S", + "T": "T", + "ST": "ST", + "TS": "TS", + "STS": "STS" + }, + "S": { + "e": "S", + "S": "e", + "T": "ST", + "ST": "T", + "TS": "STS", + "STS": "TS" + }, + "T": { + "e": "T", + "S": "TS", + "T": "e", + "ST": "STS", + "TS": "S", + "STS": "ST" + }, + "ST": { + "e": "ST", + "S": "STS", + "T": "S", + "ST": "TS", + "TS": "e", + "STS": "T" + }, + "TS": { + "e": "TS", + "S": "T", + "T": "STS", + "ST": "e", + "TS": "ST", + "STS": "S" + }, + "STS": { + "e": "STS", + "S": "ST", + "T": "TS", + "ST": "S", + "TS": "T", + "STS": "e" + } + } + }, + "multiplicationTablesMatch": true, + "equivariance": { + "law": "Psi(alpha';u,v)=Psi(alpha;u',v')", + "representatives": [ + { + "label": "e", + "modularMatrix": [ + [ + 1, + 0 + ], + [ + 0, + 1 + ] + ], + "mod2Key": "1001", + "coefficientActionSha256": "185115b9b28e422ee05212edca011c65df4c5ab0e793a4f9ace08dac293aa86b", + "transformedBasisSha256": "56b8b812cfcaafc7e7cbb0989699f17884b8d871351389df8fab693a5be06080", + "checkedGenerators": 15, + "allPassed": true + }, + { + "label": "S", + "modularMatrix": [ + [ + 0, + -1 + ], + [ + 1, + 0 + ] + ], + "mod2Key": "0110", + "coefficientActionSha256": "e1a3070fed3d7b36be835fa3880c883e0037888531ead9ddb0ec062613564b2c", + "transformedBasisSha256": "e91ccda5e80205b3d804577035fbf51c9dbfc27f6ca0ba79b6e8867260d0e490", + "checkedGenerators": 15, + "allPassed": true + }, + { + "label": "T", + "modularMatrix": [ + [ + 1, + 1 + ], + [ + 0, + 1 + ] + ], + "mod2Key": "1101", + "coefficientActionSha256": "e4caf286de06b999e592cb2c1788cd9de8729f3ea9d64736e6a22a889865c586", + "transformedBasisSha256": "a234378f666c9f1b6fac0aa49886c3c0eb7200829d8618002ef237f270444b2a", + "checkedGenerators": 15, + "allPassed": true + }, + { + "label": "ST", + "modularMatrix": [ + [ + 0, + -1 + ], + [ + 1, + 1 + ] + ], + "mod2Key": "0111", + "coefficientActionSha256": "d6463a68d274468a19686eb28784ca2e8934ee2e05adf4383fd161e5b0a54bcd", + "transformedBasisSha256": "f0ba61bed81f7920a849ab6e7c2ce4ec8df54c5000133acb1d9cfd1f10353158", + "checkedGenerators": 15, + "allPassed": true + }, + { + "label": "TS", + "modularMatrix": [ + [ + 1, + -1 + ], + [ + 1, + 0 + ] + ], + "mod2Key": "1110", + "coefficientActionSha256": "26878272d4f2110faad459ac1e4801a9754cdfe662a2abfaf4bc76cbfd41fae6", + "transformedBasisSha256": "2049f04675545c9184d3eaf566a26e23ff06da27400afc225a5fd07eb3b18227", + "checkedGenerators": 15, + "allPassed": true + }, + { + "label": "STS", + "modularMatrix": [ + [ + -1, + 0 + ], + [ + 1, + -1 + ] + ], + "mod2Key": "1011", + "coefficientActionSha256": "11b562c354f98be0661620204b85fd6d30c713e47accb2ec678cc5365e453e4d", + "transformedBasisSha256": "e9ab2b4c3a327aac1c024f56cc42470fecf9bab96e3b6723b7ac44eb9c1881d6", + "checkedGenerators": 15, + "allPassed": true + } + ], + "checkedRepresentatives": 6, + "checkedGenerators": 15, + "exactChecks": 90, + "allPassed": true, + "quotientMultiplicationTable": { + "e": { + "e": "e", + "S": "S", + "T": "T", + "ST": "ST", + "TS": "TS", + "STS": "STS" + }, + "S": { + "e": "S", + "S": "e", + "T": "ST", + "ST": "T", + "TS": "STS", + "STS": "TS" + }, + "T": { + "e": "T", + "S": "TS", + "T": "e", + "ST": "STS", + "TS": "S", + "STS": "ST" + }, + "ST": { + "e": "ST", + "S": "STS", + "T": "S", + "ST": "TS", + "TS": "e", + "STS": "T" + }, + "TS": { + "e": "TS", + "S": "T", + "T": "STS", + "ST": "e", + "TS": "ST", + "STS": "S" + }, + "STS": { + "e": "STS", + "S": "ST", + "T": "TS", + "ST": "S", + "TS": "T", + "STS": "e" + } + } + }, + "claimBoundary": "The harness verifies the exact six-element D4/F4 triality quotient orbit and exact SL2(C) covariant equivariance for the 15 constructed binary covariants under six canonical SL2(Z) representatives. It does not claim that the quotient classes themselves are a six-element subgroup of SL2(Z), and it introduces no sonification mapping." + } +} diff --git a/src/d4-triality-s3-equivariance.mjs b/src/d4-triality-s3-equivariance.mjs new file mode 100644 index 0000000..33ade74 --- /dev/null +++ b/src/d4-triality-s3-equivariance.mjs @@ -0,0 +1,683 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2026 Trent Slade / QSOL-IMC. + +/** + * D4-TIA-S3-EQUIV v0.1.0 exact orbit and equivariance harness. + * + * The finite D4/F4 side realizes the quotient W(F4)/W(D4) ~= S3 exactly via + * Sakai's wS and wT involutions. The binary-form side uses canonical SL2(Z) + * representatives for the matching quotient classes in SL2(Z)/Gamma(2) ~= S3. + * Covariant equivariance is checked upstairs in SL2(C), not by pretending the + * six quotient classes form a literal six-element subgroup of SL2(Z). + */ + +import { createHash } from "node:crypto"; + +import { + POLYNOMIAL_VARIABLES, + addPolynomials, + buildCovariantBasis, + buildSourceBinaryForms, + canonicalPolynomialText, + constantPolynomial, + monomialPolynomial, + multiplyPolynomials, + polynomialSha256, + polynomialsEqual, + powerPolynomial, + rational, + rationalToString, + scalePolynomial, + serializePolynomial, + zeroPolynomial, +} from "./d4-triality-covariant-engine.mjs"; +import { + SAKAI_WS_DENOMINATOR, + SAKAI_WS_NUMERATOR, + SAKAI_WT, + integerIdentity, + integerMatrixMultiply, +} from "./d4-triality-reference.mjs"; + +export const HARNESS_ID = "D4-TIA-S3-EQUIV"; +export const HARNESS_VERSION = "0.1.0"; +export const HARNESS_SCHEMA_ID = "qsol.d4-tia-s3-equivariance/v0.1"; + +export const S3_LABELS = Object.freeze(["e", "S", "T", "ST", "TS", "STS"]); +export const D4_ORBIT_SEED = Object.freeze([2, 4, 6, 10]); + +export const MODULAR_S = Object.freeze([ + Object.freeze([0, -1]), + Object.freeze([1, 0]), +]); +export const MODULAR_T = Object.freeze([ + Object.freeze([1, 1]), + Object.freeze([0, 1]), +]); + +const U_INDEX = POLYNOMIAL_VARIABLES.indexOf("u"); +const V_INDEX = POLYNOMIAL_VARIABLES.indexOf("v"); +const COEFFICIENT_COUNT = U_INDEX; + +if ( + U_INDEX !== 7 || + V_INDEX !== 8 || + COEFFICIENT_COUNT !== 7 || + POLYNOMIAL_VARIABLES.length !== 9 +) { + throw new Error("D4-TIA-S3-EQUIV requires the frozen nine-variable covariant ABI"); +} + +function parseRationalText(text) { + if (typeof text !== "string" || text.length === 0) { + throw new TypeError("rational text must be non-empty"); + } + const parts = text.split("/"); + if (parts.length === 1) return rational(BigInt(parts[0])); + if (parts.length === 2) return rational(BigInt(parts[0]), BigInt(parts[1])); + throw new TypeError(`invalid rational text: ${text}`); +} + +function addRationals(left, right) { + return rational( + left.numerator * right.denominator + right.numerator * left.denominator, + left.denominator * right.denominator, + ); +} + +function multiplyRationals(left, right) { + return rational( + left.numerator * right.numerator, + left.denominator * right.denominator, + ); +} + +function rationalZero() { + return rational(0n); +} + +function rationalOne() { + return rational(1n); +} + +function rationalMatrixIdentity(dimension) { + return Array.from({ length: dimension }, (_, row) => + Array.from({ length: dimension }, (_, column) => + row === column ? rationalOne() : rationalZero(), + ), + ); +} + +function integerMatrixToRational(matrix, denominator = 1) { + if (!Number.isSafeInteger(denominator) || denominator <= 0) { + throw new RangeError("matrix denominator must be a positive safe integer"); + } + return matrix.map((row) => + row.map((value) => rational(BigInt(value), BigInt(denominator))), + ); +} + +function validateRationalMatrix(matrix, name) { + if (!Array.isArray(matrix) || matrix.length === 0) { + throw new TypeError(`${name} must be a non-empty matrix`); + } + const width = matrix[0]?.length; + if (!Number.isSafeInteger(width) || width <= 0) { + throw new TypeError(`${name} must have non-empty rows`); + } + for (const row of matrix) { + if (!Array.isArray(row) || row.length !== width) { + throw new RangeError(`${name} must be rectangular`); + } + for (const value of row) { + if ( + !value || + typeof value.numerator !== "bigint" || + typeof value.denominator !== "bigint" + ) { + throw new TypeError(`${name} must contain rational entries`); + } + } + } + return { rows: matrix.length, columns: width }; +} + +export function rationalMatrixMultiply(left, right) { + const leftShape = validateRationalMatrix(left, "left"); + const rightShape = validateRationalMatrix(right, "right"); + if (leftShape.columns !== rightShape.rows) { + throw new RangeError("rational matrix dimensions are incompatible"); + } + + return left.map((row) => + Array.from({ length: rightShape.columns }, (_, column) => { + let total = rationalZero(); + for (let index = 0; index < leftShape.columns; index += 1) { + total = addRationals( + total, + multiplyRationals(row[index], right[index][column]), + ); + } + return total; + }), + ); +} + +function rationalMatrixPower(matrix, exponent) { + const shape = validateRationalMatrix(matrix, "matrix"); + if (shape.rows !== shape.columns) throw new RangeError("matrix must be square"); + if (!Number.isSafeInteger(exponent) || exponent < 0) { + throw new RangeError("matrix exponent must be a non-negative safe integer"); + } + let result = rationalMatrixIdentity(shape.rows); + let base = matrix; + let power = exponent; + while (power > 0) { + if (power % 2 === 1) result = rationalMatrixMultiply(result, base); + power = Math.floor(power / 2); + if (power > 0) base = rationalMatrixMultiply(base, base); + } + return result; +} + +function rationalMatrixKey(matrix) { + return matrix + .map((row) => row.map((value) => rationalToString(value)).join(",")) + .join(";"); +} + +function rationalMatrixToStrings(matrix) { + return matrix.map((row) => row.map((value) => rationalToString(value))); +} + +function rationalMatrixVectorMultiply(matrix, vector) { + const shape = validateRationalMatrix(matrix, "matrix"); + if (!Array.isArray(vector) || vector.length !== shape.columns) { + throw new RangeError("vector length does not match matrix width"); + } + const values = vector.map((value) => + typeof value === "number" + ? rational(BigInt(value)) + : typeof value === "bigint" + ? rational(value) + : value, + ); + return matrix.map((row) => { + let total = rationalZero(); + for (let index = 0; index < row.length; index += 1) { + total = addRationals(total, multiplyRationals(row[index], values[index])); + } + return total; + }); +} + +function rationalVectorToStrings(vector) { + return vector.map((value) => rationalToString(value)); +} + +function sameRationalMatrix(left, right) { + return rationalMatrixKey(left) === rationalMatrixKey(right); +} + +function buildD4ElementMatrices() { + const identity = rationalMatrixIdentity(4); + const S = integerMatrixToRational( + SAKAI_WS_NUMERATOR, + SAKAI_WS_DENOMINATOR, + ); + const T = integerMatrixToRational(SAKAI_WT); + const ST = rationalMatrixMultiply(S, T); + const TS = rationalMatrixMultiply(T, S); + const STS = rationalMatrixMultiply(ST, S); + return new Map([ + ["e", identity], + ["S", S], + ["T", T], + ["ST", ST], + ["TS", TS], + ["STS", STS], + ]); +} + +function multiplicationTable(labels, elements, multiply, key) { + const lookup = new Map( + labels.map((label) => [key(elements.get(label)), label]), + ); + const table = {}; + for (const leftLabel of labels) { + table[leftLabel] = {}; + for (const rightLabel of labels) { + const product = multiply( + elements.get(leftLabel), + elements.get(rightLabel), + ); + const productLabel = lookup.get(key(product)); + if (!productLabel) { + throw new Error( + `group multiplication escaped the six-element set: ${leftLabel}*${rightLabel}`, + ); + } + table[leftLabel][rightLabel] = productLabel; + } + } + return table; +} + +export function buildD4S3Orbit() { + const elements = buildD4ElementMatrices(); + if ( + new Set([...elements.values()].map(rationalMatrixKey)).size !== + S3_LABELS.length + ) { + throw new Error("D4 triality generators do not produce six distinct elements"); + } + + const identity = elements.get("e"); + if (!sameRationalMatrix(rationalMatrixPower(elements.get("S"), 2), identity)) { + throw new Error("D4 triality S involution failed"); + } + if (!sameRationalMatrix(rationalMatrixPower(elements.get("T"), 2), identity)) { + throw new Error("D4 triality T involution failed"); + } + if (!sameRationalMatrix(rationalMatrixPower(elements.get("ST"), 3), identity)) { + throw new Error("D4 triality (ST)^3 relation failed"); + } + + const table = multiplicationTable( + S3_LABELS, + elements, + rationalMatrixMultiply, + rationalMatrixKey, + ); + + const orbit = S3_LABELS.map((label) => ({ + label, + vector: rationalVectorToStrings( + rationalMatrixVectorMultiply(elements.get(label), D4_ORBIT_SEED), + ), + })); + if (new Set(orbit.map((entry) => entry.vector.join(","))).size !== 6) { + throw new Error("chosen D4 orbit seed does not have a six-element orbit"); + } + + return { + generators: { + S: rationalMatrixToStrings(elements.get("S")), + T: rationalMatrixToStrings(elements.get("T")), + }, + elements: S3_LABELS.map((label) => ({ + label, + matrix: rationalMatrixToStrings(elements.get(label)), + })), + multiplicationTable: table, + seed: [...D4_ORBIT_SEED], + orbit, + }; +} + +function assertIntegerMatrix2(matrix, name) { + if ( + !Array.isArray(matrix) || + matrix.length !== 2 || + !matrix.every( + (row) => + Array.isArray(row) && + row.length === 2 && + row.every(Number.isSafeInteger), + ) + ) { + throw new TypeError(`${name} must be a 2x2 safe-integer matrix`); + } + const determinant = + BigInt(matrix[0][0]) * BigInt(matrix[1][1]) - + BigInt(matrix[0][1]) * BigInt(matrix[1][0]); + if (determinant !== 1n) { + throw new RangeError(`${name} must have determinant one`); + } + return matrix; +} + +function mod2(value) { + return ((value % 2) + 2) % 2; +} + +function mod2Key(matrix) { + return assertIntegerMatrix2(matrix, "matrix") + .flat() + .map(mod2) + .join(""); +} + +function buildModularRepresentativeMatrices() { + const e = integerIdentity(2); + const S = MODULAR_S.map((row) => [...row]); + const T = MODULAR_T.map((row) => [...row]); + const ST = integerMatrixMultiply(S, T); + const TS = integerMatrixMultiply(T, S); + const STS = integerMatrixMultiply(ST, S); + return new Map([ + ["e", e], + ["S", S], + ["T", T], + ["ST", ST], + ["TS", TS], + ["STS", STS], + ]); +} + +export function buildModularS3QuotientRepresentatives() { + const elements = buildModularRepresentativeMatrices(); + for (const [label, matrix] of elements) { + assertIntegerMatrix2(matrix, label); + } + + const keys = S3_LABELS.map((label) => mod2Key(elements.get(label))); + if (new Set(keys).size !== 6) { + throw new Error("modular representatives do not cover six distinct mod-2 classes"); + } + + const lookup = new Map( + S3_LABELS.map((label) => [mod2Key(elements.get(label)), label]), + ); + const table = {}; + for (const leftLabel of S3_LABELS) { + table[leftLabel] = {}; + for (const rightLabel of S3_LABELS) { + const product = integerMatrixMultiply( + elements.get(leftLabel), + elements.get(rightLabel), + ); + const productLabel = lookup.get(mod2Key(product)); + if (!productLabel) { + throw new Error("modular quotient multiplication escaped S3"); + } + table[leftLabel][rightLabel] = productLabel; + } + } + + return { + generators: { + S: elements.get("S").map((row) => [...row]), + T: elements.get("T").map((row) => [...row]), + }, + representatives: S3_LABELS.map((label) => ({ + label, + matrix: elements.get(label).map((row) => [...row]), + mod2Key: mod2Key(elements.get(label)), + })), + multiplicationTable: table, + }; +} + +function variablePolynomial(variableIndex, coefficient = 1n) { + const exponents = Array(POLYNOMIAL_VARIABLES.length).fill(0); + exponents[variableIndex] = 1; + return monomialPolynomial(exponents, coefficient); +} + +function linearUvForms(matrix) { + assertIntegerMatrix2(matrix, "SL2 matrix"); + return { + uPrime: addPolynomials( + variablePolynomial(U_INDEX, BigInt(matrix[0][0])), + variablePolynomial(V_INDEX, BigInt(matrix[0][1])), + ), + vPrime: addPolynomials( + variablePolynomial(U_INDEX, BigInt(matrix[1][0])), + variablePolynomial(V_INDEX, BigInt(matrix[1][1])), + ), + }; +} + +export function substituteUv(polynomial, matrix) { + const { uPrime, vPrime } = linearUvForms(matrix); + let result = zeroPolynomial(); + + for (const term of serializePolynomial(polynomial)) { + const exponents = [...term.exponents]; + const uExponent = exponents[U_INDEX]; + const vExponent = exponents[V_INDEX]; + exponents[U_INDEX] = 0; + exponents[V_INDEX] = 0; + + let transformedTerm = monomialPolynomial( + exponents, + parseRationalText(term.coefficient), + ); + if (uExponent > 0) { + transformedTerm = multiplyPolynomials( + transformedTerm, + powerPolynomial(uPrime, uExponent), + ); + } + if (vExponent > 0) { + transformedTerm = multiplyPolynomials( + transformedTerm, + powerPolynomial(vPrime, vExponent), + ); + } + result = addPolynomials(result, transformedTerm); + } + return result; +} + +function binaryCoefficientPolynomial(polynomial, order, position) { + if ( + !Number.isSafeInteger(order) || + order < 0 || + !Number.isSafeInteger(position) || + position < 0 || + position > order + ) { + throw new RangeError("invalid binary coefficient request"); + } + + let result = zeroPolynomial(); + for (const term of serializePolynomial(polynomial)) { + const exponents = [...term.exponents]; + if (exponents[U_INDEX] + exponents[V_INDEX] !== order) { + throw new Error("transformed source form is not homogeneous in u,v"); + } + if ( + exponents[U_INDEX] !== order - position || + exponents[V_INDEX] !== position + ) { + continue; + } + exponents[U_INDEX] = 0; + exponents[V_INDEX] = 0; + result = addPolynomials( + result, + monomialPolynomial( + exponents, + parseRationalText(term.coefficient), + ), + ); + } + if (result.size === 0) { + throw new Error("binary coefficient extraction produced zero unexpectedly"); + } + return result; +} + +export function deriveCoefficientAction(matrix) { + assertIntegerMatrix2(matrix, "SL2 matrix"); + const { f, g } = buildSourceBinaryForms(); + const transformedF = substituteUv(f, matrix); + const transformedG = substituteUv(g, matrix); + + const substitutions = [ + binaryCoefficientPolynomial(transformedF, 2, 0), + binaryCoefficientPolynomial(transformedF, 2, 1), + binaryCoefficientPolynomial(transformedF, 2, 2), + binaryCoefficientPolynomial(transformedG, 3, 0), + binaryCoefficientPolynomial(transformedG, 3, 1), + binaryCoefficientPolynomial(transformedG, 3, 2), + binaryCoefficientPolynomial(transformedG, 3, 3), + ]; + + if (!polynomialsEqual( + substituteCoefficientVariables(f, substitutions), + transformedF, + )) { + throw new Error("derived quadratic coefficient action does not reconstruct f'"); + } + if (!polynomialsEqual( + substituteCoefficientVariables(g, substitutions), + transformedG, + )) { + throw new Error("derived cubic coefficient action does not reconstruct g'"); + } + + return substitutions; +} + +export function substituteCoefficientVariables(polynomial, substitutions) { + if ( + !Array.isArray(substitutions) || + substitutions.length !== COEFFICIENT_COUNT + ) { + throw new RangeError( + `coefficient substitutions must have length ${COEFFICIENT_COUNT}`, + ); + } + + const caches = substitutions.map(() => new Map([[0, constantPolynomial(1n)]])); + function substitutionPower(index, exponent) { + const cache = caches[index]; + if (!cache.has(exponent)) { + cache.set(exponent, powerPolynomial(substitutions[index], exponent)); + } + return cache.get(exponent); + } + + let result = zeroPolynomial(); + for (const term of serializePolynomial(polynomial)) { + const exponents = [...term.exponents]; + const receiverExponents = Array(POLYNOMIAL_VARIABLES.length).fill(0); + receiverExponents[U_INDEX] = exponents[U_INDEX]; + receiverExponents[V_INDEX] = exponents[V_INDEX]; + + let transformedTerm = monomialPolynomial( + receiverExponents, + parseRationalText(term.coefficient), + ); + for (let index = 0; index < COEFFICIENT_COUNT; index += 1) { + const exponent = exponents[index]; + if (exponent > 0) { + transformedTerm = multiplyPolynomials( + transformedTerm, + substitutionPower(index, exponent), + ); + } + } + result = addPolynomials(result, transformedTerm); + } + return result; +} + +export function verifyCovariantEquivariance(polynomial, matrix) { + const substitutions = deriveCoefficientAction(matrix); + const coefficientSide = substituteCoefficientVariables( + polynomial, + substitutions, + ); + const variableSide = substituteUv(polynomial, matrix); + return { + passed: polynomialsEqual(coefficientSide, variableSide), + coefficientSideSha256: polynomialSha256(coefficientSide), + variableSideSha256: polynomialSha256(variableSide), + }; +} + +function sha256Lines(lines) { + return createHash("sha256") + .update(lines.join("\n"), "utf8") + .digest("hex"); +} + +export function buildEquivarianceReport() { + const modular = buildModularS3QuotientRepresentatives(); + const matrices = buildModularRepresentativeMatrices(); + const basis = buildCovariantBasis(); + const perRepresentative = []; + + for (const label of S3_LABELS) { + const matrix = matrices.get(label); + const substitutions = deriveCoefficientAction(matrix); + const substitutionHash = sha256Lines( + substitutions.map(canonicalPolynomialText), + ); + const transformedHashes = []; + + for (const generator of basis) { + const coefficientSide = substituteCoefficientVariables( + generator.polynomial, + substitutions, + ); + const variableSide = substituteUv(generator.polynomial, matrix); + if (!polynomialsEqual(coefficientSide, variableSide)) { + throw new Error( + `covariant equivariance failed for ${generator.label} under ${label}`, + ); + } + transformedHashes.push( + `${generator.index}:${generator.label}:${polynomialSha256(variableSide)}`, + ); + } + + perRepresentative.push({ + label, + modularMatrix: matrix.map((row) => [...row]), + mod2Key: mod2Key(matrix), + coefficientActionSha256: substitutionHash, + transformedBasisSha256: sha256Lines(transformedHashes), + checkedGenerators: basis.length, + allPassed: true, + }); + } + + return { + law: "Psi(alpha';u,v)=Psi(alpha;u',v')", + representatives: perRepresentative, + checkedRepresentatives: perRepresentative.length, + checkedGenerators: basis.length, + exactChecks: perRepresentative.length * basis.length, + allPassed: true, + quotientMultiplicationTable: modular.multiplicationTable, + }; +} + +export function buildS3EquivarianceHarnessSummary() { + const d4 = buildD4S3Orbit(); + const modular = buildModularS3QuotientRepresentatives(); + if ( + JSON.stringify(d4.multiplicationTable) !== + JSON.stringify(modular.multiplicationTable) + ) { + throw new Error("D4/F4 and modular quotient multiplication tables disagree"); + } + + const equivariance = buildEquivarianceReport(); + return { + schema: HARNESS_SCHEMA_ID, + harnessId: HARNESS_ID, + harnessVersion: HARNESS_VERSION, + quotientIdentification: { + modular: "SL2(Z)/Gamma(2)~=S3", + d4: "W(F4)/W(D4)~=S3", + generatorCorrespondence: ["gS<->wS", "gT<->wT"], + warning: + "The six modular matrices are canonical SL2(Z) representatives of quotient classes; they are not asserted to form a literal six-element subgroup upstairs.", + }, + d4Orbit: d4, + modularQuotient: modular, + multiplicationTablesMatch: true, + equivariance, + claimBoundary: + "The harness verifies the exact six-element D4/F4 triality quotient orbit and exact SL2(C) covariant equivariance for the 15 constructed binary covariants under six canonical SL2(Z) representatives. It does not claim that the quotient classes themselves are a six-element subgroup of SL2(Z), and it introduces no sonification mapping.", + }; +} diff --git a/tests/d4-triality-s3-equivariance.test.mjs b/tests/d4-triality-s3-equivariance.test.mjs new file mode 100644 index 0000000..14b3c09 --- /dev/null +++ b/tests/d4-triality-s3-equivariance.test.mjs @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MPL-2.0 + +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import test from "node:test"; + +import { + D4_ORBIT_SEED, + HARNESS_ID, + HARNESS_VERSION, + MODULAR_S, + MODULAR_T, + S3_LABELS, + buildD4S3Orbit, + buildEquivarianceReport, + buildModularS3QuotientRepresentatives, + buildS3EquivarianceHarnessSummary, + deriveCoefficientAction, + substituteUv, + verifyCovariantEquivariance, +} from "../src/d4-triality-s3-equivariance.mjs"; +import { + buildCovariantBasis, + buildSourceBinaryForms, + canonicalPolynomialText, + polynomialsEqual, +} from "../src/d4-triality-covariant-engine.mjs"; + +const PROJECT_ROOT = resolve(new URL("..", import.meta.url).pathname); +const fixture = JSON.parse( + readFileSync( + resolve(PROJECT_ROOT, "examples/d4-tia-s3-equivariance.v0.1.canonical.json"), + "utf8", + ), +); +const schema = JSON.parse( + readFileSync( + resolve(PROJECT_ROOT, "spec/d4-tia-s3-equivariance.v0.1.schema.json"), + "utf8", + ), +); + +const EXPECTED_TABLE = { + e: { e: "e", S: "S", T: "T", ST: "ST", TS: "TS", STS: "STS" }, + S: { e: "S", S: "e", T: "ST", ST: "T", TS: "STS", STS: "TS" }, + T: { e: "T", S: "TS", T: "e", ST: "STS", TS: "S", STS: "ST" }, + ST: { e: "ST", S: "STS", T: "S", ST: "TS", TS: "e", STS: "T" }, + TS: { e: "TS", S: "T", T: "STS", ST: "e", TS: "ST", STS: "S" }, + STS: { e: "STS", S: "ST", T: "TS", ST: "S", TS: "T", STS: "e" }, +}; + +test("D4/F4 wS and wT generate the exact six-element S3 quotient action", () => { + const d4 = buildD4S3Orbit(); + assert.deepEqual(d4.seed, D4_ORBIT_SEED); + assert.deepEqual(d4.multiplicationTable, EXPECTED_TABLE); + assert.deepEqual( + d4.elements.map((entry) => entry.label), + S3_LABELS, + ); + assert.deepEqual(d4.orbit, [ + { label: "e", vector: ["2", "4", "6", "10"] }, + { label: "S", vector: ["1", "5", "7", "9"] }, + { label: "T", vector: ["2", "4", "6", "-10"] }, + { label: "ST", vector: ["11", "-5", "-3", "-1"] }, + { label: "TS", vector: ["1", "5", "7", "-9"] }, + { label: "STS", vector: ["11", "-5", "-3", "1"] }, + ]); +}); + +test("canonical modular representatives cover all six mod-2 quotient classes", () => { + const modular = buildModularS3QuotientRepresentatives(); + assert.deepEqual(modular.generators.S, MODULAR_S); + assert.deepEqual(modular.generators.T, MODULAR_T); + assert.deepEqual(modular.multiplicationTable, EXPECTED_TABLE); + assert.deepEqual( + modular.representatives.map((entry) => entry.label), + S3_LABELS, + ); + assert.equal( + new Set(modular.representatives.map((entry) => entry.mod2Key)).size, + 6, + ); +}); + +test("coefficient actions are derived from f(u',v') and g(u',v') exactly", () => { + const { f, g } = buildSourceBinaryForms(); + + for (const matrix of [MODULAR_S, MODULAR_T]) { + const substitutions = deriveCoefficientAction(matrix); + assert.equal(substitutions.length, 7); + + const transformedF = substituteUv(f, matrix); + const transformedG = substituteUv(g, matrix); + + // deriveCoefficientAction itself reconstructs both transformed forms. + // These checks ensure the returned action is nontrivial and deterministic. + assert.ok(substitutions.every((entry) => entry.size >= 1)); + assert.notEqual( + canonicalPolynomialText(transformedF), + canonicalPolynomialText(f), + ); + assert.notEqual( + canonicalPolynomialText(transformedG), + canonicalPolynomialText(g), + ); + } +}); + +test("all 15 covariants satisfy exact SL2 equivariance under S and T representatives", () => { + for (const matrix of [MODULAR_S, MODULAR_T]) { + for (const generator of buildCovariantBasis()) { + const result = verifyCovariantEquivariance(generator.polynomial, matrix); + assert.equal( + result.passed, + true, + `${generator.label} failed equivariance`, + ); + assert.equal( + result.coefficientSideSha256, + result.variableSideSha256, + ); + } + } +}); + +test("the full six-representative harness performs 90 exact covariant checks", () => { + const report = buildEquivarianceReport(); + assert.equal(report.checkedRepresentatives, 6); + assert.equal(report.checkedGenerators, 15); + assert.equal(report.exactChecks, 90); + assert.equal(report.allPassed, true); + assert.deepEqual(report.quotientMultiplicationTable, EXPECTED_TABLE); + assert.ok(report.representatives.every((entry) => entry.allPassed)); + assert.ok( + report.representatives.every( + (entry) => + /^[0-9a-f]{64}$/.test(entry.coefficientActionSha256) && + /^[0-9a-f]{64}$/.test(entry.transformedBasisSha256), + ), + ); +}); + +test("the harness keeps quotient S3 and upstairs SL2 actions explicitly separate", () => { + const summary = buildS3EquivarianceHarnessSummary(); + assert.equal(summary.harnessId, HARNESS_ID); + assert.equal(summary.harnessVersion, HARNESS_VERSION); + assert.equal(summary.multiplicationTablesMatch, true); + assert.deepEqual( + summary.d4Orbit.multiplicationTable, + summary.modularQuotient.multiplicationTable, + ); + assert.match( + summary.quotientIdentification.warning, + /not asserted to form a literal six-element subgroup/, + ); + assert.match(summary.claimBoundary, /introduces no sonification mapping/); +}); + + +test("the canonical S3 equivariance summary is pinned by fixture and schema", () => { + const summary = buildS3EquivarianceHarnessSummary(); + assert.deepEqual(summary, fixture); + assert.equal(schema.$id, "qsol.d4-tia-s3-equivariance/v0.1"); + assert.deepEqual(schema.const, fixture); +});