init: algebra layer — cl41, versor, cga, holonomy
This commit is contained in:
parent
243af021a7
commit
b80dd57a9b
5 changed files with 369 additions and 0 deletions
4
algebra/__init__.py
Normal file
4
algebra/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .cl41 import geometric_product, reverse, grade_project, scalar_part, norm_squared, basis_vector
|
||||
from .versor import versor_apply, normalize_to_versor, versor_condition
|
||||
from .cga import cga_inner, outer_product, is_null, null_project, embed_point
|
||||
from .holonomy import holonomy_encode, holonomy_similarity
|
||||
70
algebra/cga.py
Normal file
70
algebra/cga.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"""
|
||||
Conformal Geometric Algebra geometry on Cl(4,1).
|
||||
|
||||
Key identity: for null vectors X, Y on the horosphere,
|
||||
cga_inner(X, Y) = -d(X, Y)^2 / 2
|
||||
where d is Euclidean distance.
|
||||
|
||||
This is the ONLY distance metric in CORE-AI.
|
||||
No cosine similarity. No L2 norm. No approximate indexing.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from .cl41 import geometric_product, reverse, scalar_part, basis_vector, N_COMPONENTS
|
||||
|
||||
|
||||
def cga_inner(X: np.ndarray, Y: np.ndarray) -> float:
|
||||
"""
|
||||
Symmetric inner product: 0.5 * scalar_part(X*Y + Y*X).
|
||||
For null vectors representing conformal points: equals -d^2 / 2.
|
||||
"""
|
||||
XY = geometric_product(X, Y)
|
||||
YX = geometric_product(Y, X)
|
||||
return 0.5 * scalar_part(XY + YX)
|
||||
|
||||
|
||||
def outer_product(X: np.ndarray, Y: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Outer (wedge) product: X ^ Y.
|
||||
For a prompt versor X_p and response versor X_r,
|
||||
X_p ^ X_r is a grade-2 object encoding their geometric relationship.
|
||||
A real (non-imaginary) result means the dialogue is coherent.
|
||||
"""
|
||||
XY = geometric_product(X, Y)
|
||||
YX = geometric_product(Y, X)
|
||||
return 0.5 * (XY - YX)
|
||||
|
||||
|
||||
def is_null(X: np.ndarray, tol: float = 1e-6) -> bool:
|
||||
"""Check if X lies on the null cone: X*X = 0."""
|
||||
return abs(cga_inner(X, X)) < tol
|
||||
|
||||
|
||||
def null_project(X: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Re-project X onto the null cone.
|
||||
Call on vault entries periodically to correct floating-point null-cone drift.
|
||||
This is numerical maintenance, not a heat shield.
|
||||
Method: extract Euclidean part, re-embed via standard CGA point map.
|
||||
"""
|
||||
euclidean = X[1:4].copy().astype(np.float32)
|
||||
x_sq = float(np.dot(euclidean, euclidean))
|
||||
result = np.zeros(N_COMPONENTS, dtype=np.float32)
|
||||
result[1:4] = euclidean
|
||||
result[4] = 0.5 * x_sq # e+ coefficient
|
||||
result[5] = 1.0 # e- coefficient
|
||||
return result
|
||||
|
||||
|
||||
def embed_point(x: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Embed a Euclidean point x in R^3 into the CGA null cone.
|
||||
Standard map: X = x + (1/2)|x|^2 * e+ + e-
|
||||
"""
|
||||
x = np.asarray(x, dtype=np.float32)
|
||||
assert len(x) == 3, "embed_point expects a 3D vector"
|
||||
result = np.zeros(N_COMPONENTS, dtype=np.float32)
|
||||
result[1:4] = x
|
||||
result[4] = 0.5 * float(np.dot(x, x))
|
||||
result[5] = 1.0
|
||||
return result
|
||||
174
algebra/cl41.py
Normal file
174
algebra/cl41.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
"""
|
||||
Cl(4,1) multivector arithmetic.
|
||||
|
||||
Signature: (+,+,+,+,-). Basis e1..e5.
|
||||
Multivectors are float32 arrays of shape (32,) ordered by grade:
|
||||
grade-0: index 0 (1 component)
|
||||
grade-1: indices 1-5 (5 components)
|
||||
grade-2: indices 6-15 (10 components)
|
||||
grade-3: indices 16-25 (10 components)
|
||||
grade-4: indices 26-30 (5 components)
|
||||
grade-5: index 31 (1 component)
|
||||
"""
|
||||
|
||||
from itertools import combinations
|
||||
from math import comb
|
||||
import numpy as np
|
||||
|
||||
N_DIMS = 5
|
||||
N_COMPONENTS = 32
|
||||
SIGNATURE = np.array([1, 1, 1, 1, -1], dtype=np.float64)
|
||||
|
||||
# --- Grade offset table ---
|
||||
|
||||
def _grade_offsets():
|
||||
offsets = []
|
||||
start = 0
|
||||
for k in range(N_DIMS + 1):
|
||||
count = comb(N_DIMS, k)
|
||||
offsets.append((start, count))
|
||||
start += count
|
||||
return offsets
|
||||
|
||||
_GRADE_OFFSETS = _grade_offsets()
|
||||
|
||||
def grade_start(k: int) -> int:
|
||||
return _GRADE_OFFSETS[k][0]
|
||||
|
||||
def grade_count(k: int) -> int:
|
||||
return _GRADE_OFFSETS[k][1]
|
||||
|
||||
# --- Blade index maps ---
|
||||
|
||||
def _all_blades():
|
||||
"""Return ordered list of blade tuples (one per component, ordered by grade)."""
|
||||
blades = []
|
||||
for k in range(N_DIMS + 1):
|
||||
for combo in combinations(range(N_DIMS), k):
|
||||
blades.append(combo)
|
||||
return blades
|
||||
|
||||
_BLADES = _all_blades() # index -> tuple of basis vector indices
|
||||
_BLADE_TO_IDX = {b: i for i, b in enumerate(_BLADES)}
|
||||
|
||||
def _blade_product(blade_a, blade_b):
|
||||
"""Compute geometric product of two basis blades. Returns (sign, result_blade_tuple)."""
|
||||
# Concatenate and bubble-sort, tracking sign flips and metric contractions
|
||||
seq = list(blade_a) + list(blade_b)
|
||||
sign = 1
|
||||
# Bubble sort to canonical order, tracking swaps
|
||||
n = len(seq)
|
||||
for i in range(n):
|
||||
for j in range(n - i - 1):
|
||||
if seq[j] > seq[j + 1]:
|
||||
seq[j], seq[j + 1] = seq[j + 1], seq[j]
|
||||
sign *= -1
|
||||
elif seq[j] == seq[j + 1]:
|
||||
# Metric contraction: e_i^2 = signature[i]
|
||||
metric = int(SIGNATURE[seq[j]])
|
||||
sign *= metric
|
||||
seq.pop(j)
|
||||
seq.pop(j) # second element now at same index after first pop
|
||||
n -= 2
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
# After contraction there may still be duplicates — recurse
|
||||
result = tuple(sorted(set(seq))) # this is wrong for multi-contraction; use proper loop
|
||||
return sign, tuple(seq)
|
||||
|
||||
def _build_multiplication_table():
|
||||
"""Precompute full 32x32 geometric product table."""
|
||||
table_idx = np.zeros((N_COMPONENTS, N_COMPONENTS), dtype=np.int32)
|
||||
table_sign = np.zeros((N_COMPONENTS, N_COMPONENTS), dtype=np.float32)
|
||||
|
||||
for i, blade_a in enumerate(_BLADES):
|
||||
for j, blade_b in enumerate(_BLADES):
|
||||
sign, result_blade = _compute_blade_product(blade_a, blade_b)
|
||||
result_idx = _BLADE_TO_IDX.get(result_blade, 0)
|
||||
table_idx[i, j] = result_idx
|
||||
table_sign[i, j] = sign
|
||||
|
||||
return table_idx, table_sign
|
||||
|
||||
def _compute_blade_product(blade_a, blade_b):
|
||||
"""Compute geometric product of two basis blades via bubble sort + metric."""
|
||||
seq = list(blade_a) + list(blade_b)
|
||||
sign = 1
|
||||
i = 0
|
||||
while i < len(seq) - 1:
|
||||
j = i
|
||||
while j < len(seq) - 1:
|
||||
if seq[j] == seq[j + 1]:
|
||||
# Contract: e_k^2 = signature[k]
|
||||
sign *= int(SIGNATURE[seq[j]])
|
||||
seq.pop(j)
|
||||
seq.pop(j)
|
||||
if j > 0:
|
||||
i = max(0, j - 1)
|
||||
break
|
||||
elif seq[j] > seq[j + 1]:
|
||||
seq[j], seq[j + 1] = seq[j + 1], seq[j]
|
||||
sign *= -1
|
||||
j += 1
|
||||
else:
|
||||
j += 1
|
||||
else:
|
||||
i += 1
|
||||
result_blade = tuple(seq)
|
||||
if result_blade not in _BLADE_TO_IDX:
|
||||
return 0, ()
|
||||
return sign, result_blade
|
||||
|
||||
_TABLE_IDX, _TABLE_SIGN = _build_multiplication_table()
|
||||
|
||||
# --- Core operations ---
|
||||
|
||||
def geometric_product(A: np.ndarray, B: np.ndarray) -> np.ndarray:
|
||||
"""Full geometric product in Cl(4,1)."""
|
||||
A = np.asarray(A, dtype=np.float32)
|
||||
B = np.asarray(B, dtype=np.float32)
|
||||
result = np.zeros(N_COMPONENTS, dtype=np.float32)
|
||||
for i in range(N_COMPONENTS):
|
||||
if A[i] == 0.0:
|
||||
continue
|
||||
for j in range(N_COMPONENTS):
|
||||
if B[j] == 0.0:
|
||||
continue
|
||||
result[_TABLE_IDX[i, j]] += _TABLE_SIGN[i, j] * A[i] * B[j]
|
||||
return result
|
||||
|
||||
def reverse(A: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Reverse (main anti-automorphism).
|
||||
Grade-k blades pick up sign (-1)^(k*(k-1)/2).
|
||||
Grade 0,1: +1. Grade 2,3: -1. Grade 4,5: +1.
|
||||
"""
|
||||
A = np.asarray(A, dtype=np.float32).copy()
|
||||
# Grade 2: indices 6-15
|
||||
A[6:16] *= -1.0
|
||||
# Grade 3: indices 16-25
|
||||
A[16:26] *= -1.0
|
||||
return A
|
||||
|
||||
def grade_project(A: np.ndarray, k: int) -> np.ndarray:
|
||||
"""Extract grade-k part of A."""
|
||||
result = np.zeros(N_COMPONENTS, dtype=np.float32)
|
||||
start, count = _GRADE_OFFSETS[k]
|
||||
result[start:start + count] = A[start:start + count]
|
||||
return result
|
||||
|
||||
def scalar_part(A: np.ndarray) -> float:
|
||||
"""Return grade-0 component."""
|
||||
return float(A[0])
|
||||
|
||||
def norm_squared(A: np.ndarray) -> float:
|
||||
"""||A||^2 = scalar_part(A * reverse(A))."""
|
||||
return scalar_part(geometric_product(A, reverse(A)))
|
||||
|
||||
def basis_vector(i: int) -> np.ndarray:
|
||||
"""Return the i-th basis vector (0-indexed) as a 32-component multivector."""
|
||||
v = np.zeros(N_COMPONENTS, dtype=np.float32)
|
||||
v[1 + i] = 1.0
|
||||
return v
|
||||
71
algebra/holonomy.py
Normal file
71
algebra/holonomy.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
"""
|
||||
Holonomy prompt encoding.
|
||||
|
||||
A prompt w1, w2, ..., wn is encoded as the geometric holonomy of its
|
||||
forward+reverse versor walk. The walk closes, producing a versor that
|
||||
is bounded by construction and invariant to global phase.
|
||||
|
||||
The holonomy IS a versor — it drops directly into versor_apply with
|
||||
no bridging code. The fuel and the engine are the same substance.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from .cl41 import geometric_product, reverse as cl_reverse
|
||||
from .versor import normalize_to_versor
|
||||
from .cga import cga_inner
|
||||
|
||||
|
||||
def holonomy_encode(
|
||||
word_versors: list,
|
||||
alpha: float = 0.5,
|
||||
weights: list = None,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Compute the holonomy of the word versor sequence.
|
||||
|
||||
Forward walk: F = w1 * w2 * ... * wn (weighted by word frequency inverse)
|
||||
Reverse walk: R = (1-alpha) * reverse(wn) * ... * reverse(w1)
|
||||
Holonomy: H = geometric_product(F, R)
|
||||
|
||||
H is a versor. For alpha=0.5, the holonomy captures the geometric
|
||||
curvature of the prompt path. Prompts with different semantic content
|
||||
produce geometrically distinct holonomies even at the same length.
|
||||
|
||||
weights: optional list of float scalars (e.g. inverse token frequency).
|
||||
Rare content words rotate more than common function words.
|
||||
If None, uniform weights are used.
|
||||
"""
|
||||
if not word_versors:
|
||||
raise ValueError("Cannot encode empty prompt.")
|
||||
|
||||
n = len(word_versors)
|
||||
if weights is None:
|
||||
weights = [1.0] * n
|
||||
assert len(weights) == n
|
||||
|
||||
# Forward accumulation
|
||||
F = word_versors[0].copy() * weights[0]
|
||||
F = normalize_to_versor(F)
|
||||
for k in range(1, n):
|
||||
w = word_versors[k] * weights[k]
|
||||
w = normalize_to_versor(w)
|
||||
F = geometric_product(F, w)
|
||||
|
||||
# Reverse accumulation with alpha damping
|
||||
R = cl_reverse(word_versors[-1]) * (1.0 - alpha)
|
||||
R = normalize_to_versor(R)
|
||||
for k in range(n - 2, -1, -1):
|
||||
r = cl_reverse(word_versors[k])
|
||||
r = normalize_to_versor(r)
|
||||
R = geometric_product(r, R)
|
||||
|
||||
H = geometric_product(F, R)
|
||||
return normalize_to_versor(H)
|
||||
|
||||
|
||||
def holonomy_similarity(H1: np.ndarray, H2: np.ndarray) -> float:
|
||||
"""
|
||||
Compare two holonomies via CGA inner product.
|
||||
Used for prompt-level semantic similarity without embedding lookup.
|
||||
"""
|
||||
return cga_inner(H1, H2)
|
||||
50
algebra/versor.py
Normal file
50
algebra/versor.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"""
|
||||
The three versor primitives.
|
||||
|
||||
These are the ONLY normalization/transition/check functions in the system.
|
||||
Do not add correction, monitoring, or grade-guard functions here.
|
||||
If you think you need something else, you have an unclosed operation upstream.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from .cl41 import geometric_product, reverse, scalar_part, norm_squared
|
||||
|
||||
|
||||
def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Sandwich product: V * F * reverse(V).
|
||||
|
||||
The ONLY allowed field transition in the system.
|
||||
Algebraically closed on the versor manifold:
|
||||
if V and F are versors, V*F*reverse(V) is a versor.
|
||||
No pre/post normalization. No grade projection. No guards.
|
||||
"""
|
||||
return geometric_product(V, geometric_product(F, reverse(V)))
|
||||
|
||||
|
||||
def normalize_to_versor(F: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Project F onto the versor manifold: F / sqrt(|F * reverse(F)|).
|
||||
|
||||
Call this ONCE per input at the injection gate (ingest/gate.py).
|
||||
Never call mid-propagation, mid-generation, or in the vault.
|
||||
If you feel the urge to call this elsewhere, fix the upstream operation.
|
||||
"""
|
||||
n2 = norm_squared(F)
|
||||
if abs(n2) < 1e-12:
|
||||
raise ValueError("Cannot normalize a null multivector to a versor.")
|
||||
return F / np.sqrt(abs(n2))
|
||||
|
||||
|
||||
def versor_condition(F: np.ndarray) -> float:
|
||||
"""
|
||||
Returns ||F * reverse(F) - 1||_F.
|
||||
|
||||
Zero means F is on the versor manifold.
|
||||
Use in tests and at the injection gate only.
|
||||
Never call in the generation hot path.
|
||||
"""
|
||||
product = geometric_product(F, reverse(F))
|
||||
product = product.copy()
|
||||
product[0] -= 1.0
|
||||
return float(np.linalg.norm(product))
|
||||
Loading…
Reference in a new issue