perf(rust): versor_apply f64 parity port — 29x over Python, bit-identical

Closes the last open Rust parity gate from ADR-0020.

Kernel: new versor_apply_closed_f64 in core-rs/src/versor.rs performs
the full sandwich V·F·rev(V) + closure in f64, mirroring Python's
algebra.versor.versor_apply + _close_applied_versor exactly:
  - no null-vector early branch (Python doesn't have one)
  - unitize_versor with dense-support seed fallback gate
  - post-unitize versor_condition < 1e-6 recheck
  - seed_to_rotor on failure, passthrough as last resort

PyO3 binding: versor_apply_with_closure_f64 accepts/returns float64
arrays through new extract_f64_slice / f64_array_to_numpy helpers.
algebra/backend.py::versor_apply routes through it under CORE_BACKEND=rust.

Parity gate re-enabled (was skipped pending this port). 8/8 bit-
identical across normalized hot-path + identity-versor cases.

Bench (5000 iters, runtime hot path):
  python: 213.0 us/call
  rust:     7.4 us/call  → 28.8x speedup

All lanes green: algebra 132 (was 124+8skip), smoke 54, runtime 19,
cognition 57, teaching 17, packs 6. Cognition eval 100% across all metrics.

PROGRESS.md updated: versor_apply marked passing; Phase 5 Rust parity
track now 5/5 surfaces gated and enabled.
This commit is contained in:
Shay 2026-05-16 20:43:01 -07:00
parent 70e58ce446
commit b40422e9db
6 changed files with 161 additions and 53 deletions

View file

@ -59,17 +59,19 @@ def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray:
"""Apply a versor through the canonical algebra closure boundary. """Apply a versor through the canonical algebra closure boundary.
The Python implementation is the default source of truth for runtime The Python implementation is the default source of truth for runtime
closure semantics. closure semantics. The Rust f64 closure path
(`versor_apply_with_closure_f64`) is a bit-identity port of
Rust dispatch is **disabled** for this surface pending an f64 parity `algebra.versor.versor_apply` + `_close_applied_versor`; ADR-0020
port. The current Rust `versor_apply_closed` computes the sandwich parity gate `tests/test_versor_apply_rust_parity.py` proves the
in f32 and applies a closure path whose null-vector branch and swap is safe before this dispatch is enabled.
fallback order differ structurally from Python's
`_close_applied_versor`. The ADR-0020 gate
`tests/test_versor_apply_rust_parity.py` documents the divergence
and skips under the disabled dispatch; un-skip when the f64 port
lands. Python is canonical per CLAUDE.md sequencing rule 5.
""" """
if _RUST:
try:
Vc = np.ascontiguousarray(V, dtype=np.float64)
Fc = np.ascontiguousarray(F, dtype=np.float64)
return np.asarray(_rs.versor_apply_with_closure_f64(Vc, Fc), dtype=np.float64)
except (AttributeError, Exception):
pass
from algebra.versor import versor_apply as _va from algebra.versor import versor_apply as _va
return _va(V, F) return _va(V, F)

View file

@ -23,7 +23,10 @@ use cl41::geometric_product_raw;
use diffusion::{graph_diffusion_step, unitize_f32}; use diffusion::{graph_diffusion_step, unitize_f32};
#[allow(unused_imports)] #[allow(unused_imports)]
use vault::vault_recall_raw; use vault::vault_recall_raw;
use versor::{normalize_to_versor_raw, versor_apply_closed, versor_apply_raw, versor_condition_raw}; use versor::{
normalize_to_versor_raw, versor_apply_closed, versor_apply_closed_f64, versor_apply_raw,
versor_condition_raw,
};
/// Geometric product in Cl(4,1). Accepts two numpy-compatible f32 arrays of length 32. /// Geometric product in Cl(4,1). Accepts two numpy-compatible f32 arrays of length 32.
#[pyfunction] #[pyfunction]
@ -68,6 +71,22 @@ fn versor_apply_with_closure(
f32_array_to_numpy(py, &result) f32_array_to_numpy(py, &result)
} }
/// `versor_apply` f64 closure path — bit-identity port of Python
/// `algebra.versor.versor_apply` + `_close_applied_versor`.
/// Inputs and output are float64. ADR-0020 parity surface.
#[pyfunction]
fn versor_apply_with_closure_f64(
py: Python<'_>,
v: &pyo3::types::PyAny,
f: &pyo3::types::PyAny,
) -> PyResult<PyObject> {
let v_slice = extract_f64_slice(v)?;
let f_slice = extract_f64_slice(f)?;
let result = versor_apply_closed_f64(&v_slice, &f_slice)
.map_err(|e| PyValueError::new_err(e.to_string()))?;
f64_array_to_numpy(py, &result)
}
/// ||F*reverse(F) - 1||_F. Returns scalar f32. /// ||F*reverse(F) - 1||_F. Returns scalar f32.
#[pyfunction] #[pyfunction]
fn versor_condition(f: &pyo3::types::PyAny) -> PyResult<f32> { fn versor_condition(f: &pyo3::types::PyAny) -> PyResult<f32> {
@ -217,11 +236,35 @@ fn f32_array_to_numpy(py: Python<'_>, data: &[f32; 32]) -> PyResult<PyObject> {
Ok(arr.into_py(py)) Ok(arr.into_py(py))
} }
fn extract_f64_slice(obj: &pyo3::types::PyAny) -> PyResult<[f64; 32]> {
let np = obj.py().import("numpy")?;
let arr = np.call_method1("asarray", (obj, "float64"))?;
let flat = arr.call_method0("flatten")?;
let list: Vec<f64> = flat.extract()?;
if list.len() != 32 {
return Err(PyValueError::new_err(format!(
"Expected array of length 32, got {}",
list.len()
)));
}
let mut out = [0f64; 32];
out.copy_from_slice(&list);
Ok(out)
}
fn f64_array_to_numpy(py: Python<'_>, data: &[f64; 32]) -> PyResult<PyObject> {
let np = py.import("numpy")?;
let list: Vec<f64> = data.to_vec();
let arr = np.call_method1("array", (list, "float64"))?;
Ok(arr.into_py(py))
}
#[pymodule] #[pymodule]
fn core_rs(m: &Bound<'_, PyModule>) -> PyResult<()> { fn core_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(geometric_product, m)?)?; m.add_function(wrap_pyfunction!(geometric_product, m)?)?;
m.add_function(wrap_pyfunction!(versor_apply, m)?)?; m.add_function(wrap_pyfunction!(versor_apply, m)?)?;
m.add_function(wrap_pyfunction!(versor_apply_with_closure, m)?)?; m.add_function(wrap_pyfunction!(versor_apply_with_closure, m)?)?;
m.add_function(wrap_pyfunction!(versor_apply_with_closure_f64, m)?)?;
m.add_function(wrap_pyfunction!(versor_condition, m)?)?; m.add_function(wrap_pyfunction!(versor_condition, m)?)?;
m.add_function(wrap_pyfunction!(normalize_to_versor, m)?)?; m.add_function(wrap_pyfunction!(normalize_to_versor, m)?)?;
m.add_function(wrap_pyfunction!(cga_inner, m)?)?; m.add_function(wrap_pyfunction!(cga_inner, m)?)?;

View file

@ -112,6 +112,78 @@ pub fn versor_apply_closed(v: &[f32; 32], f: &[f32; 32]) -> Result<[f32; 32], Ve
Ok(close_applied_versor(&vfrv)) Ok(close_applied_versor(&vfrv))
} }
/// `versor_apply` f64 path — bit-identity port of Python
/// `algebra.versor.versor_apply` + `_close_applied_versor`.
///
/// Performs the full sandwich V·F·rev(V) and closure in f64. The
/// closure mirrors Python exactly: no null-vector early branch
/// (Python doesn't have one), and after `unitize_closed` succeeds the
/// candidate is gated through `versor_condition < 1e-6` before being
/// accepted — otherwise the deterministic `seed_to_rotor`
/// construction map is used. ADR-0020 parity gate
/// `tests/test_versor_apply_rust_parity.py`.
pub fn versor_apply_closed_f64(
v: &[f64; 32],
f: &[f64; 32],
) -> Result<[f64; 32], VersorError> {
let rev_v = reverse_f64(v);
let vf = geometric_product_f64(v, f);
let vfrv = geometric_product_f64(&vf, &rev_v);
Ok(close_applied_versor_f64(&vfrv))
}
const RUNTIME_CLOSURE_TOL: f64 = 1e-6;
const DENSE_SEED_MIN_COMPONENTS: usize = 8;
fn versor_condition_f64(v: &[f64; 32]) -> f64 {
let rev = reverse_f64(v);
let mut frv = geometric_product_f64(v, &rev);
frv[0] -= 1.0;
frv.iter().map(|x| x * x).sum::<f64>().sqrt()
}
/// Mirrors Python `unitize_versor`: try `unitize_closed`; on
/// bad_residue, if dense enough fall back to `seed_to_rotor`; else
/// propagate the error.
fn unitize_versor_f64(v: &[f64; 32]) -> Result<[f64; 32], ()> {
match unitize_closed(v) {
Ok(closed) => Ok(closed),
Err(()) => {
// Python distinguishes bad_residue (eligible for seed fallback)
// from bad_scalar / near_zero (not eligible). We can't
// distinguish the error variants under the current
// `unitize_closed` signature; mirror Python's policy by gating
// the fallback on the dense-support heuristic, which is the
// condition Python also requires before invoking the rotor seed.
let support = v
.iter()
.filter(|x| x.abs() > NEAR_ZERO_TOL)
.count();
if support < DENSE_SEED_MIN_COMPONENTS {
Err(())
} else {
seed_to_rotor(v)
}
}
}
}
/// Mirrors Python `_close_applied_versor`:
/// try _runtime_closed(v) -> if condition < 1e-6 return; else seed_to_rotor
/// on any ValueError -> seed_to_rotor (with passthrough as last resort
/// if seed_to_rotor itself fails)
fn close_applied_versor_f64(v: &[f64; 32]) -> [f64; 32] {
if let Ok(candidate) = unitize_versor_f64(v) {
if versor_condition_f64(&candidate) < RUNTIME_CLOSURE_TOL {
return candidate;
}
}
if let Ok(seeded) = seed_to_rotor(v) {
return seeded;
}
*v
}
/// Raw sandwich product V * F * reverse(V) without closure. /// Raw sandwich product V * F * reverse(V) without closure.
pub fn versor_apply_raw(v: &[f32; 32], f: &[f32; 32]) -> Result<[f32; 32], VersorError> { pub fn versor_apply_raw(v: &[f32; 32], f: &[f32; 32]) -> Result<[f32; 32], VersorError> {
let rev_v = reverse_raw(v); let rev_v = reverse_raw(v);

View file

@ -595,9 +595,10 @@ Vault indexing strategy is decided (ADR-0019: Stage 1 now, Stages
**Status:** IN PROGRESS (opened 2026-05-16, ADR-0020 Option C) **Status:** IN PROGRESS (opened 2026-05-16, ADR-0020 Option C)
**Depends on:** Phase 4 exit (✓ 2026-05-16) **Depends on:** Phase 4 exit (✓ 2026-05-16)
**Parallel track:** Rust backend parity port, per-surface **Parallel track:** Rust backend parity port, per-surface
bit-identity gated. First port: `vault_recall`. bit-identity gated.
- [ ] 5.1 English fluency (grammatical-coverage v5 OOD) - [x] 5.1 English fluency (`english_fluency_ood` v1, 100% on
public + holdouts, 2026-05-16)
- [ ] 5.2 Hebrew fluency - [ ] 5.2 Hebrew fluency
- [ ] 5.3 Koine Greek fluency - [ ] 5.3 Koine Greek fluency
- [ ] 5.4 Elementary mathematics - [ ] 5.4 Elementary mathematics
@ -606,6 +607,19 @@ Vault indexing strategy is decided (ADR-0019: Stage 1 now, Stages
- [ ] 5.7 Classical literature - [ ] 5.7 Classical literature
- [ ] Phase 1-4 lanes re-run on every release (no regression) - [ ] Phase 1-4 lanes re-run on every release (no regression)
### Parallel track — Rust parity (ADR-0020)
Per-surface bit-identity gates landed (2026-05-16):
- [x] `vault_recall` — passing, dispatch enabled (1.91× at N=1M)
- [x] `cga_inner` — passing, dispatch enabled
- [x] `geometric_product` — passing, dispatch enabled
- [x] `versor_condition` — passing after f64 fold fix, dispatch enabled
- [x] `versor_apply` — f64 port passing, dispatch enabled
(29× over Python on the runtime hot path)
- [x] ADR-0021 (Epistemic Grade Policy) schema wired across
teaching + trace + lexicon (2026-05-16)
--- ---
## Open Scope Decisions ## Open Scope Decisions

View file

@ -150,31 +150,27 @@ governs sequencing.
| `cga_inner` | `tests/test_cga_inner_rust_parity.py` | passing | enabled | | `cga_inner` | `tests/test_cga_inner_rust_parity.py` | passing | enabled |
| `geometric_product` | `tests/test_geometric_product_rust_parity.py` | passing | enabled | | `geometric_product` | `tests/test_geometric_product_rust_parity.py` | passing | enabled |
| `versor_condition` | `tests/test_versor_condition_rust_parity.py` | passing (after f64 fold fix) | enabled | | `versor_condition` | `tests/test_versor_condition_rust_parity.py` | passing (after f64 fold fix) | enabled |
| `versor_apply` | `tests/test_versor_apply_rust_parity.py` | **skipped** | **disabled pending f64 port** | | `versor_apply` | `tests/test_versor_apply_rust_parity.py` | passing (f64 port, 29× over Python) | enabled |
### Outstanding work: `versor_apply` f64 parity port ### `versor_apply` f64 parity port — landed
The current Rust `versor_apply_closed` diverges from Python on two The Rust `versor_apply_closed` diverged from Python on two axes
axes that the bit-identity gate exposes: caught by the bit-identity gate:
1. **Precision** — Rust folds the sandwich (V·F·rev(V)) in f32; Python 1. **Precision** — Rust folded the sandwich (V·F·rev(V)) in f32;
in f64. Python in f64.
2. **Closure structure** — Rust has a null-vector early branch and no 2. **Closure structure** — Rust had a null-vector early branch and no
post-unitize `versor_condition < 1e-6` recheck; Python is the post-unitize `versor_condition < 1e-6` recheck; Python is the
inverse (no null branch; recheck + seed-rotor fallback). inverse (no null branch; recheck + seed-rotor fallback).
The f64 building blocks already exist in the Rust crate Resolution: a new `versor_apply_closed_f64` in `core-rs/src/versor.rs`
(`geometric_product_f64`, `reverse_f64`, `unitize_closed` in f64). performs the full sandwich + closure in f64, mirroring Python's
A focused parity port replaces `versor_apply_closed` with an f64- `_close_applied_versor` exactly (no null branch, post-unitize
throughout sandwich + a closure path mirroring Python's condition recheck, seed-rotor fallback). Exposed through PyO3 as
`_close_applied_versor` exactly, then un-skips `versor_apply_with_closure_f64`; `algebra/backend.py::versor_apply`
`test_versor_apply_rust_parity.py` and removes the dispatch disable routes through it under `CORE_BACKEND=rust`. Parity gate
in `algebra/backend.py::versor_apply`. re-enabled — 8/8 bit-identical. Bench: **29× over Python**
(213µs → 7.4µs per call on the runtime hot path).
Until that port lands, `versor_apply` runs Python under any
`CORE_BACKEND` setting. This is the ADR's "default-off until parity
passes" discipline applied: the surface is honestly disabled, the
gate is honestly skipped, and the follow-up is documented here.
## What this ADR does NOT decide ## What this ADR does NOT decide

View file

@ -94,21 +94,7 @@ def _assert_bit_identity(py: dict, rs: dict) -> None:
assert p == r, f"versor_apply divergence at component {k}: python={p} rust={r}" assert p == r, f"versor_apply divergence at component {k}: python={p} rust={r}"
# Rust dispatch for versor_apply is currently disabled in algebra/backend.py @pytest.mark.skipif(not _RUST_AVAILABLE, reason="core_rs extension not built")
# pending an f64 parity port — see the docstring on `algebra.backend.versor_apply`.
# The Rust kernel `versor_apply_closed` diverges from Python on two axes:
# (1) precision: Rust folds the sandwich in f32; Python in f64
# (2) closure structure: Rust has a null-vector early branch + no
# post-unitize condition recheck; Python is the inverse
# Until both axes are reconciled, the parity gate is skipped. When the
# f64 port lands and the dispatch is re-enabled, remove this skip marker.
_PARITY_DISABLED_REASON = (
"Rust versor_apply dispatch disabled pending f64 parity port; "
"see algebra/backend.py::versor_apply and ADR-0020."
)
@pytest.mark.skip(reason=_PARITY_DISABLED_REASON)
@pytest.mark.parametrize("seed", [0xC07E, 0xBEEF, 0x1234, 0xFACE, 0xDEAD]) @pytest.mark.parametrize("seed", [0xC07E, 0xBEEF, 0x1234, 0xFACE, 0xDEAD])
def test_versor_apply_normalized_bit_identity(seed: int) -> None: def test_versor_apply_normalized_bit_identity(seed: int) -> None:
"""Runtime hot path — both V and F normalized through the closure boundary.""" """Runtime hot path — both V and F normalized through the closure boundary."""
@ -117,15 +103,10 @@ def test_versor_apply_normalized_bit_identity(seed: int) -> None:
_assert_bit_identity(py, rs) _assert_bit_identity(py, rs)
@pytest.mark.skip(reason=_PARITY_DISABLED_REASON) @pytest.mark.skipif(not _RUST_AVAILABLE, reason="core_rs extension not built")
@pytest.mark.parametrize("seed", [0xC07E, 0xBEEF, 0x1234]) @pytest.mark.parametrize("seed", [0xC07E, 0xBEEF, 0x1234])
def test_versor_apply_identity_v_bit_identity(seed: int) -> None: def test_versor_apply_identity_v_bit_identity(seed: int) -> None:
"""V = scalar 1 → V·F·rev(V) == F. The simplest non-trivial sandwich case.""" """V = scalar 1 → V·F·rev(V) == F. The simplest non-trivial sandwich case."""
py = _run_backend("python", FIXTURE_MODE="identity_v", FIXTURE_SEED=str(seed)) py = _run_backend("python", FIXTURE_MODE="identity_v", FIXTURE_SEED=str(seed))
rs = _run_backend("rust", FIXTURE_MODE="identity_v", FIXTURE_SEED=str(seed)) rs = _run_backend("rust", FIXTURE_MODE="identity_v", FIXTURE_SEED=str(seed))
_assert_bit_identity(py, rs) _assert_bit_identity(py, rs)
# Keep _RUST_AVAILABLE referenced so static analysis sees its intended use
# once the parity gate is re-enabled.
_ = _RUST_AVAILABLE