feat(ADR-0139): arithmetic-as-versor spike — add closes exactly in Cl(4,1) (#212)

First step of the Engine A lift program (CLAUDE.md commits the project to a
single deterministic cognitive engine; Engine B / math pipeline was always
intentional scaffolding per math_solver.py:24). Proves the load-bearing
unknown: one arithmetic operation can be represented as a closed versor at
the required tolerance, with no new normalization and no weakened invariant.

Scope (frozen by ADR-0139):
- One operation: add
- Single-axis embedding: quantities on e1 axis
- No graph wiring, no pipeline integration, no GSM8K case routed
- Unit carried as caller metadata

Construction:
- embed_quantity(v, u) = embed_point([v, 0, 0])  (existing CGA primitive)
- translator(b)         = 1 - 0.5 * (b*e1 * n_inf)   (textbook CGA translator)
- decode_quantity(F, u) = (F[1], u)                  (e1 coordinate)

Measured values (all 11 fixed cases + composability):

      a         b      vcond(T)         |<R,R>|     decode_err
    0.0       0.0     0.000e+00       0.000e+00      0.000e+00
    0.0       1.0     0.000e+00       0.000e+00      0.000e+00
    1.0       0.0     0.000e+00       0.000e+00      0.000e+00
    3.0       4.0     0.000e+00       0.000e+00      0.000e+00
    7.0      -3.0     0.000e+00       0.000e+00      0.000e+00
   0.25      0.75     0.000e+00       0.000e+00      0.000e+00
    1.5       2.5     0.000e+00       0.000e+00      0.000e+00
   -5.0       5.0     0.000e+00       0.000e+00      0.000e+00
   -2.0      -3.0     0.000e+00       0.000e+00      0.000e+00
  100.0       1.0     0.000e+00       0.000e+00      0.000e+00
    1.0     100.0     0.000e+00       0.000e+00      0.000e+00
  compose (2, 3, 5) → 10:   |<R2,R2>| = 0.000e+00, decode_err = 0.000e+00

Every residual is exactly 0.0 in float64. The construction is algebraically
closed: T_t * reverse(T_t) = 1 - 0.25*B^2 where B = t*n_inf, and B^2 = 0
because (e14)^2 + (e15)^2 = -1 + 1 and cross-terms cancel. No machine-epsilon
drift accumulates because the relevant cancellation happens at the algebraic
level before float arithmetic.

ADR-0139 acceptance items 1-6 (one parametrized test family each):
  1. Embedding well-formedness   — test_family1_embedding_is_null         (11 cases)
  2. Translator well-formedness  — test_family2_translator_unit_versor    (11 cases)
  3. Closure                     — test_family3_sandwich_preserves_null   (11 cases)
  4. Arithmetic correctness      — test_family4_decode_matches_sum        (11 cases)
  5. Replay determinism          — test_family5_replay_byte_identical     (11 cases)
  6. Composability               — test_family6_two_translators_compose   (1 case)
  Total: 56 tests, all passing.

Lift program decision: proceeds. Follow-on ADRs (subtract, multiply, Rate,
compare, MathProblemGraph → PropositionGraph, pipeline integration, first
GSM8K case end-to-end through Engine A) are now justified by a concrete
algebraic foundation rather than design speculation.

Out of scope per ADR-0139:
- No modifications to algebra/, core/cognition/, chat/, math_solver.py,
  math_verifier.py, math_realizer.py, math_candidate_parser.py
- No GSM8K runner changes
- No pack changes
- Engine B continues serving GSM8K unchanged; the 3/50 admission set is
  preserved

CLI lanes intentionally not run — main has known test-rot orthogonal to
this PR. The 56 new tests are self-contained and the diff touches only
three new files.
This commit is contained in:
Shay 2026-05-24 06:57:39 -07:00 committed by GitHub
parent 7d0803b457
commit 589297b79a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 678 additions and 0 deletions

View file

@ -0,0 +1,355 @@
# ADR-0139 — Arithmetic-as-Versor Spike: `add` Only
**Status:** Draft
**Date:** 2026-05-24
**Author:** CORE agents
**Parent / supersedes context:** [ADR-0114a](./ADR-0114a-math-capability-substrate.md),
[ADR-0115](./ADR-0115-math-problem-parser-and-graph.md),
[ADR-0116](./ADR-0116-deterministic-solver.md)
**Engine target:** CGA cognitive engine (`algebra/versor.py`, `algebra/cga.py`,
`core/cognition/pipeline.py`)
---
## Context
CLAUDE.md commits the project to a single deterministic cognitive engine:
```text
listen → comprehend → recall → think → articulate → learn → replay
```
built on CGA Cl(4,1) versor algebra, exact recall, PropositionGraph,
ArticulationTarget, deterministic realizer, and trace-hash invariance.
Between ADR-0114a and the present, a second engine grew alongside the first:
| | Engine A (CGA cognitive engine) | Engine B (math pipeline) |
|---|---|---|
| Substrate | versor multivectors in Cl(4,1) | frozen Python dataclasses |
| Graph | `PropositionGraph` (from `graph_planner.py`) | `MathProblemGraph` (from `math_problem_graph.py`) |
| State propagation | `versor_apply(V, F)` — sandwich product | pure-Python arithmetic in `math_solver.py` |
| Closure invariant | `versor_condition(F) < 1e-6` | `assert`s on dataclass fields |
| Trace contract | `core/cognition/trace.py` | `SolutionTrace.canonical_bytes()` |
| Used by | `chat/runtime.py`, cognition eval | `evals/gsm8k_math/runner.py` |
Engine B was always intentional scaffolding — `math_solver.py:24` states
*"the 'expert' tier (ADR-0120) is not in scope here; ADR-0116 is the Phase 2
substrate the eventual capability claim will rest on."*
The GSM8K corridor (ADR-0123 / 0131 / 0136 / 0138) has been extending the
parser side of Engine B without ever testing the lift to Engine A. Every PR
through that corridor reports `cognition eval byte-identical` — the symptom
that Engine A is not being invoked by math work, even though math is the
nominal capability claim the engine should eventually demonstrate.
This ADR begins the lift. It does not finish it. It does not even cover one
GSM8K case end-to-end through Engine A. It does one thing: prove that one
arithmetic operation can be represented as a closed versor in Cl(4,1)
without weakening any existing invariant.
---
## Decision
Run a one-operation algebraic spike: **`add` only**, **algebra only**, **no
graph or pipeline wiring**.
### Embedding choice
A `Quantity(value: int|float, unit: str)` is embedded as a single conformal
point on the e1 axis:
```text
embed_quantity(value, unit) = embed_point([value, 0, 0])
```
Existing primitive: `algebra/cga.py:embed_point`.
This choice:
- Places quantities on the CGA null cone (`cga_inner(X, X) ≈ 0`).
- Uses only the existing CGA point-embedding primitive — no new algebra
invented in this ADR.
- Treats the `unit` field as carried metadata, not as a multivector
coordinate. Unit handling is propositional, not algebraic.
- Lets the standard CGA translator versor represent additive operations.
Open question (deferred): whether multi-unit problems require multiple axes
(e1 for unit A, e2 for unit B) or whether each unit gets its own embedding
context. ADR-0139 covers single-unit `add` only and does not commit either
way.
### Operation choice
`add(addend: int|float) → versor` is constructed as the standard CGA translator
along e1 by `addend`:
```text
T_a = 1 - 0.5 * a * e1 * n_inf
```
(exact sign/normalization to be derived against the existing `cga.py` /
`cl41.py` conventions during implementation; the construction must produce a
unit versor satisfying `versor_condition(T_a) < 1e-6` at runtime.)
This is well-known CGA — translators are the canonical versor representation
of Euclidean translations. Adding `b` to a quantity is geometrically
translating its point on e1 by `b`.
### Application
```text
result = versor_apply(T_addend, embed_quantity(value, unit))
```
`versor_apply` already has the correct dual-path behavior for this
embedding: null inputs (CGA points) get the raw sandwich path
(`algebra/versor.py:160-162`) so the null property is preserved through
the operation. No change to `versor_apply` is required.
### Decoding
A `decode_quantity(F, unit) → (value, unit)` extracts the e1 coordinate of
the result point. This is the inverse of `embed_point` restricted to the
e1 axis.
---
## Acceptance
A single test module — `tests/test_arithmetic_as_versor_add.py` — passes
with these assertions on a small fixed set of `(a, b)` pairs covering
integer, fractional, negative, and zero cases:
1. **Embedding well-formedness.** For each input `value`:
- `cga_inner(embed_quantity(value, "u"), embed_quantity(value, "u")) ≈ 0`
(null cone preserved).
2. **Translator well-formedness.** For each addend `b`:
- `versor_condition(translator(b)) < 1e-6`.
3. **Closure.** For each `(a, b)`:
- Let `R = versor_apply(translator(b), embed_quantity(a, "u"))`.
- `cga_inner(R, R) ≈ 0` (result remains on null cone).
4. **Arithmetic correctness.** For each `(a, b)`:
- `decode_quantity(R, "u") == (a + b, "u")` byte-equal at the tolerance
chosen by the embedding (decimal value match within `1e-9` for the
fixed-point test cases listed below).
5. **Replay determinism.** Running the test twice produces byte-identical
multivector arrays (no nondeterministic float ordering, no platform
drift).
6. **Composability (in-ADR scope).** `versor_apply(translator(c),
versor_apply(translator(b), embed_quantity(a, "u")))` decodes to
`(a + b + c, "u")` — proves two consecutive translations compose
correctly. This is the smallest two-step program the engine path
could run.
### Fixed test cases
```text
(0, 0), (0, 1), (1, 0),
(3, 4), (7, -3),
(0.25, 0.75), (1.5, 2.5),
(-5, 5), (-2, -3),
(100, 1), (1, 100),
```
Plus the composability case `(2, 3, 5) → 10`.
---
## Non-goals
Explicit out-of-scope for this ADR:
- No `subtract`, `multiply`, `divide`, `transfer`, `apply_rate`,
`compare_additive`, `compare_multiplicative` operations. Each gets its
own follow-on ADR once `add` is proven.
- No `MathProblemGraph` consumer. The new functions take typed inputs
directly. They do not import from `math_problem_graph.py`.
- No `PropositionGraph` construction. Engine A's graph layer is not
touched.
- No `CognitiveTurnPipeline` integration. The pipeline file is not
imported.
- No `chat/runtime.py` invocation path. The chat surface is not touched.
- No GSM8K case routed through this code. The runner is not modified.
- No deprecation of Engine B. `math_solver.py`, `math_verifier.py`,
`math_realizer.py`, and the S.x corridor parsers remain in place,
unmodified, scoring GSM8K as they do today. The 3/50 admission set is
preserved.
- No pack changes. `en_arithmetic_v1` is not touched. Pack-binding for the
versor path is a separate concern.
The ADR succeeds if `add` works algebraically. It does not claim that
the math pipeline has been lifted. It only proves the lift is feasible
for one operation.
---
## Rationale
Two design choices are load-bearing and should be defended explicitly:
**Why a spike instead of a phased plan?**
The arithmetic-as-versor algebra is the single load-bearing unknown for
the entire lift program. Every follow-on ADR — subtract, multiply,
compare, graph integration, pipeline integration, GSM8K routing —
assumes that arithmetic can be represented as closed versors at the
required tolerance. If `add` doesn't work cleanly, every downstream ADR
is built on sand. The spike forces that assumption to be tested in code,
not in design documents.
**Why `add` instead of `multiply` or `compare`?**
Translators are the most canonical CGA versor. The construction
`T_t = 1 - 0.5 * t * n_inf` is textbook. If anything in the CGA
substrate is going to behave well, translators will.
Multiplication is dilation in CGA — also a known versor, but it requires
the `n_o ∧ n_inf` blade and exponentiation. Riskier first step.
Comparisons (`compare_additive`, `compare_multiplicative`) are relational
predicates, not transformations. They may not be versor-shaped at all —
they might land at the proposition layer instead. Trying to make them
versor-shaped first would entangle two unknowns.
So `add` is the smallest, cleanest, most-textbook starting point.
**Why no graph or pipeline wiring?**
Engine A's graph and pipeline layers already exist and work. The risk
isn't whether `versor_apply` integrates with `graph_from_intent` — that's
plumbing. The risk is whether arithmetic can be represented as versors
at all. Wiring before the algebra is proven would create the appearance
of progress without removing the load-bearing unknown.
---
## Open questions for follow-on ADRs
The following must be answered, but not by this ADR:
1. **Multi-axis embedding.** Does a two-unit problem (`5 apples + 3
oranges` style — even though that's not valid arithmetic, mixed-unit
intermediate states do appear in word problems) need orthogonal axes
(e1 for apples, e2 for oranges)? Or does each unit context get its
own embedding session?
2. **Multiplication as dilation.** The dilator
`D_s = cosh(α/2) + sinh(α/2)·(n_o ∧ n_inf)` where `s = exp(α)`
represents scaling. Does it close at `versor_condition < 1e-6` for
the value ranges GSM8K actually requires? At what precision?
3. **Comparison as proposition vs versor.** Is `compare_additive("more
by 5", x)` a versor operation, a proposition node, or both? Strongest
guess: proposition. But this needs an ADR.
4. **`Rate` as bivector.** A `Rate(2.0, "dollars", "apple")` is
inherently two-axis. It is probably a grade-2 object connecting two
Euclidean axes. Does the existing CGA substrate support this cleanly?
5. **PropositionGraph construction from MathProblemGraph.** Once `add`
and `subtract` are proven as versors, an ADR is needed that
constructs a `PropositionGraph` from a `MathProblemGraph` so the
engine pipeline can articulate the answer through the existing
realizer.
6. **Trace-hash story.** Engine A's `compute_trace_hash` and Engine B's
`SolutionTrace.canonical_bytes()` need to converge. Probably the
versor sequence becomes the trace, with the existing hash function
applied. Defer to the integration ADR.
7. **Refusal floor.** The versor path must preserve `wrong == 0`. When
the algebra cannot represent a needed operation, the engine must
refuse, not approximate. Mechanism TBD by the integration ADR.
---
## Risks
- **The translator construction may not close at `1e-6`.** The
construction-residue tolerance in `algebra/versor.py:13` is `1e-2` and
the runtime closure tolerance is `1e-6`. If `translator(b)` lands
between those, `_close_applied_versor` will project it through
`_seed_to_rotor`, which may not preserve the exact translation. The
spike must verify this empirically; if it fails, the embedding or the
construction has to be reconsidered before the ADR can ship.
- **Float32 truncation.** `algebra/cl41.py` uses float32 for
multivectors. Large additions (e.g. `100 + 1`) may not decode back to
exactly `101.0` after the sandwich. The test cases above include
values that probe this. If float32 doesn't carry the required
precision, the embedding may need to use the float64 path that
`algebra/versor.py:18` already defines for runtime fields.
- **Decoding may not be exact for arbitrary float values.** The e1
component of an embedded point is the raw value, but the e4/e5
coefficients carry `0.5 * (value^2 ± 1)`. Round-tripping requires the
e1 coordinate alone — the e4/e5 components are dependent. If the
sandwich introduces error in e1 vs e4/e5 differently, decoding from
e1 alone may not equal the input. This is the most likely failure
mode and the spike's primary falsification target.
- **The user-facing capability gauge does not move in this ADR.** GSM8K
admissions stay at 3/50. The cognition eval stays byte-identical. The
only signal this ADR produces is a test file that does or does not
pass. That is intentional but easy to misread as "no progress."
---
## Replay & invariants
The spike is governed by the same invariants as the rest of CORE:
- `versor_condition(F) < 1e-6` for all unit versors constructed
(translators in this ADR).
- Null inputs to `versor_apply` stay null. Verified by `cga_inner(R, R)
≈ 0` on every result.
- No normalization is introduced outside the allowed sites
(`ingest/gate.py`, `language_packs/compiler.py`,
`algebra/versor.py`). The new functions live in a new module —
proposed path `generate/math_versor_arithmetic.py` — and call only
existing primitives. They do not add any new normalization.
- Determinism: float64 path used end-to-end where precision matters;
no platform-conditional code; no randomness.
---
## Work sequencing for follow-on
Only if this ADR's tests pass:
1. ADR-0140: `subtract` as inverse translator. (Trivial follow-on; should
pass nearly for free.)
2. ADR-0141: `multiply` as dilator. (Risk concentrates here.)
3. ADR-0142: `Rate` as bivector and `apply_rate` as combined
translator-dilator. (Open question 4.)
4. ADR-0143: `compare_*` at the proposition layer, not versor layer.
(Open question 3.)
5. ADR-0144: `PropositionGraph` from `MathProblemGraph`. (Open
question 5.)
6. ADR-0145: One GSM8K case (gsm8k-0014) routed end-to-end through
Engine A. First moment the capability gauge is honestly attached to
the engine.
If this ADR's tests fail, the lift program is paused and the failure
mode is documented. Engine B continues serving GSM8K. A revised
embedding strategy is required before any follow-on ADR.
---
## Decision summary
Add one new module (`generate/math_versor_arithmetic.py` — name
provisional) with three functions: `embed_quantity`, `translator`,
`decode_quantity`. Add one test module verifying `add` works as a closed
versor at the required tolerance. Change nothing else. Ship as a single
PR small enough to audit in one sitting.
Acceptance is binary: every test in the new module passes, or the ADR is
withdrawn and the lift program is paused pending a new embedding choice.

View file

@ -0,0 +1,152 @@
"""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 embed_point
from algebra.cl41 import N_COMPONENTS, geometric_product
__all__ = [
"embed_quantity",
"translator",
"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}")
point_float32 = embed_point(np.array([value, 0.0, 0.0], dtype=np.float32))
# Upcast to float64 for the runtime field-state path.
return point_float32.astype(np.float64)
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 decode_quantity(F: np.ndarray, unit: str) -> tuple[float, str]:
"""Decode a multivector back to a (value, unit) scalar quantity.
For a CGA point on the e1 axis, the e1 component directly carries
the Euclidean coordinate (and thus the encoded scalar value). The
unit string is passed through from the caller this function does
not infer or change the unit.
The decoder reads only the e1 component (index 1). It does not
cross-check the e4/e5 components for consistency with the null
property; that check is the test layer's job (assertion family 1
and 3 in the ADR).
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 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}")
return float(arr[1]), unit

View file

@ -0,0 +1,171 @@
"""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}"
)