Research-Stack/4-Infrastructure/shim/qr_spatial_hash.py
Devin AI 0639eae30a chore(consolidation): integrate E8Sidon stack (PRs #79 #80 #81 #89) into one PR
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>
2026-06-16 02:01:31 +00:00

288 lines
10 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.

#!/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()