Research-Stack/4-Infrastructure/shim/qr_spatial_hash.py
Devin AI a496d8d3ce fix(infra): Q16_SCALE int not float, add missing Path/sys imports
- Q16_SCALE: float → int in lib/q16.py (int // float returns float,
  breaking Q16_16 integer contract in gccl_waveprobe, vcn_famm_transport)
- qr_spatial_hash.py: sys → _sys (only _sys was imported)
- Add missing 'from pathlib import Path' to 7 files that use Path()
  without importing it: test_fpga, compare_tier1_vs_tier2,
  generate_avm_gold_trace, burgers_triad_core, hot_path_cold_path,
  sabotage_prevention, generate_diat_tables
- hot_path_cold_path.py: also add missing 'import sys'

Co-Authored-By: Allaun Silverfox <bigdataiscoming+9i37y6j2@protonmail.com>
2026-06-16 01:16:31 +00:00

288 lines
10 KiB
Python
Raw 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()