From 89defef30be7f45d868dbb43105dd115b346d02e Mon Sep 17 00:00:00 2001 From: Shay Date: Thu, 28 May 2026 07:00:33 -0700 Subject: [PATCH] =?UTF-8?q?chore(audit):=20substrate=20cleanup=20=E2=80=94?= =?UTF-8?q?=20dead=20spike,=20gitignore,=20deprecation,=20reader=20diagnos?= =?UTF-8?q?is?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C1: delete generate/math_versor_arithmetic.py and its 3 tests (ADR-0139 add-only arithmetic spike; no runtime consumers, no pipeline wiring, follow-on lift paused per module docstring). C3: gitignore engine_state runtime artifacts (manifest.json, recognizers.jsonl, discovery_candidates.jsonl). Module code (engine_state/__init__.py) remains tracked; generated checkpoint files should not be. C5: document reader zero-delta root cause in train_sample/v1/README.md. Both Phase 2 (whole-problem) and Phase 1 (question-only) reader paths are called but inert because all 47 refusals are statement-level NO_INJECTOR gaps, not question-sentence gaps. Reader unblocks when injector coverage expands (C2 work). report.json use_reader flag corrected to reflect last run. C6: add deprecation header to generate/math_parser.py pointing at generate.math_candidate_graph.parse_and_solve as the live path. C2/C4 briefs: docs/handoff/CLEANUP-C2-run-lane-migration.md and docs/handoff/CLEANUP-C4-compositions-compile.md added as operator dispatch docs for the medium-scope wiring tasks. --- .gitignore | 7 + docs/handoff/CLEANUP-C2-run-lane-migration.md | 126 +++++++ .../CLEANUP-C4-compositions-compile.md | 127 +++++++ evals/gsm8k_math/train_sample/v1/README.md | 21 ++ evals/gsm8k_math/train_sample/v1/report.json | 2 +- generate/math_parser.py | 8 + generate/math_versor_arithmetic.py | 221 ------------ tests/test_arithmetic_as_versor_add.py | 171 ---------- tests/test_arithmetic_multiply_as_dilator.py | 311 ----------------- tests/test_arithmetic_subtract_and_group.py | 320 ------------------ 10 files changed, 290 insertions(+), 1024 deletions(-) create mode 100644 docs/handoff/CLEANUP-C2-run-lane-migration.md create mode 100644 docs/handoff/CLEANUP-C4-compositions-compile.md delete mode 100644 generate/math_versor_arithmetic.py delete mode 100644 tests/test_arithmetic_as_versor_add.py delete mode 100644 tests/test_arithmetic_multiply_as_dilator.py delete mode 100644 tests/test_arithmetic_subtract_and_group.py diff --git a/.gitignore b/.gitignore index 5fc2b80c..6ecf65bc 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,10 @@ evals/gsm8k_math/holdouts/v1/cases-train* # ADR-0172 W3 — math-contemplation proposals are generated on demand; the # directory skeleton (.gitkeep) is committed, the generated JSONL is not. teaching/math_proposals/proposals.jsonl + +# ADR-0146 — engine_state/ is per-session mutable checkpoint state; only the +# module itself (engine_state/__init__.py) is tracked. Runtime-generated files +# are not source artifacts. +engine_state/manifest.json +engine_state/recognizers.jsonl +engine_state/discovery_candidates.jsonl diff --git a/docs/handoff/CLEANUP-C2-run-lane-migration.md b/docs/handoff/CLEANUP-C2-run-lane-migration.md new file mode 100644 index 00000000..cca86882 --- /dev/null +++ b/docs/handoff/CLEANUP-C2-run-lane-migration.md @@ -0,0 +1,126 @@ +# C2 — Wire `run_lane` to `_score_one_candidate_graph` + +**Classification**: Unfinished migration (audit finding C2) +**Risk**: Medium — must verify baseline comparison files still reproduce +**Scope**: `evals/gsm8k_math/runner.py` only + +--- + +## Problem + +`run_lane` (line 449) calls `_score_one`, the legacy regex path +(`generate/math_parser.py` → `MathProblemGraph` → `solve`). Only the +per-lane override in `train_sample/v1/runner.py:33` uses +`_score_one_candidate_graph`. Any caller that goes through `run_lane` — +including the `public/`, `dev/`, and `holdout` lanes — is evaluated on the +legacy parser, not the candidate-graph architecture we've been measuring. + +**Impact**: lift numbers on `train_sample/v1` are not comparable to what +`run_lane` would report on the other slices. The three-slice concern from the +audit is real. + +--- + +## What `_score_one_candidate_graph` does differently + +`_score_one_candidate_graph` (line 241) calls `parse_and_solve` from +`generate/math_candidate_graph`, which layers: + +1. Whole-problem comprehension reader (ADR-0164 Phase 2, flag-gated) +2. Per-statement recognizer candidate graph (ME-1..ME-5 matchers) +3. Question-sentence hybrid reader (ADR-0164 Phase 1) +4. Injector dispatch (ADR-0170 + recognizer_anchor_inject.py) +5. Cartesian-product branch enumeration + admissibility gate +6. Existing solver + verifier (same as `_score_one`) + +`_score_one` calls the regex parser directly, skipping steps 1–5. + +--- + +## Migration plan + +### Step 1 — Baseline snapshot (before changing `run_lane`) + +```bash +# Capture current run_lane output on all slices that have unsealed cases +uv run python -m evals.gsm8k_math.runner --lane public > /tmp/pre_public.json +uv run python -m evals.gsm8k_math.runner --lane dev > /tmp/pre_dev.json +# Do NOT touch holdout; it is sealed. +``` + +### Step 2 — Wire `run_lane` + +In `evals/gsm8k_math/runner.py` line 449, replace: + +```python +outcomes = [_score_one(c) for c in cases] +``` + +with: + +```python +outcomes = [_score_one_candidate_graph(c) for c in cases] +``` + +Add a `--legacy-parser` CLI flag that restores `_score_one` for the +comparison baseline file. The flag is only needed to reproduce +`baselines/frontier.json` and `baselines/comparison_v1.json`, both of which +were captured under the legacy path. + +```python +# In run_lane signature: +def run_lane( + cases: list[dict[str, Any]], + *, + config: Any = None, + legacy_parser: bool = False, +) -> LaneReport: + score_fn = _score_one if legacy_parser else _score_one_candidate_graph + outcomes = [score_fn(c) for c in cases] +``` + +### Step 3 — Verify wrong == 0 on public/dev + +```bash +uv run python -m evals.gsm8k_math.runner --lane public > /tmp/post_public.json +uv run python -m evals.gsm8k_math.runner --lane dev > /tmp/post_dev.json +# Assert wrong == 0 in both outputs +``` + +### Step 4 — Update baselines if counts differ + +If `correct` counts differ (expected — the new path refuses differently), +update `baselines/frontier.json` and `baselines/comparison_v1.json` by +re-running with `--legacy-parser` so the files still reflect legacy baseline +numbers: + +```bash +uv run python -m evals.gsm8k_math.runner --lane public --legacy-parser \ + > evals/gsm8k_math/baselines/frontier.json +``` + +### Step 5 — Run smoke + packs suites + +```bash +uv run core test --suite smoke -q +uv run core test --suite packs -q +``` + +--- + +## Invariant gates + +- `wrong == 0` must hold on public + dev after the migration. +- `_score_one_candidate_graph` already enforces this by construction (same + verifier as `_score_one`, additive reader, refusal-preferring injector). +- The baseline snapshots captured in Step 1 give you a before/after diff to + explain any count change in a PR description. + +--- + +## Relation to other findings + +- **C4** (compositions compile): C4 should land first or simultaneously so + that `run_lane` immediately benefits from the registry being non-empty. +- **C5** (reader zero-delta): confirmed that the reader is inert until + statement-level injector gaps close. C2 does not depend on C5. diff --git a/docs/handoff/CLEANUP-C4-compositions-compile.md b/docs/handoff/CLEANUP-C4-compositions-compile.md new file mode 100644 index 00000000..22770b20 --- /dev/null +++ b/docs/handoff/CLEANUP-C4-compositions-compile.md @@ -0,0 +1,127 @@ +# C4 — Compile and commit `compositions.jsonl` for the math pack + +**Classification**: Half-built producer/consumer loop (audit finding C4) +**Risk**: Low — purely additive, wrong == 0 guaranteed by registry design +**Scope**: `language_packs/data/en_core_math_v1/`, `language_packs/compile_compositions.py` + +--- + +## Problem + +`language_packs/data/en_core_math_v1/compositions/multiplicative_composition.jsonl` +was ratified via ADR-0169 and is present on disk. But +`language_packs/data/en_core_math_v1/compositions.jsonl` (the compiled +artifact that the runtime reads) **does not exist**. + +The consumer chain is: + +``` +compositions/multiplicative_composition.jsonl ← ratified source (EXISTS) + ↓ compile_compositions.py +compositions.jsonl ← compiled registry (MISSING) + ↓ comprehension/composition_registry.py::load_composition_registry() + ↓ recognizer_anchor_inject.py (line 157–159) +InjectorEmission[] ← runtime output (always empty) +``` + +`load_composition_registry()` handles the missing file gracefully — it +returns an empty registry when `compositions.jsonl` is absent and +`manifest.json` does not declare `composition_checksum`. So the system is +stable; the ratified claims simply have no effect on runtime decisions. + +--- + +## What to do + +### Step 1 — Run the compile step + +```bash +uv run python -c " +from pathlib import Path +from language_packs.compile_compositions import compile_pack_compositions + +pack = Path('language_packs/data/en_core_math_v1') +result = compile_pack_compositions(pack) +print('written to:', result.output_path) +print('sha256:', result.sha256) +print('entries:', result.entry_count) +" +``` + +Verify `entry_count > 0` (at least the multiplicative_composition entries). + +### Step 2 — Update manifest.json with composition_checksum + +The manifest checksum enforces that the committed compiled artifact matches +what `compile_compositions.py` would produce from the source files. Once the +compiled artifact is committed, `manifest.json` should be updated to declare +`composition_checksum` so any future drift raises +`CompositionRegistryLoadError` at load time (defense in depth). + +```bash +sha=$(sha256sum language_packs/data/en_core_math_v1/compositions.jsonl | cut -d' ' -f1) +# or on macOS: +sha=$(shasum -a 256 language_packs/data/en_core_math_v1/compositions.jsonl | cut -d' ' -f1) +``` + +Then add `"composition_checksum": ""` to `manifest.json`. + +### Step 3 — Verify the registry is non-empty at runtime + +```bash +uv run python -c " +from generate.comprehension.composition_registry import load_composition_registry +r = load_composition_registry() +print('empty:', r.is_empty()) +print('categories:', list(r.by_category.keys())) +" +``` + +Expected: `empty: False`, at least `multiplicative_composition` in categories. + +### Step 4 — Run packs + smoke suites + +```bash +uv run core test --suite packs -q +uv run core test --suite smoke -q +``` + +### Step 5 — Commit + +``` +language_packs/data/en_core_math_v1/compositions.jsonl (new) +language_packs/data/en_core_math_v1/manifest.json (updated: composition_checksum) +``` + +Commit message: +``` +feat(packs): compile multiplicative_composition registry for math pack + +Runs compile_compositions.py to produce compositions.jsonl from the +ratified multiplicative_composition.jsonl source. Updates manifest.json +with composition_checksum. load_composition_registry() now returns a +non-empty registry; recognizer_anchor_inject.py can emit InjectorEmission +for composition-shape matches. Closes the producer/consumer gap from +ADR-0169. +``` + +--- + +## Invariant gates + +- `wrong == 0` is preserved by the registry's refusal-preferring discipline: + `is_falsified` returns `()` immediately; `is_affirmed` gates every emission. +- The `WrongCompositionCategory` check at load time prevents any unsafe + category from being accepted even if the source file is mutated. +- The manifest checksum (after Step 2) provides ongoing compile-drift + detection. + +--- + +## Relation to other findings + +- **C2** (run_lane migration): C4 should land at the same time or before C2 + so the candidate-graph path immediately sees a non-empty composition + registry. +- **C5** (reader zero-delta): the reader's zero-delta is unrelated to + compositions — it is a statement-level injector gap. C4 does not fix C5. diff --git a/evals/gsm8k_math/train_sample/v1/README.md b/evals/gsm8k_math/train_sample/v1/README.md index 5d004ade..ace10ce1 100644 --- a/evals/gsm8k_math/train_sample/v1/README.md +++ b/evals/gsm8k_math/train_sample/v1/README.md @@ -49,3 +49,24 @@ Each case in `cases.jsonl` preserves: - `question`: the verbatim question string - `answer_expression`: the verbatim answer field containing reasoning steps and number suffix - `answer_numeric`: the integer or float parsed from the `#### N` suffix in the answer expression + +## ADR-0164 Reader — Zero-Delta Diagnosis + +`report.json` records `use_reader: true` (3 correct / 47 refused / 0 wrong), identical counts +to the baseline. The reader is not silent: both Phase 2 (whole-problem) and Phase 1 +(question-only hybrid) are called. The zero-delta has a structural cause — all 47 refusals +are **statement-level**, not question-level: + +- Phase 2 (`_try_comprehension_reader`) processes every sentence in order. It refuses on the + first statement it cannot classify (unknown rate, multiplicative aggregation, fractional + scale, etc.) and returns `None`, passing control to the regex pipeline. +- Phase 1 (`_try_reader_for_question`) is subsequently reached but only substitutes the + question's `CandidateUnknown`. It cannot recover cases whose statement sentences produced + no injector output (`NO_INJECTOR`) or no admissible candidate — those remain refused after + Phase 1's contribution. + +Decision: the reader is structurally correct (`wrong == 0` preserved), inert on this sample +because the bottleneck is statement parsing / injector dispatch gaps (C2 in the cleanup wave), +not question parsing. No scope change needed; the reader unblocks when injector coverage +expands. Record this as the truth-test gate: reader lift will show only after `NO_INJECTOR` +refusals convert to admitted cases. diff --git a/evals/gsm8k_math/train_sample/v1/report.json b/evals/gsm8k_math/train_sample/v1/report.json index 14796bbb..e880bb0a 100644 --- a/evals/gsm8k_math/train_sample/v1/report.json +++ b/evals/gsm8k_math/train_sample/v1/report.json @@ -265,5 +265,5 @@ "sample_count": 50, "sample_path": "evals/gsm8k_math/train_sample/v1/cases.jsonl", "schema_version": 1, - "use_reader": false + "use_reader": true } diff --git a/generate/math_parser.py b/generate/math_parser.py index 88cd53a5..59df6600 100644 --- a/generate/math_parser.py +++ b/generate/math_parser.py @@ -1,5 +1,13 @@ """ADR-0115 Phase 1.3 — deterministic math word-problem parser. +.. deprecated:: + This module is the **legacy regex parser path**. New callers should use + :func:`generate.math_candidate_graph.parse_and_solve`, which layers the + recognizer-driven candidate-graph topology on top. This module remains as + the fallback path wired inside ``math_candidate_graph.py``; it should not + be invoked directly by pipeline or eval code. + + Turns a grade-school math word problem into a :class:`MathProblemGraph` via rule-based extraction. No LLM, no sampling, no statistical anything. Same input string always produces the same graph; failures raise diff --git a/generate/math_versor_arithmetic.py b/generate/math_versor_arithmetic.py deleted file mode 100644 index 25440d0d..00000000 --- a/generate/math_versor_arithmetic.py +++ /dev/null @@ -1,221 +0,0 @@ -"""ADR-0139 — Arithmetic-as-versor spike: `add` only. - -Algebraic substrate for representing scalar arithmetic as closed versors -in Cl(4,1). This module proves the **load-bearing unknown** of the -Engine A lift program: that one arithmetic operation can be represented -as a closed unit versor satisfying ``versor_condition < 1e-6`` without -weakening any existing invariant. - -Scope (frozen by ADR-0139): - -- Single operation: ``add``. -- Single-axis embedding: quantities live on the e1 axis of the CGA - conformal model. -- No graph wiring (no ``MathProblemGraph`` consumer). -- No pipeline wiring (no ``CognitiveTurnPipeline`` integration). -- No GSM8K case routed. -- Unit is carried as caller metadata; not encoded in the multivector. - -If acceptance assertions hold for ``add``, follow-on ADRs cover -``subtract`` (inverse translator), ``multiply`` (dilator), and the lift -to ``MathProblemGraph`` consumers. If they do not, the lift program is -paused. - -Determinism: float64 end-to-end. No platform-conditional code. No -randomness. - -References: -- ``algebra/cga.py:embed_point`` — conformal point embedding -- ``algebra/cga.py:cga_inner`` — null-cone metric -- ``algebra/versor.py:versor_apply`` — sandwich product (null inputs - preserved via raw sandwich) -- ``algebra/versor.py:versor_condition`` — ``|V·reverse(V) - 1|`` -- ``algebra/cl41.py:geometric_product`` — Cl(4,1) geometric product -""" - -from __future__ import annotations - -import numpy as np - -from algebra.cga import cga_inner -from algebra.cl41 import N_COMPONENTS, geometric_product - -__all__ = [ - "embed_quantity", - "translator", - "subtract", - "multiply", - "decode_quantity", - "N_INF", -] - - -# Conformal point at infinity: n_inf = e4 + e5 (per algebra/cga.py -# convention). Constructed as a 32-component grade-1 multivector with -# components at indices 4 (e4) and 5 (e5) both equal to 1.0. -def _n_inf() -> np.ndarray: - v = np.zeros(N_COMPONENTS, dtype=np.float64) - v[4] = 1.0 - v[5] = 1.0 - return v - - -N_INF: np.ndarray = _n_inf() - - -def embed_quantity(value: float, unit: str) -> np.ndarray: - """Embed a scalar quantity as a conformal point on the e1 axis. - - The quantity ``value`` becomes a CGA null point at Euclidean - coordinates ``[value, 0, 0]``. The ``unit`` argument is not - encoded in the multivector — it is carried as caller metadata and - enforced by ``decode_quantity`` returning the same unit string. - - Returns a float64 32-component multivector lying on the null cone: - ``cga_inner(X, X) ≈ 0``. - - Args: - value: Numeric value of the quantity. - unit: Unit string (carried metadata; not encoded). - - Returns: - 32-component float64 multivector representing the embedded point. - """ - if not isinstance(unit, str) or not unit: - raise ValueError(f"embed_quantity: unit must be a non-empty string, got {unit!r}") - # Embed directly in float64 to avoid float32 quantization error for - # values like 0.01 that have no exact float32 representation. - # Formula: X = v*e1 + n_o + 0.5*v²*n_inf, n_o = 0.5*(e5-e4), n_inf = e4+e5. - v = float(value) - v_sq = v * v - result = np.zeros(N_COMPONENTS, dtype=np.float64) - result[1] = v # e1 component - result[4] = 0.5 * (v_sq - 1.0) # e4: n_o contribution -0.5, n_inf contribution +0.5*v² - result[5] = 0.5 * (v_sq + 1.0) # e5: n_o contribution +0.5, n_inf contribution +0.5*v² - return result - - -def translator(addend: float) -> np.ndarray: - """Construct the CGA translator versor for additive shift along e1. - - Standard CGA translator construction: - - T_t = 1 - 0.5 * (t · n_inf) - - where ``t = addend * e1`` is the Euclidean translation vector lifted - to grade-1, and ``n_inf = e4 + e5``. Since ``t`` and ``n_inf`` are - orthogonal null/non-null vectors, their geometric product is purely - a bivector and ``(t · n_inf)² = 0``, so the closed-form expression - is exact (no higher-order terms in the exponential expansion). - - The construction guarantees ``T_t · reverse(T_t) = 1`` exactly in - exact arithmetic; in float64 the residual measured by - ``versor_condition`` should be at machine epsilon. - - Args: - addend: Scalar to add along e1. - - Returns: - 32-component float64 unit versor satisfying - ``versor_condition(T) < 1e-6``. - """ - # t = addend * e1 — grade-1 vector with only e1 component - t = np.zeros(N_COMPONENTS, dtype=np.float64) - t[1] = float(addend) - - # B = t * n_inf — geometric product (bivector since t ⊥ n_inf) - bivector = geometric_product(t, N_INF) - - # T = 1 - 0.5 * B - T = np.zeros(N_COMPONENTS, dtype=np.float64) - T[0] = 1.0 # scalar part - T -= 0.5 * bivector - return T - - -def subtract(addend: float) -> np.ndarray: - """Construct the CGA translator versor for subtractive shift along e1. - - Delegates to ``translator(-addend)``. No new algebra. - """ - return translator(-float(addend)) - - -def multiply(scale: float) -> np.ndarray: - """Construct the CGA dilator versor for multiplicative scaling along e1. - - Restricted to scale > 0 strictly. Calls with scale <= 0 raise - ValueError. Negative scales (require composition with reflection) - and multiplication by zero (degenerate) are deferred to follow-on ADRs. - - Construction: D_s = cosh(α/2) + sinh(α/2) * (n_o ∧ n_inf) - where s = exp(α), α = ln(s). - - Measured in this CGA implementation (blade indices 0-indexed): - N = n_o ∧ n_inf has a single non-zero component at index 15 - (blade (3,4) = e4∧e5) with value -1.0. - N² = +1 (pure scalar, verified empirically and analytically). - - Because N² = +1 the exponential exp(α/2 · N) = cosh(α/2) + sinh(α/2)·N - is exact in float64 — no series truncation error. - - The sandwich D_s · X · ~D_s applied to a null CGA point P(a) yields - a null point projectively equal to P(a·s) with n_inf normalization - factor 1/s. decode_quantity normalizes by n_inf to recover a·s. - - Args: - scale: Positive real multiplier. Must satisfy scale > 0. - - Returns: - 32-component float64 unit versor satisfying - ``versor_condition(D) < 1e-6``. - - Raises: - ValueError: If scale <= 0. - """ - scale = float(scale) - if scale <= 0.0: - raise ValueError( - f"multiply: scale must be strictly positive, got {scale!r}. " - f"Negative scales and zero are deferred to follow-on ADRs." - ) - alpha = np.log(scale) - half = alpha / 2.0 - D = np.zeros(N_COMPONENTS, dtype=np.float64) - D[0] = np.cosh(half) - # N = n_o ∧ n_inf has component -1 at index 15 (blade (3,4), measured). - # D_s = cosh(α/2)·1 + sinh(α/2)·N → D[15] = sinh · (-1) = -sinh. - D[15] = -np.sinh(half) - return D - - -def decode_quantity(F: np.ndarray, unit: str) -> tuple[float, str]: - """Decode a multivector back to a (value, unit) scalar quantity. - - CGA points are projective: D_s * P * ~D_s produces a point - proportional to P(s·x) with scale factor 1/s. Normalizing by the - n_inf inner product recovers the true Euclidean coordinate regardless - of projective scale. For translator outputs (n_inf·X = -1) the - normalization is 1 and the result is identical to the previous - direct e1 read. - - Args: - F: 32-component multivector to decode. - unit: Unit string to attach to the returned scalar. - - Returns: - Tuple of ``(value, unit)`` where ``value`` is the normalized - e1 coordinate. - """ - if not isinstance(unit, str) or not unit: - raise ValueError(f"decode_quantity: unit must be a non-empty string, got {unit!r}") - arr = np.asarray(F, dtype=np.float64) - if arr.shape != (N_COMPONENTS,): - raise ValueError(f"decode_quantity: expected shape ({N_COMPONENTS},), got {arr.shape}") - # Normalize e1 by the n_inf inner product. For normalized conformal - # points (n_inf·X = -1) this divides by 1; for dilated points with - # scale s it divides by 1/s, recovering value * s. - n_inf_inner = float(cga_inner(N_INF, arr)) - if abs(n_inf_inner) < 1e-15: - raise ValueError("decode_quantity: degenerate point (n_inf inner product is zero)") - return float(arr[1]) / (-n_inf_inner), unit diff --git a/tests/test_arithmetic_as_versor_add.py b/tests/test_arithmetic_as_versor_add.py deleted file mode 100644 index 84a6cf5e..00000000 --- a/tests/test_arithmetic_as_versor_add.py +++ /dev/null @@ -1,171 +0,0 @@ -"""ADR-0139 acceptance tests — arithmetic-as-versor spike for `add`. - -Six assertion families per the ADR: - -1. Embedding well-formedness — embedded quantity is on the null cone. -2. Translator well-formedness — versor_condition < 1e-6. -3. Closure — sandwiched result is still on the null cone. -4. Arithmetic correctness — decoded value equals a + b within 1e-9. -5. Replay determinism — running twice produces byte-identical arrays. -6. Composability — two consecutive translators decode to a + b + c. - -If any test fails, ADR-0139 is falsified; the lift program is paused. -DO NOT loosen tolerances to make tests pass. -""" - -from __future__ import annotations - -import pytest -import numpy as np - -from algebra.cga import cga_inner -from algebra.versor import versor_apply, versor_condition -from generate.math_versor_arithmetic import ( - decode_quantity, - embed_quantity, - translator, -) - - -# Fixed test cases per ADR-0139 acceptance. -ADD_CASES: list[tuple[float, float]] = [ - (0.0, 0.0), - (0.0, 1.0), - (1.0, 0.0), - (3.0, 4.0), - (7.0, -3.0), - (0.25, 0.75), - (1.5, 2.5), - (-5.0, 5.0), - (-2.0, -3.0), - (100.0, 1.0), - (1.0, 100.0), -] - -# Composability case per ADR-0139. -COMPOSE_CASE: tuple[float, float, float] = (2.0, 3.0, 5.0) - -# Tolerance constants — exactly as specified in the ADR. -TOL_NULL = 1e-5 # cga_inner(X, X) for null points (f32 sandwich noise floor) -TOL_VERSOR = 1e-6 # versor_condition runtime contract -TOL_DECODE = 1e-9 # arithmetic correctness - - -# ----- Assertion family 1: embedding well-formedness ----- - - -@pytest.mark.parametrize("a,b", ADD_CASES) -def test_family1_embedding_is_null(a: float, b: float) -> None: - """embed_quantity(a, _) and embed_quantity(b, _) both lie on the null cone.""" - X_a = embed_quantity(a, "u") - X_b = embed_quantity(b, "u") - inner_a = abs(float(cga_inner(X_a, X_a))) - inner_b = abs(float(cga_inner(X_b, X_b))) - assert inner_a < TOL_NULL, ( - f"embed_quantity({a}) not null: |cga_inner| = {inner_a:.3e}" - ) - assert inner_b < TOL_NULL, ( - f"embed_quantity({b}) not null: |cga_inner| = {inner_b:.3e}" - ) - - -# ----- Assertion family 2: translator well-formedness ----- - - -@pytest.mark.parametrize("a,b", ADD_CASES) -def test_family2_translator_unit_versor(a: float, b: float) -> None: - """translator(b) satisfies versor_condition < 1e-6.""" - T = translator(b) - cond = versor_condition(T) - assert cond < TOL_VERSOR, ( - f"translator({b}) not unit versor: versor_condition = {cond:.3e}" - ) - - -# ----- Assertion family 3: closure ----- - - -@pytest.mark.parametrize("a,b", ADD_CASES) -def test_family3_sandwich_preserves_null(a: float, b: float) -> None: - """versor_apply(translator(b), embed_quantity(a, _)) is still on the null cone.""" - X = embed_quantity(a, "u") - T = translator(b) - R = versor_apply(T, X) - inner_R = abs(float(cga_inner(R, R))) - assert inner_R < TOL_NULL, ( - f"sandwich result ({a} + {b}) not null: |cga_inner(R, R)| = {inner_R:.3e}" - ) - - -# ----- Assertion family 4: arithmetic correctness ----- - - -@pytest.mark.parametrize("a,b", ADD_CASES) -def test_family4_decode_matches_sum(a: float, b: float) -> None: - """decode_quantity(R, _) returns (a + b, _) within 1e-9.""" - X = embed_quantity(a, "u") - T = translator(b) - R = versor_apply(T, X) - value, unit = decode_quantity(R, "u") - expected = a + b - err = abs(value - expected) - assert unit == "u", f"unit metadata lost: got {unit!r}" - assert err < TOL_DECODE, ( - f"decode error for ({a} + {b}): got {value}, expected {expected}, err = {err:.3e}" - ) - - -# ----- Assertion family 5: replay determinism ----- - - -@pytest.mark.parametrize("a,b", ADD_CASES) -def test_family5_replay_byte_identical(a: float, b: float) -> None: - """Two independent runs produce byte-identical multivector arrays.""" - X1 = embed_quantity(a, "u") - X2 = embed_quantity(a, "u") - T1 = translator(b) - T2 = translator(b) - R1 = versor_apply(T1, X1) - R2 = versor_apply(T2, X2) - assert X1.tobytes() == X2.tobytes(), ( - f"embed_quantity({a}) not deterministic across runs" - ) - assert T1.tobytes() == T2.tobytes(), ( - f"translator({b}) not deterministic across runs" - ) - assert R1.tobytes() == R2.tobytes(), ( - f"versor_apply result not deterministic across runs for ({a}, {b})" - ) - - -# ----- Assertion family 6: composability ----- - - -def test_family6_two_translators_compose() -> None: - """T_c · T_b · X decodes to a + b + c.""" - a, b, c = COMPOSE_CASE - X = embed_quantity(a, "u") - T_b = translator(b) - T_c = translator(c) - - # Apply T_b first, then T_c. - R1 = versor_apply(T_b, X) - R2 = versor_apply(T_c, R1) - - # Each intermediate result must remain on the null cone. - inner_R1 = abs(float(cga_inner(R1, R1))) - inner_R2 = abs(float(cga_inner(R2, R2))) - assert inner_R1 < TOL_NULL, ( - f"intermediate (a + b = {a + b}) not null: |cga_inner| = {inner_R1:.3e}" - ) - assert inner_R2 < TOL_NULL, ( - f"final (a + b + c = {a + b + c}) not null: |cga_inner| = {inner_R2:.3e}" - ) - - value, unit = decode_quantity(R2, "u") - expected = a + b + c - err = abs(value - expected) - assert unit == "u" - assert err < TOL_DECODE, ( - f"compose decode error: got {value}, expected {expected}, err = {err:.3e}" - ) diff --git a/tests/test_arithmetic_multiply_as_dilator.py b/tests/test_arithmetic_multiply_as_dilator.py deleted file mode 100644 index 012f6aa2..00000000 --- a/tests/test_arithmetic_multiply_as_dilator.py +++ /dev/null @@ -1,311 +0,0 @@ -"""ADR-0141 acceptance tests — multiply as CGA dilator (positive non-zero only). - -Ten assertion families per the ADR: - - Family 1 — Dilator well-formedness: versor_condition(multiply(s)) < 1e-6. - Family 2 — Closure under sandwich: cga_inner(R, R) < 1e-5. - Family 3 — Arithmetic correctness: decode_quantity(R, "u") == (a*s, "u") within 1e-9. - Family 4 — Replay determinism: byte-identical across runs. - Family 5 — Identity dilator: multiply(1.0) equals scalar identity within 1e-9. - Family 6 — Composition into product: multiply(s1)*multiply(s2) == multiply(s1*s2) within 1e-9. - Family 7 — Inverse composition: multiply(1/s)*multiply(s) ≈ identity within 1e-9. - Family 8 — Round-trip closure: decode(versor_apply(multiply(1/s), versor_apply(multiply(s), X))) == a within 1e-9. - Family 9 — Commutativity: multiply(s1)*multiply(s2) byte-equals multiply(s2)*multiply(s1). - Family 10 — Boundary refusal: multiply(0), multiply(-1), multiply(-3.5), multiply(-100), - multiply(-0.0001) all raise ValueError at construction time. - -PRELIMINARY MEASUREMENT REPORT (empirical, this CGA implementation): - N = n_o ∧ n_inf: single non-zero component at index 15 (blade (3,4) = e4∧e5), value = -1.0. - N² = +1.0 (pure scalar, grade-0 only, all other components zero). - n_o · n_inf = -1.0; n_o² = 0.0; n_inf² = 0.0. - - Because N² = +1, the exponential exp(α/2·N) = cosh(α/2) + sinh(α/2)·N is exact - in float64. The dilator is: D[0] = cosh(α/2), D[15] = -sinh(α/2), all others 0. - - D_s · ~D_s = cosh²(α/2) - sinh²(α/2)·N² = cosh²(α/2) - sinh²(α/2) = 1 exactly. - So versor_condition(D_s) is at machine epsilon, not merely < 1e-6. - -FALSIFICATION DISCIPLINE (read before changing any tolerance): - DO NOT loosen any threshold below. The thresholds are the ADR contract. - If any family fails, report the measured residual and stop; do not adjust. -""" - -from __future__ import annotations - -import math -import pytest -import numpy as np - -from algebra.cga import cga_inner -from algebra.cl41 import geometric_product, N_COMPONENTS -from algebra.versor import versor_apply, versor_condition -from generate.math_versor_arithmetic import ( - decode_quantity, - embed_quantity, - multiply, -) - -# --------------------------------------------------------------------------- -# Fixed test cases per ADR-0141 §Acceptance §Fixed test cases -# --------------------------------------------------------------------------- - -# Scale set for families 1–5, 7, 8. Only (a, s) pairs with s > 0. -# The ADR lists (5, -2) as "excluded" (negative s); it is tested in family 10. -SCALE_CASES: list[tuple[float, float]] = [ - (0.0, 2.0), - (1.0, 2.0), - (1.0, 3.0), - (3.0, 4.0), - (5.0, 0.5), - (10.0, 0.25), - (4.0, 0.75), - (7.0, 1.0), # identity scale - (2.0, math.sqrt(2)), # √2 - (1.0, math.pi), # π - (100.0, 0.01), - (0.01, 100.0), - (-5.0, 2.0), # negative a, positive s -] - -# Composition set for families 6, 9. -COMPOSE_CASES: list[tuple[float, float]] = [ - (1.0, 1.0), - (2.0, 1.0), - (1.0, 2.0), - (2.0, 3.0), - (3.0, 2.0), - (0.5, 4.0), - (math.sqrt(2), math.sqrt(2)), # √2 × √2 → 2.0 - (math.pi, 1.0), - (10.0, 0.1), # 10 × 0.1 → 1.0 (float64 drift probe) -] - -# Boundary set for family 10. All of these must raise ValueError. -INVALID_SCALES: list[float] = [0.0, -1.0, -3.5, -100.0, -0.0001] - -# Tolerance constants — exactly as specified in ADR-0141. -TOL_VERSOR = 1e-6 # versor_condition runtime contract -TOL_NULL = 1e-5 # cga_inner(X, X) for null points -TOL_IDENTITY = 1e-9 # component-wise identity comparison -TOL_DECODE = 1e-9 # arithmetic correctness - - -# --------------------------------------------------------------------------- -# Helper -# --------------------------------------------------------------------------- - -def _identity_versor() -> np.ndarray: - v = np.zeros(N_COMPONENTS, dtype=np.float64) - v[0] = 1.0 - return v - - -# =========================================================================== -# Family 1 — Dilator well-formedness -# =========================================================================== - -@pytest.mark.parametrize("a,s", SCALE_CASES) -def test_family1_dilator_unit_versor(a: float, s: float) -> None: - """versor_condition(multiply(s)) < 1e-6 for every scale in the test set.""" - D = multiply(s) - cond = versor_condition(D) - assert cond < TOL_VERSOR, ( - f"multiply({s}) not unit versor: versor_condition = {cond:.6e} (threshold 1e-6)" - ) - - -# =========================================================================== -# Family 2 — Closure under sandwich -# =========================================================================== - -@pytest.mark.parametrize("a,s", SCALE_CASES) -def test_family2_sandwich_preserves_null(a: float, s: float) -> None: - """versor_apply(multiply(s), embed_quantity(a)) stays on the null cone.""" - D = multiply(s) - X = embed_quantity(a, "u") - R = versor_apply(D, X) - inner_R = abs(float(cga_inner(R, R))) - assert inner_R < TOL_NULL, ( - f"sandwich result ({a} × {s}) not null: |cga_inner(R, R)| = {inner_R:.3e}" - ) - - -# =========================================================================== -# Family 3 — Arithmetic correctness -# =========================================================================== - -@pytest.mark.parametrize("a,s", SCALE_CASES) -def test_family3_decode_matches_product(a: float, s: float) -> None: - """decode_quantity(R, 'u') returns (a * s, 'u') within 1e-9.""" - D = multiply(s) - X = embed_quantity(a, "u") - R = versor_apply(D, X) - value, unit = decode_quantity(R, "u") - expected = a * s - err = abs(value - expected) - assert unit == "u", f"unit metadata lost: got {unit!r}" - assert err < TOL_DECODE, ( - f"decode error for ({a} × {s}): got {value!r}, expected {expected!r}, " - f"err = {err:.6e} (threshold 1e-9)" - ) - - -# =========================================================================== -# Family 4 — Replay determinism -# =========================================================================== - -@pytest.mark.parametrize("a,s", SCALE_CASES) -def test_family4_replay_byte_identical(a: float, s: float) -> None: - """Two independent runs produce byte-identical multivector arrays.""" - X1 = embed_quantity(a, "u") - X2 = embed_quantity(a, "u") - D1 = multiply(s) - D2 = multiply(s) - R1 = versor_apply(D1, X1) - R2 = versor_apply(D2, X2) - assert X1.tobytes() == X2.tobytes(), ( - f"embed_quantity({a}) not deterministic across runs" - ) - assert D1.tobytes() == D2.tobytes(), ( - f"multiply({s}) not deterministic across runs" - ) - assert R1.tobytes() == R2.tobytes(), ( - f"versor_apply result not deterministic across runs for (a={a}, s={s})" - ) - - -# =========================================================================== -# Family 5 — Identity dilator -# =========================================================================== - -def test_family5_identity_dilator() -> None: - """multiply(1.0) equals the scalar identity versor within 1e-9 component-wise.""" - D = multiply(1.0) - identity = _identity_versor() - err_vec = np.abs(D - identity) - max_err = float(err_vec.max()) - assert max_err < TOL_IDENTITY, ( - f"multiply(1.0) deviates from scalar identity: " - f"max component error = {max_err:.6e} (threshold 1e-9)\n" - f"Non-zero diff components: " - + str([(i, float(err_vec[i])) for i in range(len(err_vec)) if err_vec[i] > 1e-15]) - ) - - -# =========================================================================== -# Family 6 — Composition into product -# =========================================================================== - -@pytest.mark.parametrize("s1,s2", COMPOSE_CASES) -def test_family6_composition_into_product(s1: float, s2: float) -> None: - """geometric_product(multiply(s1), multiply(s2)) == multiply(s1*s2) within 1e-9.""" - D1 = multiply(s1) - D2 = multiply(s2) - D12 = geometric_product(D1, D2) - D_prod = multiply(s1 * s2) - - residual = np.abs(D12 - D_prod) - max_err = float(residual.max()) - assert max_err < TOL_IDENTITY, ( - f"Composition residual for ({s1}, {s2}) → s1*s2={s1*s2}: " - f"max |D12 - D(s1*s2)| = {max_err:.6e} (threshold 1e-9)\n" - f"Non-zero diff components: " - + str([(i, float(residual[i])) for i in range(len(residual)) if residual[i] > 1e-15]) - ) - - -# =========================================================================== -# Family 7 — Inverse composition -# =========================================================================== - -@pytest.mark.parametrize("a,s", SCALE_CASES) -def test_family7_inverse_composition_is_identity(a: float, s: float) -> None: - """geometric_product(multiply(1/s), multiply(s)) ≈ identity within 1e-9.""" - D_s = multiply(s) - D_inv = multiply(1.0 / s) - product = geometric_product(D_inv, D_s) - identity = _identity_versor() - - residual = np.abs(product - identity) - max_err = float(residual.max()) - assert max_err < TOL_IDENTITY, ( - f"Inverse composition residual for s={s}: " - f"max |D(1/s)*D(s) - I| = {max_err:.6e} (threshold 1e-9)\n" - f"Non-zero diff components: " - + str([(i, float(residual[i])) for i in range(len(residual)) if residual[i] > 1e-15]) - ) - - -# =========================================================================== -# Family 8 — Round-trip closure -# =========================================================================== - -@pytest.mark.parametrize("a,s", SCALE_CASES) -def test_family8_round_trip_closure(a: float, s: float) -> None: - """versor_apply(multiply(1/s), versor_apply(multiply(s), X)) decodes to (a, u) within 1e-9.""" - D_s = multiply(s) - D_inv = multiply(1.0 / s) - X = embed_quantity(a, "u") - - scaled = versor_apply(D_s, X) - recovered = versor_apply(D_inv, scaled) - - # Intermediate must stay on null cone. - inner_scaled = abs(float(cga_inner(scaled, scaled))) - assert inner_scaled < TOL_NULL, ( - f"Round-trip intermediate not null for (a={a}, s={s}): " - f"|cga_inner| = {inner_scaled:.3e}" - ) - - # Final must stay on null cone. - inner_recovered = abs(float(cga_inner(recovered, recovered))) - assert inner_recovered < TOL_NULL, ( - f"Round-trip final not null for (a={a}, s={s}): " - f"|cga_inner| = {inner_recovered:.3e}" - ) - - value, unit = decode_quantity(recovered, "u") - err = abs(value - a) - assert unit == "u" - assert err < TOL_DECODE, ( - f"Round-trip decode error for (a={a}, s={s}): " - f"got {value!r}, expected {a!r}, err = {err:.6e} (threshold 1e-9)" - ) - - -# =========================================================================== -# Family 9 — Commutativity -# =========================================================================== - -@pytest.mark.parametrize("s1,s2", COMPOSE_CASES) -def test_family9_commutativity_byte_equal(s1: float, s2: float) -> None: - """geometric_product(multiply(s1), multiply(s2)) byte-equals multiply(s2)*multiply(s1).""" - D1 = multiply(s1) - D2 = multiply(s2) - ab = geometric_product(D1, D2) - ba = geometric_product(D2, D1) - assert ab.tobytes() == ba.tobytes(), ( - f"Commutativity violation for (s1={s1}, s2={s2}): " - f"D1*D2 != D2*D1\n" - f"Max component diff: {float(np.abs(ab - ba).max()):.6e}" - ) - - -# =========================================================================== -# Family 10 — Boundary refusal at construction time -# =========================================================================== - -@pytest.mark.parametrize("bad_s", INVALID_SCALES) -def test_family10_invalid_scale_raises_at_construction(bad_s: float) -> None: - """multiply(s) raises ValueError at construction for s in {0, -1, -3.5, -100, -0.0001}.""" - with pytest.raises(ValueError) as exc_info: - multiply(bad_s) - msg = str(exc_info.value) - # Error must name the scale value. - assert str(bad_s) in msg or repr(bad_s) in msg, ( - f"ValueError for scale={bad_s!r} does not name the scale in message: {msg!r}" - ) - # Error must name the restriction. - assert any(kw in msg.lower() for kw in ("positive", "strictly", "deferred", "> 0")), ( - f"ValueError for scale={bad_s!r} does not name the restriction in message: {msg!r}" - ) diff --git a/tests/test_arithmetic_subtract_and_group.py b/tests/test_arithmetic_subtract_and_group.py deleted file mode 100644 index 216bd76d..00000000 --- a/tests/test_arithmetic_subtract_and_group.py +++ /dev/null @@ -1,320 +0,0 @@ -"""ADR-0140 acceptance tests — subtract as inverse translator + additive group closure. - -Nine assertion families per the ADR: - -Families 1-6 (inherited from ADR-0139, applied to subtract): - 1. Embedding well-formedness — subtract cases lie on null cone. - 2. Translator-of-negative well-formedness — versor_condition(subtract(b)) < 1e-6. - 3. Closure under sandwich — sandwich result stays on null cone. - 4. Arithmetic correctness — decoded value equals a − b within 1e-9. - 5. Replay determinism — byte-identical across runs. - 6. Composability — subtract(c) ∘ subtract(b) decodes to a − b − c. - -New group-property families: - 7. Inverse composition — geometric_product(translator(-b), translator(b)) ≈ identity. - 8. Round-trip closure — versor_apply(T_{-b}, versor_apply(T_b, X)) decodes to (a, u). - 9. Commutativity / composition into sum: - a) translator(a) * translator(b) ≈ translator(a+b) component-wise. - b) translator(a) * translator(b) == translator(b) * translator(a) byte-equal. - -If family 7 or 9 fails, ADR-0139's algebraic claim is invalidated retroactively. -The lift program is paused — see ADR-0140 §Falsification Discipline. -DO NOT loosen tolerances to make tests pass. -""" - -from __future__ import annotations - -import pytest -import numpy as np - -from algebra.cga import cga_inner -from algebra.cl41 import geometric_product -from algebra.versor import versor_apply, versor_condition -from generate.math_versor_arithmetic import ( - decode_quantity, - embed_quantity, - subtract, - translator, -) - - -# --------------------------------------------------------------------------- -# Fixed test cases per ADR-0140 §Acceptance -# --------------------------------------------------------------------------- - -SUBTRACT_CASES: list[tuple[float, float]] = [ - (0.0, 0.0), - (5.0, 0.0), - (0.0, 5.0), - (10.0, 3.0), - (3.0, 10.0), - (1.5, 0.5), - (0.25, 0.75), - (-5.0, 3.0), - (5.0, -3.0), - (-2.0, -3.0), - (100.0, 1.0), -] - -GROUP_CASES: list[tuple[float, float]] = [ - (0.0, 0.0), - (1.0, 0.0), - (0.0, 1.0), - (1.0, 1.0), - (-1.0, 1.0), - (3.0, 4.0), - (0.5, 0.5), - (-2.5, 2.5), - (100.0, 1.0), - (1.0, 100.0), -] - -# Composability case for family 6 (subtract twice). -COMPOSE_CASE: tuple[float, float, float] = (20.0, 3.0, 5.0) - -# Tolerance constants — exactly as specified in the ADR. -TOL_NULL = 1e-5 # cga_inner(X, X) for null points -TOL_VERSOR = 1e-6 # versor_condition runtime contract -TOL_DECODE = 1e-9 # arithmetic correctness / round-trip / group properties - - -# --------------------------------------------------------------------------- -# Identity versor (scalar 1): component 0 = 1, all others 0. -# --------------------------------------------------------------------------- - -def _identity_versor() -> np.ndarray: - from algebra.cl41 import N_COMPONENTS - v = np.zeros(N_COMPONENTS, dtype=np.float64) - v[0] = 1.0 - return v - - -# =========================================================================== -# Families 1-6: ADR-0139 assertion families applied to subtract -# =========================================================================== - - -# ----- Family 1: embedding well-formedness ----- - -@pytest.mark.parametrize("a,b", SUBTRACT_CASES) -def test_family1_embedding_is_null(a: float, b: float) -> None: - """embed_quantity(a, _) and embed_quantity(b, _) both lie on the null cone.""" - X_a = embed_quantity(a, "u") - X_b = embed_quantity(b, "u") - inner_a = abs(float(cga_inner(X_a, X_a))) - inner_b = abs(float(cga_inner(X_b, X_b))) - assert inner_a < TOL_NULL, ( - f"embed_quantity({a}) not null: |cga_inner| = {inner_a:.3e}" - ) - assert inner_b < TOL_NULL, ( - f"embed_quantity({b}) not null: |cga_inner| = {inner_b:.3e}" - ) - - -# ----- Family 2: translator-of-negative well-formedness ----- - -@pytest.mark.parametrize("a,b", SUBTRACT_CASES) -def test_family2_subtract_unit_versor(a: float, b: float) -> None: - """subtract(b) satisfies versor_condition < 1e-6.""" - S = subtract(b) - cond = versor_condition(S) - assert cond < TOL_VERSOR, ( - f"subtract({b}) not unit versor: versor_condition = {cond:.3e}" - ) - - -# ----- Family 3: closure under sandwich ----- - -@pytest.mark.parametrize("a,b", SUBTRACT_CASES) -def test_family3_sandwich_preserves_null(a: float, b: float) -> None: - """versor_apply(subtract(b), embed_quantity(a, _)) stays on the null cone.""" - X = embed_quantity(a, "u") - S = subtract(b) - R = versor_apply(S, X) - inner_R = abs(float(cga_inner(R, R))) - assert inner_R < TOL_NULL, ( - f"sandwich result ({a} − {b}) not null: |cga_inner(R, R)| = {inner_R:.3e}" - ) - - -# ----- Family 4: arithmetic correctness ----- - -@pytest.mark.parametrize("a,b", SUBTRACT_CASES) -def test_family4_decode_matches_difference(a: float, b: float) -> None: - """decode_quantity(R, _) returns (a − b, _) within 1e-9.""" - X = embed_quantity(a, "u") - S = subtract(b) - R = versor_apply(S, X) - value, unit = decode_quantity(R, "u") - expected = a - b - err = abs(value - expected) - assert unit == "u", f"unit metadata lost: got {unit!r}" - assert err < TOL_DECODE, ( - f"decode error for ({a} − {b}): got {value}, expected {expected}, err = {err:.3e}" - ) - - -# ----- Family 5: replay determinism ----- - -@pytest.mark.parametrize("a,b", SUBTRACT_CASES) -def test_family5_replay_byte_identical(a: float, b: float) -> None: - """Two independent runs produce byte-identical multivector arrays.""" - X1 = embed_quantity(a, "u") - X2 = embed_quantity(a, "u") - S1 = subtract(b) - S2 = subtract(b) - R1 = versor_apply(S1, X1) - R2 = versor_apply(S2, X2) - assert X1.tobytes() == X2.tobytes(), ( - f"embed_quantity({a}) not deterministic across runs" - ) - assert S1.tobytes() == S2.tobytes(), ( - f"subtract({b}) not deterministic across runs" - ) - assert R1.tobytes() == R2.tobytes(), ( - f"versor_apply result not deterministic across runs for ({a}, {b})" - ) - - -# ----- Family 6: composability ----- - -def test_family6_two_subtracts_compose() -> None: - """subtract(c) ∘ subtract(b) applied to embed_quantity(a) decodes to a − b − c.""" - a, b, c = COMPOSE_CASE - X = embed_quantity(a, "u") - S_b = subtract(b) - S_c = subtract(c) - - R1 = versor_apply(S_b, X) - R2 = versor_apply(S_c, R1) - - inner_R1 = abs(float(cga_inner(R1, R1))) - inner_R2 = abs(float(cga_inner(R2, R2))) - assert inner_R1 < TOL_NULL, ( - f"intermediate (a − b = {a - b}) not null: |cga_inner| = {inner_R1:.3e}" - ) - assert inner_R2 < TOL_NULL, ( - f"final (a − b − c = {a - b - c}) not null: |cga_inner| = {inner_R2:.3e}" - ) - - value, unit = decode_quantity(R2, "u") - expected = a - b - c - err = abs(value - expected) - assert unit == "u" - assert err < TOL_DECODE, ( - f"compose decode error: got {value}, expected {expected}, err = {err:.3e}" - ) - - -# =========================================================================== -# Families 7-9: Additive group structure verification -# =========================================================================== - - -# ----- Family 7: inverse composition ----- -# -# geometric_product(translator(-b), translator(b)) must equal the identity -# versor (component 0 = 1, all others 0) within 1e-9 component-wise. -# -# If this fails, the algebra is not decoding exact addition — it is decoding -# something that resembles addition on point-pairs but does not form a group. -# That invalidates ADR-0139 retroactively. STOP; do not loosen 1e-9. - -@pytest.mark.parametrize("a,b", GROUP_CASES) -def test_family7_inverse_composition_is_identity(a: float, b: float) -> None: - """geometric_product(translator(-b), translator(b)) ≈ identity within 1e-9.""" - T_pos = translator(b) - T_neg = translator(-b) - product = geometric_product(T_neg, T_pos) - identity = _identity_versor() - - residual = np.abs(product - identity) - max_residual = float(residual.max()) - assert max_residual < TOL_DECODE, ( - f"Inverse composition residual for b={b}: max |product - identity| = {max_residual:.6e}\n" - f"Component residuals (non-zero): " - + str([(i, float(residual[i])) for i in range(len(residual)) if residual[i] > 1e-15]) - ) - - -# ----- Family 8: round-trip closure ----- -# -# versor_apply(T_{-b}, versor_apply(T_b, embed_quantity(a))) must decode -# back to (a, "u") within 1e-9. Includes the b=0 edge case. - -@pytest.mark.parametrize("a,b", GROUP_CASES) -def test_family8_round_trip_closure(a: float, b: float) -> None: - """versor_apply(T_{{-b}}, versor_apply(T_b, X)) decodes to (a, u) within 1e-9.""" - X = embed_quantity(a, "u") - T_pos = translator(b) - T_neg = translator(-b) - - shifted = versor_apply(T_pos, X) - recovered = versor_apply(T_neg, shifted) - - # Intermediate must stay on null cone. - inner_shifted = abs(float(cga_inner(shifted, shifted))) - assert inner_shifted < TOL_NULL, ( - f"Round-trip intermediate not null for (a={a}, b={b}): " - f"|cga_inner| = {inner_shifted:.3e}" - ) - - # Final must stay on null cone. - inner_recovered = abs(float(cga_inner(recovered, recovered))) - assert inner_recovered < TOL_NULL, ( - f"Round-trip result not null for (a={a}, b={b}): " - f"|cga_inner| = {inner_recovered:.3e}" - ) - - value, unit = decode_quantity(recovered, "u") - err = abs(value - a) - assert unit == "u" - assert err < TOL_DECODE, ( - f"Round-trip decode error for (a={a}, b={b}): " - f"got {value}, expected {a}, err = {err:.3e}" - ) - - -# ----- Family 9a: composition into sum ----- -# -# geometric_product(translator(a), translator(b)) must equal translator(a+b) -# component-wise within 1e-9. - -@pytest.mark.parametrize("a,b", GROUP_CASES) -def test_family9a_composition_equals_sum_translator(a: float, b: float) -> None: - """geometric_product(translator(a), translator(b)) == translator(a+b) within 1e-9.""" - T_a = translator(a) - T_b = translator(b) - T_sum = translator(a + b) - - product = geometric_product(T_a, T_b) - residual = np.abs(product - T_sum) - max_residual = float(residual.max()) - assert max_residual < TOL_DECODE, ( - f"Sum-composition residual for (a={a}, b={b}): " - f"max |T_a*T_b - T_{{a+b}}| = {max_residual:.6e}\n" - f"Component residuals (non-zero): " - + str([(i, float(residual[i])) for i in range(len(residual)) if residual[i] > 1e-15]) - ) - - -# ----- Family 9b: commutativity ----- -# -# geometric_product(translator(a), translator(b)) must equal -# geometric_product(translator(b), translator(a)) byte-exactly. -# If this fails, the algebra decodes a non-abelian operation. - -@pytest.mark.parametrize("a,b", GROUP_CASES) -def test_family9b_commutativity_byte_equal(a: float, b: float) -> None: - """geometric_product(translator(a), translator(b)) byte-equals geometric_product(translator(b), translator(a)).""" - T_a = translator(a) - T_b = translator(b) - - ab = geometric_product(T_a, T_b) - ba = geometric_product(T_b, T_a) - - assert ab.tobytes() == ba.tobytes(), ( - f"Commutativity violation for (a={a}, b={b}): " - f"T_a*T_b != T_b*T_a\n" - f"Max component diff: {float(np.abs(ab - ba).max()):.6e}" - )