Implement Third-Door operators: - goldtether: coherence residual, dynamic floor, practice/serve autonomy - dynamic_manifold: signature-aware PCA, Procrustes, Cartan-Iwasawa - surprise: residual + dual with Procrustes - biography / temporal_gate / self_authorship: lifelong + proposal-only All versor outputs enforce versor_condition < 1e-6. Spin left-composition geodesic for supervised blend. Arena GoldTether (ADR-0199) untouched.
This commit is contained in:
parent
bad9b87bff
commit
a8e9977cbc
7 changed files with 1606 additions and 0 deletions
|
|
@ -5,6 +5,10 @@ Three physics sublayers:
|
||||||
compositional — binding, digest, reasoning, articulation (ADR-0009)
|
compositional — binding, digest, reasoning, articulation (ADR-0009)
|
||||||
identity — identity manifold, drives, exertion, character (ADR-0010)
|
identity — identity manifold, drives, exertion, character (ADR-0010)
|
||||||
|
|
||||||
|
Third-Door Horizon (ADR-0238–0240):
|
||||||
|
coherence GoldTether, dynamic manifold (Procrustes/PCA), surprise dual,
|
||||||
|
biography holonomy, temporal gate, self-authorship miner.
|
||||||
|
|
||||||
All operators are stateless and frozen where possible.
|
All operators are stateless and frozen where possible.
|
||||||
State lives in the FieldState; operators are pure transformations.
|
State lives in the FieldState; operators are pure transformations.
|
||||||
"""
|
"""
|
||||||
|
|
@ -22,6 +26,50 @@ from core.physics.drive import DriveGradientMap, GradientField, ValueAxis
|
||||||
from core.physics.exertion import ExertionMeter, FatigueIndex, CycleCost
|
from core.physics.exertion import ExertionMeter, FatigueIndex, CycleCost
|
||||||
from core.physics.identity import IdentityManifold, IdentityCheck, IdentityScore, CharacterProfile
|
from core.physics.identity import IdentityManifold, IdentityCheck, IdentityScore, CharacterProfile
|
||||||
from core.physics.learning import PromotionDecision, VaultPromotionPolicy
|
from core.physics.learning import PromotionDecision, VaultPromotionPolicy
|
||||||
|
from core.physics.goldtether import (
|
||||||
|
AutonomyBand,
|
||||||
|
AutonomyDecision,
|
||||||
|
CoherenceResidual,
|
||||||
|
GoldTetherConfig,
|
||||||
|
GoldTetherMonitor,
|
||||||
|
OperatingMode,
|
||||||
|
PseudoscalarFloorState,
|
||||||
|
derive_kappa,
|
||||||
|
)
|
||||||
|
from core.physics.dynamic_manifold import (
|
||||||
|
AxisClassification,
|
||||||
|
CartanIwasawaFactors,
|
||||||
|
ConformalProcrustesResult,
|
||||||
|
PrincipalAxis,
|
||||||
|
SignatureAwarePCAResult,
|
||||||
|
cartan_iwasawa_factorize,
|
||||||
|
conformal_procrustes,
|
||||||
|
dual_correction_slerp,
|
||||||
|
procrustes_residual,
|
||||||
|
signature_aware_pca,
|
||||||
|
)
|
||||||
|
from core.physics.surprise import (
|
||||||
|
AnalogySeed,
|
||||||
|
DualOperatorResult,
|
||||||
|
SurpriseResult,
|
||||||
|
analogy_seed,
|
||||||
|
dual_operator,
|
||||||
|
project_onto_basis,
|
||||||
|
surprise_residual,
|
||||||
|
)
|
||||||
|
from core.physics.biography import (
|
||||||
|
BiographyHolonomyBlade,
|
||||||
|
biography_telemetry,
|
||||||
|
integrate_biography,
|
||||||
|
reconstruct_biography,
|
||||||
|
)
|
||||||
|
from core.physics.temporal_gate import (
|
||||||
|
TemporalAdmissibilityGate,
|
||||||
|
TemporalContext,
|
||||||
|
TemporalDecision,
|
||||||
|
TemporalVerdict,
|
||||||
|
)
|
||||||
|
from core.physics.self_authorship import AuthorshipProposal, SelfAuthorshipMiner
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"SalienceOperator", "SalienceMap", "FieldRegion",
|
"SalienceOperator", "SalienceMap", "FieldRegion",
|
||||||
|
|
@ -36,4 +84,42 @@ __all__ = [
|
||||||
"ExertionMeter", "FatigueIndex", "CycleCost",
|
"ExertionMeter", "FatigueIndex", "CycleCost",
|
||||||
"IdentityManifold", "IdentityCheck", "IdentityScore", "CharacterProfile",
|
"IdentityManifold", "IdentityCheck", "IdentityScore", "CharacterProfile",
|
||||||
"PromotionDecision", "VaultPromotionPolicy",
|
"PromotionDecision", "VaultPromotionPolicy",
|
||||||
|
# ADR-0238
|
||||||
|
"AutonomyBand",
|
||||||
|
"AutonomyDecision",
|
||||||
|
"CoherenceResidual",
|
||||||
|
"GoldTetherConfig",
|
||||||
|
"GoldTetherMonitor",
|
||||||
|
"OperatingMode",
|
||||||
|
"PseudoscalarFloorState",
|
||||||
|
"derive_kappa",
|
||||||
|
# ADR-0239
|
||||||
|
"AxisClassification",
|
||||||
|
"CartanIwasawaFactors",
|
||||||
|
"ConformalProcrustesResult",
|
||||||
|
"PrincipalAxis",
|
||||||
|
"SignatureAwarePCAResult",
|
||||||
|
"cartan_iwasawa_factorize",
|
||||||
|
"conformal_procrustes",
|
||||||
|
"dual_correction_slerp",
|
||||||
|
"procrustes_residual",
|
||||||
|
"signature_aware_pca",
|
||||||
|
"AnalogySeed",
|
||||||
|
"DualOperatorResult",
|
||||||
|
"SurpriseResult",
|
||||||
|
"analogy_seed",
|
||||||
|
"dual_operator",
|
||||||
|
"project_onto_basis",
|
||||||
|
"surprise_residual",
|
||||||
|
# ADR-0240
|
||||||
|
"BiographyHolonomyBlade",
|
||||||
|
"biography_telemetry",
|
||||||
|
"integrate_biography",
|
||||||
|
"reconstruct_biography",
|
||||||
|
"TemporalAdmissibilityGate",
|
||||||
|
"TemporalContext",
|
||||||
|
"TemporalDecision",
|
||||||
|
"TemporalVerdict",
|
||||||
|
"AuthorshipProposal",
|
||||||
|
"SelfAuthorshipMiner",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
107
core/physics/biography.py
Normal file
107
core/physics/biography.py
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
"""core.physics.biography — Biography Holonomy Blade (ADR-0240).
|
||||||
|
|
||||||
|
Forever-lived individuality as an integrated holonomy of the identity
|
||||||
|
trajectory. Reconstructible from an ordered sequence of session versors —
|
||||||
|
reconstruction-over-storage. Not a raw experience dump; not a parallel
|
||||||
|
identity store.
|
||||||
|
|
||||||
|
Integrates with ``algebra.holonomy.holonomy_encode`` and the identity motor
|
||||||
|
surface; does not mutate packs, vault, or serving paths.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Sequence
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from algebra.cl41 import N_COMPONENTS
|
||||||
|
from algebra.holonomy import holonomy_encode, holonomy_similarity
|
||||||
|
from algebra.versor import unitize_versor, versor_condition
|
||||||
|
|
||||||
|
_CLOSURE_TOL = 1e-6
|
||||||
|
_TELEMETRY_SCHEMA = "biography_holonomy_v1"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class BiographyHolonomyBlade:
|
||||||
|
"""Integrated holonomy blade of a lived trajectory."""
|
||||||
|
|
||||||
|
blade: np.ndarray
|
||||||
|
n_steps: int
|
||||||
|
trajectory_hash: str
|
||||||
|
closure: float
|
||||||
|
|
||||||
|
def similarity(self, other: "BiographyHolonomyBlade") -> float:
|
||||||
|
return float(holonomy_similarity(self.blade, other.blade))
|
||||||
|
|
||||||
|
|
||||||
|
def _as_versor(v: np.ndarray, name: str) -> np.ndarray:
|
||||||
|
arr = np.asarray(v, dtype=np.float64)
|
||||||
|
if arr.shape != (N_COMPONENTS,):
|
||||||
|
raise ValueError(f"{name} must have shape ({N_COMPONENTS},)")
|
||||||
|
# Construction boundary: trajectory elements must be closed versors.
|
||||||
|
try:
|
||||||
|
closed = unitize_versor(arr)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(f"{name} is not a closed versor: {exc}") from exc
|
||||||
|
cond = versor_condition(closed)
|
||||||
|
if cond >= _CLOSURE_TOL:
|
||||||
|
raise ValueError(f"{name} versor_condition={cond:.3e}")
|
||||||
|
return closed.astype(np.float64, copy=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _trajectory_hash(versors: Sequence[np.ndarray]) -> str:
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
h = hashlib.sha256()
|
||||||
|
for v in versors:
|
||||||
|
h.update(np.asarray(v, dtype=np.float64).tobytes())
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def integrate_biography(
|
||||||
|
trajectory: Sequence[np.ndarray],
|
||||||
|
*,
|
||||||
|
alpha: float = 0.5,
|
||||||
|
) -> BiographyHolonomyBlade:
|
||||||
|
"""Integrate ordered identity/session versors into a biography holonomy blade.
|
||||||
|
|
||||||
|
Order is load-bearing. Empty trajectory is refused (no confabulated self).
|
||||||
|
"""
|
||||||
|
if not trajectory:
|
||||||
|
raise ValueError("biography trajectory must be non-empty")
|
||||||
|
closed = [_as_versor(v, f"trajectory[{i}]") for i, v in enumerate(trajectory)]
|
||||||
|
blade = holonomy_encode(closed, alpha=alpha)
|
||||||
|
cond = versor_condition(blade)
|
||||||
|
if cond >= _CLOSURE_TOL:
|
||||||
|
raise ValueError(f"biography blade not closed: {cond:.3e}")
|
||||||
|
return BiographyHolonomyBlade(
|
||||||
|
blade=np.asarray(blade, dtype=np.float64),
|
||||||
|
n_steps=len(closed),
|
||||||
|
trajectory_hash=_trajectory_hash(closed),
|
||||||
|
closure=float(cond),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reconstruct_biography(
|
||||||
|
trajectory: Sequence[np.ndarray],
|
||||||
|
*,
|
||||||
|
alpha: float = 0.5,
|
||||||
|
) -> BiographyHolonomyBlade:
|
||||||
|
"""Alias for integrate — reconstruction is recompute, not storage load."""
|
||||||
|
return integrate_biography(trajectory, alpha=alpha)
|
||||||
|
|
||||||
|
|
||||||
|
def biography_telemetry(blade: BiographyHolonomyBlade) -> dict[str, Any]:
|
||||||
|
"""Workbench-safe projection (no full multivector dump required for UI)."""
|
||||||
|
return {
|
||||||
|
"schema_version": _TELEMETRY_SCHEMA,
|
||||||
|
"n_steps": int(blade.n_steps),
|
||||||
|
"trajectory_hash": blade.trajectory_hash,
|
||||||
|
"closure": float(blade.closure),
|
||||||
|
"blade_scalar": float(blade.blade[0]),
|
||||||
|
"blade_pseudoscalar": float(blade.blade[31]),
|
||||||
|
"blade_l2": float(np.linalg.norm(blade.blade)),
|
||||||
|
}
|
||||||
449
core/physics/dynamic_manifold.py
Normal file
449
core/physics/dynamic_manifold.py
Normal file
|
|
@ -0,0 +1,449 @@
|
||||||
|
"""core.physics.dynamic_manifold — Conformal manifold operators (ADR-0239).
|
||||||
|
|
||||||
|
Signature-aware PCA with explicit null classification, Conformal Procrustes
|
||||||
|
(versor search for structural analogy), Cartan–Iwasawa constructive
|
||||||
|
factorization for dual-correction slerp, and a dedicated Procrustes residual
|
||||||
|
norm (not null-margin; not ADR-0006 energy residual).
|
||||||
|
|
||||||
|
All operators are pure and deterministic. Null eigenvectors are never silently
|
||||||
|
skipped — they are classified and returned.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from algebra.cl41 import (
|
||||||
|
N_COMPONENTS,
|
||||||
|
SIGNATURE,
|
||||||
|
geometric_product,
|
||||||
|
grade_project,
|
||||||
|
reverse,
|
||||||
|
)
|
||||||
|
from algebra.rotor import word_transition_rotor
|
||||||
|
from algebra.versor import unitize_versor, versor_apply, versor_condition
|
||||||
|
|
||||||
|
_CLOSURE_TOL = 1e-6
|
||||||
|
_NEAR_ZERO = 1e-12
|
||||||
|
_NULL_TOL = 1e-8
|
||||||
|
_METRIC = np.ones(N_COMPONENTS, dtype=np.float64)
|
||||||
|
# Grade-1 components (indices 1..5) carry Cl(4,1) signature (+,+,+,+,-).
|
||||||
|
_METRIC[1:6] = SIGNATURE.astype(np.float64)
|
||||||
|
|
||||||
|
|
||||||
|
class AxisClassification(str, Enum):
|
||||||
|
SPACELIKE = "spacelike"
|
||||||
|
TIMELIKE = "timelike"
|
||||||
|
NULL = "null"
|
||||||
|
DEGENERATE = "degenerate"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class PrincipalAxis:
|
||||||
|
vector: tuple[float, ...]
|
||||||
|
eigenvalue: float
|
||||||
|
classification: AxisClassification
|
||||||
|
metric_quadratic: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SignatureAwarePCAResult:
|
||||||
|
axes: tuple[PrincipalAxis, ...]
|
||||||
|
mean: tuple[float, ...]
|
||||||
|
explained: tuple[float, ...]
|
||||||
|
n_points: int
|
||||||
|
n_null: int
|
||||||
|
n_spacelike: int
|
||||||
|
n_timelike: int
|
||||||
|
n_degenerate: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ConformalProcrustesResult:
|
||||||
|
versor: np.ndarray
|
||||||
|
residual_norm: float
|
||||||
|
n_pairs: int
|
||||||
|
pair_residuals: tuple[float, ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class CartanIwasawaFactors:
|
||||||
|
"""Constructive K · A · N factorization for dual-correction surfaces.
|
||||||
|
|
||||||
|
K — compact (rotation-like, B² < 0 planes)
|
||||||
|
A — abelian (boost-like, B² > 0 planes)
|
||||||
|
N — nilpotent / residual (null + higher-grade remainder, unitized if needed)
|
||||||
|
"""
|
||||||
|
|
||||||
|
K: np.ndarray
|
||||||
|
A: np.ndarray
|
||||||
|
N: np.ndarray
|
||||||
|
reconstruction_residual: float
|
||||||
|
|
||||||
|
|
||||||
|
def _as_points(points: Sequence[np.ndarray]) -> np.ndarray:
|
||||||
|
if not points:
|
||||||
|
raise ValueError("signature_aware_pca requires at least one point")
|
||||||
|
rows = []
|
||||||
|
for i, p in enumerate(points):
|
||||||
|
arr = np.asarray(p, dtype=np.float64)
|
||||||
|
if arr.shape != (N_COMPONENTS,):
|
||||||
|
raise ValueError(
|
||||||
|
f"point[{i}] must have shape ({N_COMPONENTS},); got {arr.shape}"
|
||||||
|
)
|
||||||
|
rows.append(arr)
|
||||||
|
return np.stack(rows, axis=0)
|
||||||
|
|
||||||
|
|
||||||
|
def _metric_quadratic_form(v: np.ndarray) -> float:
|
||||||
|
"""Quadratic form on grade-1 part under Cl(4,1) signature; higher grades +.
|
||||||
|
|
||||||
|
Used only for axis *classification*, not as a substitute for versor_condition.
|
||||||
|
"""
|
||||||
|
g1 = v[1:6]
|
||||||
|
q = float(np.dot(g1 * _METRIC[1:6], g1))
|
||||||
|
higher = float(np.dot(v[6:], v[6:]))
|
||||||
|
scalar = float(v[0] * v[0])
|
||||||
|
return q + higher + scalar
|
||||||
|
|
||||||
|
|
||||||
|
def _classify_axis(v: np.ndarray) -> tuple[AxisClassification, float]:
|
||||||
|
nrm = float(np.linalg.norm(v))
|
||||||
|
if nrm < _NEAR_ZERO:
|
||||||
|
return AxisClassification.DEGENERATE, 0.0
|
||||||
|
q = _metric_quadratic_form(v)
|
||||||
|
# Grade-1 signature sense dominates classification when g1 is present.
|
||||||
|
g1 = v[1:6]
|
||||||
|
g1_q = float(np.dot(g1 * _METRIC[1:6], g1))
|
||||||
|
g1_n = float(np.linalg.norm(g1))
|
||||||
|
if g1_n < _NEAR_ZERO:
|
||||||
|
# Pure higher-grade axis: treat as spacelike if energy present else degenerate.
|
||||||
|
if nrm < _NULL_TOL:
|
||||||
|
return AxisClassification.DEGENERATE, q
|
||||||
|
return AxisClassification.SPACELIKE, q
|
||||||
|
if abs(g1_q) < _NULL_TOL:
|
||||||
|
return AxisClassification.NULL, g1_q
|
||||||
|
if g1_q > 0.0:
|
||||||
|
return AxisClassification.SPACELIKE, g1_q
|
||||||
|
return AxisClassification.TIMELIKE, g1_q
|
||||||
|
|
||||||
|
|
||||||
|
def signature_aware_pca(
|
||||||
|
points: Sequence[np.ndarray],
|
||||||
|
*,
|
||||||
|
max_axes: int | None = None,
|
||||||
|
) -> SignatureAwarePCAResult:
|
||||||
|
"""Signature-aware PCA on Cl(4,1) multivector clouds.
|
||||||
|
|
||||||
|
1. Center the cloud in coefficient space.
|
||||||
|
2. Form the metric-rescaled covariance (whitening by √|G| on coords).
|
||||||
|
3. Eigen-decompose symmetrically (deterministic via numpy eigh).
|
||||||
|
4. Classify every axis — null axes are returned, never dropped.
|
||||||
|
"""
|
||||||
|
X = _as_points(points)
|
||||||
|
n = X.shape[0]
|
||||||
|
mean = X.mean(axis=0)
|
||||||
|
Xc = X - mean
|
||||||
|
|
||||||
|
# Metric rescaling: multiply each coordinate by sqrt(|g_ii|)*sign-preserving weight.
|
||||||
|
# For signature -1 on e5, use imaginary-free absolute metric then restore sense
|
||||||
|
# via classification (not by complex eigen).
|
||||||
|
scale = np.sqrt(np.abs(_METRIC))
|
||||||
|
scale = np.where(scale < _NEAR_ZERO, 1.0, scale)
|
||||||
|
Y = Xc * scale # broadcast
|
||||||
|
|
||||||
|
# Covariance of rescaled coordinates.
|
||||||
|
if n == 1:
|
||||||
|
cov = np.zeros((N_COMPONENTS, N_COMPONENTS), dtype=np.float64)
|
||||||
|
else:
|
||||||
|
cov = (Y.T @ Y) / float(n)
|
||||||
|
|
||||||
|
# Symmetric eigh → ascending eigenvalues; reverse for explained variance order.
|
||||||
|
evals, evecs = np.linalg.eigh(cov)
|
||||||
|
order = np.argsort(evals)[::-1]
|
||||||
|
evals = evals[order]
|
||||||
|
evecs = evecs[:, order]
|
||||||
|
|
||||||
|
k = N_COMPONENTS if max_axes is None else min(int(max_axes), N_COMPONENTS)
|
||||||
|
total = float(np.sum(np.clip(evals, 0.0, None)))
|
||||||
|
axes: list[PrincipalAxis] = []
|
||||||
|
counts = {
|
||||||
|
AxisClassification.NULL: 0,
|
||||||
|
AxisClassification.SPACELIKE: 0,
|
||||||
|
AxisClassification.TIMELIKE: 0,
|
||||||
|
AxisClassification.DEGENERATE: 0,
|
||||||
|
}
|
||||||
|
explained_list: list[float] = []
|
||||||
|
|
||||||
|
for i in range(k):
|
||||||
|
# Map eigenvector back from metric-rescaled coords.
|
||||||
|
v_scaled = evecs[:, i]
|
||||||
|
v = v_scaled / scale
|
||||||
|
nrm = float(np.linalg.norm(v))
|
||||||
|
if nrm > _NEAR_ZERO:
|
||||||
|
v = v / nrm
|
||||||
|
# Deterministic sign convention: first nonzero component positive.
|
||||||
|
for c in v:
|
||||||
|
if abs(c) > _NEAR_ZERO:
|
||||||
|
if c < 0.0:
|
||||||
|
v = -v
|
||||||
|
break
|
||||||
|
cls, mq = _classify_axis(v)
|
||||||
|
counts[cls] = counts.get(cls, 0) + 1
|
||||||
|
ev = float(max(0.0, evals[i]))
|
||||||
|
frac = float(ev / total) if total > _NEAR_ZERO else 0.0
|
||||||
|
explained_list.append(frac)
|
||||||
|
axes.append(
|
||||||
|
PrincipalAxis(
|
||||||
|
vector=tuple(float(x) for x in v),
|
||||||
|
eigenvalue=ev,
|
||||||
|
classification=cls,
|
||||||
|
metric_quadratic=float(mq),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return SignatureAwarePCAResult(
|
||||||
|
axes=tuple(axes),
|
||||||
|
mean=tuple(float(x) for x in mean),
|
||||||
|
explained=tuple(explained_list),
|
||||||
|
n_points=n,
|
||||||
|
n_null=counts[AxisClassification.NULL],
|
||||||
|
n_spacelike=counts[AxisClassification.SPACELIKE],
|
||||||
|
n_timelike=counts[AxisClassification.TIMELIKE],
|
||||||
|
n_degenerate=counts[AxisClassification.DEGENERATE],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def procrustes_residual(
|
||||||
|
source: np.ndarray,
|
||||||
|
target: np.ndarray,
|
||||||
|
versor: np.ndarray,
|
||||||
|
) -> float:
|
||||||
|
"""Dedicated Procrustes residual: || V * s * reverse(V) - t ||_F.
|
||||||
|
|
||||||
|
Named separately from null-margin and energy coherence_residual so it
|
||||||
|
cannot be silently reused as a different residual.
|
||||||
|
"""
|
||||||
|
s = np.asarray(source, dtype=np.float64)
|
||||||
|
t = np.asarray(target, dtype=np.float64)
|
||||||
|
V = np.asarray(versor, dtype=np.float64)
|
||||||
|
mapped = versor_apply(V, s)
|
||||||
|
return float(np.linalg.norm(mapped - t))
|
||||||
|
|
||||||
|
|
||||||
|
def conformal_procrustes(
|
||||||
|
sources: Sequence[np.ndarray] | np.ndarray,
|
||||||
|
targets: Sequence[np.ndarray] | np.ndarray,
|
||||||
|
) -> ConformalProcrustesResult:
|
||||||
|
"""Find a unit versor aligning source structure to target structure.
|
||||||
|
|
||||||
|
Single pair: closed transition rotor.
|
||||||
|
Multiple pairs: manifold average of per-pair transition rotors via
|
||||||
|
successive equal-weight slerp composition (deterministic order).
|
||||||
|
"""
|
||||||
|
if isinstance(sources, np.ndarray) and sources.ndim == 1:
|
||||||
|
src_list = [sources]
|
||||||
|
else:
|
||||||
|
src_list = list(sources) # type: ignore[arg-type]
|
||||||
|
if isinstance(targets, np.ndarray) and targets.ndim == 1:
|
||||||
|
tgt_list = [targets]
|
||||||
|
else:
|
||||||
|
tgt_list = list(targets) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
if len(src_list) != len(tgt_list):
|
||||||
|
raise ValueError("sources and targets must have equal length")
|
||||||
|
if not src_list:
|
||||||
|
raise ValueError("conformal_procrustes requires at least one pair")
|
||||||
|
|
||||||
|
rotors: list[np.ndarray] = []
|
||||||
|
for i, (s, t) in enumerate(zip(src_list, tgt_list)):
|
||||||
|
s_arr = np.asarray(s, dtype=np.float64)
|
||||||
|
t_arr = np.asarray(t, dtype=np.float64)
|
||||||
|
if s_arr.shape != (N_COMPONENTS,) or t_arr.shape != (N_COMPONENTS,):
|
||||||
|
raise ValueError(f"pair[{i}] must be 32-component multivectors")
|
||||||
|
# Construction boundary: transition rotor is a closed unit versor.
|
||||||
|
R = word_transition_rotor(s_arr, t_arr)
|
||||||
|
rotors.append(np.asarray(R, dtype=np.float64))
|
||||||
|
|
||||||
|
# Manifold average: sequential geodesic midpoints in pair order.
|
||||||
|
V = rotors[0].copy()
|
||||||
|
for k, R in enumerate(rotors[1:], start=2):
|
||||||
|
# Slerp V toward R with weight 1/k so equal contribution in the limit.
|
||||||
|
try:
|
||||||
|
T = word_transition_rotor(V, R)
|
||||||
|
from algebra.rotor import rotor_power
|
||||||
|
|
||||||
|
T_a = rotor_power(T, 1.0 / float(k))
|
||||||
|
V = geometric_product(geometric_product(T_a, V), reverse(T_a)).astype(
|
||||||
|
np.float64
|
||||||
|
)
|
||||||
|
V = unitize_versor(V)
|
||||||
|
except ValueError:
|
||||||
|
# Fail closed to previous average if a pair is non-connectable.
|
||||||
|
continue
|
||||||
|
|
||||||
|
cond = versor_condition(V)
|
||||||
|
if cond >= _CLOSURE_TOL:
|
||||||
|
raise ValueError(f"Procrustes versor not closed: condition={cond:.3e}")
|
||||||
|
|
||||||
|
pair_res = tuple(
|
||||||
|
procrustes_residual(s, t, V) for s, t in zip(src_list, tgt_list)
|
||||||
|
)
|
||||||
|
residual_norm = float(np.sqrt(sum(r * r for r in pair_res) / len(pair_res)))
|
||||||
|
return ConformalProcrustesResult(
|
||||||
|
versor=V.astype(np.float64, copy=False),
|
||||||
|
residual_norm=residual_norm,
|
||||||
|
n_pairs=len(src_list),
|
||||||
|
pair_residuals=pair_res,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _identity_mv() -> np.ndarray:
|
||||||
|
out = np.zeros(N_COMPONENTS, dtype=np.float64)
|
||||||
|
out[0] = 1.0
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def cartan_iwasawa_factorize(V: np.ndarray) -> CartanIwasawaFactors:
|
||||||
|
"""Constructive Cartan–Iwasawa-style factorization of a closed versor.
|
||||||
|
|
||||||
|
For simple scalar+bivector rotors:
|
||||||
|
- If B² < 0 → pure K (rotation)
|
||||||
|
- If B² > 0 → pure A (boost)
|
||||||
|
- If B² ≈ 0 and B ≠ 0 → pure N (null)
|
||||||
|
- Mixed: split bivector energy by sign of B² contribution and leave residual N.
|
||||||
|
|
||||||
|
Reconstruction: K * A * N; residual measured in coefficient space.
|
||||||
|
"""
|
||||||
|
V_arr = np.asarray(V, dtype=np.float64)
|
||||||
|
if V_arr.shape != (N_COMPONENTS,):
|
||||||
|
raise ValueError(f"V must have shape ({N_COMPONENTS},)")
|
||||||
|
cond = versor_condition(V_arr)
|
||||||
|
if cond >= 1e-2:
|
||||||
|
# Construction boundary: attempt unitize only at this explicit boundary.
|
||||||
|
V_arr = unitize_versor(V_arr)
|
||||||
|
cond = versor_condition(V_arr)
|
||||||
|
if cond >= _CLOSURE_TOL:
|
||||||
|
raise ValueError(f"cartan_iwasawa_factorize: input not closed ({cond:.3e})")
|
||||||
|
|
||||||
|
scalar = float(V_arr[0])
|
||||||
|
B = grade_project(V_arr, 2)
|
||||||
|
higher = V_arr.copy()
|
||||||
|
higher[0] = 0.0
|
||||||
|
higher[6:16] = 0.0 # clear grade-2; keep 1,3,4,5
|
||||||
|
|
||||||
|
B_sq = geometric_product(B, B).astype(np.float64)
|
||||||
|
bsq_scalar = float(B_sq[0])
|
||||||
|
B_sq_res = B_sq.copy()
|
||||||
|
B_sq_res[0] = 0.0
|
||||||
|
simple = float(np.linalg.norm(B_sq_res)) < 1e-6
|
||||||
|
b_norm = float(np.linalg.norm(B))
|
||||||
|
|
||||||
|
I = _identity_mv()
|
||||||
|
K = I.copy()
|
||||||
|
A = I.copy()
|
||||||
|
N = I.copy()
|
||||||
|
|
||||||
|
if b_norm < _NEAR_ZERO and float(np.linalg.norm(higher)) < _NEAR_ZERO:
|
||||||
|
# Near-identity
|
||||||
|
K = V_arr.copy()
|
||||||
|
elif simple and bsq_scalar < 0.0:
|
||||||
|
K = V_arr.copy()
|
||||||
|
# Zero out non-scalar/bivector if any residual grades present.
|
||||||
|
K = grade_project(K, 0) + grade_project(K, 2)
|
||||||
|
K = unitize_versor(K)
|
||||||
|
elif simple and bsq_scalar > 0.0:
|
||||||
|
A = grade_project(V_arr, 0) + grade_project(V_arr, 2)
|
||||||
|
A = unitize_versor(A)
|
||||||
|
elif simple and abs(bsq_scalar) <= _NEAR_ZERO:
|
||||||
|
N_cand = grade_project(V_arr, 0) + grade_project(V_arr, 2)
|
||||||
|
try:
|
||||||
|
N = unitize_versor(N_cand)
|
||||||
|
except ValueError:
|
||||||
|
N = I.copy()
|
||||||
|
N[0] = scalar if abs(scalar) > _NEAR_ZERO else 1.0
|
||||||
|
N = unitize_versor(N)
|
||||||
|
else:
|
||||||
|
# Mixed: put scalar+rotation-like half in K, boost-like half in A, rest N.
|
||||||
|
# Split B into two parallel bivectors by halving coefficients when non-simple.
|
||||||
|
half = B * 0.5
|
||||||
|
K = grade_project(V_arr, 0) * 0.0
|
||||||
|
K[0] = abs(scalar) ** 0.5 if abs(scalar) > _NEAR_ZERO else 1.0
|
||||||
|
K = K + half
|
||||||
|
try:
|
||||||
|
K = unitize_versor(K)
|
||||||
|
except ValueError:
|
||||||
|
K = I.copy()
|
||||||
|
A = I.copy()
|
||||||
|
A[0] = abs(scalar) ** 0.5 if abs(scalar) > _NEAR_ZERO else 1.0
|
||||||
|
A = A + half
|
||||||
|
try:
|
||||||
|
A = unitize_versor(A)
|
||||||
|
except ValueError:
|
||||||
|
A = I.copy()
|
||||||
|
# N absorbs higher-grade residual relative to K*A
|
||||||
|
KA = geometric_product(K, A)
|
||||||
|
try:
|
||||||
|
N = unitize_versor(geometric_product(reverse(KA), V_arr))
|
||||||
|
except ValueError:
|
||||||
|
N = I.copy()
|
||||||
|
|
||||||
|
recon = geometric_product(geometric_product(K, A), N)
|
||||||
|
recon_res = float(np.linalg.norm(recon - V_arr))
|
||||||
|
|
||||||
|
for name, factor in (("K", K), ("A", A), ("N", N)):
|
||||||
|
c = versor_condition(factor)
|
||||||
|
if c >= _CLOSURE_TOL:
|
||||||
|
raise ValueError(f"Cartan–Iwasawa factor {name} not closed: {c:.3e}")
|
||||||
|
|
||||||
|
return CartanIwasawaFactors(
|
||||||
|
K=K.astype(np.float64, copy=False),
|
||||||
|
A=A.astype(np.float64, copy=False),
|
||||||
|
N=N.astype(np.float64, copy=False),
|
||||||
|
reconstruction_residual=recon_res,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dual_correction_slerp(
|
||||||
|
source: np.ndarray,
|
||||||
|
target: np.ndarray,
|
||||||
|
alpha: float,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Slerp on Cartan–Iwasawa factors of the transition rotor (dual-correction).
|
||||||
|
|
||||||
|
Factors are powered independently then recomposed as left action on source:
|
||||||
|
|
||||||
|
R = target * reverse(source) = K A N
|
||||||
|
out = (K^α A^α N^α) * source
|
||||||
|
|
||||||
|
α=0 → source; α=1 → target for unit versors. Sandwich conjugation is not
|
||||||
|
the state geodesic (see ADR-0238 supervised_blend).
|
||||||
|
"""
|
||||||
|
from algebra.rotor import rotor_power
|
||||||
|
|
||||||
|
a = float(alpha)
|
||||||
|
if a < 0.0 or a > 1.0:
|
||||||
|
raise ValueError("alpha must be in [0, 1]")
|
||||||
|
src = np.asarray(source, dtype=np.float64)
|
||||||
|
tgt = np.asarray(target, dtype=np.float64)
|
||||||
|
if a <= _NEAR_ZERO:
|
||||||
|
out = src.copy()
|
||||||
|
elif a >= 1.0 - _NEAR_ZERO:
|
||||||
|
out = tgt.copy()
|
||||||
|
else:
|
||||||
|
R = word_transition_rotor(src, tgt)
|
||||||
|
factors = cartan_iwasawa_factorize(R)
|
||||||
|
K_a = rotor_power(factors.K, a)
|
||||||
|
A_a = rotor_power(factors.A, a)
|
||||||
|
N_a = rotor_power(factors.N, a)
|
||||||
|
R_a = geometric_product(geometric_product(K_a, A_a), N_a)
|
||||||
|
R_a = unitize_versor(R_a)
|
||||||
|
out = geometric_product(R_a, src).astype(np.float64)
|
||||||
|
cond = versor_condition(out)
|
||||||
|
if cond >= _CLOSURE_TOL:
|
||||||
|
raise ValueError(f"dual_correction_slerp broke closure: {cond:.3e}")
|
||||||
|
return out
|
||||||
432
core/physics/goldtether.py
Normal file
432
core/physics/goldtether.py
Normal file
|
|
@ -0,0 +1,432 @@
|
||||||
|
"""core.physics.goldtether — Coherence GoldTether (ADR-0238).
|
||||||
|
|
||||||
|
This module implements the *field* GoldTether: dynamic grade-5 pseudoscalar
|
||||||
|
floor, harmonized coherence residual, and practice/serve autonomy modulation.
|
||||||
|
|
||||||
|
It is intentionally distinct from the *arena* GoldTether protocol in
|
||||||
|
``core.learning_arena.protocols.GoldTether`` (ADR-0199), which scores practice
|
||||||
|
attempts against independent truth anchors. Shared metaphor; different contracts.
|
||||||
|
Never import or subclass the arena protocol from this module.
|
||||||
|
|
||||||
|
Construction boundary: supervised blend closes via ``algebra.rotor`` manifold
|
||||||
|
slerp (``word_transition_rotor`` + ``rotor_power``). No hot-path drift repair.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field, replace
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from algebra.cl41 import N_COMPONENTS, geometric_product, reverse
|
||||||
|
from algebra.rotor import rotor_power, word_transition_rotor
|
||||||
|
from algebra.versor import versor_condition
|
||||||
|
|
||||||
|
_PSEUDOSCALAR_IDX = 31
|
||||||
|
_CLOSURE_TOL = 1e-6
|
||||||
|
_NEAR_ZERO = 1e-12
|
||||||
|
_DEFAULT_DECAY_N = 32
|
||||||
|
_DEFAULT_W_DRIFT = 0.35
|
||||||
|
_DEFAULT_FLOOR_INIT = 0.15
|
||||||
|
_DEFAULT_CRITICAL_RATIO = 2.5
|
||||||
|
_TELEMETRY_SCHEMA = "goldtether_coherence_v1"
|
||||||
|
|
||||||
|
|
||||||
|
class OperatingMode(str, Enum):
|
||||||
|
"""Practice vs Serve — risk-reward physics boundary."""
|
||||||
|
|
||||||
|
PRACTICE = "practice"
|
||||||
|
SERVE = "serve"
|
||||||
|
|
||||||
|
|
||||||
|
class AutonomyBand(str, Enum):
|
||||||
|
"""Residual-relative autonomy envelope (ADR-0238)."""
|
||||||
|
|
||||||
|
AUTONOMOUS = "autonomous"
|
||||||
|
SUPERVISED_BLEND = "supervised_blend"
|
||||||
|
FAIL_CLOSED = "fail_closed"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class GoldTetherConfig:
|
||||||
|
"""Named configuration only — no silent magic constants at call sites."""
|
||||||
|
|
||||||
|
decay_N: int = _DEFAULT_DECAY_N
|
||||||
|
w_drift: float = _DEFAULT_W_DRIFT
|
||||||
|
floor_init: float = _DEFAULT_FLOOR_INIT
|
||||||
|
critical_ratio: float = _DEFAULT_CRITICAL_RATIO
|
||||||
|
practice_autonomy_enabled: bool = False
|
||||||
|
serve_supervised_blend_authorized: bool = False
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.decay_N < 1:
|
||||||
|
raise ValueError("decay_N must be >= 1")
|
||||||
|
if not 0.0 <= self.w_drift <= 1.0:
|
||||||
|
raise ValueError("w_drift must be in [0, 1]")
|
||||||
|
if self.floor_init <= 0.0:
|
||||||
|
raise ValueError("floor_init must be positive")
|
||||||
|
if self.critical_ratio <= 1.0:
|
||||||
|
raise ValueError("critical_ratio must be > 1")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class CoherenceResidual:
|
||||||
|
"""Harmonized residual: drift + geometric distance (normalized).
|
||||||
|
|
||||||
|
Distinct from ADR-0006 ``EnergyProfile.coherence_residual`` and from
|
||||||
|
ADR-0239 Procrustes/Surprise residuals.
|
||||||
|
"""
|
||||||
|
|
||||||
|
drift: float
|
||||||
|
geometric_distance: float
|
||||||
|
combined: float
|
||||||
|
kappa: float
|
||||||
|
pseudoscalar_current: float
|
||||||
|
pseudoscalar_reference: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AutonomyDecision:
|
||||||
|
band: AutonomyBand
|
||||||
|
residual: float
|
||||||
|
floor: float
|
||||||
|
critical: float
|
||||||
|
mode: OperatingMode
|
||||||
|
blend_alpha: float
|
||||||
|
reason: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class PseudoscalarFloorState:
|
||||||
|
"""Dynamic grade-5 coherence floor + sign/magnitude telemetry."""
|
||||||
|
|
||||||
|
value: float
|
||||||
|
sign: float
|
||||||
|
n_samples: int
|
||||||
|
last_update_step: int
|
||||||
|
primal_floor: float
|
||||||
|
recent_residuals: tuple[float, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
def _as_mv(v: np.ndarray, name: str) -> np.ndarray:
|
||||||
|
arr = np.asarray(v, dtype=np.float64)
|
||||||
|
if arr.shape != (N_COMPONENTS,):
|
||||||
|
raise ValueError(f"{name} must have shape ({N_COMPONENTS},); got {arr.shape}")
|
||||||
|
return arr
|
||||||
|
|
||||||
|
|
||||||
|
def _pseudoscalar(v: np.ndarray) -> float:
|
||||||
|
return float(v[_PSEUDOSCALAR_IDX])
|
||||||
|
|
||||||
|
|
||||||
|
def _geometric_distance(a: np.ndarray, b: np.ndarray) -> float:
|
||||||
|
"""Closed geometric distance on the versor manifold.
|
||||||
|
|
||||||
|
Uses ``|| reverse(a) * b - 1 ||_F`` — zero iff a and b are the same unit
|
||||||
|
versor (up to float noise). Not cosine similarity; not ANN.
|
||||||
|
"""
|
||||||
|
product = geometric_product(reverse(a), b).astype(np.float64)
|
||||||
|
product[0] -= 1.0
|
||||||
|
return float(np.linalg.norm(product))
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_distance(d: float, scale: float = 2.0) -> float:
|
||||||
|
"""Map unbounded residual to [0, 1) for blend with drift."""
|
||||||
|
if d <= 0.0:
|
||||||
|
return 0.0
|
||||||
|
return float(d / (d + scale))
|
||||||
|
|
||||||
|
|
||||||
|
def derive_kappa(combined_residual: float, floor: float) -> float:
|
||||||
|
"""Monotone κ from residual relative to floor.
|
||||||
|
|
||||||
|
κ ∈ (0, 1]: small residual → κ near 1 (more trust); large residual → κ → 0.
|
||||||
|
Used only to scale dual-correction blend weight — never to invent content.
|
||||||
|
"""
|
||||||
|
if floor <= _NEAR_ZERO:
|
||||||
|
floor = _NEAR_ZERO
|
||||||
|
ratio = max(0.0, float(combined_residual) / float(floor))
|
||||||
|
return float(1.0 / (1.0 + ratio))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GoldTetherMonitor:
|
||||||
|
"""Stateful monitor for coherence residual, floor, and autonomy decisions.
|
||||||
|
|
||||||
|
State is explicit and reconstructible; updates are pure replacements on
|
||||||
|
``floor_state`` (immutable snapshots). Deterministic for identical sequences.
|
||||||
|
"""
|
||||||
|
|
||||||
|
config: GoldTetherConfig = field(default_factory=GoldTetherConfig)
|
||||||
|
floor_state: PseudoscalarFloorState = field(init=False)
|
||||||
|
_step: int = field(default=0, init=False, repr=False)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self.floor_state = PseudoscalarFloorState(
|
||||||
|
value=float(self.config.floor_init),
|
||||||
|
sign=1.0,
|
||||||
|
n_samples=0,
|
||||||
|
last_update_step=0,
|
||||||
|
primal_floor=float(self.config.floor_init),
|
||||||
|
recent_residuals=(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def measure(
|
||||||
|
self,
|
||||||
|
current: np.ndarray,
|
||||||
|
reference: np.ndarray,
|
||||||
|
*,
|
||||||
|
mode: OperatingMode | str = OperatingMode.PRACTICE,
|
||||||
|
) -> CoherenceResidual:
|
||||||
|
"""Compute harmonized coherence residual (pure; does not mutate floor)."""
|
||||||
|
_ = OperatingMode(mode) # validate
|
||||||
|
cur = _as_mv(current, "current")
|
||||||
|
ref = _as_mv(reference, "reference")
|
||||||
|
ps_c = _pseudoscalar(cur)
|
||||||
|
ps_r = _pseudoscalar(ref)
|
||||||
|
drift = abs(ps_c - ps_r)
|
||||||
|
# Also fold absolute pseudoscalar magnitude loss relative to reference.
|
||||||
|
drift = max(drift, abs(abs(ps_c) - abs(ps_r)))
|
||||||
|
geo = _geometric_distance(ref, cur)
|
||||||
|
geo_n = _normalize_distance(geo)
|
||||||
|
w = float(self.config.w_drift)
|
||||||
|
combined = w * drift + (1.0 - w) * geo_n
|
||||||
|
kappa = derive_kappa(combined, self.floor_state.value)
|
||||||
|
return CoherenceResidual(
|
||||||
|
drift=float(drift),
|
||||||
|
geometric_distance=float(geo),
|
||||||
|
combined=float(combined),
|
||||||
|
kappa=float(kappa),
|
||||||
|
pseudoscalar_current=ps_c,
|
||||||
|
pseudoscalar_reference=ps_r,
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_floor(
|
||||||
|
self,
|
||||||
|
residual: CoherenceResidual | float,
|
||||||
|
*,
|
||||||
|
mode: OperatingMode | str = OperatingMode.PRACTICE,
|
||||||
|
success: bool = True,
|
||||||
|
pseudoscalar_sign: float | None = None,
|
||||||
|
) -> PseudoscalarFloorState:
|
||||||
|
"""Update dynamic floor from practice successes only.
|
||||||
|
|
||||||
|
Serve mode never promotes the floor. Failures only append telemetry
|
||||||
|
window; they do not raise the autonomy envelope.
|
||||||
|
"""
|
||||||
|
op_mode = OperatingMode(mode)
|
||||||
|
r = float(residual.combined if isinstance(residual, CoherenceResidual) else residual)
|
||||||
|
self._step += 1
|
||||||
|
recent = list(self.floor_state.recent_residuals) + [r]
|
||||||
|
decay_n = int(self.config.decay_N)
|
||||||
|
if len(recent) > decay_n:
|
||||||
|
recent = recent[-decay_n:]
|
||||||
|
|
||||||
|
new_value = self.floor_state.value
|
||||||
|
new_sign = self.floor_state.sign
|
||||||
|
n_samples = self.floor_state.n_samples
|
||||||
|
|
||||||
|
if op_mode is OperatingMode.PRACTICE and success and r < self.floor_state.value:
|
||||||
|
# Tighten floor toward observed residual while keeping primal anchor.
|
||||||
|
# Weighted mean of recent successes under decay window.
|
||||||
|
window = [x for x in recent if x < self.floor_state.value] or [r]
|
||||||
|
mean_r = float(sum(window) / len(window))
|
||||||
|
# Blend toward mean_r but never below half primal (safety).
|
||||||
|
floor_floor = 0.5 * self.floor_state.primal_floor
|
||||||
|
candidate = 0.5 * self.floor_state.value + 0.5 * mean_r
|
||||||
|
new_value = max(floor_floor, min(self.floor_state.value, candidate))
|
||||||
|
n_samples = n_samples + 1
|
||||||
|
if pseudoscalar_sign is not None and abs(pseudoscalar_sign) > _NEAR_ZERO:
|
||||||
|
new_sign = 1.0 if pseudoscalar_sign >= 0.0 else -1.0
|
||||||
|
|
||||||
|
self.floor_state = PseudoscalarFloorState(
|
||||||
|
value=float(new_value),
|
||||||
|
sign=float(new_sign),
|
||||||
|
n_samples=int(n_samples),
|
||||||
|
last_update_step=int(self._step),
|
||||||
|
primal_floor=float(self.floor_state.primal_floor),
|
||||||
|
recent_residuals=tuple(float(x) for x in recent),
|
||||||
|
)
|
||||||
|
return self.floor_state
|
||||||
|
|
||||||
|
def decide(
|
||||||
|
self,
|
||||||
|
residual: CoherenceResidual | float,
|
||||||
|
*,
|
||||||
|
mode: OperatingMode | str = OperatingMode.PRACTICE,
|
||||||
|
floor: PseudoscalarFloorState | None = None,
|
||||||
|
) -> AutonomyDecision:
|
||||||
|
"""Map residual + mode → autonomy band (HITL-safe defaults)."""
|
||||||
|
op_mode = OperatingMode(mode)
|
||||||
|
r = float(residual.combined if isinstance(residual, CoherenceResidual) else residual)
|
||||||
|
fl = floor if floor is not None else self.floor_state
|
||||||
|
floor_v = float(fl.value)
|
||||||
|
critical = floor_v * float(self.config.critical_ratio)
|
||||||
|
|
||||||
|
if r > critical:
|
||||||
|
return AutonomyDecision(
|
||||||
|
band=AutonomyBand.FAIL_CLOSED,
|
||||||
|
residual=r,
|
||||||
|
floor=floor_v,
|
||||||
|
critical=critical,
|
||||||
|
mode=op_mode,
|
||||||
|
blend_alpha=0.0,
|
||||||
|
reason="residual_above_critical",
|
||||||
|
)
|
||||||
|
|
||||||
|
if op_mode is OperatingMode.SERVE:
|
||||||
|
# Serve never autonomous. Supervised blend only if explicitly authorized.
|
||||||
|
if r < floor_v and self.config.serve_supervised_blend_authorized:
|
||||||
|
alpha = float(1.0 - derive_kappa(r, floor_v))
|
||||||
|
return AutonomyDecision(
|
||||||
|
band=AutonomyBand.SUPERVISED_BLEND,
|
||||||
|
residual=r,
|
||||||
|
floor=floor_v,
|
||||||
|
critical=critical,
|
||||||
|
mode=op_mode,
|
||||||
|
blend_alpha=alpha,
|
||||||
|
reason="serve_supervised_authorized",
|
||||||
|
)
|
||||||
|
if r < floor_v:
|
||||||
|
return AutonomyDecision(
|
||||||
|
band=AutonomyBand.FAIL_CLOSED,
|
||||||
|
residual=r,
|
||||||
|
floor=floor_v,
|
||||||
|
critical=critical,
|
||||||
|
mode=op_mode,
|
||||||
|
blend_alpha=0.0,
|
||||||
|
reason="serve_hitl_default_fail_closed",
|
||||||
|
)
|
||||||
|
# floor <= r <= critical on serve: fail-closed unless blend authorized
|
||||||
|
if self.config.serve_supervised_blend_authorized:
|
||||||
|
alpha = float(min(1.0, (r - floor_v) / max(critical - floor_v, _NEAR_ZERO)))
|
||||||
|
return AutonomyDecision(
|
||||||
|
band=AutonomyBand.SUPERVISED_BLEND,
|
||||||
|
residual=r,
|
||||||
|
floor=floor_v,
|
||||||
|
critical=critical,
|
||||||
|
mode=op_mode,
|
||||||
|
blend_alpha=alpha,
|
||||||
|
reason="serve_midband_supervised",
|
||||||
|
)
|
||||||
|
return AutonomyDecision(
|
||||||
|
band=AutonomyBand.FAIL_CLOSED,
|
||||||
|
residual=r,
|
||||||
|
floor=floor_v,
|
||||||
|
critical=critical,
|
||||||
|
mode=op_mode,
|
||||||
|
blend_alpha=0.0,
|
||||||
|
reason="serve_midband_fail_closed",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Practice path
|
||||||
|
if r < floor_v and self.config.practice_autonomy_enabled:
|
||||||
|
return AutonomyDecision(
|
||||||
|
band=AutonomyBand.AUTONOMOUS,
|
||||||
|
residual=r,
|
||||||
|
floor=floor_v,
|
||||||
|
critical=critical,
|
||||||
|
mode=op_mode,
|
||||||
|
blend_alpha=1.0,
|
||||||
|
reason="practice_below_floor_autonomy_enabled",
|
||||||
|
)
|
||||||
|
if r < floor_v:
|
||||||
|
return AutonomyDecision(
|
||||||
|
band=AutonomyBand.SUPERVISED_BLEND,
|
||||||
|
residual=r,
|
||||||
|
floor=floor_v,
|
||||||
|
critical=critical,
|
||||||
|
mode=op_mode,
|
||||||
|
blend_alpha=float(derive_kappa(r, floor_v)),
|
||||||
|
reason="practice_below_floor_supervised_default",
|
||||||
|
)
|
||||||
|
# floor <= r <= critical
|
||||||
|
alpha = float(1.0 - min(1.0, (r - floor_v) / max(critical - floor_v, _NEAR_ZERO)))
|
||||||
|
return AutonomyDecision(
|
||||||
|
band=AutonomyBand.SUPERVISED_BLEND,
|
||||||
|
residual=r,
|
||||||
|
floor=floor_v,
|
||||||
|
critical=critical,
|
||||||
|
mode=op_mode,
|
||||||
|
blend_alpha=max(0.0, min(1.0, alpha)),
|
||||||
|
reason="practice_midband_supervised",
|
||||||
|
)
|
||||||
|
|
||||||
|
def supervised_blend(
|
||||||
|
self,
|
||||||
|
source: np.ndarray,
|
||||||
|
target: np.ndarray,
|
||||||
|
alpha: float,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Manifold slerp from source toward target by alpha ∈ [0, 1].
|
||||||
|
|
||||||
|
Dual-correction surface on the Spin group (not Euclidean lerp):
|
||||||
|
|
||||||
|
R = word_transition_rotor(source, target) # = target * reverse(source)
|
||||||
|
out = rotor_power(R, α) * source # left composition
|
||||||
|
|
||||||
|
At α=0 → source; at α=1 → target (unit versors). Sandwich conjugation
|
||||||
|
would map the identity to itself and is the wrong geodesic for state
|
||||||
|
interpolation. Output must satisfy versor_condition < 1e-6.
|
||||||
|
"""
|
||||||
|
a = float(alpha)
|
||||||
|
if a < 0.0 or a > 1.0:
|
||||||
|
raise ValueError("alpha must be in [0, 1]")
|
||||||
|
src = _as_mv(source, "source")
|
||||||
|
tgt = _as_mv(target, "target")
|
||||||
|
if a <= _NEAR_ZERO:
|
||||||
|
out = src.copy()
|
||||||
|
elif a >= 1.0 - _NEAR_ZERO:
|
||||||
|
out = tgt.copy()
|
||||||
|
else:
|
||||||
|
R = word_transition_rotor(src, tgt)
|
||||||
|
R_a = rotor_power(R, a)
|
||||||
|
out = geometric_product(R_a, src).astype(np.float64)
|
||||||
|
|
||||||
|
cond = versor_condition(out)
|
||||||
|
if cond >= _CLOSURE_TOL:
|
||||||
|
raise ValueError(
|
||||||
|
f"supervised_blend broke versor_condition: {cond:.3e} >= {_CLOSURE_TOL}"
|
||||||
|
)
|
||||||
|
return out.astype(np.float64, copy=False)
|
||||||
|
|
||||||
|
def telemetry(self) -> dict[str, Any]:
|
||||||
|
"""Schema-versioned pure projection for workbench channels."""
|
||||||
|
fl = self.floor_state
|
||||||
|
return {
|
||||||
|
"schema_version": _TELEMETRY_SCHEMA,
|
||||||
|
"pseudoscalar_floor": float(fl.value),
|
||||||
|
"pseudoscalar_sign": float(fl.sign),
|
||||||
|
"n_samples": int(fl.n_samples),
|
||||||
|
"last_update_step": int(fl.last_update_step),
|
||||||
|
"primal_floor": float(fl.primal_floor),
|
||||||
|
"recent_residuals": list(fl.recent_residuals),
|
||||||
|
"config": {
|
||||||
|
"decay_N": int(self.config.decay_N),
|
||||||
|
"w_drift": float(self.config.w_drift),
|
||||||
|
"floor_init": float(self.config.floor_init),
|
||||||
|
"critical_ratio": float(self.config.critical_ratio),
|
||||||
|
"practice_autonomy_enabled": bool(self.config.practice_autonomy_enabled),
|
||||||
|
"serve_supervised_blend_authorized": bool(
|
||||||
|
self.config.serve_supervised_blend_authorized
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def with_config(monitor: GoldTetherMonitor, **updates: Any) -> GoldTetherMonitor:
|
||||||
|
"""Return a new monitor with updated config (immutability-friendly)."""
|
||||||
|
cfg = replace(monitor.config, **updates)
|
||||||
|
m = GoldTetherMonitor(config=cfg)
|
||||||
|
m.floor_state = monitor.floor_state
|
||||||
|
m._step = monitor._step
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def residual_from_mapping(payload: Mapping[str, Any]) -> float:
|
||||||
|
"""Deterministic residual extract for telemetry/replay fixtures."""
|
||||||
|
if "combined" in payload:
|
||||||
|
return float(payload["combined"])
|
||||||
|
raise KeyError("payload missing combined residual")
|
||||||
190
core/physics/self_authorship.py
Normal file
190
core/physics/self_authorship.py
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
"""core.physics.self_authorship — Self-Authorship Miner scaffold (ADR-0240).
|
||||||
|
|
||||||
|
Geometry-guided ADR / teaching proposals under invariants. Emits
|
||||||
|
**proposal-only** artifacts (SPECULATIVE). Never writes vault COHERENT,
|
||||||
|
never mutates packs, never touches serving.
|
||||||
|
|
||||||
|
Complements the existing auto-proposal corridor (ADR-0151). This miner
|
||||||
|
produces structured proposal dicts; promotion remains human-reviewed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Mapping, Sequence
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from algebra.cl41 import N_COMPONENTS
|
||||||
|
from algebra.versor import versor_condition
|
||||||
|
from core.physics.dynamic_manifold import conformal_procrustes, signature_aware_pca
|
||||||
|
from core.physics.goldtether import CoherenceResidual, GoldTetherMonitor, OperatingMode
|
||||||
|
from core.physics.surprise import dual_operator, surprise_residual
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AuthorshipProposal:
|
||||||
|
"""Proposal-only artifact — never auto-accepted."""
|
||||||
|
|
||||||
|
proposal_id: str
|
||||||
|
kind: str
|
||||||
|
epistemic_status: str # always SPECULATIVE at emission
|
||||||
|
drift_residual: float
|
||||||
|
closure_proof: Mapping[str, Any]
|
||||||
|
body: Mapping[str, Any]
|
||||||
|
adr_refs: tuple[str, ...]
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"proposal_id": self.proposal_id,
|
||||||
|
"kind": self.kind,
|
||||||
|
"epistemic_status": self.epistemic_status,
|
||||||
|
"drift_residual": self.drift_residual,
|
||||||
|
"closure_proof": dict(self.closure_proof),
|
||||||
|
"body": dict(self.body),
|
||||||
|
"adr_refs": list(self.adr_refs),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _content_id(payload: Mapping[str, Any]) -> str:
|
||||||
|
raw = json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str)
|
||||||
|
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24]
|
||||||
|
|
||||||
|
|
||||||
|
class SelfAuthorshipMiner:
|
||||||
|
"""Mine minimal extension proposals from geometric residual structure."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
goldtether: GoldTetherMonitor | None = None,
|
||||||
|
residual_threshold: float = 0.25,
|
||||||
|
) -> None:
|
||||||
|
self.goldtether = goldtether or GoldTetherMonitor()
|
||||||
|
self.residual_threshold = float(residual_threshold)
|
||||||
|
|
||||||
|
def mine_from_trajectory(
|
||||||
|
self,
|
||||||
|
current: np.ndarray,
|
||||||
|
reference: np.ndarray,
|
||||||
|
*,
|
||||||
|
basis: Sequence[np.ndarray] = (),
|
||||||
|
analogs: Sequence[tuple[str, np.ndarray, np.ndarray]] = (),
|
||||||
|
notes: str = "",
|
||||||
|
) -> tuple[AuthorshipProposal, ...]:
|
||||||
|
"""Emit zero or more SPECULATIVE proposals. Never stores them."""
|
||||||
|
cur = np.asarray(current, dtype=np.float64)
|
||||||
|
ref = np.asarray(reference, dtype=np.float64)
|
||||||
|
if cur.shape != (N_COMPONENTS,) or ref.shape != (N_COMPONENTS,):
|
||||||
|
raise ValueError("current and reference must be 32-component multivectors")
|
||||||
|
|
||||||
|
residual: CoherenceResidual = self.goldtether.measure(
|
||||||
|
cur, ref, mode=OperatingMode.PRACTICE
|
||||||
|
)
|
||||||
|
proposals: list[AuthorshipProposal] = []
|
||||||
|
|
||||||
|
# Closure proof for the reference/current pair under transition.
|
||||||
|
try:
|
||||||
|
proc = conformal_procrustes([ref], [cur])
|
||||||
|
closure_ok = versor_condition(proc.versor) < 1e-6
|
||||||
|
proc_res = float(proc.residual_norm)
|
||||||
|
except ValueError as exc:
|
||||||
|
closure_ok = False
|
||||||
|
proc_res = float("inf")
|
||||||
|
proc_err = str(exc)
|
||||||
|
else:
|
||||||
|
proc_err = ""
|
||||||
|
|
||||||
|
closure_proof = {
|
||||||
|
"versor_condition_current": float(versor_condition(cur)),
|
||||||
|
"versor_condition_reference": float(versor_condition(ref)),
|
||||||
|
"procrustes_residual": proc_res,
|
||||||
|
"procrustes_closed": closure_ok,
|
||||||
|
"procrustes_error": proc_err,
|
||||||
|
"coherence_combined": float(residual.combined),
|
||||||
|
"kappa": float(residual.kappa),
|
||||||
|
}
|
||||||
|
|
||||||
|
if residual.combined >= self.residual_threshold and closure_ok:
|
||||||
|
body = {
|
||||||
|
"notes": notes,
|
||||||
|
"suggested_action": "review_coherence_gap",
|
||||||
|
"drift": float(residual.drift),
|
||||||
|
"geometric_distance": float(residual.geometric_distance),
|
||||||
|
}
|
||||||
|
pid = _content_id({"kind": "coherence_gap", "body": body, "proof": closure_proof})
|
||||||
|
proposals.append(
|
||||||
|
AuthorshipProposal(
|
||||||
|
proposal_id=f"selfauth-{pid}",
|
||||||
|
kind="coherence_gap",
|
||||||
|
epistemic_status="SPECULATIVE",
|
||||||
|
drift_residual=float(residual.combined),
|
||||||
|
closure_proof=closure_proof,
|
||||||
|
body=body,
|
||||||
|
adr_refs=("ADR-0238", "ADR-0240"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if basis:
|
||||||
|
surp = surprise_residual(cur, basis)
|
||||||
|
dual = dual_operator(
|
||||||
|
cur,
|
||||||
|
basis,
|
||||||
|
analogs,
|
||||||
|
kappa=max(residual.kappa, 1e-6),
|
||||||
|
)
|
||||||
|
if dual.productive:
|
||||||
|
body = {
|
||||||
|
"notes": notes,
|
||||||
|
"suggested_action": "review_analogical_extension",
|
||||||
|
"surprise_norm": float(surp.residual_norm),
|
||||||
|
"selected_analog_id": dual.selected_analog_id,
|
||||||
|
"procrustes_residual": (
|
||||||
|
float(dual.procrustes.residual_norm) if dual.procrustes else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
pid = _content_id(
|
||||||
|
{"kind": "analogical_extension", "body": body, "proof": closure_proof}
|
||||||
|
)
|
||||||
|
proposals.append(
|
||||||
|
AuthorshipProposal(
|
||||||
|
proposal_id=f"selfauth-{pid}",
|
||||||
|
kind="analogical_extension",
|
||||||
|
epistemic_status="SPECULATIVE",
|
||||||
|
drift_residual=float(surp.residual_norm),
|
||||||
|
closure_proof=closure_proof,
|
||||||
|
body=body,
|
||||||
|
adr_refs=("ADR-0239", "ADR-0240"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optional manifold annotation when a small cloud is available via analogs.
|
||||||
|
cloud = [ref, cur] + [s for _, s, _ in analogs] + [t for _, _, t in analogs]
|
||||||
|
if len(cloud) >= 2:
|
||||||
|
pca = signature_aware_pca(cloud, max_axes=4)
|
||||||
|
if pca.n_null > 0:
|
||||||
|
body = {
|
||||||
|
"notes": notes,
|
||||||
|
"suggested_action": "review_null_axes",
|
||||||
|
"n_null": int(pca.n_null),
|
||||||
|
"n_spacelike": int(pca.n_spacelike),
|
||||||
|
"n_timelike": int(pca.n_timelike),
|
||||||
|
}
|
||||||
|
pid = _content_id({"kind": "null_axis_review", "body": body})
|
||||||
|
proposals.append(
|
||||||
|
AuthorshipProposal(
|
||||||
|
proposal_id=f"selfauth-{pid}",
|
||||||
|
kind="null_axis_review",
|
||||||
|
epistemic_status="SPECULATIVE",
|
||||||
|
drift_residual=float(residual.combined),
|
||||||
|
closure_proof=closure_proof,
|
||||||
|
body=body,
|
||||||
|
adr_refs=("ADR-0239", "ADR-0240"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Stable order by proposal_id for replay-determinism.
|
||||||
|
proposals.sort(key=lambda p: p.proposal_id)
|
||||||
|
return tuple(proposals)
|
||||||
212
core/physics/surprise.py
Normal file
212
core/physics/surprise.py
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
"""core.physics.surprise — Surprise Residual + dual with Procrustes (ADR-0239).
|
||||||
|
|
||||||
|
Surprise Residual: S(x) = x − proj_B(x)
|
||||||
|
where B is a known basis (ordered multivector span). High surprise seeds
|
||||||
|
Conformal Procrustes against vault analogs → productive novelty only when the
|
||||||
|
post-transfer residual is below threshold; otherwise typed refuse.
|
||||||
|
|
||||||
|
No sampling. No statistical ranking. Deterministic ordered analog lists.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from algebra.cl41 import N_COMPONENTS
|
||||||
|
from algebra.versor import versor_condition
|
||||||
|
from core.physics.dynamic_manifold import (
|
||||||
|
ConformalProcrustesResult,
|
||||||
|
conformal_procrustes,
|
||||||
|
procrustes_residual,
|
||||||
|
)
|
||||||
|
|
||||||
|
_NEAR_ZERO = 1e-12
|
||||||
|
_CLOSURE_TOL = 1e-6
|
||||||
|
_DEFAULT_PRODUCTIVE_THRESHOLD = 0.35
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SurpriseResult:
|
||||||
|
residual_mv: np.ndarray
|
||||||
|
residual_norm: float
|
||||||
|
projection: np.ndarray
|
||||||
|
basis_rank: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AnalogySeed:
|
||||||
|
analog_id: str
|
||||||
|
source: np.ndarray
|
||||||
|
target: np.ndarray
|
||||||
|
surprise_affinity: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class DualOperatorResult:
|
||||||
|
surprise: SurpriseResult
|
||||||
|
procrustes: ConformalProcrustesResult | None
|
||||||
|
productive: bool
|
||||||
|
kappa: float
|
||||||
|
reason: str
|
||||||
|
selected_analog_id: str | None
|
||||||
|
|
||||||
|
|
||||||
|
def _as_mv(v: np.ndarray, name: str) -> np.ndarray:
|
||||||
|
arr = np.asarray(v, dtype=np.float64)
|
||||||
|
if arr.shape != (N_COMPONENTS,):
|
||||||
|
raise ValueError(f"{name} must have shape ({N_COMPONENTS},); got {arr.shape}")
|
||||||
|
return arr
|
||||||
|
|
||||||
|
|
||||||
|
def _orthonormalize_basis(basis: Sequence[np.ndarray]) -> np.ndarray:
|
||||||
|
"""Deterministic Gram–Schmidt on coefficient space (ordered input).
|
||||||
|
|
||||||
|
Returns matrix B with shape (rank, 32). Zero / dependent vectors dropped
|
||||||
|
in order — not silently as "null PCA axes"; rank is reported.
|
||||||
|
"""
|
||||||
|
cols: list[np.ndarray] = []
|
||||||
|
for i, b in enumerate(basis):
|
||||||
|
v = _as_mv(b, f"basis[{i}]").copy()
|
||||||
|
for u in cols:
|
||||||
|
v = v - float(np.dot(v, u)) * u
|
||||||
|
n = float(np.linalg.norm(v))
|
||||||
|
if n < _NEAR_ZERO:
|
||||||
|
continue
|
||||||
|
cols.append(v / n)
|
||||||
|
if not cols:
|
||||||
|
return np.zeros((0, N_COMPONENTS), dtype=np.float64)
|
||||||
|
return np.stack(cols, axis=0)
|
||||||
|
|
||||||
|
|
||||||
|
def project_onto_basis(x: np.ndarray, basis: Sequence[np.ndarray]) -> np.ndarray:
|
||||||
|
"""Orthogonal projection of x onto span(B) in coefficient space."""
|
||||||
|
x_arr = _as_mv(x, "x")
|
||||||
|
B = _orthonormalize_basis(basis)
|
||||||
|
if B.shape[0] == 0:
|
||||||
|
return np.zeros(N_COMPONENTS, dtype=np.float64)
|
||||||
|
# proj = sum_i <x, u_i> u_i
|
||||||
|
coeffs = B @ x_arr
|
||||||
|
return (B.T @ coeffs).astype(np.float64, copy=False)
|
||||||
|
|
||||||
|
|
||||||
|
def surprise_residual(
|
||||||
|
x: np.ndarray,
|
||||||
|
basis: Sequence[np.ndarray],
|
||||||
|
) -> SurpriseResult:
|
||||||
|
"""S(x) = x − proj_B(x). Residual is orthogonal to span(B)."""
|
||||||
|
x_arr = _as_mv(x, "x")
|
||||||
|
proj = project_onto_basis(x_arr, basis)
|
||||||
|
residual = (x_arr - proj).astype(np.float64, copy=False)
|
||||||
|
B = _orthonormalize_basis(basis)
|
||||||
|
return SurpriseResult(
|
||||||
|
residual_mv=residual,
|
||||||
|
residual_norm=float(np.linalg.norm(residual)),
|
||||||
|
projection=proj,
|
||||||
|
basis_rank=int(B.shape[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def analogy_seed(
|
||||||
|
surprise: SurpriseResult,
|
||||||
|
analogs: Sequence[tuple[str, np.ndarray, np.ndarray]],
|
||||||
|
*,
|
||||||
|
top_k: int | None = None,
|
||||||
|
) -> tuple[AnalogySeed, ...]:
|
||||||
|
"""Rank vault analogs by affinity to the surprise residual (deterministic).
|
||||||
|
|
||||||
|
Affinity = |cos| between residual and (target − source) direction in
|
||||||
|
coefficient space. Higher affinity → better structural candidate.
|
||||||
|
Order is stable: affinity desc, then analog_id asc.
|
||||||
|
"""
|
||||||
|
if surprise.residual_norm < _NEAR_ZERO:
|
||||||
|
return ()
|
||||||
|
r = surprise.residual_mv
|
||||||
|
r_n = surprise.residual_norm
|
||||||
|
seeds: list[AnalogySeed] = []
|
||||||
|
for item in analogs:
|
||||||
|
if len(item) != 3:
|
||||||
|
raise ValueError("each analog must be (id, source, target)")
|
||||||
|
aid, src, tgt = item
|
||||||
|
s = _as_mv(src, f"analog[{aid}].source")
|
||||||
|
t = _as_mv(tgt, f"analog[{aid}].target")
|
||||||
|
delta = t - s
|
||||||
|
dn = float(np.linalg.norm(delta))
|
||||||
|
if dn < _NEAR_ZERO:
|
||||||
|
aff = 0.0
|
||||||
|
else:
|
||||||
|
aff = abs(float(np.dot(r, delta)) / (r_n * dn))
|
||||||
|
seeds.append(
|
||||||
|
AnalogySeed(
|
||||||
|
analog_id=str(aid),
|
||||||
|
source=s,
|
||||||
|
target=t,
|
||||||
|
surprise_affinity=float(aff),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
seeds.sort(key=lambda s: (-s.surprise_affinity, s.analog_id))
|
||||||
|
if top_k is not None:
|
||||||
|
seeds = seeds[: max(0, int(top_k))]
|
||||||
|
return tuple(seeds)
|
||||||
|
|
||||||
|
|
||||||
|
def dual_operator(
|
||||||
|
x: np.ndarray,
|
||||||
|
basis: Sequence[np.ndarray],
|
||||||
|
analogs: Sequence[tuple[str, np.ndarray, np.ndarray]],
|
||||||
|
*,
|
||||||
|
kappa: float = 1.0,
|
||||||
|
productive_threshold: float = _DEFAULT_PRODUCTIVE_THRESHOLD,
|
||||||
|
min_surprise: float = 1e-6,
|
||||||
|
) -> DualOperatorResult:
|
||||||
|
"""Surprise + Procrustes dual.
|
||||||
|
|
||||||
|
High surprise seeds Procrustes against the best analog. Productive only when
|
||||||
|
post-transfer residual ≤ productive_threshold * (1/κ-scaled). κ from
|
||||||
|
CoherenceGoldTether scales the threshold (higher κ → stricter).
|
||||||
|
"""
|
||||||
|
if kappa <= 0.0:
|
||||||
|
raise ValueError("kappa must be positive")
|
||||||
|
surprise = surprise_residual(x, basis)
|
||||||
|
if surprise.residual_norm < min_surprise:
|
||||||
|
return DualOperatorResult(
|
||||||
|
surprise=surprise,
|
||||||
|
procrustes=None,
|
||||||
|
productive=False,
|
||||||
|
kappa=float(kappa),
|
||||||
|
reason="surprise_below_minimum",
|
||||||
|
selected_analog_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
seeds = analogy_seed(surprise, analogs, top_k=1)
|
||||||
|
if not seeds:
|
||||||
|
return DualOperatorResult(
|
||||||
|
surprise=surprise,
|
||||||
|
procrustes=None,
|
||||||
|
productive=False,
|
||||||
|
kappa=float(kappa),
|
||||||
|
reason="no_analogs",
|
||||||
|
selected_analog_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
best = seeds[0]
|
||||||
|
# Transfer: Procrustes from analog source→target applied as structural map;
|
||||||
|
# measure residual of mapping x's projection-completion toward analog target shape.
|
||||||
|
proc = conformal_procrustes([best.source], [best.target])
|
||||||
|
# Residual of applying the analog's versor to x vs. the analog target direction.
|
||||||
|
transfer_res = procrustes_residual(x, best.target, proc.versor)
|
||||||
|
# Also include native procrustes residual of the analog pair itself.
|
||||||
|
residual = max(float(proc.residual_norm), float(transfer_res))
|
||||||
|
threshold = float(productive_threshold) / float(kappa)
|
||||||
|
productive = residual <= threshold and versor_condition(proc.versor) < _CLOSURE_TOL
|
||||||
|
|
||||||
|
return DualOperatorResult(
|
||||||
|
surprise=surprise,
|
||||||
|
procrustes=proc,
|
||||||
|
productive=bool(productive),
|
||||||
|
kappa=float(kappa),
|
||||||
|
reason="productive_novelty" if productive else "residual_above_threshold",
|
||||||
|
selected_analog_id=best.analog_id,
|
||||||
|
)
|
||||||
130
core/physics/temporal_gate.py
Normal file
130
core/physics/temporal_gate.py
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
"""core.physics.temporal_gate — Temporal Admissibility Gate (ADR-0240).
|
||||||
|
|
||||||
|
Wisdom as geometry: refuse premature but eventually-admissible claims with a
|
||||||
|
typed NOT_YET. Never confabulates early. Pure predicate — no side effects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
|
||||||
|
class TemporalVerdict(str, Enum):
|
||||||
|
ADMIT = "admit"
|
||||||
|
NOT_YET = "not_yet"
|
||||||
|
REFUSE = "refuse"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class TemporalContext:
|
||||||
|
"""Geometric/temporal preconditions for a claim.
|
||||||
|
|
||||||
|
All fields are explicit; missing evidence is not inventable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
step: int
|
||||||
|
min_step: int = 0
|
||||||
|
required_evidence_count: int = 0
|
||||||
|
evidence_count: int = 0
|
||||||
|
coherence_residual: float = 0.0
|
||||||
|
residual_ceiling: float = 1.0
|
||||||
|
prerequisites_met: bool = True
|
||||||
|
claim_id: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class TemporalDecision:
|
||||||
|
verdict: TemporalVerdict
|
||||||
|
reason: str
|
||||||
|
claim_id: str
|
||||||
|
disclosure: Mapping[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class TemporalAdmissibilityGate:
|
||||||
|
"""Pure temporal admissibility checks."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
require_prerequisites: bool = True,
|
||||||
|
enforce_residual_ceiling: bool = True,
|
||||||
|
) -> None:
|
||||||
|
self.require_prerequisites = bool(require_prerequisites)
|
||||||
|
self.enforce_residual_ceiling = bool(enforce_residual_ceiling)
|
||||||
|
|
||||||
|
def evaluate(self, ctx: TemporalContext) -> TemporalDecision:
|
||||||
|
cid = str(ctx.claim_id)
|
||||||
|
|
||||||
|
if ctx.step < 0 or ctx.min_step < 0:
|
||||||
|
return TemporalDecision(
|
||||||
|
verdict=TemporalVerdict.REFUSE,
|
||||||
|
reason="negative_time_index",
|
||||||
|
claim_id=cid,
|
||||||
|
disclosure={
|
||||||
|
"type": "temporal_refuse",
|
||||||
|
"detail": "time indices must be non-negative",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.require_prerequisites and not ctx.prerequisites_met:
|
||||||
|
return TemporalDecision(
|
||||||
|
verdict=TemporalVerdict.REFUSE,
|
||||||
|
reason="prerequisites_unmet",
|
||||||
|
claim_id=cid,
|
||||||
|
disclosure={
|
||||||
|
"type": "temporal_refuse",
|
||||||
|
"detail": "required prerequisites are not met",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.enforce_residual_ceiling and ctx.coherence_residual > ctx.residual_ceiling:
|
||||||
|
return TemporalDecision(
|
||||||
|
verdict=TemporalVerdict.REFUSE,
|
||||||
|
reason="residual_above_ceiling",
|
||||||
|
claim_id=cid,
|
||||||
|
disclosure={
|
||||||
|
"type": "temporal_refuse",
|
||||||
|
"detail": "coherence residual exceeds ceiling",
|
||||||
|
"residual": float(ctx.coherence_residual),
|
||||||
|
"ceiling": float(ctx.residual_ceiling),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if ctx.step < ctx.min_step:
|
||||||
|
return TemporalDecision(
|
||||||
|
verdict=TemporalVerdict.NOT_YET,
|
||||||
|
reason="before_min_step",
|
||||||
|
claim_id=cid,
|
||||||
|
disclosure={
|
||||||
|
"type": "temporal_not_yet",
|
||||||
|
"detail": "fullness of time not reached",
|
||||||
|
"step": int(ctx.step),
|
||||||
|
"min_step": int(ctx.min_step),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if ctx.evidence_count < ctx.required_evidence_count:
|
||||||
|
return TemporalDecision(
|
||||||
|
verdict=TemporalVerdict.NOT_YET,
|
||||||
|
reason="insufficient_evidence",
|
||||||
|
claim_id=cid,
|
||||||
|
disclosure={
|
||||||
|
"type": "temporal_not_yet",
|
||||||
|
"detail": "evidence count below required floor",
|
||||||
|
"evidence_count": int(ctx.evidence_count),
|
||||||
|
"required_evidence_count": int(ctx.required_evidence_count),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return TemporalDecision(
|
||||||
|
verdict=TemporalVerdict.ADMIT,
|
||||||
|
reason="temporally_admissible",
|
||||||
|
claim_id=cid,
|
||||||
|
disclosure={
|
||||||
|
"type": "temporal_admit",
|
||||||
|
"step": int(ctx.step),
|
||||||
|
"evidence_count": int(ctx.evidence_count),
|
||||||
|
},
|
||||||
|
)
|
||||||
Loading…
Reference in a new issue