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"
|
||||
|
||||
BENCHMARK_NAME = "CORE Apple Silicon UMA Mechanical Sympathy Benchmark"
|
||||
BENCHMARK_VERSION = "1.0.1"
|
||||
BENCHMARK_VERSION = "1.0.2"
|
||||
|
||||
N_COMPONENTS = 32
|
||||
DEFAULT_WARMUP = 5
|
||||
|
|
@ -227,7 +227,8 @@ def rust_backend_status() -> dict[str, Any]:
|
|||
"activation_hint": activation_hint,
|
||||
"diffusion_step_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)."
|
||||
)
|
||||
rust_backend_notes.append(
|
||||
"Scalar Cl(4,1) Rust helpers still copy inputs via extract_f32_slice; "
|
||||
"zero-copy scalar cleanup is future work (ADR-0235 Lane 2 / PR C)."
|
||||
"Scalar Cl(4,1) Rust helpers (geometric_product, cga_inner, "
|
||||
"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(
|
||||
"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.",
|
||||
],
|
||||
"known_copy_paths": [
|
||||
"Scalar Cl(4,1) Rust helpers copy via extract_f32_slice list conversion "
|
||||
"and allocate new NumPy outputs (geometric_product, versor_condition, cga_inner).",
|
||||
"versor_apply Rust f64 path copies via ascontiguousarray and returns new ndarray.",
|
||||
"Scalar Cl(4,1) Rust helpers allocate new NumPy outputs "
|
||||
"(geometric_product, versor_apply, versor_condition).",
|
||||
"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.",
|
||||
"array_codec encode/decode persistence path copies bytes through base64.",
|
||||
"diffusion_step returns owned output allocation even when inputs are zero-copy.",
|
||||
],
|
||||
"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 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.",
|
||||
"Metal kernel experiment requires separate ADR/parity lane.",
|
||||
"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.",
|
||||
],
|
||||
}
|
||||
|
|
@ -370,27 +381,27 @@ def build_copy_zero_copy_truth_table(*, using_rust: bool) -> list[dict[str, str]
|
|||
rows = [
|
||||
{
|
||||
"path": "algebra.backend.geometric_product (Rust)",
|
||||
"input": "copy via extract_f32_slice",
|
||||
"input": "PyReadonlyArray1 zero-copy when contiguous float32 (32,)",
|
||||
"output": "new NumPy allocation",
|
||||
"zero_copy_input": "no",
|
||||
"zero_copy_input": "yes (contiguous float32)",
|
||||
},
|
||||
{
|
||||
"path": "algebra.backend.versor_condition (Rust)",
|
||||
"input": "copy via extract_f32_slice",
|
||||
"input": "PyReadonlyArray1 zero-copy when contiguous float32 (32,)",
|
||||
"output": "scalar",
|
||||
"zero_copy_input": "no",
|
||||
"zero_copy_input": "yes (contiguous float32)",
|
||||
},
|
||||
{
|
||||
"path": "algebra.backend.cga_inner (Rust)",
|
||||
"input": "copy via extract_f32_slice",
|
||||
"input": "PyReadonlyArray1 zero-copy when contiguous float32 (32,)",
|
||||
"output": "scalar",
|
||||
"zero_copy_input": "no",
|
||||
"zero_copy_input": "yes (contiguous float32)",
|
||||
},
|
||||
{
|
||||
"path": "algebra.backend.versor_apply (Rust f64 closure)",
|
||||
"input": "ascontiguousarray copy",
|
||||
"input": "PyReadonlyArray1 zero-copy when contiguous float64 (32,)",
|
||||
"output": "new NumPy allocation",
|
||||
"zero_copy_input": "no",
|
||||
"zero_copy_input": "yes (contiguous float64)",
|
||||
},
|
||||
{
|
||||
"path": "algebra.backend.vault_recall (Python)",
|
||||
|
|
@ -477,8 +488,8 @@ def track_cl41_scalar_ops(
|
|||
]
|
||||
|
||||
memory_note = (
|
||||
"Rust scalar path copies through extract_f32_slice list conversion "
|
||||
"and allocates new NumPy outputs."
|
||||
"Rust scalar path reads contiguous float32/float64 inputs via "
|
||||
"PyReadonlyArray1 zero-copy views; outputs still allocate new NumPy arrays."
|
||||
if using_rust
|
||||
else "Python path is the canonical semantic fallback."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,15 +28,19 @@ use versor::{
|
|||
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]
|
||||
fn geometric_product(
|
||||
py: Python<'_>,
|
||||
a: &pyo3::types::PyAny,
|
||||
b: &pyo3::types::PyAny,
|
||||
a: numpy::PyReadonlyArray1<'_, f32>,
|
||||
b: numpy::PyReadonlyArray1<'_, f32>,
|
||||
) -> PyResult<PyObject> {
|
||||
let a_slice = extract_f32_slice(a)?;
|
||||
let b_slice = extract_f32_slice(b)?;
|
||||
let a_slice = read_f32_cl41_mv(a)?;
|
||||
let b_slice = read_f32_cl41_mv(b)?;
|
||||
let result = geometric_product_raw(&a_slice, &b_slice)
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
||||
f32_array_to_numpy(py, &result)
|
||||
|
|
@ -61,11 +65,11 @@ fn versor_apply(
|
|||
#[pyfunction]
|
||||
fn versor_apply_with_closure(
|
||||
py: Python<'_>,
|
||||
v: &pyo3::types::PyAny,
|
||||
f: &pyo3::types::PyAny,
|
||||
v: numpy::PyReadonlyArray1<'_, f32>,
|
||||
f: numpy::PyReadonlyArray1<'_, f32>,
|
||||
) -> PyResult<PyObject> {
|
||||
let v_slice = extract_f32_slice(v)?;
|
||||
let f_slice = extract_f32_slice(f)?;
|
||||
let v_slice = read_f32_cl41_mv(v)?;
|
||||
let f_slice = read_f32_cl41_mv(f)?;
|
||||
let result = versor_apply_closed(&v_slice, &f_slice)
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
||||
f32_array_to_numpy(py, &result)
|
||||
|
|
@ -77,11 +81,11 @@ fn versor_apply_with_closure(
|
|||
#[pyfunction]
|
||||
fn versor_apply_with_closure_f64(
|
||||
py: Python<'_>,
|
||||
v: &pyo3::types::PyAny,
|
||||
f: &pyo3::types::PyAny,
|
||||
v: numpy::PyReadonlyArray1<'_, f64>,
|
||||
f: numpy::PyReadonlyArray1<'_, f64>,
|
||||
) -> PyResult<PyObject> {
|
||||
let v_slice = extract_f64_slice(v)?;
|
||||
let f_slice = extract_f64_slice(f)?;
|
||||
let v_slice = read_f64_cl41_mv(v)?;
|
||||
let f_slice = read_f64_cl41_mv(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)
|
||||
|
|
@ -89,8 +93,8 @@ fn versor_apply_with_closure_f64(
|
|||
|
||||
/// ||F*reverse(F) - 1||_F. Returns scalar f32.
|
||||
#[pyfunction]
|
||||
fn versor_condition(f: &pyo3::types::PyAny) -> PyResult<f32> {
|
||||
let f_slice = extract_f32_slice(f)?;
|
||||
fn versor_condition(f: numpy::PyReadonlyArray1<'_, f32>) -> PyResult<f32> {
|
||||
let f_slice = read_f32_cl41_mv(f)?;
|
||||
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).
|
||||
#[pyfunction]
|
||||
fn cga_inner(x: &pyo3::types::PyAny, y: &pyo3::types::PyAny) -> PyResult<f32> {
|
||||
let x_slice = extract_f32_slice(x)?;
|
||||
let y_slice = extract_f32_slice(y)?;
|
||||
fn cga_inner(
|
||||
x: numpy::PyReadonlyArray1<'_, f32>,
|
||||
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()))
|
||||
}
|
||||
|
||||
|
|
@ -253,6 +260,44 @@ fn diffusion_step<'py>(
|
|||
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]> {
|
||||
let np = obj.py().import("numpy")?;
|
||||
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"):
|
||||
assert "reason" in diffusion
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -90,7 +93,10 @@ def test_backend_status_present_and_python_fallback(
|
|||
report = run_benchmark(**fast_bench_kwargs)
|
||||
status = report["backend_status"]
|
||||
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["activation_hint"]
|
||||
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)
|
||||
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 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"])
|
||||
|
|
@ -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
|
||||
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)"
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -54,9 +54,10 @@ def test_rust_versor_apply_matches_python_for_identity():
|
|||
identity[0] = 1.0
|
||||
F = _positive_unit_reflector(42)
|
||||
|
||||
F32 = np.asarray(F, dtype=np.float32)
|
||||
py_result = python_versor_apply(identity, F)
|
||||
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), (
|
||||
|
|
@ -70,9 +71,11 @@ def test_rust_versor_apply_matches_python_for_rotors():
|
|||
V = _random_rotor(seed)
|
||||
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)
|
||||
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), (
|
||||
|
|
@ -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))
|
||||
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(
|
||||
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)
|
||||
rust_is_null = is_null(rust_result)
|
||||
|
|
@ -101,11 +105,11 @@ def test_rust_versor_apply_preserves_null_vectors():
|
|||
@skip_no_rust
|
||||
def test_rust_versor_apply_preserves_versor_condition():
|
||||
for seed in range(20):
|
||||
V = _positive_unit_reflector(seed)
|
||||
F = _positive_unit_reflector(seed + 1000)
|
||||
V32 = np.asarray(_positive_unit_reflector(seed), dtype=np.float32)
|
||||
F32 = np.asarray(_positive_unit_reflector(seed + 1000), dtype=np.float32)
|
||||
|
||||
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)
|
||||
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