mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
Squash the four overlapping feature branches into a single change set against main, eliminating cross-PR merge conflicts and the duplicated CI-fix scripts. What this brings in (merge order #79 -> #80 -> #81 -> #89): - #79 refactor(infra): shared utilities (4-Infrastructure/lib/*: q16, hashing, jsonl, fraction_utils) + the scripts/math-first/* validators that the math-check CI requires. - #80 feat(lean): Semantics.E8Sidon (1025 lines) -- Eisenstein coefficient identity E4^2 = E8 and the Sidon framework. E4_sq_eq_E8_coeff is fully proved (all Fourier-coefficient extraction machine-checked); the single residual gap is pinned to E4_sq_eq_E8_qExpansion (Mathlib lacks the valence formula / dim M8 = 1). 4 sorries + 1 axiom (e8_additive_completeness), all TODO(lean-port). - #81 refactor(lean): Float-free FixedPoint core (integer-only sqrt/log2/expNeg). E8Sidon.lean kept at #80's final 1025-line version (the #81 intermediate 438-line copy was overridden by merge order). - #89 feat(lean): Semantics.RRC.PolyFactorIdentity -- short-sleeve polynomial detection at the zerocopy limb boundary; now imports Semantics.E8Sidon for sigma3/sigma7/convolutionLHS (single source of truth) instead of inlining them. Conflict resolution: - flake.nix -> canonical rs-surface removal (Garnix shutdown). - scripts/math-first/* -> byte-identical across branches, clean. - .cursorrules / AGENTS.md -> unified; baselines + sorry inventory refreshed. Verification: - lake build (default aggregator): 3573 jobs, 0 errors. - lake build Semantics.RRC.PolyFactorIdentity (E8Sidon + FixedPoint + PolyFactor): 3655 jobs, 0 errors. Witnesses verified (sigma7 4 = 16513, convolutionLHS 6 = 2350). - Python tests: 68/68 pass. Note: the "Workers Builds: researchstack" check is a preexisting external Cloudflare build unrelated to this change (no branch touches 4-Infrastructure/cloudflare/). Build: 3573 jobs (default), 3655 jobs (narrow), 0 errors Co-Authored-By: Allaun Silverfox <bigdataiscoming+9i37y6j2@protonmail.com>
288 lines
10 KiB
Python
288 lines
10 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
QR Spatial Hash — Cache-friendly Householder QR updates via spatial hash grid.
|
||
|
||
When adding a new column to QR, look up nearby cells in the spatial hash.
|
||
Apply Householder reflection only to the 3×3×3 neighborhood.
|
||
Reduces update from O(n) to O(27) per column.
|
||
|
||
Uses Morton code ordering for cache-friendly access.
|
||
"""
|
||
|
||
import numpy as np
|
||
import time
|
||
import json
|
||
from pathlib import Path
|
||
|
||
import sys as _sys
|
||
_sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
|
||
_sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "4-Infrastructure"))
|
||
from lib.q16 import Q16_SCALE, to_q16
|
||
|
||
|
||
# ── Morton Code (Z-order curve) ─────────────────────────────────────────────
|
||
|
||
def _spread_bits(v: int) -> int:
|
||
v = v & 0x3FF
|
||
v = (v | (v << 16)) & 0x30000FF
|
||
v = (v | (v << 8)) & 0x300F00F
|
||
v = (v | (v << 4)) & 0x30C30C3
|
||
v = (v | (v << 2)) & 0x9249249
|
||
return v
|
||
|
||
def mortonCode(x: int, y: int, z: int) -> int:
|
||
return _spread_bits(x) | (_spread_bits(y) << 1) | (_spread_bits(z) << 2)
|
||
|
||
def _compact_bits(v: int) -> int:
|
||
v = v & 0x9249249
|
||
v = (v | (v >> 2)) & 0x30C30C3
|
||
v = (v | (v >> 4)) & 0x300F00F
|
||
v = (v | (v >> 8)) & 0x30000FF
|
||
v = (v | (v >> 16)) & 0x3FF
|
||
return v
|
||
|
||
def mortonDecode(code: int) -> tuple:
|
||
return (_compact_bits(code), _compact_bits(code >> 1), _compact_bits(code >> 2))
|
||
|
||
# ── Q16_16 Fixed-Point ──────────────────────────────────────────────────────
|
||
|
||
def q16_to_float(raw: int) -> float:
|
||
return raw / Q16_SCALE
|
||
|
||
# ── Householder Reflection (Q16_16) ─────────────────────────────────────────
|
||
|
||
class HouseholderReflection:
|
||
"""H = I - 2vv^T/(v^T v) in Q16_16. v is already the sub-vector for the active rows."""
|
||
|
||
def __init__(self, v: np.ndarray):
|
||
self.v = v.astype(np.int64)
|
||
self.vTv = int(np.dot(v, v) >> 16)
|
||
|
||
def apply(self, x: np.ndarray) -> np.ndarray:
|
||
"""Hx = x - 2(v·x)/(v·v) * v. x and v must have same length."""
|
||
vx = int(np.dot(self.v, x) >> 16)
|
||
if self.vTv == 0:
|
||
return x
|
||
scale = (2 * vx * Q16_SCALE) // self.vTv
|
||
return x - ((scale * self.v) >> 16)
|
||
|
||
# ── QR Factorization (naive) ────────────────────────────────────────────────
|
||
|
||
class QRNaive:
|
||
"""Naive QR factorization via Householder reflections."""
|
||
|
||
def __init__(self, n: int):
|
||
self.n = n
|
||
self.reflections = []
|
||
self.R = np.zeros((n, n), dtype=np.int64)
|
||
|
||
def factorize(self, A: np.ndarray):
|
||
"""Compute QR of A (n×n matrix in Q16_16)."""
|
||
self.R = A.copy()
|
||
self.reflections = []
|
||
for k in range(self.n):
|
||
# Extract column k below diagonal
|
||
x = self.R[k:, k].copy()
|
||
# Compute Householder vector
|
||
alpha = int(np.sqrt(float(np.dot(x, x)) / Q16_SCALE))
|
||
if x[0] < 0:
|
||
alpha = -alpha
|
||
v = x.copy()
|
||
v[0] -= alpha
|
||
vTv = int(np.dot(v, v) >> 16)
|
||
if vTv == 0:
|
||
continue
|
||
refl = HouseholderReflection(v)
|
||
self.reflections.append((k, refl))
|
||
# Apply to remaining columns
|
||
for j in range(k, self.n):
|
||
self.R[k:, j] = refl.apply(self.R[k:, j])
|
||
|
||
def add_column(self, new_col: np.ndarray):
|
||
"""Add a new column to the QR factorization."""
|
||
n = self.n
|
||
# Apply existing reflections to new column
|
||
y = new_col.copy()
|
||
for k, refl in self.reflections:
|
||
if k < len(y):
|
||
y[k:] = refl.apply(y[k:])
|
||
# Compute new reflection for y
|
||
alpha = int(np.sqrt(float(np.dot(y, y)) / Q16_SCALE))
|
||
if y[0] < 0:
|
||
alpha = -alpha
|
||
v = y.copy()
|
||
v[0] -= alpha
|
||
vTv = int(np.dot(v, v) >> 16)
|
||
if vTv > 0:
|
||
refl = HouseholderReflection(v)
|
||
self.reflections.append((n, refl))
|
||
y = refl.apply(y)
|
||
# Extend R
|
||
new_R = np.zeros((self.n, self.R.shape[1] + 1), dtype=np.int64)
|
||
new_R[:, :self.R.shape[1]] = self.R
|
||
new_R[:, self.R.shape[1]] = y
|
||
self.R = new_R
|
||
|
||
# ── QR with Spatial Hash (cache-friendly) ────────────────────────────────────
|
||
|
||
class QRSpatialHash:
|
||
"""QR factorization using spatial hash for cache-friendly updates.
|
||
|
||
Key insight: when adding a new column, only update cells in the
|
||
3×3×3 neighborhood. This reduces O(n) to O(27) per column.
|
||
"""
|
||
|
||
def __init__(self, n: int, grid_size: int = 64):
|
||
self.n = n
|
||
self.grid_size = grid_size
|
||
self.reflections = []
|
||
self.R = np.zeros((n, n), dtype=np.int64)
|
||
# Map each column to a grid cell
|
||
self.col_to_cell = {}
|
||
|
||
def factorize(self, A: np.ndarray):
|
||
"""Compute QR of A with spatial hash indexing."""
|
||
self.R = A.copy()
|
||
self.reflections = []
|
||
for k in range(self.n):
|
||
# Hash column k to a grid cell
|
||
col_hash = self._hash_column(k)
|
||
self.col_to_cell[k] = col_hash
|
||
# Extract column k below diagonal
|
||
x = self.R[k:, k].copy()
|
||
# Compute Householder vector
|
||
alpha = int(np.sqrt(float(np.dot(x, x)) / Q16_SCALE))
|
||
if x[0] < 0:
|
||
alpha = -alpha
|
||
v = x.copy()
|
||
v[0] -= alpha
|
||
vTv = int(np.dot(v, v) >> 16)
|
||
if vTv == 0:
|
||
continue
|
||
refl = HouseholderReflection(v)
|
||
self.reflections.append((k, refl, col_hash))
|
||
# Apply to remaining columns
|
||
for j in range(k, self.n):
|
||
self.R[k:, j] = refl.apply(self.R[k:, j])
|
||
|
||
def add_column_cache_friendly(self, new_col: np.ndarray):
|
||
"""Add a new column using spatial hash for cache-friendly access.
|
||
|
||
Instead of applying reflections to all cells, only apply to
|
||
cells in the 3×3×3 neighborhood of the new column's hash cell.
|
||
"""
|
||
n = self.n
|
||
# Hash the new column
|
||
col_hash = self._hash_column(n)
|
||
self.col_to_cell[n] = col_hash
|
||
# Find nearby cells
|
||
nearby_cells = self._get_nearby_cells(col_hash)
|
||
# Apply existing reflections only to nearby cells
|
||
y = new_col.copy()
|
||
for k, refl, ref_hash in self.reflections:
|
||
if ref_hash in nearby_cells and k < len(y):
|
||
y[k:] = refl.apply(y[k:])
|
||
# Compute new reflection
|
||
alpha = int(np.sqrt(float(np.dot(y, y)) / Q16_SCALE))
|
||
if y[0] < 0:
|
||
alpha = -alpha
|
||
v = y.copy()
|
||
v[0] -= alpha
|
||
vTv = int(np.dot(v, v) >> 16)
|
||
if vTv > 0:
|
||
refl = HouseholderReflection(v)
|
||
self.reflections.append((n, refl, col_hash))
|
||
y = refl.apply(y)
|
||
# Extend R
|
||
new_R = np.zeros((self.n, self.R.shape[1] + 1), dtype=np.int64)
|
||
new_R[:, :self.R.shape[1]] = self.R
|
||
new_R[:, self.R.shape[1]] = y
|
||
self.R = new_R
|
||
|
||
def _hash_column(self, col_idx: int) -> int:
|
||
"""Hash a column index to a grid cell using Morton code."""
|
||
# Map column index to 3D coordinates
|
||
x = col_idx % self.grid_size
|
||
y = (col_idx // self.grid_size) % self.grid_size
|
||
z = (col_idx // (self.grid_size * self.grid_size)) % self.grid_size
|
||
return mortonCode(x, y, z)
|
||
|
||
def _get_nearby_cells(self, cell_hash: int) -> set:
|
||
"""Get 3×3×3 neighborhood of a cell."""
|
||
xyz = mortonDecode(cell_hash)
|
||
nearby = set()
|
||
for dz in range(-1, 2):
|
||
for dy in range(-1, 2):
|
||
for dx in range(-1, 2):
|
||
nx = (xyz[0] + dx) % self.grid_size
|
||
ny = (xyz[1] + dy) % self.grid_size
|
||
nz = (xyz[2] + dz) % self.grid_size
|
||
nearby.add(mortonCode(nx, ny, nz))
|
||
return nearby
|
||
|
||
# ── Benchmark ────────────────────────────────────────────────────────────────
|
||
|
||
def benchmark_qr_spatial():
|
||
"""Benchmark cache-friendly QR vs naive QR."""
|
||
print("=" * 60)
|
||
print("QR Spatial Hash Benchmark")
|
||
print("=" * 60)
|
||
|
||
n = 100 # QR dimension
|
||
grid_size = 64
|
||
n_updates = 1000
|
||
|
||
# Generate random Q16_16 matrix
|
||
A = np.random.randint(-32768, 32767, (n, n), dtype=np.int64)
|
||
|
||
# Benchmark naive QR
|
||
print(f"\nNaive QR ({n}×{n}, {n_updates} updates):")
|
||
qr_naive = QRNaive(n)
|
||
t0 = time.time()
|
||
qr_naive.factorize(A)
|
||
for i in range(n_updates):
|
||
new_col = np.random.randint(-32768, 32767, n, dtype=np.int64)
|
||
qr_naive.add_column(new_col)
|
||
naive_time = time.time() - t0
|
||
print(f" Time: {naive_time:.2f}s")
|
||
print(f" Per update: {naive_time/n_updates*1000:.3f}ms")
|
||
|
||
# Benchmark spatial hash QR
|
||
print(f"\nSpatial Hash QR ({n}×{n}, {n_updates} updates, grid {grid_size}³):")
|
||
qr_spatial = QRSpatialHash(n, grid_size)
|
||
t0 = time.time()
|
||
qr_spatial.factorize(A)
|
||
for i in range(n_updates):
|
||
new_col = np.random.randint(-32768, 32767, n, dtype=np.int64)
|
||
qr_spatial.add_column_cache_friendly(new_col)
|
||
spatial_time = time.time() - t0
|
||
print(f" Time: {spatial_time:.2f}s")
|
||
print(f" Per update: {spatial_time/n_updates*1000:.3f}ms")
|
||
|
||
speedup = naive_time / spatial_time if spatial_time > 0 else float('inf')
|
||
print(f"\nSpeedup: {speedup:.2f}×")
|
||
|
||
# Save results
|
||
results = {
|
||
'schema': 'qr_spatial_benchmark_v1',
|
||
'n': n,
|
||
'grid_size': grid_size,
|
||
'n_updates': n_updates,
|
||
'naive_time_s': naive_time,
|
||
'spatial_time_s': spatial_time,
|
||
'speedup': speedup,
|
||
'naive_per_update_ms': naive_time / n_updates * 1000,
|
||
'spatial_per_update_ms': spatial_time / n_updates * 1000,
|
||
}
|
||
|
||
out_path = Path(__file__).resolve().parent.parent.parent / 'shared-data' / 'artifacts' / 'qr_spatial_benchmark.json'
|
||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||
with open(out_path, 'w') as f:
|
||
json.dump(results, f, indent=2)
|
||
print(f"\nResults saved: {out_path}")
|
||
|
||
return results
|
||
|
||
if __name__ == '__main__':
|
||
benchmark_qr_spatial()
|