156 lines
4.7 KiB
Python
156 lines
4.7 KiB
Python
"""
|
|
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 _compute_blade_product(blade_a, blade_b):
|
|
"""
|
|
Compute the geometric product of two canonical basis blades.
|
|
|
|
For blades A=e_{a1}...e_{am} and B=e_{b1}...e_{bn}, the sign is the
|
|
parity of swaps required to move the concatenated basis list into
|
|
canonical order, multiplied by the metric contractions for repeated
|
|
basis vectors. The resulting blade is the symmetric difference of the
|
|
two blade basis sets.
|
|
|
|
This implementation is deliberately bit/set based rather than mutating
|
|
a bubble-sort list while contracting; the previous list mutation path
|
|
corrupted multi-contractions and produced an invalid multiplication
|
|
table.
|
|
"""
|
|
sign = 1
|
|
|
|
# Anticommutation sign: each pair (a_i, b_j) with a_i > b_j requires
|
|
# one swap to canonicalize A followed by B.
|
|
swaps = 0
|
|
for a in blade_a:
|
|
for b in blade_b:
|
|
if a > b:
|
|
swaps += 1
|
|
if swaps % 2:
|
|
sign *= -1
|
|
|
|
# Metric contractions for duplicate basis vectors.
|
|
common = set(blade_a).intersection(blade_b)
|
|
for idx in common:
|
|
sign *= int(SIGNATURE[idx])
|
|
|
|
result_blade = tuple(sorted(set(blade_a).symmetric_difference(blade_b)))
|
|
return sign, result_blade
|
|
|
|
|
|
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)
|
|
table_idx[i, j] = _BLADE_TO_IDX[result_blade]
|
|
table_sign[i, j] = sign
|
|
|
|
return table_idx, table_sign
|
|
|
|
_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):
|
|
ai = A[i]
|
|
if ai == 0.0:
|
|
continue
|
|
for j in range(N_COMPONENTS):
|
|
bj = B[j]
|
|
if bj == 0.0:
|
|
continue
|
|
result[_TABLE_IDX[i, j]] += _TABLE_SIGN[i, j] * ai * bj
|
|
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
|