feat(third-door): #21 trajectory invariants + ADR-DAG embedding (Python)
Some checks failed
smoke / smoke (-m "not quarantine") (pull_request) Successful in 5m54s
lane-shas / verify pinned lane SHAs (pull_request) Failing after 44m23s

Geometry-first Python authorities for the last Third-Door blueprint gaps:
- trajectory_invariants: relative holonomy, divergence integral, energy
  boundary, zero-fabrication refusals (R&D §2.2)
- core/adr/validator: SHA-256→bivector Ψ(M), simple project, master blade,
  proposal drift (R&D §2.4); not a parallel GeometricDelta ABI validator
Ledger rows 7–8 green; optional Rust Ring-1 port deferred.
This commit is contained in:
Shay 2026-07-13 21:51:42 -07:00
parent 9f5c8c222f
commit db924550fc
7 changed files with 584 additions and 7 deletions

23
core/adr/__init__.py Normal file
View file

@ -0,0 +1,23 @@
"""ADR-DAG conformal embedding (R&D-Revised §2.4 / issue #21).
Deterministic embedding of ADR markdown into Cl(4,1) bivector space and
master-blade drift checks for proposal coherence.
"""
from core.adr.validator import (
AdrDagValidationError,
embed_adr_markdown,
master_architecture_blade,
proposal_drift,
simple_bivector_project,
validate_proposal_against_master,
)
__all__ = [
"AdrDagValidationError",
"embed_adr_markdown",
"master_architecture_blade",
"proposal_drift",
"simple_bivector_project",
"validate_proposal_against_master",
]

168
core/adr/validator.py Normal file
View file

@ -0,0 +1,168 @@
"""
core/adr/validator.py
ADR-DAG conformal embedding Ψ(M) (R&D-Revised §2.4 / #21).
SHA-256(M) 10×3-byte segments c_k [1, 1]
10 basis bivectors (planes 6..15) simple-bivector projection
master blade = successive wedge of load-bearing ADR embeddings
proposal drift = B_p A_master
Cross-check: does **not** reimplement GeometricDelta ABI validation
(``core/abi/geometric_delta_validator.py``). This module embeds ADR text into
geometry; that module validates GeometricDelta envelopes.
"""
from __future__ import annotations
import hashlib
from typing import Sequence
import numpy as np
from algebra.cl41 import N_COMPONENTS, geometric_product, grade_project
_BIVECTOR_PLANES = tuple(range(6, 16)) # 10 planes
_NEAR_ZERO = 1e-12
_SIMPLE_G4_TOL = 1e-9
class AdrDagValidationError(ValueError):
"""Fail-closed refusal from ADR-DAG embedding / drift checks."""
def __init__(self, reason: str, **disclosure) -> None:
self.reason = reason
self.disclosure = dict(disclosure)
super().__init__(f"adr_dag refused [{reason}]: {self.disclosure}")
def _grade_mass(v: np.ndarray) -> int:
for g in range(5, -1, -1):
if float(np.linalg.norm(grade_project(v, g))) > _NEAR_ZERO:
return g
return 0
def multivector_wedge(A: np.ndarray, B: np.ndarray) -> np.ndarray:
"""Grade-raising wedge approximation: grade-project of geometric product.
For pure blades this matches the outer product grade. Used for master-blade
assembly and drift (B_p A_master).
"""
a = np.asarray(A, dtype=np.float64)
b = np.asarray(B, dtype=np.float64)
ga, gb = _grade_mass(a), _grade_mass(b)
target = min(5, ga + gb)
if target == 0:
return grade_project(geometric_product(a, b), 0)
return grade_project(geometric_product(a, b), target).astype(np.float64)
def simple_bivector_project(B: np.ndarray) -> np.ndarray:
"""Project a multivector generator onto a simple bivector.
Spec intent: pure grade-2 support that is *simple* (single plane). Pure
multiplane grade-2 has nontrivial ``B B``; we always collapse to the
dominant plane when more than one plane is occupied (deterministic).
"""
arr = np.asarray(B, dtype=np.float64)
if arr.shape != (N_COMPONENTS,):
raise AdrDagValidationError("bad_shape", shape=tuple(arr.shape))
B2 = grade_project(arr, 2).astype(np.float64)
occupied = [i for i in _BIVECTOR_PLANES if abs(float(B2[i])) > _NEAR_ZERO]
if len(occupied) <= 1:
return B2
# Multiplane → dominant-plane collapse (simple by construction).
best_i = max(occupied, key=lambda i: abs(float(B2[i])))
out = np.zeros(N_COMPONENTS, dtype=np.float64)
out[best_i] = float(B2[best_i])
return out
def embed_adr_markdown(markdown: str) -> np.ndarray:
"""Ψ(M): deterministic SHA-256 → 10 bivector coefficients → simple project.
Identical markdown identical 32-vector (replay pin).
"""
if not isinstance(markdown, str):
raise AdrDagValidationError("not_str", type=type(markdown).__name__)
# Empty markdown is a valid document identity (still deterministic).
digest = hashlib.sha256(markdown.encode("utf-8")).digest() # 32 bytes
B = np.zeros(N_COMPONENTS, dtype=np.float64)
for k, plane in enumerate(_BIVECTOR_PLANES):
# 3-byte segments; last 2 hash bytes unused (spec: 10×3=30).
chunk = digest[k * 3 : k * 3 + 3]
u = int.from_bytes(chunk, "big") # 0 .. 2^24-1
c = (u / float(0xFFFFFF)) * 2.0 - 1.0 # [-1, 1]
B[plane] = c
return simple_bivector_project(B)
def master_architecture_blade(
embeddings: Sequence[np.ndarray],
) -> np.ndarray:
"""Assemble load-bearing ADR embeddings into a master architecture blade.
Prefer successive wedge when non-degenerate; if a wedge step vanishes
(parallel simple planes after projection), fall back to algebraic sum of
simple bivectors so the master never fabricates a zero blade.
"""
if not embeddings:
raise AdrDagValidationError("empty_master_set")
simples = [
simple_bivector_project(np.asarray(e, dtype=np.float64))
for e in embeddings
]
wedge = simples[0].copy()
for i, e in enumerate(simples[1:], start=1):
w = multivector_wedge(wedge, e)
if float(np.linalg.norm(w)) > _NEAR_ZERO:
wedge = w
# else: parallel/collinear under wedge — keep prior wedge, continue
if float(np.linalg.norm(wedge)) > _NEAR_ZERO:
return wedge.astype(np.float64)
# Full wedge chain degenerate: superposition master (still deterministic).
acc = np.zeros(N_COMPONENTS, dtype=np.float64)
for e in simples:
acc = acc + e
if float(np.linalg.norm(acc)) < _NEAR_ZERO:
raise AdrDagValidationError("degenerate_master_blade", at_index=0)
return simple_bivector_project(acc)
def proposal_drift(B_proposal: np.ndarray, A_master: np.ndarray) -> float:
"""Drift = ‖B_p ∧ A_master‖ (Euclidean coeff norm of the wedge)."""
Bp = simple_bivector_project(np.asarray(B_proposal, dtype=np.float64))
Am = np.asarray(A_master, dtype=np.float64)
if Am.shape != (N_COMPONENTS,):
raise AdrDagValidationError("bad_master_shape", shape=tuple(Am.shape))
w = multivector_wedge(Bp, Am)
return float(np.linalg.norm(w))
def validate_proposal_against_master(
proposal_markdown: str,
master_markdowns: Sequence[str],
*,
max_drift: float = 1.0,
) -> tuple[bool, float, np.ndarray, np.ndarray]:
"""Embed proposal + masters; return (ok, drift, B_p, A_master)."""
if not master_markdowns:
raise AdrDagValidationError("empty_master_set")
masters = [embed_adr_markdown(m) for m in master_markdowns]
A = master_architecture_blade(masters)
Bp = embed_adr_markdown(proposal_markdown)
d = proposal_drift(Bp, A)
ok = bool(d <= float(max_drift) + _NEAR_ZERO)
return ok, d, Bp, A
__all__ = [
"AdrDagValidationError",
"embed_adr_markdown",
"master_architecture_blade",
"multivector_wedge",
"proposal_drift",
"simple_bivector_project",
"validate_proposal_against_master",
]

View file

@ -69,6 +69,14 @@ from core.physics.temporal_gate import (
)
from core.physics.self_authorship import AuthorshipProposal, SelfAuthorshipMiner
from core.physics.wave_manifold import WaveManifold
from core.physics.trajectory_invariants import (
TrajectoryAssessment,
TrajectoryInvariantError,
assess_trajectory,
energy_boundary_ok,
relative_holonomy,
trajectory_divergence,
)
__all__ = [
"SalienceOperator", "SalienceMap", "FieldRegion",
@ -99,4 +107,7 @@ __all__ = [
"TemporalDecision", "TemporalVerdict",
"AuthorshipProposal", "SelfAuthorshipMiner",
"WaveManifold",
"TrajectoryAssessment", "TrajectoryInvariantError",
"assess_trajectory", "energy_boundary_ok",
"relative_holonomy", "trajectory_divergence",
]

View file

@ -0,0 +1,172 @@
"""
core/physics/trajectory_invariants.py
Continuous-space trajectory invariants + zero-fabrication (R&D-Revised §2.2 / #21).
Python geometry-first surface (algebra/*). A Ring-1 Rust port may later mirror
this contract under ``core-rs``; this module is the behavioral source of truth.
Invariants:
* Relative holonomy H(t) = V_i · reverse(V_{i+1})
* Divergence D = Σ log(1 + H·reverse(H) 1_F) · Δt (discrete integral)
* Replay bound D < ε_trajectory
* Hamiltonian energy boundary E_exertion κ · E_sensory
* Zero-fabrication: refuse empty / non-closed trajectory steps
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import Sequence
import numpy as np
from algebra.cl41 import N_COMPONENTS, geometric_product, reverse
from algebra.versor import versor_condition, versor_unit_residual
_CLOSURE_TOL = 1e-6
_DEFAULT_EPS_TRAJECTORY = 1e-5
_NEAR_ZERO = 1e-15
class TrajectoryInvariantError(ValueError):
"""Fail-closed refusal from trajectory invariant checks."""
def __init__(self, reason: str, **disclosure) -> None:
self.reason = reason
self.disclosure = dict(disclosure)
super().__init__(f"trajectory_invariant refused [{reason}]: {self.disclosure}")
def _as_versor(V: np.ndarray, name: str) -> np.ndarray:
arr = np.asarray(V, dtype=np.float64)
if arr.shape != (N_COMPONENTS,):
raise TrajectoryInvariantError(
"bad_shape", name=name, shape=tuple(arr.shape)
)
cond = float(versor_condition(arr))
if cond >= _CLOSURE_TOL:
raise TrajectoryInvariantError(
"not_closed", name=name, versor_condition=cond
)
return arr
def relative_holonomy(V1: np.ndarray, V2: np.ndarray) -> np.ndarray:
"""H = V1 · reverse(V2) — relative transport between consecutive steps."""
a = _as_versor(V1, "V1")
b = _as_versor(V2, "V2")
return geometric_product(a, reverse(b)).astype(np.float64)
def holonomy_unit_residual(H: np.ndarray) -> float:
"""‖H · reverse(H) 1‖_F (dual-checked via versor_unit_residual)."""
H_arr = np.asarray(H, dtype=np.float64)
if H_arr.shape != (N_COMPONENTS,):
raise TrajectoryInvariantError("bad_shape", name="H", shape=tuple(H_arr.shape))
r = float(versor_unit_residual(H_arr))
r_rev = float(versor_unit_residual(reverse(H_arr)))
return max(r, r_rev)
def trajectory_divergence(
versors: Sequence[np.ndarray],
*,
dt: float = 1.0,
) -> float:
"""Discrete divergence integral D = Σ log(1 + residual(H_i)) · Δt.
Zero-fabrication: empty or single-step trajectories refused (no confabulated
path). Each step must be a closed unit versor.
"""
if not versors:
raise TrajectoryInvariantError("empty_trajectory")
if len(versors) < 2:
raise TrajectoryInvariantError(
"trajectory_too_short", n=len(versors)
)
if float(dt) <= 0.0:
raise TrajectoryInvariantError("non_positive_dt", dt=float(dt))
closed = [_as_versor(v, f"versors[{i}]") for i, v in enumerate(versors)]
D = 0.0
for i in range(len(closed) - 1):
H = relative_holonomy(closed[i], closed[i + 1])
r = holonomy_unit_residual(H)
D += math.log1p(max(r, 0.0)) * float(dt)
return float(D)
def energy_boundary_ok(
E_exertion: float,
E_sensory: float,
*,
kappa: float = 1.0,
) -> bool:
"""Hamiltonian energy boundary: E_exertion ≤ κ · E_sensory.
Refuse negative energies (zero-fabrication of free action).
"""
ee = float(E_exertion)
es = float(E_sensory)
k = float(kappa)
if ee < -_NEAR_ZERO or es < -_NEAR_ZERO:
raise TrajectoryInvariantError(
"negative_energy", E_exertion=ee, E_sensory=es
)
if k < 0.0:
raise TrajectoryInvariantError("negative_kappa", kappa=k)
return ee <= k * es + _NEAR_ZERO
@dataclass(frozen=True, slots=True)
class TrajectoryAssessment:
"""Result of assessing a finite trajectory against #21 invariants."""
divergence: float
within_replay_bound: bool
energy_ok: bool
n_steps: int
eps_trajectory: float
kappa: float
E_exertion: float
E_sensory: float
def assess_trajectory(
versors: Sequence[np.ndarray],
*,
E_exertion: float,
E_sensory: float,
eps_trajectory: float = _DEFAULT_EPS_TRAJECTORY,
kappa: float = 1.0,
dt: float = 1.0,
) -> TrajectoryAssessment:
"""Full trajectory gate: divergence bound + energy boundary."""
D = trajectory_divergence(versors, dt=dt)
eps = float(eps_trajectory)
if eps <= 0.0:
raise TrajectoryInvariantError("non_positive_eps", eps_trajectory=eps)
e_ok = energy_boundary_ok(E_exertion, E_sensory, kappa=kappa)
return TrajectoryAssessment(
divergence=float(D),
within_replay_bound=bool(D < eps),
energy_ok=bool(e_ok),
n_steps=len(versors),
eps_trajectory=eps,
kappa=float(kappa),
E_exertion=float(E_exertion),
E_sensory=float(E_sensory),
)
__all__ = [
"TrajectoryAssessment",
"TrajectoryInvariantError",
"assess_trajectory",
"energy_boundary_ok",
"holonomy_unit_residual",
"relative_holonomy",
"trajectory_divergence",
]

View file

@ -35,8 +35,8 @@
| 4 | GoldTether residual + α law | Super §2.3, R&D §2.3/§5 | 🟢 residual+α + bootstrap gates + principal-axis prune (#24+#18) | #18 |
| 5 | Grade-5 pseudoscalar invariant | Super §3.3 | ⚪ RETIRED — vacuous in odd-dim Cl(4,1) | #19 (closed) |
| 6 | Surprise residual operator | Super §3.2 | 🟢 math + DiscoveryCandidate wiring landed (#26 + #31) | #20 |
| 7 | Trajectory invariants + zero-fabrication | R&D §2.2 | ⚫ absent | #21 |
| 8 | ADR-DAG conformal embedding | R&D §2.4 | ⚫ absent | #21 |
| 7 | Trajectory invariants + zero-fabrication | R&D §2.2 | 🟢 Python geometry surface (`trajectory_invariants.py`) | #21 |
| 8 | ADR-DAG conformal embedding | R&D §2.4 | 🟢 Python surface (`core/adr/validator.py`) | #21 |
| W1 | WaveManifold unitary / sandwich step | ADR-0241 §2 | 🟢 | ADR-0241 |
| W2 | Spectral leakage surprise | ADR-0241 §2.4B | 🟢 subsumed into `surprise_residual` | ADR-0241 |
| W3 | Wave polar + multi-pair conjugacy | ADR-0241 §2.4A | 🟢 single polar + multi-pair thin wrap | ADR-0241 |
@ -206,10 +206,23 @@ The null add-on is **untested**: `test_signature_aware_pca_keeps_nulls` classifi
---
## 9. Absent whole proposals — ⚫ (#21)
## 9. Trajectory invariants + ADR-DAG — 🟢 Python surfaces (#21)
- **Trajectory invariants + zero-fabrication (R&D §2.2 → `core-rs/src/sensorimotor.rs`):** relative holonomy `H(t)=V₁Ṽ₂`, divergence integral `D < ε_trajectory`, Hamiltonian energy boundary `E_exertion ≤ κ·E_sensory`. Not landed. Gated by the Zig/Rust substrate doctrine (Ring-1 only).
- **ADR-DAG conformal embedding (R&D §2.4 → `core/adr/validator.py`):** `Ψ(M)` = SHA-256 → 10 bivector coeffs → simple-bivector projection → master-blade wedge → drift check. Not landed. Cross-check `core/abi/geometric_delta_validator.py` before adding a parallel validator.
> **Landed (2026-07-14):** geometry-first Python authorities. Rust Ring-1 port remains optional acceleration, not a second truth.
### Trajectory invariants (R&D §2.2 → `core/physics/trajectory_invariants.py`)
- Relative holonomy `H = V_i · reverse(V_{i+1})`
- Divergence `D = Σ log(1 + ‖H reverse(H) 1‖) · Δt`; bound `D < ε_trajectory`
- Energy boundary `E_exertion ≤ κ · E_sensory`
- Zero-fabrication: empty / short / non-closed steps refused
- Tests: `tests/test_third_door_trajectory_invariants.py`
### ADR-DAG conformal embedding (R&D §2.4 → `core/adr/validator.py`)
- `Ψ(M)`: SHA-256 → 10×3-byte → `c_k ∈ [1,1]` → planes 6..15 → simple-bivector project
- Master blade = successive wedge of load-bearing ADR embeddings
- Proposal drift = `‖B_p ∧ A_master‖`
- Does **not** parallel `core/abi/geometric_delta_validator.py` (that validates GeometricDelta ABI envelopes)
- Tests: `tests/test_third_door_adr_dag_embedding.py`
---
@ -297,7 +310,7 @@ PY
- Durable holographic memory **vault store** (CRDT-backed standing-wave spectrum) — session registry only today.
- Rust/MLX acceleration of exp-map / cross-spectral (ADR-0235 later).
- Full ADR-0092 reviewer-service integration (promote remains caller-gated).
- R&D #21 trajectory invariants + ADR-DAG embedding.
- Optional Rust Ring-1 port of trajectory invariants (Python is authority today).
---
@ -310,7 +323,7 @@ PY
| GoldTether gold-set + harmonized residual + α=Φ(R) + bootstrap/prune — 🟢 | #18 |
| Grade-5 pseudoscalar preservation gate — ⚪ RETIRED (vacuous; see §5) | #19 (closed) |
| Surprise: metric projection + productivity polarity + DiscoveryCandidate wiring — 🟢 done | #20 (math #26; wiring #31) |
| Absent proposals: sensorimotor + ADR-DAG | #21 |
| Trajectory invariants + ADR-DAG embedding — 🟢 Python surfaces | #21 |
| Wave-field substrate + operator subsumption (W1W6) — 🟢 on branch | ADR-0241 |
| `core_ha` deprecation — 🟢 no live tree + hygiene pin | ADR-0241 / deprecation plan |
| Durable holographic vault spectrum — deferred | ADR-0241 follow-on |

View file

@ -0,0 +1,98 @@
"""#21 B — ADR-DAG conformal embedding Ψ(M) (R&D §2.4)."""
from __future__ import annotations
import numpy as np
import pytest
from algebra.cl41 import grade_project
from core.adr.validator import (
AdrDagValidationError,
embed_adr_markdown,
master_architecture_blade,
proposal_drift,
simple_bivector_project,
validate_proposal_against_master,
)
def test_embed_deterministic_replay():
m = "# ADR-TEST\n\nBody of the decision.\n"
a = embed_adr_markdown(m)
b = embed_adr_markdown(m)
assert np.array_equal(a, b)
assert a.shape == (32,)
def test_embed_differs_for_different_markdown():
a = embed_adr_markdown("# ADR-A\nfoo")
b = embed_adr_markdown("# ADR-B\nbar")
assert not np.allclose(a, b)
def test_embed_is_grade2_supported():
B = embed_adr_markdown("# ADR-G2\ncontent")
# After simple project: only grade-2 (and zeros elsewhere)
for g in (0, 1, 3, 4, 5):
assert float(np.linalg.norm(grade_project(B, g))) < 1e-12
assert float(np.linalg.norm(grade_project(B, 2))) > 0.0
def test_simple_bivector_project_collapses_multiplane():
B = np.zeros(32, dtype=np.float64)
B[6] = 0.9
B[7] = 0.8
B[8] = 0.1
S = simple_bivector_project(B)
# Dominant plane kept
assert abs(S[6] - 0.9) < 1e-12 or abs(S[7] - 0.8) < 1e-12
# At most one plane nonzero after collapse when multiplane non-simple
n_planes = sum(1 for i in range(6, 16) if abs(S[i]) > 1e-12)
assert n_planes == 1
def test_master_blade_refuses_empty():
with pytest.raises(AdrDagValidationError, match="empty"):
master_architecture_blade([])
def test_master_blade_from_two_adrs():
e1 = embed_adr_markdown("# ADR-0003\nCoordinate dissolution.")
e2 = embed_adr_markdown("# ADR-0006\nField energy.")
A = master_architecture_blade([e1, e2])
assert A.shape == (32,)
assert float(np.linalg.norm(A)) > 0.0
def test_proposal_drift_nonneg_and_deterministic():
masters = [
embed_adr_markdown("# M1\none"),
embed_adr_markdown("# M2\ntwo"),
]
A = master_architecture_blade(masters)
Bp = embed_adr_markdown("# Proposal\nnew idea")
d1 = proposal_drift(Bp, A)
d2 = proposal_drift(Bp, A)
assert d1 == d2
assert d1 >= 0.0
def test_validate_proposal_returns_ok_flag():
masters_md = ["# ADR-A\nalpha", "# ADR-B\nbeta"]
ok, drift, Bp, A = validate_proposal_against_master(
"# Proposal\ngamma", masters_md, max_drift=1e9
)
assert ok is True
assert drift >= 0.0
assert Bp.shape == (32,)
assert A.shape == (32,)
def test_validate_proposal_tight_max_drift_can_fail():
masters_md = ["# ADR-A\nalpha", "# ADR-B\nbeta"]
ok, drift, _Bp, _A = validate_proposal_against_master(
"# Proposal\nentirely different body xyz", masters_md, max_drift=-1.0
)
# max_drift < 0 forces fail unless drift is negative (impossible)
assert ok is False
assert drift >= 0.0

View file

@ -0,0 +1,92 @@
"""#21 A — trajectory invariants + zero-fabrication (R&D §2.2)."""
from __future__ import annotations
import numpy as np
import pytest
from algebra.rotor import make_rotor_from_angle
from algebra.versor import versor_condition
from core.physics.trajectory_invariants import (
TrajectoryInvariantError,
assess_trajectory,
energy_boundary_ok,
relative_holonomy,
trajectory_divergence,
)
def _id() -> np.ndarray:
v = np.zeros(32, dtype=np.float64)
v[0] = 1.0
return v
def test_relative_holonomy_identity_pair_is_identity():
H = relative_holonomy(_id(), _id())
assert versor_condition(H) < 1e-6
assert abs(H[0] - 1.0) < 1e-9
def test_relative_holonomy_order_sensitive():
A = make_rotor_from_angle(0.3, bivector_idx=6)
B = make_rotor_from_angle(0.9, bivector_idx=7)
H_ab = relative_holonomy(A, B)
H_ba = relative_holonomy(B, A)
assert not np.allclose(H_ab, H_ba, atol=1e-9)
def test_trajectory_divergence_zero_on_constant_path():
path = [_id(), _id(), _id()]
assert trajectory_divergence(path) == 0.0
def test_trajectory_divergence_positive_on_moving_path():
path = [
make_rotor_from_angle(0.1 * i, bivector_idx=6) for i in range(1, 6)
]
D = trajectory_divergence(path)
assert D > 0.0
def test_trajectory_divergence_deterministic():
path = [make_rotor_from_angle(0.2 * i) for i in range(1, 4)]
assert trajectory_divergence(path) == trajectory_divergence(path)
def test_trajectory_divergence_refuses_empty_and_short():
with pytest.raises(TrajectoryInvariantError, match="empty"):
trajectory_divergence([])
with pytest.raises(TrajectoryInvariantError, match="too_short"):
trajectory_divergence([_id()])
def test_trajectory_divergence_refuses_non_closed():
dirty = np.zeros(32, dtype=np.float64)
dirty[0] = 0.5
dirty[1] = 0.5
with pytest.raises(TrajectoryInvariantError, match="not_closed"):
trajectory_divergence([_id(), dirty])
def test_energy_boundary_ok_and_refuse_negative():
assert energy_boundary_ok(1.0, 2.0, kappa=1.0) is True
assert energy_boundary_ok(3.0, 2.0, kappa=1.0) is False
assert energy_boundary_ok(3.0, 2.0, kappa=2.0) is True
with pytest.raises(TrajectoryInvariantError, match="negative_energy"):
energy_boundary_ok(-0.1, 1.0)
def test_assess_trajectory_replay_bound():
path = [make_rotor_from_angle(0.05 * i) for i in range(1, 4)]
a = assess_trajectory(
path, E_exertion=0.5, E_sensory=1.0, eps_trajectory=1.0, kappa=1.0
)
assert a.energy_ok is True
assert a.n_steps == 3
assert a.divergence >= 0.0
# Tight eps → may fail replay bound on moving path
tight = assess_trajectory(
path, E_exertion=0.5, E_sensory=1.0, eps_trajectory=1e-30, kappa=1.0
)
assert tight.within_replay_bound is False