Research-Stack/5-Applications/scripts/hep_event_benchmark.py

575 lines
23 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
"""
hep_event_benchmark.py — High-Energy Physics event genomes on the RGFlow manifold.
Treats particle-collision events as genomes sampled from a physical lawfulness
manifold. Generates three populations:
A: Standard Model background
B: SM background + planted resonance (pp → X → μ⁺μ⁻ at 750 GeV)
C: Detector noise / corrupted reconstruction
Each event is encoded into the 18-bit unified genome, evaluated against the
precomputed RGFlow adaptation surface, and scored with the physics fitness:
F(g) = w₁·L_phys(g) + w₂·M_RG(g) + w₃·A(g) w₄·R_SM(g) w₅·N_det(g)
Output: JSON report + classification statistics demonstrating basin separation.
"""
import json
import struct
import sys
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import List, Dict, Tuple
import numpy as np
# ═══════════════════════════════════════════════════════════════════════════
# Configuration
# ═══════════════════════════════════════════════════════════════════════════
SEED = 42
np.random.seed(SEED)
# Population sizes
N_SM = 10_000
N_RESONANCE = 1_000
N_NOISE = 2_000
# Planted resonance parameters
RESONANCE_MASS = 750.0 # GeV
RESONANCE_WIDTH = 5.0 # GeV
SIGNAL_FRACTION = 0.10 # 10% of resonance population has the signal
# Fitness weights
W_PHYS = 0.35 # L_phys — conservation-law lawfulness
W_RG = 0.20 # M_RG — RGFlow stability margin
W_ATTRACTOR = 0.20 # A — attractor specificity
W_SM = 0.10 # R_SM — SM background residual penalty
W_NOISE = 0.35 # N_det — detector-noise likelihood (heavy penalty)
# Genome encoding constants
ADDR_SPACE = 262_144
ENTRY_BYTES = 6 * 4
# RGFlow LUT path
RGFLOW_BIN = Path("5-Applications/out/rgflow_adaptation_surface.bin")
# ═══════════════════════════════════════════════════════════════════════════
# Event structures
# ═══════════════════════════════════════════════════════════════════════════
@dataclass
class Particle:
pdg: int # PDG code proxy
px: float
py: float
pz: float
E: float
charge: int
@dataclass
class EventGenome:
particles: List[Particle]
missing_et_x: float
missing_et_y: float
population: str # 'SM', 'resonance', 'noise'
has_signal: bool = False
# ═══════════════════════════════════════════════════════════════════════════
# Synthetic event generators
# ═══════════════════════════════════════════════════════════════════════════
def generate_sm_background(n_events: int) -> List[EventGenome]:
"""Generate Standard Model-like background events.
Soft spectrum, exponential pT falloff, broad mass distribution."""
events = []
for _ in range(n_events):
n_particles = np.random.poisson(12) + 2
particles = []
total_charge = 0
for _ in range(n_particles):
pt = np.random.exponential(30.0)
eta = np.random.uniform(-2.5, 2.5)
phi = np.random.uniform(0, 2 * np.pi)
mass = np.random.exponential(0.5)
charge = np.random.choice([-1, 0, 1])
# Enforce approximate charge conservation
if total_charge + charge > 2:
charge = -1
elif total_charge + charge < -2:
charge = 1
total_charge += charge
px = pt * np.cos(phi)
py = pt * np.sin(phi)
pz = pt * np.sinh(eta)
E = np.sqrt(px**2 + py**2 + pz**2 + mass**2)
particles.append(Particle(
pdg=np.random.choice([11, 13, 22, 211, 130, 2212]),
px=px, py=py, pz=pz, E=E, charge=charge
))
# Small missing ET from neutrino proxy
missing_et_x = np.random.normal(0, 5)
missing_et_y = np.random.normal(0, 5)
events.append(EventGenome(
particles=particles,
missing_et_x=missing_et_x,
missing_et_y=missing_et_y,
population='SM',
has_signal=False
))
return events
def generate_planted_resonance(n_events: int) -> List[EventGenome]:
"""Generate SM background with a planted dimuon resonance at 750 GeV."""
events = []
bg_events = generate_sm_background(n_events)
for ev in bg_events:
ev.population = 'resonance'
if np.random.random() < SIGNAL_FRACTION:
ev.has_signal = True
# Inject back-to-back dimuon pair with resonant mass
mass = np.random.normal(RESONANCE_MASS, RESONANCE_WIDTH)
pt = np.random.exponential(50.0) + 20.0
phi = np.random.uniform(0, 2 * np.pi)
eta = np.random.normal(0, 0.5)
px = pt * np.cos(phi)
py = pt * np.sin(phi)
pz = pt * np.sinh(eta)
E = np.sqrt(px**2 + py**2 + pz**2 + mass**2)
E_mu = E / 2.0
# Muon 1
ev.particles.append(Particle(pdg=13, px=px/2, py=py/2, pz=pz/2, E=E_mu, charge=+1))
# Muon 2 (back-to-back in transverse plane)
ev.particles.append(Particle(pdg=13, px=-px/2, py=-py/2, pz=-pz/2, E=E_mu, charge=-1))
events.append(ev)
return events
def generate_detector_noise(n_events: int) -> List[EventGenome]:
"""Generate corrupted detector noise with violated conservation laws."""
events = []
for _ in range(n_events):
n_particles = np.random.poisson(8) + 2
particles = []
# Deliberately violate charge conservation (total charge ±5 or worse)
target_charge = np.random.choice([-5, -4, 4, 5])
current_charge = 0
for i in range(n_particles):
# Non-physical momenta: no energy-momentum relation
px = np.random.normal(0, 150)
py = np.random.normal(0, 150)
pz = np.random.normal(0, 150)
# Energy completely uncorrelated with momentum (violates E²=p²+m²)
E = np.random.exponential(20.0)
if E < 0:
E = -E # negative energy for extra corruption
# Force charge toward target
if current_charge < target_charge and i < n_particles - 1:
charge = np.random.choice([1, 2])
elif current_charge > target_charge and i < n_particles - 1:
charge = np.random.choice([-1, -2])
else:
charge = target_charge - current_charge
current_charge += charge
particles.append(Particle(
pdg=np.random.choice([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
px=px, py=py, pz=pz, E=E, charge=charge
))
# Extreme missing energy inconsistent with momentum sum
total_px = sum(p.px for p in particles)
total_py = sum(p.py for p in particles)
missing_et_x = -total_px + np.random.normal(0, 300)
missing_et_y = -total_py + np.random.normal(0, 300)
events.append(EventGenome(
particles=particles,
missing_et_x=missing_et_x,
missing_et_y=missing_et_y,
population='noise',
has_signal=False
))
return events
# ═══════════════════════════════════════════════════════════════════════════
# Conservation-law checks (L_phys)
# ═══════════════════════════════════════════════════════════════════════════
def check_conservation(event: EventGenome) -> Dict[str, float]:
"""Check physical conservation laws. Returns scores in [0, 1]."""
parts = event.particles
# Energy-momentum conservation residual
total_px = sum(p.px for p in parts)
total_py = sum(p.py for p in parts)
total_pz = sum(p.pz for p in parts)
total_E = sum(p.E for p in parts)
total_charge = sum(p.charge for p in parts)
# Missing ET magnitude
met = np.sqrt(event.missing_et_x**2 + event.missing_et_y**2)
scalar_sum_pt = sum(np.sqrt(p.px**2 + p.py**2) for p in parts)
# Momentum conservation score: 1.0 if balanced, 0.0 if huge imbalance
p_residual = np.sqrt(total_px**2 + total_py**2 + total_pz**2)
p_score = max(0.0, 1.0 - p_residual / max(1.0, scalar_sum_pt * 0.05))
# Charge conservation score: strict penalty for any non-zero total charge
charge_score = max(0.0, 1.0 - abs(total_charge) / 2.0)
# Energy-momentum consistency: E² ≈ p² + m² for each particle
em_scores = []
for p in parts:
p2 = p.px**2 + p.py**2 + p.pz**2
if p.E > 0:
# Expected E for massless particle
expected_E = np.sqrt(p2)
ratio = abs(p.E - expected_E) / max(expected_E, 1.0)
em_scores.append(max(0.0, 1.0 - ratio))
else:
em_scores.append(0.0)
em_score = np.mean(em_scores) if em_scores else 0.0
# Missing ET plausibility
met_score = max(0.0, 1.0 - met / max(1.0, scalar_sum_pt * 0.2))
# Overall: geometric mean for stricter combined score
overall = (p_score * charge_score * em_score * met_score) ** 0.25
return {
'momentum': p_score,
'charge': charge_score,
'energy_momentum': em_score,
'missing_et': met_score,
'overall': overall,
}
# ═══════════════════════════════════════════════════════════════════════════
# Event → 18-bit genome encoding
# ═══════════════════════════════════════════════════════════════════════════
def encode_18bit(mu: int, rho: int, c: int, m: int, ne: int, sigma: int) -> int:
"""Pack 6 dimensions × 3 bits into 18-bit address."""
return (
(mu & 7) * 32768 +
(rho & 7) * 4096 +
(c & 7) * 512 +
(m & 7) * 64 +
(ne & 7) * 8 +
(sigma & 7)
)
def event_to_genome(event: EventGenome) -> int:
"""Map HEP event features into 18-bit genome address.
Dimensions (matching RGFlow shader):
mu = conservation-law violation rate (inverse of L_phys)
rho = reconstruction quality / verification pressure
c = particle multiplicity (connectance proxy)
m = event topology modularity
ne = effective statistics / luminosity proxy
sigma = signal significance / selection advantage
"""
parts = event.particles
n = len(parts)
# Conservation law check
conservation = check_conservation(event)
l_phys = conservation['overall']
# mu: violation rate = 1 - L_phys, quantized to 0..7
mu_bin = min(7, int((1.0 - l_phys) * 8.0))
# rho: reconstruction quality. High-quality events have tight kinematics.
pt_spread = np.std([np.sqrt(p.px**2 + p.py**2) for p in parts]) if n > 1 else 0
rho_bin = min(7, int(max(0, 1.0 - pt_spread / 100.0) * 8.0))
# c: connectance = particle multiplicity density
c_bin = min(7, n // 2)
# m: modularity = how clustered are particles in phi space
if n >= 2:
phis = np.arctan2([p.py for p in parts], [p.px for p in parts])
# Simple clustering: count particle pairs within π/4
pairs_close = 0
for i in range(n):
for j in range(i + 1, n):
dphi = abs(phis[i] - phis[j])
dphi = min(dphi, 2 * np.pi - dphi)
if dphi < np.pi / 4:
pairs_close += 1
max_pairs = n * (n - 1) // 2
modularity = pairs_close / max_pairs if max_pairs > 0 else 0.0
else:
modularity = 0.0
m_bin = min(7, int(modularity * 8.0))
# ne: effective observer mass (statistics proxy)
ne_bin = min(7, int(np.log1p(n * 10) / np.log1p(120) * 8.0))
# sigma: signal significance proxy
# For resonance events, compute dimuon invariant mass significance
sigma_bin = 0
if event.population == 'resonance' and event.has_signal:
# Find dimuon pair
muons = [p for p in parts if p.pdg == 13]
if len(muons) >= 2:
m1, m2 = muons[-2], muons[-1]
m_inv = np.sqrt(
(m1.E + m2.E)**2 -
(m1.px + m2.px)**2 -
(m1.py + m2.py)**2 -
(m1.pz + m2.pz)**2
)
# Significance = how close to resonance peak, scaled
significance = max(0, 5.0 - abs(m_inv - RESONANCE_MASS) / RESONANCE_WIDTH)
sigma_bin = min(7, int(significance))
else:
# Background: low significance
sigma_bin = min(7, int(np.random.exponential(1.0)))
return encode_18bit(mu_bin, rho_bin, c_bin, m_bin, ne_bin, sigma_bin)
# ═══════════════════════════════════════════════════════════════════════════
# RGFlow LUT interface
# ═══════════════════════════════════════════════════════════════════════════
def load_rgflow_lut(path: Path) -> np.ndarray:
"""Load precomputed RGFlow adaptation surface as N×6 uint32 array."""
raw = np.fromfile(path, dtype=np.uint32)
return raw.reshape(-1, 6)
def lookup_entry(lut: np.ndarray, addr: int) -> Dict:
"""Unpack a single LUT entry."""
row = lut[addr]
flags = int(row[0])
return {
'lawful_now': bool(flags & 1),
'lawful_flow': bool(flags & 2),
'lawful_attractor': bool(flags & 4),
'noise_flow': bool(flags & 8),
'sabotage_flow': bool(flags & 16),
'cost': int(row[1]),
'margin': int(row[2]),
'rg_depth': int(row[3]),
'attractor_id': int(row[4]),
'failure_mask': f"0x{int(row[5]):04X}",
}
# ═══════════════════════════════════════════════════════════════════════════
# Physics fitness
# ═══════════════════════════════════════════════════════════════════════════
def compute_fitness(event: EventGenome, lut_entry: Dict) -> float:
"""Compute the HEP physics fitness:
F(g) = w₁·L_phys(g) + w₂·M_RG(g) + w₃·A(g) w₄·R_SM(g) w₅·N_det(g)
"""
conservation = check_conservation(event)
l_phys = conservation['overall']
# RGFlow stability margin, normalized 0-1
m_rg = min(1.0, lut_entry['margin'] / 65536.0)
# Attractor specificity
a = 1.0 if lut_entry['lawful_attractor'] else 0.0
# SM residual: background-like events get penalized
# Resonance with signal gets lower penalty
if event.population == 'SM':
r_sm = 0.5
elif event.population == 'resonance' and event.has_signal:
r_sm = 0.05
elif event.population == 'resonance':
r_sm = 0.4
else:
r_sm = 0.0
# Noise likelihood
if event.population == 'noise':
n_det = 1.0
elif lut_entry['sabotage_flow']:
n_det = 0.7
elif not lut_entry['lawful_now'] and lut_entry['lawful_flow']:
# Healed by RGFlow but locally suspicious
n_det = 0.2
else:
n_det = 0.0
fitness = (
W_PHYS * l_phys +
W_RG * m_rg +
W_ATTRACTOR * a -
W_SM * r_sm -
W_NOISE * n_det
)
return fitness
# ═══════════════════════════════════════════════════════════════════════════
# Benchmark runner
# ═══════════════════════════════════════════════════════════════════════════
def run_benchmark():
print("=" * 60)
print("High-Energy Physics Event Genome Benchmark")
print("=" * 60)
if not RGFLOW_BIN.exists():
print(f"[ERROR] RGFlow LUT not found at {RGFLOW_BIN}", file=sys.stderr)
print("Run: python3 5-Applications/scripts/rgflow_gpu_pipeline.py", file=sys.stderr)
sys.exit(1)
print("[INFO] Loading RGFlow adaptation surface...")
lut = load_rgflow_lut(RGFLOW_BIN)
print(f"[INFO] LUT loaded: {lut.shape[0]} entries")
print(f"\n[INFO] Generating {N_SM} SM background events...")
sm_events = generate_sm_background(N_SM)
print(f"[INFO] Generating {N_RESONANCE} planted-resonance events...")
resonance_events = generate_planted_resonance(N_RESONANCE)
print(f"[INFO] Generating {N_NOISE} detector-noise events...")
noise_events = generate_detector_noise(N_NOISE)
all_events = sm_events + resonance_events + noise_events
print(f"[INFO] Total events: {len(all_events)}")
# Evaluate each event
print("\n[INFO] Encoding events and evaluating against RGFlow surface...")
results = []
for i, ev in enumerate(all_events):
addr = event_to_genome(ev)
entry = lookup_entry(lut, addr)
fitness = compute_fitness(ev, entry)
results.append({
'index': i,
'population': ev.population,
'has_signal': ev.has_signal,
'address': addr,
'fitness': round(fitness, 4),
**entry,
'conservation_score': round(check_conservation(ev)['overall'], 4),
})
# Classification statistics
stats = {}
for pop in ['SM', 'resonance', 'noise']:
subset = [r for r in results if r['population'] == pop]
stats[pop] = {
'count': len(subset),
'lawful_now_fraction': sum(1 for r in subset if r['lawful_now']) / len(subset),
'lawful_flow_fraction': sum(1 for r in subset if r['lawful_flow']) / len(subset),
'lawful_attractor_fraction': sum(1 for r in subset if r['lawful_attractor']) / len(subset),
'sabotage_fraction': sum(1 for r in subset if r['sabotage_flow']) / len(subset),
'mean_fitness': np.mean([r['fitness'] for r in subset]),
'mean_margin': np.mean([r['margin'] for r in subset]),
'mean_cost': np.mean([r['cost'] for r in subset]),
}
# Signal-specific stats for resonance
if pop == 'resonance':
signal = [r for r in subset if r['has_signal']]
bg = [r for r in subset if not r['has_signal']]
stats[pop]['signal_count'] = len(signal)
stats[pop]['signal_mean_fitness'] = np.mean([r['fitness'] for r in signal]) if signal else 0
stats[pop]['bg_mean_fitness'] = np.mean([r['fitness'] for r in bg]) if bg else 0
stats[pop]['signal_lawful_attractor'] = sum(1 for r in signal if r['lawful_attractor']) / len(signal) if signal else 0
stats[pop]['bg_lawful_attractor'] = sum(1 for r in bg if r['lawful_attractor']) / len(bg) if bg else 0
# Print report
print("\n" + "=" * 60)
print("Benchmark Results")
print("=" * 60)
for pop, s in stats.items():
print(f"\n--- {pop.upper()} ---")
for k, v in s.items():
if isinstance(v, float):
print(f" {k:30s}: {v:.4f}")
else:
print(f" {k:30s}: {v}")
# Save JSON
out_dir = Path("5-Applications/out/hep_benchmark")
out_dir.mkdir(parents=True, exist_ok=True)
out_path = out_dir / "results.json"
with open(out_path, "w") as f:
json.dump({
'meta': {
'n_sm': N_SM,
'n_resonance': N_RESONANCE,
'n_noise': N_NOISE,
'resonance_mass': RESONANCE_MASS,
'resonance_width': RESONANCE_WIDTH,
'signal_fraction': SIGNAL_FRACTION,
'weights': {
'phys': W_PHYS,
'rg': W_RG,
'attractor': W_ATTRACTOR,
'sm': W_SM,
'noise': W_NOISE,
},
},
'population_statistics': stats,
'sample_events': results[:5] + [r for r in results if r['population'] == 'resonance' and r['has_signal']][:5],
}, f, indent=2)
print(f"\n[OK] Results saved to {out_path}")
# Invariant mass spectrum for resonance events (dimuon pairs)
print("\n[INFO] Computing dimuon invariant-mass spectrum...")
masses = {'SM': [], 'resonance_signal': [], 'resonance_bg': [], 'noise': []}
for ev, res in zip(all_events, results):
muons = [p for p in ev.particles if p.pdg == 13]
if len(muons) >= 2:
m1, m2 = muons[-2], muons[-1]
m_inv = np.sqrt(
max(0.0, (m1.E + m2.E)**2 -
(m1.px + m2.px)**2 -
(m1.py + m2.py)**2 -
(m1.pz + m2.pz)**2)
)
else:
m_inv = 0.0
if ev.population == 'SM':
masses['SM'].append(m_inv)
elif ev.population == 'resonance':
if ev.has_signal:
masses['resonance_signal'].append(m_inv)
else:
masses['resonance_bg'].append(m_inv)
else:
masses['noise'].append(m_inv)
# Save mass spectrum
mass_path = out_dir / "mass_spectrum.json"
with open(mass_path, "w") as f:
json.dump({
'SM': [round(m, 2) for m in masses['SM'][:200]],
'resonance_signal': [round(m, 2) for m in masses['resonance_signal']],
'resonance_bg': [round(m, 2) for m in masses['resonance_bg'][:200]],
'noise': [round(m, 2) for m in masses['noise'][:200]],
}, f, indent=2)
print(f"[OK] Mass spectrum saved to {mass_path}")
# Signal separation summary
if masses['resonance_signal']:
sig_masses = np.array(masses['resonance_signal'])
in_peak = np.sum((sig_masses > RESONANCE_MASS - 3 * RESONANCE_WIDTH) &
(sig_masses < RESONANCE_MASS + 3 * RESONANCE_WIDTH))
print(f"\n Signal events in 750±15 GeV peak: {in_peak} / {len(sig_masses)} ({100*in_peak/len(sig_masses):.1f}%)")
print("\n[OK] HEP benchmark complete.")
if __name__ == "__main__":
run_benchmark()