fix(algebra): P11a physics hot paths via algebra.backend (Rust-ready)
Stop wave/Third-Door physics from bypassing native dispatch: - Route geometric_product / versor_apply / versor_condition / cga_inner through algebra.backend in wave_manifold, goldtether, trajectory, dynamic_manifold, surprise, holographic_vault, atlas_packing, biography, self_authorship. - Backend: dtype-aware Rust use — f32 workloads use core_rs; f64 wave residual pins keep Python SOT until f64 GP parity exists. Coerce arrays for PyO3 bindings; fail soft to Python. - AST hygiene pin: tests/test_physics_backend_dispatch_hygiene.py - Docs: RUST.md, runtime_contracts, fidelity (ADR-0235 / UMA hygiene). Verified: wave + cohesion suites green default and CORE_BACKEND=rust (with core_rs built). MLX still exploratory off-serve.
This commit is contained in:
parent
db6430ed4e
commit
44f7258b16
14 changed files with 208 additions and 25 deletions
|
|
@ -48,9 +48,36 @@ def _build_cga_inner_metric() -> np.ndarray:
|
||||||
_CGA_INNER_METRIC: np.ndarray = _build_cga_inner_metric()
|
_CGA_INNER_METRIC: np.ndarray = _build_cga_inner_metric()
|
||||||
|
|
||||||
|
|
||||||
|
def _f32_1d32(x: np.ndarray) -> np.ndarray:
|
||||||
|
"""Contiguous f32 (32,) for core_rs PyReadonlyArray1 bindings."""
|
||||||
|
return np.ascontiguousarray(
|
||||||
|
np.asarray(x, dtype=np.float32).reshape(-1)[:32], dtype=np.float32
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_f32_workload(*arrays: np.ndarray) -> bool:
|
||||||
|
"""True when all arrays are float32 (Rust f32 kernel is parity-safe).
|
||||||
|
|
||||||
|
float64 wave residual pins require Python SOT (or future f64 Rust GP).
|
||||||
|
Forcing f64→f32 would break 1e-9 chiral / leakage pins (ADR-0241).
|
||||||
|
"""
|
||||||
|
return all(np.asarray(a).dtype == np.float32 for a in arrays)
|
||||||
|
|
||||||
|
|
||||||
def geometric_product(A: np.ndarray, B: np.ndarray) -> np.ndarray:
|
def geometric_product(A: np.ndarray, B: np.ndarray) -> np.ndarray:
|
||||||
if _RUST:
|
"""Cl(4,1) geometric product via Rust f32 when enabled, else Python.
|
||||||
return np.asarray(_rs.geometric_product(A, B), dtype=np.float32)
|
|
||||||
|
float64 inputs always use the pure-Python product (semantic SOT for
|
||||||
|
wave-field residual math). float32 field-graph workloads get Rust.
|
||||||
|
"""
|
||||||
|
if _RUST and _is_f32_workload(A, B):
|
||||||
|
try:
|
||||||
|
return np.asarray(
|
||||||
|
_rs.geometric_product(_f32_1d32(A), _f32_1d32(B)),
|
||||||
|
dtype=np.float32,
|
||||||
|
)
|
||||||
|
except (AttributeError, TypeError, ValueError, Exception):
|
||||||
|
pass
|
||||||
from algebra.cl41 import geometric_product as _gp
|
from algebra.cl41 import geometric_product as _gp
|
||||||
return _gp(A, B)
|
return _gp(A, B)
|
||||||
|
|
||||||
|
|
@ -67,25 +94,34 @@ def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
if _RUST:
|
if _RUST:
|
||||||
try:
|
try:
|
||||||
Vc = np.ascontiguousarray(V, dtype=np.float64)
|
Vc = np.ascontiguousarray(V, dtype=np.float64).reshape(-1)[:32]
|
||||||
Fc = np.ascontiguousarray(F, dtype=np.float64)
|
Fc = np.ascontiguousarray(F, dtype=np.float64).reshape(-1)[:32]
|
||||||
return np.asarray(_rs.versor_apply_with_closure_f64(Vc, Fc), dtype=np.float64)
|
return np.asarray(
|
||||||
except (AttributeError, Exception):
|
_rs.versor_apply_with_closure_f64(Vc, Fc), dtype=np.float64
|
||||||
|
)
|
||||||
|
except (AttributeError, TypeError, ValueError, Exception):
|
||||||
pass
|
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)
|
||||||
|
|
||||||
|
|
||||||
def versor_condition(F: np.ndarray) -> float:
|
def versor_condition(F: np.ndarray) -> float:
|
||||||
if _RUST:
|
"""Versor residual. Rust f32 path only for float32 inputs (see GP note)."""
|
||||||
return float(_rs.versor_condition(F))
|
if _RUST and _is_f32_workload(F):
|
||||||
|
try:
|
||||||
|
return float(_rs.versor_condition(_f32_1d32(F)))
|
||||||
|
except (AttributeError, TypeError, ValueError, Exception):
|
||||||
|
pass
|
||||||
from algebra.versor import versor_condition as _vc
|
from algebra.versor import versor_condition as _vc
|
||||||
return _vc(F)
|
return _vc(F)
|
||||||
|
|
||||||
|
|
||||||
def cga_inner(X: np.ndarray, Y: np.ndarray) -> float:
|
def cga_inner(X: np.ndarray, Y: np.ndarray) -> float:
|
||||||
if _RUST:
|
if _RUST and _is_f32_workload(X, Y):
|
||||||
return float(_rs.cga_inner(X, Y))
|
try:
|
||||||
|
return float(_rs.cga_inner(_f32_1d32(X), _f32_1d32(Y)))
|
||||||
|
except (AttributeError, TypeError, ValueError, Exception):
|
||||||
|
pass
|
||||||
from algebra.cga import cga_inner as _ci
|
from algebra.cga import cga_inner as _ci
|
||||||
return _ci(X, Y)
|
return _ci(X, Y)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ from typing import Sequence
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from algebra.cga import cga_inner, embed_point
|
from algebra.backend import cga_inner
|
||||||
|
from algebra.cga import embed_point
|
||||||
from core.physics.wave_manifold import WaveManifold
|
from core.physics.wave_manifold import WaveManifold
|
||||||
|
|
||||||
PHI = (1.0 + math.sqrt(5.0)) / 2.0
|
PHI = (1.0 + math.sqrt(5.0)) / 2.0
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,10 @@ from typing import Any, Sequence
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from algebra.backend import versor_condition
|
||||||
from algebra.cl41 import N_COMPONENTS
|
from algebra.cl41 import N_COMPONENTS
|
||||||
from algebra.holonomy import holonomy_encode, holonomy_similarity
|
from algebra.holonomy import holonomy_encode, holonomy_similarity
|
||||||
from algebra.versor import unitize_versor, versor_condition
|
from algebra.versor import unitize_versor
|
||||||
from core.physics.wave_manifold import WaveManifold
|
from core.physics.wave_manifold import WaveManifold
|
||||||
|
|
||||||
_CLOSURE_TOL = 1e-6
|
_CLOSURE_TOL = 1e-6
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,9 @@ from typing import Optional, Sequence, Tuple, Union
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from algebra.backend import geometric_product, versor_condition
|
||||||
from algebra.cga import is_null
|
from algebra.cga import is_null
|
||||||
from algebra.cl41 import N_COMPONENTS, geometric_product, grade_project, reverse
|
from algebra.cl41 import N_COMPONENTS, grade_project, reverse
|
||||||
from algebra.null_point import (
|
from algebra.null_point import (
|
||||||
NullPointRecoveryError,
|
NullPointRecoveryError,
|
||||||
dilator,
|
dilator,
|
||||||
|
|
@ -26,7 +27,6 @@ from algebra.null_point import (
|
||||||
translator,
|
translator,
|
||||||
)
|
)
|
||||||
from algebra.rotor import rotor_power, word_transition_rotor
|
from algebra.rotor import rotor_power, word_transition_rotor
|
||||||
from algebra.versor import versor_condition
|
|
||||||
|
|
||||||
_CLOSURE_TOL = 1e-6
|
_CLOSURE_TOL = 1e-6
|
||||||
_NEAR_ZERO = 1e-12
|
_NEAR_ZERO = 1e-12
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,10 @@ from typing import Any, Literal, Optional, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from algebra.cl41 import N_COMPONENTS, geometric_product, reverse
|
from algebra.backend import geometric_product, versor_condition
|
||||||
|
from algebra.cl41 import N_COMPONENTS, reverse
|
||||||
from algebra.rotor import rotor_power, word_transition_rotor
|
from algebra.rotor import rotor_power, word_transition_rotor
|
||||||
from algebra.versor import versor_condition, versor_unit_residual
|
from algebra.versor import versor_unit_residual
|
||||||
from core.physics.wave_manifold import WaveManifold
|
from core.physics.wave_manifold import WaveManifold
|
||||||
|
|
||||||
_CLOSURE_TOL = 1e-6
|
_CLOSURE_TOL = 1e-6
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ from typing import Any, Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from algebra.backend import versor_condition
|
||||||
from algebra.cl41 import N_COMPONENTS
|
from algebra.cl41 import N_COMPONENTS
|
||||||
from algebra.versor import versor_condition
|
|
||||||
from core.physics.wave_manifold import WaveManifold
|
from core.physics.wave_manifold import WaveManifold
|
||||||
from teaching.epistemic import EpistemicStatus
|
from teaching.epistemic import EpistemicStatus
|
||||||
from vault.store import VaultStore, _parse_entry_status, _status_admits
|
from vault.store import VaultStore, _parse_entry_status, _status_admits
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ from typing import Any, Mapping, Sequence
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from algebra.backend import versor_condition
|
||||||
from algebra.cl41 import N_COMPONENTS
|
from algebra.cl41 import N_COMPONENTS
|
||||||
from algebra.versor import versor_condition
|
|
||||||
from core.physics.dynamic_manifold import conformal_procrustes
|
from core.physics.dynamic_manifold import conformal_procrustes
|
||||||
from core.physics.goldtether import GoldTetherMonitor, coherence_residual
|
from core.physics.goldtether import GoldTetherMonitor, coherence_residual
|
||||||
from core.physics.surprise import dual_procrustes_surprise, surprise_residual
|
from core.physics.surprise import dual_procrustes_surprise, surprise_residual
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,8 @@ from typing import Optional, Sequence, Tuple, Union
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from algebra.cga import cga_inner
|
from algebra.backend import cga_inner, versor_condition
|
||||||
from algebra.cl41 import N_COMPONENTS, grade_project
|
from algebra.cl41 import N_COMPONENTS, grade_project
|
||||||
from algebra.versor import versor_condition
|
|
||||||
from core.physics.dynamic_manifold import conformal_procrustes
|
from core.physics.dynamic_manifold import conformal_procrustes
|
||||||
from core.physics.wave_manifold import WaveManifold, WaveSpectralLeakageError
|
from core.physics.wave_manifold import WaveManifold, WaveSpectralLeakageError
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,9 @@ from typing import Sequence
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from algebra.cl41 import N_COMPONENTS, geometric_product, reverse
|
from algebra.backend import geometric_product, versor_condition
|
||||||
from algebra.versor import versor_condition, versor_unit_residual
|
from algebra.cl41 import N_COMPONENTS, reverse
|
||||||
|
from algebra.versor import versor_unit_residual
|
||||||
|
|
||||||
_CLOSURE_TOL = 1e-6
|
_CLOSURE_TOL = 1e-6
|
||||||
_DEFAULT_EPS_TRAJECTORY = 1e-5
|
_DEFAULT_EPS_TRAJECTORY = 1e-5
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,12 @@ Continuous multivector wave fields ψ ∈ ℝ³² under:
|
||||||
|
|
||||||
Algebra-native only (algebra/*). No scipy-as-truth. No teaching/vault imports.
|
Algebra-native only (algebra/*). No scipy-as-truth. No teaching/vault imports.
|
||||||
Off-serving until explicit gates; dual-checked unitary residual.
|
Off-serving until explicit gates; dual-checked unitary residual.
|
||||||
|
|
||||||
|
**Backend dispatch (P11a / ADR-0235):** Cl(4,1) hot ops go through
|
||||||
|
``algebra.backend`` so ``CORE_BACKEND=rust`` can accelerate them when
|
||||||
|
``core_rs`` is built. Python remains semantic source of truth when Rust
|
||||||
|
is unset. Helpers without a Rust path (``reverse``, ``scalar_part``,
|
||||||
|
``versor_unit_residual``) stay on pure algebra modules.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -20,9 +26,14 @@ from typing import Any, Sequence, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from algebra.cga import cga_inner
|
from algebra.backend import (
|
||||||
from algebra.cl41 import N_COMPONENTS, geometric_product, reverse, scalar_part
|
cga_inner,
|
||||||
from algebra.versor import versor_apply, versor_condition, versor_unit_residual
|
geometric_product,
|
||||||
|
versor_apply,
|
||||||
|
versor_condition,
|
||||||
|
)
|
||||||
|
from algebra.cl41 import N_COMPONENTS, reverse, scalar_part
|
||||||
|
from algebra.versor import versor_unit_residual
|
||||||
|
|
||||||
_CLOSURE_TOL = 1e-6
|
_CLOSURE_TOL = 1e-6
|
||||||
_NEAR_ZERO = 1e-12
|
_NEAR_ZERO = 1e-12
|
||||||
|
|
|
||||||
26
docs/RUST.md
26
docs/RUST.md
|
|
@ -1,5 +1,31 @@
|
||||||
# Rust Extension (core-rs)
|
# Rust Extension (core-rs)
|
||||||
|
|
||||||
|
## Physics hot-path hygiene (P11a)
|
||||||
|
|
||||||
|
Third-Door / ADR-0241 physics modules must import Cl(4,1) multiplies and
|
||||||
|
closure residual helpers from **`algebra.backend`**, not directly from
|
||||||
|
`algebra.cl41` / `algebra.versor` / `algebra.cga` for:
|
||||||
|
|
||||||
|
- `geometric_product`
|
||||||
|
- `versor_apply`
|
||||||
|
- `versor_condition`
|
||||||
|
- `cga_inner`
|
||||||
|
|
||||||
|
Pinned by `tests/test_physics_backend_dispatch_hygiene.py`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default: pure Python (semantic SOT)
|
||||||
|
uv run pytest tests/test_adr_0241_wave_manifold.py -q
|
||||||
|
|
||||||
|
# Apple Silicon / native acceleration (after maturin build)
|
||||||
|
export CORE_BACKEND=rust
|
||||||
|
uv run --with maturin maturin develop --release --manifest-path core-rs/Cargo.toml
|
||||||
|
uv run python -c "from algebra.backend import using_rust; assert using_rust()"
|
||||||
|
```
|
||||||
|
|
||||||
|
MLX remains an **exploratory** UMA lane (ADR-0235); not required for serving.
|
||||||
|
|
||||||
|
|
||||||
## Why Rust
|
## Why Rust
|
||||||
|
|
||||||
The active Rust extension is an opt-in native substrate for parity-gated hot
|
The active Rust extension is an opt-in native substrate for parity-gated hot
|
||||||
|
|
|
||||||
|
|
@ -306,6 +306,7 @@ PY
|
||||||
| Fibonacci-word scheduler (V4) | 🟢 `fibonacci_word_schedule` (telemetry only) |
|
| Fibonacci-word scheduler (V4) | 🟢 `fibonacci_word_schedule` (telemetry only) |
|
||||||
| Fibonacci anyons (V5) | 🟢 quarantine package only; zero production imports |
|
| Fibonacci anyons (V5) | 🟢 quarantine package only; zero production imports |
|
||||||
| Sensorium → ψ feed (I-04) | 🟢 `sensorium_wave_feed` (fake packets + real \(\rho\)) |
|
| Sensorium → ψ feed (I-04) | 🟢 `sensorium_wave_feed` (fake packets + real \(\rho\)) |
|
||||||
|
| Physics Cl(4,1) via `algebra.backend` (P11a) | 🟢 wave/goldtether/trajectory/procrustes/surprise/vault/packing; AST pin; Rust when `CORE_BACKEND=rust` |
|
||||||
| Contemplation Trace A SPECULATIVE holographic seal (P9) | 🟢 `core/contemplation/wave_seam.py` (hypothesis vs COHERENT evidence) |
|
| Contemplation Trace A SPECULATIVE holographic seal (P9) | 🟢 `core/contemplation/wave_seam.py` (hypothesis vs COHERENT evidence) |
|
||||||
| Energy boundary + multi-scale τ (P10 Trace B) | 🟢 `wave_energy_boundary` (wave residual → energy/trajectory; τ_n=F_n·τ_0; E0–E1 crystallization) |
|
| Energy boundary + multi-scale τ (P10 Trace B) | 🟢 `wave_energy_boundary` (wave residual → energy/trajectory; τ_n=F_n·τ_0; E0–E1 crystallization) |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -919,3 +919,17 @@ Acceptance inventory: `docs/audit/adr_0241_cohesion_acceptance_checklist.md`.
|
||||||
|
|
||||||
Every wave transition still obeys `versor_condition(F) < 1e-6`. Residual breach
|
Every wave transition still obeys `versor_condition(F) < 1e-6`. Residual breach
|
||||||
is **fail-closed** — no hot-path nearest-versor drift repair.
|
is **fail-closed** — no hot-path nearest-versor drift repair.
|
||||||
|
|
||||||
|
### Algebra backend / Apple Silicon (P11a hygiene)
|
||||||
|
|
||||||
|
Cl(4,1) hot ops in physics (`geometric_product`, `versor_apply`,
|
||||||
|
`versor_condition`, `cga_inner`) must import from **`algebra.backend`**, not
|
||||||
|
direct pure-algebra modules. Pin: `tests/test_physics_backend_dispatch_hygiene.py`.
|
||||||
|
|
||||||
|
| Mode | How |
|
||||||
|
|------|-----|
|
||||||
|
| Default | Pure Python (semantic source of truth) |
|
||||||
|
| Native accel | `CORE_BACKEND=rust` + `core_rs` built (`maturin develop --release -m core-rs/Cargo.toml`) |
|
||||||
|
| float64 wave residual pins | Stay on Python product when inputs are f64 (Rust f32 GP not parity-safe for 1e-9 pins yet) |
|
||||||
|
| float32 field graphs | Rust f32 GP / residual when enabled |
|
||||||
|
| MLX / UMA | Exploratory (ADR-0235); not serving until parity gates |
|
||||||
|
|
|
||||||
92
tests/test_physics_backend_dispatch_hygiene.py
Normal file
92
tests/test_physics_backend_dispatch_hygiene.py
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
"""P11a — physics hot paths must dispatch Cl(4,1) ops via algebra.backend.
|
||||||
|
|
||||||
|
Prevents silent drift back to pure-Python-only imports for geometric_product /
|
||||||
|
versor_apply / cga_inner / versor_condition in wave-field and related modules.
|
||||||
|
Python remains default when CORE_BACKEND is unset; Rust accelerates when
|
||||||
|
CORE_BACKEND=rust and core_rs is built (ADR-0235).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
_PHYSICS = _ROOT / "core" / "physics"
|
||||||
|
|
||||||
|
# Modules that perform Cl(4,1) multiplies / residuals in the cognitive physics
|
||||||
|
# layer — must take the load-bearing ops from algebra.backend.
|
||||||
|
_BACKEND_HOT_MODULES = (
|
||||||
|
"wave_manifold.py",
|
||||||
|
"goldtether.py",
|
||||||
|
"trajectory_invariants.py",
|
||||||
|
"dynamic_manifold.py",
|
||||||
|
"surprise.py",
|
||||||
|
"holographic_vault.py",
|
||||||
|
"atlas_packing.py",
|
||||||
|
"biography.py",
|
||||||
|
"self_authorship.py",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Names that must not be imported from algebra.cl41 / algebra.versor / algebra.cga
|
||||||
|
# in those modules (use backend instead).
|
||||||
|
_BANNED_FROM_PURE = frozenset(
|
||||||
|
{
|
||||||
|
"geometric_product",
|
||||||
|
"versor_apply",
|
||||||
|
"versor_condition",
|
||||||
|
"cga_inner",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _imports_from(module: str, path: Path) -> set[str]:
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||||
|
names: set[str] = set()
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.ImportFrom) and node.module == module:
|
||||||
|
for alias in node.names:
|
||||||
|
names.add(alias.name)
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def test_hot_modules_import_backend_for_algebra_ops():
|
||||||
|
for name in _BACKEND_HOT_MODULES:
|
||||||
|
path = _PHYSICS / name
|
||||||
|
assert path.is_file(), f"missing {path}"
|
||||||
|
backend_names = _imports_from("algebra.backend", path)
|
||||||
|
# Each file should pull at least one of the dispatch ops.
|
||||||
|
assert backend_names & _BANNED_FROM_PURE, (
|
||||||
|
f"{name} must import Cl(4,1) hot ops from algebra.backend; "
|
||||||
|
f"got backend imports {sorted(backend_names)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_hot_modules_do_not_import_hot_ops_from_pure_algebra():
|
||||||
|
pure_modules = ("algebra.cl41", "algebra.versor", "algebra.cga")
|
||||||
|
for name in _BACKEND_HOT_MODULES:
|
||||||
|
path = _PHYSICS / name
|
||||||
|
for mod in pure_modules:
|
||||||
|
pure_names = _imports_from(mod, path)
|
||||||
|
offenders = pure_names & _BANNED_FROM_PURE
|
||||||
|
assert not offenders, (
|
||||||
|
f"{name} imports {sorted(offenders)} from {mod}; "
|
||||||
|
"route through algebra.backend for CORE_BACKEND=rust"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wave_manifold_uses_backend_geometric_product_and_versor_apply():
|
||||||
|
path = _PHYSICS / "wave_manifold.py"
|
||||||
|
backend = _imports_from("algebra.backend", path)
|
||||||
|
assert "geometric_product" in backend
|
||||||
|
assert "versor_apply" in backend
|
||||||
|
assert "versor_condition" in backend
|
||||||
|
assert "cga_inner" in backend
|
||||||
|
|
||||||
|
|
||||||
|
def test_backend_using_rust_api_exists():
|
||||||
|
from algebra.backend import using_rust
|
||||||
|
|
||||||
|
# Default env: Python path (no silent force of Rust).
|
||||||
|
assert using_rust() is False or using_rust() is True # bool only
|
||||||
|
assert isinstance(using_rust(), bool)
|
||||||
Loading…
Reference in a new issue