""" 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).""" dtype = np.result_type(A, B) if dtype not in (np.dtype(np.float32), np.dtype(np.float64)): dtype = np.dtype(np.float32) A = np.asarray(A, dtype=dtype) B = np.asarray(B, dtype=dtype) result = np.zeros(N_COMPONENTS, dtype=dtype) 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. """ dtype = np.asarray(A).dtype if dtype not in (np.dtype(np.float32), np.dtype(np.float64)): dtype = np.dtype(np.float32) A = np.asarray(A, dtype=dtype).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.""" dtype = np.asarray(A).dtype if dtype not in (np.dtype(np.float32), np.dtype(np.float64)): dtype = np.dtype(np.float32) result = np.zeros(N_COMPONENTS, dtype=dtype) 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