SilverSight/qubo/finsler_metric.py
allaun 7a973a06f6 feat(core): harden SilverSightCore and port canonical FixedPoint
- Remove Float from Core/SilverSightCore.lean (Receipt.pathCost is now Option Nat)
- Prove TIC theorems tic_never_decreases and computation_generates_time
- Add lakefile.lean, lean-toolchain, and .gitignore for .lake/
- Port Research-Stack Semantics.FixedPoint.lean to formal/CoreFormalism/FixedPoint.lean
- Delete thin Q16_16_Spec.lean; update Python/QUBO comments and docs
- Create AGENTS.md distilled from Research Stack core bindings
- Update REBASE_RULES.md to strip legacy hacks and reference AGENTS.md

Build: 2978 jobs, 0 errors (lake build)
2026-06-21 06:30:12 -05:00

427 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
finsler_metric.py -- Randers Metric F = α + β computation
Computes the Finsler-Randers metric on the Hachimoji 8-state simplex.
The metric is the UNIQUE geometry on Δ⁷ (proven by ChentsovFinite.lean):
F(a→b) = α(a,b) + β(a,b)
α: Fisher information metric (symmetric, from Chentsov uniqueness theorem)
β: Drift 1-form (asymmetric, encodes torsion / wind field)
The Fisher metric is canonical: Chentsov's theorem proves it is the ONLY
Riemannian metric on the probability simplex that is invariant under all
Markov embeddings (stochastic refinements).
Reference: CoreFormalism/ChentsovFinite.lean -- chentsov_hachimoji theorem
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
from typing import Any, Optional
import numpy as np
# =========================================================================
# Q16_16 Fixed-Point Constants (from CoreFormalism/FixedPoint.lean)
# =========================================================================
Q16_SCALE: int = 65536 # 2^16 -- number of subdivisions per unit
def to_q16(value: float) -> int:
"""Convert float to Q16_16 raw integer with canonical rounding."""
scaled = value * Q16_SCALE
rounded = round(scaled)
return max(-2147483648, min(2147483647, rounded))
def from_q16(raw: int) -> float:
"""Convert Q16_16 raw integer to float."""
return raw / Q16_SCALE
# =========================================================================
# Hachimoji 8-State System
# =========================================================================
# Greek states with phases (45° steps, from HachimojiBase.lean §5)
GREEK_STATES: list[str] = ["\u03a6", "\u039b", "\u03a1", "\u039a", "\u03a9", "\u03a3", "\u03a0", "\u0396"]
# Phase angles in degrees (0, 45, 90, 135, 180, 225, 270, 315)
GREEK_PHASE: dict[str, float] = {
"\u03a6": 0.0, "\u039b": 45.0, "\u03a1": 90.0, "\u039a": 135.0,
"\u03a9": 180.0, "\u03a3": 225.0, "\u03a0": 270.0, "\u0396": 315.0,
}
# Latin ↔ Greek bijection (from HachimojiBase.lean §2)
LATIN_TO_GREEK: dict[str, str] = {
"A": "\u03a6", "T": "\u039b", "G": "\u03a1", "C": "\u039a",
"B": "\u03a9", "S": "\u03a3", "P": "\u03a0", "Z": "\u0396",
}
GREEK_TO_LATIN: dict[str, str] = {v: k for k, v in LATIN_TO_GREEK.items()}
# =========================================================================
# State Descriptors (4D HachimojiState)
# =========================================================================
@dataclass
class HachimojiState4D:
"""4D descriptor for a Hachimoji state on the probability simplex.
Components:
- symbol: Greek letter (Φ Λ Ρ Κ Ω Σ Π Ζ)
- phase: angle on S¹ in degrees [0, 360)
- probability: point on Δ⁷ (8 probabilities, sum to 1)
- fisher_curvature: local Fisher information scalar at this state
- drift: β component (wind field) at this state
"""
symbol: str
phase: float
probability: np.ndarray # 8-element, sums to 1
fisher_curvature: float = 0.0
drift: np.ndarray = field(default_factory=lambda: np.zeros(8))
def __post_init__(self):
self.probability = np.asarray(self.probability, dtype=float)
self.drift = np.asarray(self.drift, dtype=float)
# Normalize probability
s = self.probability.sum()
if s > 0:
self.probability /= s
def to_dict(self) -> dict:
return {
"symbol": self.symbol,
"phase": self.phase,
"probability": self.probability.tolist(),
"fisher_curvature": self.fisher_curvature,
"drift": self.drift.tolist(),
}
@classmethod
def from_dict(cls, d: dict) -> "HachimojiState4D":
return cls(
symbol=d["symbol"],
phase=d["phase"],
probability=np.array(d["probability"]),
fisher_curvature=d.get("fisher_curvature", 0.0),
drift=np.array(d.get("drift", [0.0] * 8)),
)
def make_uniform_hachimoji_states() -> list[HachimojiState4D]:
"""Create the 8 canonical Hachimoji states at the uniform distribution.
Each state corresponds to one vertex of the Greek alphabet on Δ⁷.
At the uniform distribution p_i = 1/8, the Fisher metric is:
g_Fisher(X, X) = 8 * Σ_i X_i² (since 1/p_i = 8)
"""
states = []
for sym in GREEK_STATES:
phase = GREEK_PHASE[sym]
# Uniform distribution on Δ⁷
prob = np.ones(8) / 8.0
# Fisher curvature at uniform: g_ii = 1/p_i = 8
fisher_curvature = 8.0
# Drift (β) points in the direction of increasing phase
# This encodes the circular topology of the Hachimoji states
drift = np.zeros(8)
idx = GREEK_STATES.index(sym)
# Wind field: stronger drift toward adjacent states on the circle
drift[(idx + 1) % 8] = 0.3 # forward neighbor
drift[(idx - 1) % 8] = -0.1 # backward neighbor
states.append(HachimojiState4D(
symbol=sym,
phase=phase,
probability=prob,
fisher_curvature=fisher_curvature,
drift=drift,
))
return states
# =========================================================================
# Fisher Information Metric (α component -- symmetric, canonical)
# =========================================================================
def fisher_information_metric(
p: np.ndarray,
X: np.ndarray,
Y: np.ndarray,
) -> float:
"""Compute the Fisher information metric g_Fisher(X, Y) at point p.
g_Fisher(X, Y) = Σ_i X_i · Y_i / p_i
This is the UNIQUE Riemannian metric on the probability simplex
that is invariant under all Markov embeddings (Chentsov's theorem).
Args:
p: probability distribution (positive, sums to 1)
X, Y: tangent vectors (components sum to 0)
Returns:
Fisher inner product
"""
p = np.asarray(p, dtype=float)
X = np.asarray(X, dtype=float)
Y = np.asarray(Y, dtype=float)
eps = 1e-12
result = 0.0
for i in range(len(p)):
if p[i] > eps:
result += X[i] * Y[i] / p[i]
return float(result)
def fisher_metric_matrix(p: np.ndarray) -> np.ndarray:
"""Compute the Fisher metric matrix G_ij = δ_ij / p_i at point p.
Returns:
8×8 diagonal matrix with G_ii = 1/p_i
"""
p = np.asarray(p, dtype=float)
eps = 1e-12
G = np.zeros((len(p), len(p)))
for i in range(len(p)):
if p[i] > eps:
G[i, i] = 1.0 / p[i]
return G
# =========================================================================
# Randers Finsler Metric: F = α + β
# =========================================================================
def compute_alpha_component(
state_a: HachimojiState4D | dict,
state_b: HachimojiState4D | dict,
) -> float:
"""Compute the α (Fisher) component: symmetric Riemannian distance.
α(a,b) = arccosh(1 + ½ · g_Fisher(v, v))
where v = b - a (tangent vector at the midpoint)
For the uniform distribution, this simplifies to the
Fisher-Rao distance on Δ⁷.
"""
if isinstance(state_a, dict):
state_a = HachimojiState4D.from_dict(state_a)
if isinstance(state_b, dict):
state_b = HachimojiState4D.from_dict(state_b)
# Tangent vector: difference of probability distributions
v = state_b.probability - state_a.probability
# Use midpoint for metric evaluation
p_mid = (state_a.probability + state_b.probability) / 2.0
p_mid = np.maximum(p_mid, 1e-12)
p_mid /= p_mid.sum()
# Fisher norm: g_Fisher(v, v) = Σ_i v_i² / p_i
fisher_norm_sq = fisher_information_metric(p_mid, v, v)
# α = sqrt(g_Fisher(v, v)) -- the Riemannian length
alpha = math.sqrt(max(0.0, fisher_norm_sq))
return alpha
def compute_beta_component(
state_a: HachimojiState4D | dict,
state_b: HachimojiState4D | dict,
) -> float:
"""Compute the β (drift) component: antisymmetric 1-form.
β(a,b) = ½ · (drift_a + drift_b) · (b - a)
β is a 1-form: β(-v) = -β(v), so β(b,a) = -β(a,b).
This encodes the torsion / wind field on the Hachimoji manifold.
"""
if isinstance(state_a, dict):
state_a = HachimojiState4D.from_dict(state_a)
if isinstance(state_b, dict):
state_b = HachimojiState4D.from_dict(state_b)
# Average drift field
avg_drift = (state_a.drift + state_b.drift) / 2.0
# Displacement vector
v = state_b.probability - state_a.probability
# β = drift · v
beta = float(np.dot(avg_drift, v))
return beta
def compute_finsler_metric(
state_a: HachimojiState4D | dict,
state_b: HachimojiState4D | dict,
) -> float:
"""Compute Randers metric F(a→b) = α(a,b) + β(a,b).
α: Fisher information metric (symmetric, from Chentsov uniqueness)
β: Drift 1-form (asymmetric, encodes torsion)
The metric is the UNIQUE geometry on the Hachimoji simplex
(proven by ChentsovFinite.lean: chentsov_hachimoji theorem).
Args:
state_a: HachimojiState4D for source (or dict)
state_b: HachimojiState4D for target (or dict)
Returns:
F(a→b): positive float, direction-dependent distance
"""
alpha = compute_alpha_component(state_a, state_b)
beta = compute_beta_component(state_a, state_b)
# F = α + β, but ensure positivity
# For a valid Finsler metric, we need α > |β|
F = alpha + beta
return max(F, 1e-10) # clamp to positive
def compute_finsler_distance_matrix(
states: list[HachimojiState4D | dict],
) -> np.ndarray:
"""Compute the full 8×8 Finsler distance matrix.
D[i,j] = F(states[i] → states[j])
Note: D is NOT symmetric because β is antisymmetric:
D[i,j] ≠ D[j,i] when drift ≠ 0
"""
n = len(states)
D = np.zeros((n, n))
for i in range(n):
for j in range(n):
if i != j:
D[i, j] = compute_finsler_metric(states[i], states[j])
return D
# =========================================================================
# Phase Distance on S¹ (circular topology)
# =========================================================================
def phase_distance_s1(phase_a: float, phase_b: float) -> float:
"""Compute the shortest distance between two phases on S¹.
d_phase(a,b) = min(|a-b|, 360 - |a-b|) · π/180
The phases live on a circle (0° = 360°), so the distance
is the arc length along the shorter arc.
"""
diff = abs(phase_a - phase_b)
diff = min(diff, 360.0 - diff)
# Convert to radians for geometric distance
return diff * (math.pi / 180.0)
def circular_phase_matrix(states: list[HachimojiState4D | dict]) -> np.ndarray:
"""Compute the 8×8 phase distance matrix on S¹.
P[i,j] = d_phase(phase_i, phase_j)² -- squared circular distance
"""
n = len(states)
P = np.zeros((n, n))
for i in range(n):
pa = states[i].phase if hasattr(states[i], "phase") else states[i]["phase"]
for j in range(n):
pb = states[j].phase if hasattr(states[j], "phase") else states[j]["phase"]
d = phase_distance_s1(pa, pb)
P[i, j] = d * d
return P
# =========================================================================
# Fisher Metric (canonical) factory
# =========================================================================
def build_fisher_metric(states: list[HachimojiState4D]) -> dict:
"""Build the canonical Fisher metric dict for the state simplex.
Returns dict compatible with compute_finsler_metric's
fisher_metric parameter:
{
"type": "Fisher",
"metric_matrix": 8×8 array,
"chentsov_constant": c, # positive constant from theorem
"at_uniform": True, # at p_i = 1/8
}
"""
# At the uniform distribution, the Fisher metric is 8·I
G = fisher_metric_matrix(np.ones(8) / 8.0)
return {
"type": "Fisher",
"metric_matrix": G.tolist(),
"chentsov_constant": 1.0, # c = 1 at uniform
"at_uniform": True,
}
# =========================================================================
# Convenience: full pipeline from states to distance matrix
# =========================================================================
def build_full_finsler_pipeline(
states: Optional[list[HachimojiState4D]] = None,
) -> dict:
"""Run the full Finsler metric computation pipeline.
Returns:
{
"states": [state dicts],
"finsler_matrix": 8×8 distance matrix,
"alpha_matrix": 8×8 symmetric α component,
"beta_matrix": 8×8 antisymmetric β component,
"phase_matrix": 8×8 phase distance on S¹,
"fisher_metric": Fisher metric dict,
"is_anisotropic": bool, # True if β ≠ 0
}
"""
if states is None:
states = make_uniform_hachimoji_states()
n = len(states)
F_mat = np.zeros((n, n))
A_mat = np.zeros((n, n))
B_mat = np.zeros((n, n))
for i in range(n):
for j in range(n):
if i != j:
A_mat[i, j] = compute_alpha_component(states[i], states[j])
B_mat[i, j] = compute_beta_component(states[i], states[j])
F_mat[i, j] = A_mat[i, j] + B_mat[i, j]
P_mat = circular_phase_matrix(states)
fisher = build_fisher_metric(states)
# Check anisotropy: max |B_ij + B_ji| should be ~0 (antisymmetric)
# but the Finsler matrix has asymmetric off-diagonals
anisotropic = np.any(np.abs(B_mat) > 1e-9)
return {
"states": [s.to_dict() for s in states],
"finsler_matrix": F_mat.tolist(),
"alpha_matrix": A_mat.tolist(),
"beta_matrix": B_mat.tolist(),
"phase_matrix": P_mat.tolist(),
"fisher_metric": fisher,
"is_anisotropic": bool(anisotropic),
}
if __name__ == "__main__":
result = build_full_finsler_pipeline()
print(f"Finsler metric computed for 8 Hachimoji states")
print(f"Anisotropic: {result['is_anisotropic']}")
print(f"Finsler matrix (first row): {result['finsler_matrix'][0]}")
print(f"Alpha matrix (first row): {result['alpha_matrix'][0]}")
print(f"Beta matrix (first row): {result['beta_matrix'][0]}")