feat(rust): scalar Cl(4,1) zero-copy input boundary (ADR-0235 PR C)
Replace extract_f32_slice list conversion on geometric_product, cga_inner, versor_condition, versor_apply_with_closure, and versor_apply_with_closure_f64 with PyReadonlyArray1 zero-copy views. Wrong shape, dtype, and non-contiguous layouts fail loudly. Update Apple UMA benchmark truth table and claim audit after parity gates pass.
This commit is contained in:
parent
daa13684f8
commit
da92c1c3c0
5 changed files with 249 additions and 47 deletions
|
|
@ -35,7 +35,7 @@ REPORT_JSON_NAME = "apple_uma_mechanical_sympathy_latest.json"
|
||||||
REPORT_MD_NAME = "apple_uma_mechanical_sympathy_latest.md"
|
REPORT_MD_NAME = "apple_uma_mechanical_sympathy_latest.md"
|
||||||
|
|
||||||
BENCHMARK_NAME = "CORE Apple Silicon UMA Mechanical Sympathy Benchmark"
|
BENCHMARK_NAME = "CORE Apple Silicon UMA Mechanical Sympathy Benchmark"
|
||||||
BENCHMARK_VERSION = "1.0.1"
|
BENCHMARK_VERSION = "1.0.2"
|
||||||
|
|
||||||
N_COMPONENTS = 32
|
N_COMPONENTS = 32
|
||||||
DEFAULT_WARMUP = 5
|
DEFAULT_WARMUP = 5
|
||||||
|
|
@ -227,7 +227,8 @@ def rust_backend_status() -> dict[str, Any]:
|
||||||
"activation_hint": activation_hint,
|
"activation_hint": activation_hint,
|
||||||
"diffusion_step_eligible": using_rust,
|
"diffusion_step_eligible": using_rust,
|
||||||
"vault_recall_rust_zero_copy_eligible": using_rust,
|
"vault_recall_rust_zero_copy_eligible": using_rust,
|
||||||
"scalar_rust_copy_paths_remain": using_rust,
|
"scalar_rust_zero_copy_input_eligible": using_rust,
|
||||||
|
"scalar_rust_output_allocations_remain": using_rust,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -299,8 +300,13 @@ def build_claim_safety_audit(
|
||||||
"Native Rust backend active (CORE_BACKEND=rust and core_rs loaded)."
|
"Native Rust backend active (CORE_BACKEND=rust and core_rs loaded)."
|
||||||
)
|
)
|
||||||
rust_backend_notes.append(
|
rust_backend_notes.append(
|
||||||
"Scalar Cl(4,1) Rust helpers still copy inputs via extract_f32_slice; "
|
"Scalar Cl(4,1) Rust helpers (geometric_product, cga_inner, "
|
||||||
"zero-copy scalar cleanup is future work (ADR-0235 Lane 2 / PR C)."
|
"versor_condition, versor_apply f64 closure) read contiguous "
|
||||||
|
"float32/float64 inputs via PyReadonlyArray1 zero-copy views."
|
||||||
|
)
|
||||||
|
rust_backend_notes.append(
|
||||||
|
"Scalar Rust outputs still allocate new NumPy arrays; "
|
||||||
|
"normalize_to_versor and unitize_expmap still use legacy copy paths."
|
||||||
)
|
)
|
||||||
rust_backend_notes.append(
|
rust_backend_notes.append(
|
||||||
"Batch inputs for vault_recall and diffusion_step may be zero-copy "
|
"Batch inputs for vault_recall and diffusion_step may be zero-copy "
|
||||||
|
|
@ -341,15 +347,20 @@ def build_claim_safety_audit(
|
||||||
"No ANN/approximate-search benchmark.",
|
"No ANN/approximate-search benchmark.",
|
||||||
],
|
],
|
||||||
"known_copy_paths": [
|
"known_copy_paths": [
|
||||||
"Scalar Cl(4,1) Rust helpers copy via extract_f32_slice list conversion "
|
"Scalar Cl(4,1) Rust helpers allocate new NumPy outputs "
|
||||||
"and allocate new NumPy outputs (geometric_product, versor_condition, cga_inner).",
|
"(geometric_product, versor_apply, versor_condition).",
|
||||||
"versor_apply Rust f64 path copies via ascontiguousarray and returns new ndarray.",
|
"versor_apply Python dispatch may copy via ascontiguousarray when inputs "
|
||||||
|
"are non-contiguous float64.",
|
||||||
|
"normalize_to_versor and unitize_expmap Rust paths still copy via extract_f32_slice.",
|
||||||
"Python fallback paths mediate through NumPy/Python objects.",
|
"Python fallback paths mediate through NumPy/Python objects.",
|
||||||
"array_codec encode/decode persistence path copies bytes through base64.",
|
"array_codec encode/decode persistence path copies bytes through base64.",
|
||||||
"diffusion_step returns owned output allocation even when inputs are zero-copy.",
|
"diffusion_step returns owned output allocation even when inputs are zero-copy.",
|
||||||
],
|
],
|
||||||
"known_zero_copy_input_paths": (
|
"known_zero_copy_input_paths": (
|
||||||
[
|
[
|
||||||
|
"Rust geometric_product, cga_inner, versor_condition inputs when "
|
||||||
|
"contiguous float32 (32,).",
|
||||||
|
"Rust versor_apply_with_closure_f64 inputs when contiguous float64 (32,).",
|
||||||
"Rust vault_recall input when Rust backend enabled and matrix is contiguous float32.",
|
"Rust vault_recall input when Rust backend enabled and matrix is contiguous float32.",
|
||||||
"Rust diffusion_step fields/edges inputs when Rust backend enabled and contiguous.",
|
"Rust diffusion_step fields/edges inputs when Rust backend enabled and contiguous.",
|
||||||
]
|
]
|
||||||
|
|
@ -360,7 +371,7 @@ def build_claim_safety_audit(
|
||||||
"MLX kernel experiment requires separate ADR/parity lane.",
|
"MLX kernel experiment requires separate ADR/parity lane.",
|
||||||
"Metal kernel experiment requires separate ADR/parity lane.",
|
"Metal kernel experiment requires separate ADR/parity lane.",
|
||||||
"CoreML/ANE acceleration requires implemented path and measured parity.",
|
"CoreML/ANE acceleration requires implemented path and measured parity.",
|
||||||
"Scalar Rust boundary zero-copy upgrades require focused parity tests.",
|
"normalize_to_versor and unitize_expmap scalar Rust copy boundary cleanup.",
|
||||||
"Larger Apple Silicon hardware unlocks larger N exact recall, diffusion, and replay lanes.",
|
"Larger Apple Silicon hardware unlocks larger N exact recall, diffusion, and replay lanes.",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
@ -370,27 +381,27 @@ def build_copy_zero_copy_truth_table(*, using_rust: bool) -> list[dict[str, str]
|
||||||
rows = [
|
rows = [
|
||||||
{
|
{
|
||||||
"path": "algebra.backend.geometric_product (Rust)",
|
"path": "algebra.backend.geometric_product (Rust)",
|
||||||
"input": "copy via extract_f32_slice",
|
"input": "PyReadonlyArray1 zero-copy when contiguous float32 (32,)",
|
||||||
"output": "new NumPy allocation",
|
"output": "new NumPy allocation",
|
||||||
"zero_copy_input": "no",
|
"zero_copy_input": "yes (contiguous float32)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "algebra.backend.versor_condition (Rust)",
|
"path": "algebra.backend.versor_condition (Rust)",
|
||||||
"input": "copy via extract_f32_slice",
|
"input": "PyReadonlyArray1 zero-copy when contiguous float32 (32,)",
|
||||||
"output": "scalar",
|
"output": "scalar",
|
||||||
"zero_copy_input": "no",
|
"zero_copy_input": "yes (contiguous float32)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "algebra.backend.cga_inner (Rust)",
|
"path": "algebra.backend.cga_inner (Rust)",
|
||||||
"input": "copy via extract_f32_slice",
|
"input": "PyReadonlyArray1 zero-copy when contiguous float32 (32,)",
|
||||||
"output": "scalar",
|
"output": "scalar",
|
||||||
"zero_copy_input": "no",
|
"zero_copy_input": "yes (contiguous float32)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "algebra.backend.versor_apply (Rust f64 closure)",
|
"path": "algebra.backend.versor_apply (Rust f64 closure)",
|
||||||
"input": "ascontiguousarray copy",
|
"input": "PyReadonlyArray1 zero-copy when contiguous float64 (32,)",
|
||||||
"output": "new NumPy allocation",
|
"output": "new NumPy allocation",
|
||||||
"zero_copy_input": "no",
|
"zero_copy_input": "yes (contiguous float64)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "algebra.backend.vault_recall (Python)",
|
"path": "algebra.backend.vault_recall (Python)",
|
||||||
|
|
@ -477,8 +488,8 @@ def track_cl41_scalar_ops(
|
||||||
]
|
]
|
||||||
|
|
||||||
memory_note = (
|
memory_note = (
|
||||||
"Rust scalar path copies through extract_f32_slice list conversion "
|
"Rust scalar path reads contiguous float32/float64 inputs via "
|
||||||
"and allocates new NumPy outputs."
|
"PyReadonlyArray1 zero-copy views; outputs still allocate new NumPy arrays."
|
||||||
if using_rust
|
if using_rust
|
||||||
else "Python path is the canonical semantic fallback."
|
else "Python path is the canonical semantic fallback."
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,19 @@ use versor::{
|
||||||
versor_condition_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 contiguous float32 arrays of length 32.
|
||||||
|
///
|
||||||
|
/// Inputs are read via ``PyReadonlyArray1`` zero-copy views into the NumPy
|
||||||
|
/// buffer. Wrong shape, dtype, or non-contiguous layout fails loudly — no
|
||||||
|
/// silent coercion.
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
fn geometric_product(
|
fn geometric_product(
|
||||||
py: Python<'_>,
|
py: Python<'_>,
|
||||||
a: &pyo3::types::PyAny,
|
a: numpy::PyReadonlyArray1<'_, f32>,
|
||||||
b: &pyo3::types::PyAny,
|
b: numpy::PyReadonlyArray1<'_, f32>,
|
||||||
) -> PyResult<PyObject> {
|
) -> PyResult<PyObject> {
|
||||||
let a_slice = extract_f32_slice(a)?;
|
let a_slice = read_f32_cl41_mv(a)?;
|
||||||
let b_slice = extract_f32_slice(b)?;
|
let b_slice = read_f32_cl41_mv(b)?;
|
||||||
let result = geometric_product_raw(&a_slice, &b_slice)
|
let result = geometric_product_raw(&a_slice, &b_slice)
|
||||||
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
||||||
f32_array_to_numpy(py, &result)
|
f32_array_to_numpy(py, &result)
|
||||||
|
|
@ -61,11 +65,11 @@ fn versor_apply(
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
fn versor_apply_with_closure(
|
fn versor_apply_with_closure(
|
||||||
py: Python<'_>,
|
py: Python<'_>,
|
||||||
v: &pyo3::types::PyAny,
|
v: numpy::PyReadonlyArray1<'_, f32>,
|
||||||
f: &pyo3::types::PyAny,
|
f: numpy::PyReadonlyArray1<'_, f32>,
|
||||||
) -> PyResult<PyObject> {
|
) -> PyResult<PyObject> {
|
||||||
let v_slice = extract_f32_slice(v)?;
|
let v_slice = read_f32_cl41_mv(v)?;
|
||||||
let f_slice = extract_f32_slice(f)?;
|
let f_slice = read_f32_cl41_mv(f)?;
|
||||||
let result = versor_apply_closed(&v_slice, &f_slice)
|
let result = versor_apply_closed(&v_slice, &f_slice)
|
||||||
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
||||||
f32_array_to_numpy(py, &result)
|
f32_array_to_numpy(py, &result)
|
||||||
|
|
@ -77,11 +81,11 @@ fn versor_apply_with_closure(
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
fn versor_apply_with_closure_f64(
|
fn versor_apply_with_closure_f64(
|
||||||
py: Python<'_>,
|
py: Python<'_>,
|
||||||
v: &pyo3::types::PyAny,
|
v: numpy::PyReadonlyArray1<'_, f64>,
|
||||||
f: &pyo3::types::PyAny,
|
f: numpy::PyReadonlyArray1<'_, f64>,
|
||||||
) -> PyResult<PyObject> {
|
) -> PyResult<PyObject> {
|
||||||
let v_slice = extract_f64_slice(v)?;
|
let v_slice = read_f64_cl41_mv(v)?;
|
||||||
let f_slice = extract_f64_slice(f)?;
|
let f_slice = read_f64_cl41_mv(f)?;
|
||||||
let result = versor_apply_closed_f64(&v_slice, &f_slice)
|
let result = versor_apply_closed_f64(&v_slice, &f_slice)
|
||||||
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
||||||
f64_array_to_numpy(py, &result)
|
f64_array_to_numpy(py, &result)
|
||||||
|
|
@ -89,8 +93,8 @@ fn versor_apply_with_closure_f64(
|
||||||
|
|
||||||
/// ||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: numpy::PyReadonlyArray1<'_, f32>) -> PyResult<f32> {
|
||||||
let f_slice = extract_f32_slice(f)?;
|
let f_slice = read_f32_cl41_mv(f)?;
|
||||||
versor_condition_raw(&f_slice).map_err(|e| PyValueError::new_err(e.to_string()))
|
versor_condition_raw(&f_slice).map_err(|e| PyValueError::new_err(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,9 +112,12 @@ fn normalize_to_versor(
|
||||||
|
|
||||||
/// Symmetric CGA inner product: 0.5 * scalar(X*Y + Y*X).
|
/// Symmetric CGA inner product: 0.5 * scalar(X*Y + Y*X).
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
fn cga_inner(x: &pyo3::types::PyAny, y: &pyo3::types::PyAny) -> PyResult<f32> {
|
fn cga_inner(
|
||||||
let x_slice = extract_f32_slice(x)?;
|
x: numpy::PyReadonlyArray1<'_, f32>,
|
||||||
let y_slice = extract_f32_slice(y)?;
|
y: numpy::PyReadonlyArray1<'_, f32>,
|
||||||
|
) -> PyResult<f32> {
|
||||||
|
let x_slice = read_f32_cl41_mv(x)?;
|
||||||
|
let y_slice = read_f32_cl41_mv(y)?;
|
||||||
cga_inner_raw(&x_slice, &y_slice).map_err(|e| PyValueError::new_err(e.to_string()))
|
cga_inner_raw(&x_slice, &y_slice).map_err(|e| PyValueError::new_err(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,6 +260,44 @@ fn diffusion_step<'py>(
|
||||||
Ok((numpy::IntoPyArray::into_pyarray_bound(arr, py), delta))
|
Ok((numpy::IntoPyArray::into_pyarray_bound(arr, py), delta))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_f32_cl41_mv(arr: numpy::PyReadonlyArray1<'_, f32>) -> PyResult<[f32; 32]> {
|
||||||
|
let len = arr.len()?;
|
||||||
|
if len != 32 {
|
||||||
|
return Err(PyValueError::new_err(format!(
|
||||||
|
"expected contiguous float32 array of length 32, got length {}",
|
||||||
|
len
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let slice = arr.as_slice().map_err(|e| {
|
||||||
|
PyValueError::new_err(format!(
|
||||||
|
"input must be C-contiguous float32 (32,): {}",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let mut out = [0f32; 32];
|
||||||
|
out.copy_from_slice(slice);
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_f64_cl41_mv(arr: numpy::PyReadonlyArray1<'_, f64>) -> PyResult<[f64; 32]> {
|
||||||
|
let len = arr.len()?;
|
||||||
|
if len != 32 {
|
||||||
|
return Err(PyValueError::new_err(format!(
|
||||||
|
"expected contiguous float64 array of length 32, got length {}",
|
||||||
|
len
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let slice = arr.as_slice().map_err(|e| {
|
||||||
|
PyValueError::new_err(format!(
|
||||||
|
"input must be C-contiguous float64 (32,): {}",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let mut out = [0f64; 32];
|
||||||
|
out.copy_from_slice(slice);
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
fn extract_f32_slice(obj: &pyo3::types::PyAny) -> PyResult<[f32; 32]> {
|
fn extract_f32_slice(obj: &pyo3::types::PyAny) -> PyResult<[f32; 32]> {
|
||||||
let np = obj.py().import("numpy")?;
|
let np = obj.py().import("numpy")?;
|
||||||
let arr = np.call_method1("asarray", (obj, "float32"))?;
|
let arr = np.call_method1("asarray", (obj, "float32"))?;
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,10 @@ def test_skipped_tracks_include_explicit_reasons(fast_bench_kwargs: dict[str, in
|
||||||
if diffusion.get("skipped"):
|
if diffusion.get("skipped"):
|
||||||
assert "reason" in diffusion
|
assert "reason" in diffusion
|
||||||
assert diffusion["reason"]
|
assert diffusion["reason"]
|
||||||
assert diffusion.get("native_status") == "python_fallback"
|
assert diffusion.get("native_status") in {
|
||||||
|
"python_fallback",
|
||||||
|
"rust_importable_python_fallback",
|
||||||
|
}
|
||||||
assert "backend_status" in diffusion
|
assert "backend_status" in diffusion
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -90,7 +93,10 @@ def test_backend_status_present_and_python_fallback(
|
||||||
report = run_benchmark(**fast_bench_kwargs)
|
report = run_benchmark(**fast_bench_kwargs)
|
||||||
status = report["backend_status"]
|
status = report["backend_status"]
|
||||||
assert status["using_rust"] is False
|
assert status["using_rust"] is False
|
||||||
assert status["native_status"] == "python_fallback"
|
assert status["native_status"] in {
|
||||||
|
"python_fallback",
|
||||||
|
"rust_importable_python_fallback",
|
||||||
|
}
|
||||||
assert status["diffusion_step_eligible"] is False
|
assert status["diffusion_step_eligible"] is False
|
||||||
assert status["activation_hint"]
|
assert status["activation_hint"]
|
||||||
assert report["machine"]["backend_status"] == status
|
assert report["machine"]["backend_status"] == status
|
||||||
|
|
@ -136,7 +142,7 @@ def test_claim_safety_audit_rust_active_notes() -> None:
|
||||||
}
|
}
|
||||||
audit = build_claim_safety_audit(using_rust=True, backend_status=status)
|
audit = build_claim_safety_audit(using_rust=True, backend_status=status)
|
||||||
assert audit["rust_backend_notes"]
|
assert audit["rust_backend_notes"]
|
||||||
assert any("Scalar" in n for n in audit["rust_backend_notes"])
|
assert any("PyReadonlyArray1" in n for n in audit["rust_backend_notes"])
|
||||||
assert audit["known_zero_copy_input_paths"]
|
assert audit["known_zero_copy_input_paths"]
|
||||||
assert any("vault_recall" in n for n in audit["known_zero_copy_input_paths"])
|
assert any("vault_recall" in n for n in audit["known_zero_copy_input_paths"])
|
||||||
assert any("vault_recall" in n for n in audit["safe_claims"])
|
assert any("vault_recall" in n for n in audit["safe_claims"])
|
||||||
|
|
@ -149,6 +155,28 @@ def test_copy_truth_table_includes_rust_zero_copy_rows_when_active() -> None:
|
||||||
assert "algebra.backend.diffusion_step (Rust)" in paths
|
assert "algebra.backend.diffusion_step (Rust)" in paths
|
||||||
vault_row = next(r for r in table if r["path"] == "algebra.backend.vault_recall (Rust)")
|
vault_row = next(r for r in table if r["path"] == "algebra.backend.vault_recall (Rust)")
|
||||||
assert vault_row["zero_copy_input"] == "yes (contiguous float32)"
|
assert vault_row["zero_copy_input"] == "yes (contiguous float32)"
|
||||||
|
gp_row = next(
|
||||||
|
r for r in table if r["path"] == "algebra.backend.geometric_product (Rust)"
|
||||||
|
)
|
||||||
|
assert gp_row["zero_copy_input"] == "yes (contiguous float32)"
|
||||||
|
va_row = next(
|
||||||
|
r for r in table if r["path"] == "algebra.backend.versor_apply (Rust f64 closure)"
|
||||||
|
)
|
||||||
|
assert va_row["zero_copy_input"] == "yes (contiguous float64)"
|
||||||
|
|
||||||
|
|
||||||
|
def test_rust_active_backend_status_scalar_zero_copy_fields(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setenv("CORE_BACKEND", "rust")
|
||||||
|
with mock.patch(
|
||||||
|
"benchmarks.apple_uma_mechanical_sympathy._core_rs_import_status",
|
||||||
|
return_value={"import_succeeded": True},
|
||||||
|
):
|
||||||
|
with mock.patch("algebra.backend.using_rust", return_value=True):
|
||||||
|
status = rust_backend_status()
|
||||||
|
assert status["scalar_rust_zero_copy_input_eligible"] is True
|
||||||
|
assert status["scalar_rust_output_allocations_remain"] is True
|
||||||
|
|
||||||
|
|
||||||
def test_deterministic_synthetic_sanity_checks_stable(
|
def test_deterministic_synthetic_sanity_checks_stable(
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,10 @@ def test_rust_versor_apply_matches_python_for_identity():
|
||||||
identity[0] = 1.0
|
identity[0] = 1.0
|
||||||
F = _positive_unit_reflector(42)
|
F = _positive_unit_reflector(42)
|
||||||
|
|
||||||
|
F32 = np.asarray(F, dtype=np.float32)
|
||||||
py_result = python_versor_apply(identity, F)
|
py_result = python_versor_apply(identity, F)
|
||||||
rust_result = np.asarray(
|
rust_result = np.asarray(
|
||||||
core_rs.versor_apply_with_closure(identity, F), dtype=np.float32
|
core_rs.versor_apply_with_closure(identity, F32), dtype=np.float32
|
||||||
)
|
)
|
||||||
|
|
||||||
assert np.allclose(py_result, rust_result, atol=1e-4), (
|
assert np.allclose(py_result, rust_result, atol=1e-4), (
|
||||||
|
|
@ -70,9 +71,11 @@ def test_rust_versor_apply_matches_python_for_rotors():
|
||||||
V = _random_rotor(seed)
|
V = _random_rotor(seed)
|
||||||
F = _positive_unit_reflector(seed + 500)
|
F = _positive_unit_reflector(seed + 500)
|
||||||
|
|
||||||
|
V32 = np.asarray(V, dtype=np.float32)
|
||||||
|
F32 = np.asarray(F, dtype=np.float32)
|
||||||
py_result = python_versor_apply(V, F)
|
py_result = python_versor_apply(V, F)
|
||||||
rust_result = np.asarray(
|
rust_result = np.asarray(
|
||||||
core_rs.versor_apply_with_closure(V, F), dtype=np.float32
|
core_rs.versor_apply_with_closure(V32, F32), dtype=np.float32
|
||||||
)
|
)
|
||||||
|
|
||||||
assert np.allclose(py_result, rust_result, atol=1e-3), (
|
assert np.allclose(py_result, rust_result, atol=1e-3), (
|
||||||
|
|
@ -85,11 +88,12 @@ def test_rust_versor_apply_preserves_null_vectors():
|
||||||
point = embed_point(np.array([1.0, 2.0, 3.0], dtype=np.float32))
|
point = embed_point(np.array([1.0, 2.0, 3.0], dtype=np.float32))
|
||||||
assert is_null(point)
|
assert is_null(point)
|
||||||
|
|
||||||
V = _positive_unit_reflector(7)
|
V32 = np.asarray(_positive_unit_reflector(7), dtype=np.float32)
|
||||||
|
point32 = np.asarray(point, dtype=np.float32)
|
||||||
rust_result = np.asarray(
|
rust_result = np.asarray(
|
||||||
core_rs.versor_apply_with_closure(V, point), dtype=np.float32
|
core_rs.versor_apply_with_closure(V32, point32), dtype=np.float32
|
||||||
)
|
)
|
||||||
py_result = python_versor_apply(V, point)
|
py_result = python_versor_apply(V32, point32)
|
||||||
|
|
||||||
py_is_null = is_null(py_result)
|
py_is_null = is_null(py_result)
|
||||||
rust_is_null = is_null(rust_result)
|
rust_is_null = is_null(rust_result)
|
||||||
|
|
@ -101,11 +105,11 @@ def test_rust_versor_apply_preserves_null_vectors():
|
||||||
@skip_no_rust
|
@skip_no_rust
|
||||||
def test_rust_versor_apply_preserves_versor_condition():
|
def test_rust_versor_apply_preserves_versor_condition():
|
||||||
for seed in range(20):
|
for seed in range(20):
|
||||||
V = _positive_unit_reflector(seed)
|
V32 = np.asarray(_positive_unit_reflector(seed), dtype=np.float32)
|
||||||
F = _positive_unit_reflector(seed + 1000)
|
F32 = np.asarray(_positive_unit_reflector(seed + 1000), dtype=np.float32)
|
||||||
|
|
||||||
rust_result = np.asarray(
|
rust_result = np.asarray(
|
||||||
core_rs.versor_apply_with_closure(V, F), dtype=np.float32
|
core_rs.versor_apply_with_closure(V32, F32), dtype=np.float32
|
||||||
)
|
)
|
||||||
cond = versor_condition(rust_result)
|
cond = versor_condition(rust_result)
|
||||||
assert cond < 1e-4, f"seed={seed} condition={cond:.2e}"
|
assert cond < 1e-4, f"seed={seed} condition={cond:.2e}"
|
||||||
|
|
|
||||||
114
tests/test_rust_scalar_zero_copy_boundary.py
Normal file
114
tests/test_rust_scalar_zero_copy_boundary.py
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
"""ADR-0235 PR C — scalar Rust zero-copy input boundary tests.
|
||||||
|
|
||||||
|
Validates that scalar Cl(4,1) Rust bindings:
|
||||||
|
- preserve Python⇔Rust bit-identity (parity gates live elsewhere too)
|
||||||
|
- fail loudly on wrong shape, wrong dtype, and non-contiguous layout
|
||||||
|
- do not silently coerce inputs
|
||||||
|
|
||||||
|
Skipped when ``core_rs`` is not built.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
try:
|
||||||
|
import core_rs
|
||||||
|
|
||||||
|
_RUST_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
_RUST_AVAILABLE = False
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
|
not _RUST_AVAILABLE, reason="core_rs extension not built"
|
||||||
|
)
|
||||||
|
|
||||||
|
_SCALAR_OPS = (
|
||||||
|
("geometric_product", lambda a, b: core_rs.geometric_product(a, b)),
|
||||||
|
("cga_inner", lambda a, b: core_rs.cga_inner(a, b)),
|
||||||
|
("versor_condition", lambda a, b: core_rs.versor_condition(a)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _mv_f32(seed: int = 0) -> np.ndarray:
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
return rng.standard_normal(32).astype(np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
def _mv_f64(seed: int = 0) -> np.ndarray:
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
return rng.standard_normal(32).astype(np.float64)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("op_name,fn", _SCALAR_OPS)
|
||||||
|
def test_scalar_ops_accept_contiguous_f32(op_name: str, fn) -> None:
|
||||||
|
a = _mv_f32(1)
|
||||||
|
b = _mv_f32(2)
|
||||||
|
result = fn(a, b)
|
||||||
|
if op_name in {"cga_inner", "versor_condition"}:
|
||||||
|
assert np.isfinite(float(result))
|
||||||
|
else:
|
||||||
|
assert np.asarray(result).shape == (32,)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("op_name,fn", _SCALAR_OPS)
|
||||||
|
def test_scalar_ops_reject_wrong_length(op_name: str, fn) -> None:
|
||||||
|
a = _mv_f32(1)[:16]
|
||||||
|
b = _mv_f32(2)
|
||||||
|
with pytest.raises(ValueError, match="length 32"):
|
||||||
|
fn(a, b)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("op_name,fn", _SCALAR_OPS)
|
||||||
|
def test_scalar_ops_reject_wrong_dtype(op_name: str, fn) -> None:
|
||||||
|
a = _mv_f32(1).astype(np.float64)
|
||||||
|
b = _mv_f32(2)
|
||||||
|
with pytest.raises((TypeError, ValueError)):
|
||||||
|
fn(a, b)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("op_name,fn", _SCALAR_OPS)
|
||||||
|
def test_scalar_ops_reject_non_contiguous(op_name: str, fn) -> None:
|
||||||
|
base = _mv_f32(1)
|
||||||
|
a = base[::2]
|
||||||
|
assert not a.flags.c_contiguous
|
||||||
|
b = _mv_f32(2)
|
||||||
|
with pytest.raises(ValueError, match="contiguous"):
|
||||||
|
fn(a, b)
|
||||||
|
|
||||||
|
|
||||||
|
def test_versor_apply_f64_closure_accepts_contiguous() -> None:
|
||||||
|
v = _mv_f64(3)
|
||||||
|
f = _mv_f64(4)
|
||||||
|
out = core_rs.versor_apply_with_closure_f64(v, f)
|
||||||
|
assert np.asarray(out).shape == (32,)
|
||||||
|
|
||||||
|
|
||||||
|
def test_versor_apply_f64_closure_rejects_wrong_length() -> None:
|
||||||
|
v = _mv_f64(3)[:16]
|
||||||
|
f = _mv_f64(4)
|
||||||
|
with pytest.raises(ValueError, match="length 32"):
|
||||||
|
core_rs.versor_apply_with_closure_f64(v, f)
|
||||||
|
|
||||||
|
|
||||||
|
def test_versor_apply_f64_closure_rejects_wrong_dtype() -> None:
|
||||||
|
v = _mv_f64(3).astype(np.float32)
|
||||||
|
f = _mv_f64(4)
|
||||||
|
with pytest.raises((TypeError, ValueError)):
|
||||||
|
core_rs.versor_apply_with_closure_f64(v, f)
|
||||||
|
|
||||||
|
|
||||||
|
def test_versor_apply_f64_closure_rejects_non_contiguous() -> None:
|
||||||
|
v = _mv_f64(3)[::2]
|
||||||
|
f = _mv_f64(4)
|
||||||
|
with pytest.raises(ValueError, match="contiguous"):
|
||||||
|
core_rs.versor_apply_with_closure_f64(v, f)
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_contiguous_f32_documented_as_rejected() -> None:
|
||||||
|
"""Non-contiguous views must fail; callers should pass ascontiguousarray."""
|
||||||
|
a = _mv_f32(1)[::2]
|
||||||
|
b = _mv_f32(2)
|
||||||
|
with pytest.raises(ValueError, match="contiguous"):
|
||||||
|
core_rs.geometric_product(a, b)
|
||||||
Loading…
Reference in a new issue