Merge pull request 'feat(third-door): GoldTether harmonized residual + α=Φ(R) constraint control (ADR-0238 §2.3, #18)' (#24) from feat/goldtether-alpha-control-gold-set into main
This commit is contained in:
commit
2118726dfe
2 changed files with 272 additions and 0 deletions
|
|
@ -27,6 +27,25 @@ _CLOSURE_TOL = 1e-6
|
|||
_NEAR_ZERO = 1e-12
|
||||
_PSEUDOSCALAR_IDX = 31
|
||||
_TELEMETRY_SCHEMA = "goldtether_coherence_v1"
|
||||
_E4_IDX = 4
|
||||
_E5_IDX = 5
|
||||
|
||||
|
||||
def _primal_gold_invariants() -> list:
|
||||
"""R&D-Revised §5 bootstrapping seeds: the identity versor and the two
|
||||
conformal null directions ``n_o = 0.5(e5-e4)`` and ``n_inf = e4+e5``.
|
||||
Coordinate-free algebraic anchors so the geometric distance term never
|
||||
degenerates to drift-only at cold start.
|
||||
"""
|
||||
ident = np.zeros(N_COMPONENTS, dtype=np.float64)
|
||||
ident[0] = 1.0
|
||||
n_o = np.zeros(N_COMPONENTS, dtype=np.float64)
|
||||
n_o[_E5_IDX] = 0.5
|
||||
n_o[_E4_IDX] = -0.5
|
||||
n_inf = np.zeros(N_COMPONENTS, dtype=np.float64)
|
||||
n_inf[_E4_IDX] = 1.0
|
||||
n_inf[_E5_IDX] = 1.0
|
||||
return [ident, n_o, n_inf]
|
||||
|
||||
|
||||
class OperatingMode(str, Enum):
|
||||
|
|
@ -102,6 +121,11 @@ class GoldTetherMonitor:
|
|||
autonomy_step: float = 0.01
|
||||
hitl_floor_threshold: float = 0.7
|
||||
hitl_autonomy_threshold: float = 0.5
|
||||
# Harmonized residual + alpha control law (ADR-0238 §2.3 / R&D-Revised §2.3).
|
||||
w_drift: float = 0.5
|
||||
r_floor: float = 0.1
|
||||
r_critical: float = 1.0
|
||||
gold_invariants: list = field(default_factory=_primal_gold_invariants, compare=False)
|
||||
|
||||
@property
|
||||
def supervised_autonomy_level(self) -> float:
|
||||
|
|
@ -158,6 +182,101 @@ class GoldTetherMonitor:
|
|||
self.floor = 0.0
|
||||
self.history.clear()
|
||||
|
||||
# --- Harmonized residual + alpha control law (ADR-0238 §2.3) ---------------
|
||||
|
||||
def goldtether_residual(self, F: np.ndarray) -> float:
|
||||
"""Scale-harmonized coherence residual (ADR-0238 §2.3):
|
||||
|
||||
R = w·(drift / ε_drift) + (1−w)·(min_{I∈𝓘_gold} ‖F−I‖_F / ‖F‖_F)
|
||||
|
||||
The algebraic drift term (normalized by the numerical floor ε_drift) and
|
||||
the geometric distance-to-gold term (normalized by ‖F‖) are each scaled to
|
||||
``[0, O(1)]`` so neither masks the other — the exact defect §2.3 exists to
|
||||
fix. This is the ALIGNMENT signal that drives the constraint weight α; the
|
||||
raw :func:`coherence_residual` stays the fail-closed *closure* gate.
|
||||
"""
|
||||
F_arr = _as_mv(F)
|
||||
drift = coherence_residual(F_arr)
|
||||
drift_term = drift / self.epsilon_drift if self.epsilon_drift > 0.0 else drift
|
||||
scale = float(np.linalg.norm(F_arr))
|
||||
if self.gold_invariants and scale > _NEAR_ZERO:
|
||||
min_dist = min(
|
||||
float(np.linalg.norm(F_arr - np.asarray(inv, dtype=np.float64)))
|
||||
for inv in self.gold_invariants
|
||||
)
|
||||
geo_term = min_dist / scale
|
||||
else:
|
||||
geo_term = 0.0
|
||||
w = float(self.w_drift)
|
||||
return float(w * drift_term + (1.0 - w) * geo_term)
|
||||
|
||||
def alpha_constraint(
|
||||
self,
|
||||
F: np.ndarray,
|
||||
*,
|
||||
mode: OperatingMode | str = OperatingMode.PRACTICE,
|
||||
) -> float:
|
||||
"""Human-constraint weight ``α ∈ [0,1]`` for the supervised transition
|
||||
surface (R&D-Revised §2.3): ``α = Φ(R_gt; r_floor, r_critical)`` — a smooth
|
||||
step of the *instantaneous* harmonized residual — composed with the
|
||||
earned-autonomy ceiling and the serve-never-autonomous rule.
|
||||
|
||||
``α = 0`` fully autonomous (trust self); ``α = 1`` full human override.
|
||||
Earned autonomy sets the FLOOR on α: the engine may never act more
|
||||
autonomously than it has earned over its trajectory, and SERVE is pinned
|
||||
to full override.
|
||||
"""
|
||||
op = OperatingMode(mode)
|
||||
if op is OperatingMode.SERVE:
|
||||
return 1.0
|
||||
r = self.goldtether_residual(F)
|
||||
lo, hi = float(self.r_floor), float(self.r_critical)
|
||||
if r <= lo:
|
||||
phi = 0.0
|
||||
elif r >= hi or hi <= lo:
|
||||
phi = 1.0
|
||||
else:
|
||||
phi = (r - lo) / (hi - lo)
|
||||
alpha_floor = 1.0 - float(self.autonomy)
|
||||
return float(min(1.0, max(phi, alpha_floor)))
|
||||
|
||||
def supervised_transition(
|
||||
self,
|
||||
v_self: np.ndarray,
|
||||
v_constraint: np.ndarray,
|
||||
F: np.ndarray,
|
||||
*,
|
||||
mode: OperatingMode | str = OperatingMode.PRACTICE,
|
||||
) -> np.ndarray:
|
||||
"""Blend the engine's own transition ``v_self`` toward the human/gold
|
||||
``v_constraint`` by the residual-driven constraint weight α.
|
||||
``α=0 → v_self`` (autonomous), ``α=1 → v_constraint`` (override).
|
||||
Rides the exact geodesic (`supervised_blend`), so closure is preserved.
|
||||
"""
|
||||
alpha = self.alpha_constraint(F, mode=mode)
|
||||
return self.supervised_blend(v_self, v_constraint, alpha)
|
||||
|
||||
def promote_gold_invariant(self, F: np.ndarray, *, authorized: bool = False) -> None:
|
||||
"""Add a state versor to 𝓘_gold. CALLER-GATED: the ADR-0092 signed /
|
||||
replay-verified promotion happens in the caller; this refuses to
|
||||
self-authorize (one-mutation-path discipline). The full replay-verified
|
||||
promotion pipeline + principal-axis decay are deferred (issue #18 follow-up).
|
||||
"""
|
||||
if not authorized:
|
||||
raise ValueError(
|
||||
"promote_gold_invariant requires explicit authorization (ADR-0092 gate)"
|
||||
)
|
||||
self.gold_invariants.append(_as_mv(F).copy())
|
||||
|
||||
def prune_gold_invariants(self, max_size: int = 64) -> None:
|
||||
"""Bound 𝓘_gold (decay hook), always retaining the three primal seeds.
|
||||
Full principal-axis pruning (R&D-Revised §5) is deferred."""
|
||||
max_size = max(3, int(max_size))
|
||||
if len(self.gold_invariants) > max_size:
|
||||
primal = self.gold_invariants[:3]
|
||||
recent = self.gold_invariants[3:][-(max_size - 3):]
|
||||
self.gold_invariants = primal + recent
|
||||
|
||||
def measure(self, F: np.ndarray, reference: Optional[np.ndarray] = None) -> CoherenceResidual:
|
||||
"""Structured residual (primary + optional geometric distance to reference)."""
|
||||
F_arr = _as_mv(F)
|
||||
|
|
|
|||
153
tests/test_adr_0238_goldtether_alpha.py
Normal file
153
tests/test_adr_0238_goldtether_alpha.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
"""ADR-0238 §2.3 / R&D-Revised §2.3 — harmonized GoldTether residual + α=Φ(R).
|
||||
|
||||
The prior GoldTether shipped only the drift term and an "earned-autonomy" ramp
|
||||
found in neither blueprint. This adds the scale-harmonized residual (drift +
|
||||
distance-to-gold-set), the gold-invariant set (primal seeds), and the
|
||||
α=Φ(R_gt) constraint-weight control law — composed with the earned-autonomy
|
||||
ceiling and the serve-never-autonomous rule, so the two mechanisms operate at
|
||||
their two timescales (lifetime ceiling × per-transition blend) rather than
|
||||
compete. See `docs/research/third-door-blueprint-fidelity.md` finding #4 (#18).
|
||||
"""
|
||||
|
||||
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.goldtether import GoldTetherMonitor, OperatingMode, coherence_residual
|
||||
|
||||
|
||||
def _id() -> np.ndarray:
|
||||
v = np.zeros(32, dtype=np.float64)
|
||||
v[0] = 1.0
|
||||
return v
|
||||
|
||||
|
||||
# --- gold-invariant set (R&D §5 bootstrapping seeds) -----------------------
|
||||
|
||||
def test_primal_gold_seeds():
|
||||
m = GoldTetherMonitor()
|
||||
assert len(m.gold_invariants) == 3
|
||||
ident, n_o, n_inf = m.gold_invariants
|
||||
assert ident[0] == 1.0
|
||||
assert n_o[5] == 0.5 and n_o[4] == -0.5 # n_o = 0.5(e5 - e4)
|
||||
assert n_inf[4] == 1.0 and n_inf[5] == 1.0 # n_inf = e4 + e5
|
||||
|
||||
|
||||
# --- harmonized residual (§2.3) --------------------------------------------
|
||||
|
||||
def test_harmonized_residual_zero_on_identity():
|
||||
# identity is both closed (drift 0) and a seed (distance 0)
|
||||
assert GoldTetherMonitor().goldtether_residual(_id()) == 0.0
|
||||
|
||||
|
||||
def test_harmonized_residual_nonneg_and_geo_driven_on_closed_versor():
|
||||
m = GoldTetherMonitor()
|
||||
F = make_rotor_from_angle(1.0)
|
||||
r = m.goldtether_residual(F)
|
||||
assert r >= 0.0
|
||||
# on a closed distant versor the geo term dominates → strictly above drift-only
|
||||
assert r > coherence_residual(F)
|
||||
|
||||
|
||||
def test_harmonized_residual_explodes_on_drift_fail_closed():
|
||||
m = GoldTetherMonitor()
|
||||
dirty = np.zeros(32, dtype=np.float64)
|
||||
dirty[0] = 0.5
|
||||
dirty[1] = 0.5
|
||||
assert m.goldtether_residual(dirty) > 1.0 # drift/ε makes non-closure loud
|
||||
m.autonomy = 1.0
|
||||
assert m.alpha_constraint(dirty) == 1.0 # → full override
|
||||
|
||||
|
||||
# --- α = Φ(R) control law (§2.3) -------------------------------------------
|
||||
|
||||
def test_alpha_serve_is_always_full_override():
|
||||
m = GoldTetherMonitor()
|
||||
m.autonomy = 1.0
|
||||
assert m.alpha_constraint(_id(), mode=OperatingMode.SERVE) == 1.0
|
||||
|
||||
|
||||
def test_alpha_earned_autonomy_is_the_floor():
|
||||
m = GoldTetherMonitor() # fresh: autonomy 0
|
||||
assert m.alpha_constraint(_id()) == 1.0 # cannot go autonomous yet
|
||||
m.autonomy = 0.4
|
||||
for th in (0.0, 0.5, 1.0, 2.0):
|
||||
a = m.alpha_constraint(make_rotor_from_angle(th))
|
||||
assert a >= 1.0 - m.autonomy - 1e-12 # α floored by (1 − earned)
|
||||
|
||||
|
||||
def test_alpha_autonomous_when_earned_and_coherent():
|
||||
m = GoldTetherMonitor()
|
||||
m.autonomy = 1.0
|
||||
assert m.alpha_constraint(_id()) == 0.0 # earned + on-seed + closed
|
||||
|
||||
|
||||
def test_alpha_smooth_step_thresholds():
|
||||
m = GoldTetherMonitor()
|
||||
m.autonomy = 1.0
|
||||
F = make_rotor_from_angle(1.0)
|
||||
r = m.goldtether_residual(F)
|
||||
m.r_floor, m.r_critical = r + 0.1, r + 0.2
|
||||
assert m.alpha_constraint(F) == 0.0 # below floor → autonomous
|
||||
m.r_floor, m.r_critical = r - 0.2, r - 0.1
|
||||
assert m.alpha_constraint(F) == 1.0 # above critical → override
|
||||
m.r_floor, m.r_critical = r - 0.1, r + 0.1
|
||||
assert 0.0 < m.alpha_constraint(F) < 1.0 # in the ramp
|
||||
|
||||
|
||||
def test_alpha_monotone_non_decreasing_in_distance():
|
||||
m = GoldTetherMonitor()
|
||||
m.autonomy = 1.0
|
||||
m.r_floor, m.r_critical = 0.0, 1.0
|
||||
prev = -1.0
|
||||
for th in np.linspace(0.05, 3.0, 25):
|
||||
a = m.alpha_constraint(make_rotor_from_angle(float(th)))
|
||||
assert 0.0 <= a <= 1.0
|
||||
assert a >= prev - 1e-9
|
||||
prev = a
|
||||
|
||||
|
||||
def test_alpha_determinism_replay():
|
||||
F = make_rotor_from_angle(0.7)
|
||||
m1 = GoldTetherMonitor()
|
||||
m2 = GoldTetherMonitor()
|
||||
m1.autonomy = m2.autonomy = 0.5
|
||||
assert m1.alpha_constraint(F) == m2.alpha_constraint(F)
|
||||
|
||||
|
||||
# --- supervised transition surface -----------------------------------------
|
||||
|
||||
def test_supervised_transition_endpoints_and_closure():
|
||||
m = GoldTetherMonitor()
|
||||
src, tgt = _id(), make_rotor_from_angle(0.6)
|
||||
# fresh (autonomy 0) → α = 1 → transition lands on the constraint
|
||||
out = m.supervised_transition(src, tgt, _id())
|
||||
assert np.allclose(out, tgt, atol=1e-6)
|
||||
assert versor_condition(out) < 1e-6
|
||||
# earned + coherent → α = 0 → transition stays on self
|
||||
m.autonomy = 1.0
|
||||
out = m.supervised_transition(src, tgt, _id())
|
||||
assert np.allclose(out, src)
|
||||
|
||||
|
||||
# --- gold-set mutation discipline ------------------------------------------
|
||||
|
||||
def test_promote_requires_explicit_authorization():
|
||||
m = GoldTetherMonitor()
|
||||
with pytest.raises(ValueError):
|
||||
m.promote_gold_invariant(make_rotor_from_angle(0.3))
|
||||
m.promote_gold_invariant(make_rotor_from_angle(0.3), authorized=True)
|
||||
assert len(m.gold_invariants) == 4
|
||||
|
||||
|
||||
def test_prune_retains_primal_seeds():
|
||||
m = GoldTetherMonitor()
|
||||
for i in range(50):
|
||||
m.promote_gold_invariant(make_rotor_from_angle(0.01 * i + 0.01), authorized=True)
|
||||
m.prune_gold_invariants(max_size=10)
|
||||
assert len(m.gold_invariants) == 10
|
||||
assert m.gold_invariants[0][0] == 1.0 # identity seed retained
|
||||
assert m.gold_invariants[1][5] == 0.5 # n_o seed retained
|
||||
Loading…
Reference in a new issue