mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
feat: particle physics LUT — 50 years of PDG data as Q16_16 BRAM tables
34 particle masses, 8 decay widths, 11 cross-sections, 8 trigger thresholds, 8 calibration constants — all encoded as Q16_16 integers. BRAM layout: 4 banks × 256 entries × 32-bit Bank 0: particle_masses (electron → upsilon_3S) Bank 1: decay_widths (W, Z, Higgs, top, J/ψ, ϒ) Bank 2: cross_sections (ttbar, W, Z, Higgs, jets at 13 TeV) Bank 3: triggers_calibration (LHC HLT + ECAL/HCAL constants) Cross-section interpolation: log-log between 7/8/13/14 TeV. Export: Verilog initial blocks for FPGA BRAM loading. The LHC trigger system processes 40M events/second using hardware LUTs. These tables are the same lookup operations — just in Q16_16 fixed-point.
This commit is contained in:
parent
5698d6e54b
commit
73b2c3ba32
1 changed files with 323 additions and 0 deletions
323
4-Infrastructure/shim/particle_physics_lut.py
Normal file
323
4-Infrastructure/shim/particle_physics_lut.py
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
"""Particle Physics LUT — Q16_16 lookup tables from 50 years of PDG data.
|
||||
|
||||
The LHC trigger system classifies billions of events/second using hardware LUTs.
|
||||
The Particle Data Group (PDG) maintains precision measurements of every known
|
||||
particle property. This module encodes the most useful tables as Q16_16 values
|
||||
suitable for FPGA BRAM storage.
|
||||
|
||||
Key insight: particle physics has been doing "computation via lookup tables"
|
||||
since the 1970s. The PDG tables ARE a LUT for the Standard Model.
|
||||
|
||||
Tables encoded:
|
||||
1. Particle masses (Q16_16 in MeV)
|
||||
2. Decay widths (Q16_16 in GeV)
|
||||
3. Cross-sections (Q16_16 in pb)
|
||||
4. Trigger thresholds (Q16_16 in GeV)
|
||||
5. Calorimeter calibration constants (Q16_16)
|
||||
|
||||
All values are deterministic Q16_16 integers — no floats in compute paths.
|
||||
"""
|
||||
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
# ── Q16_16 Constants ──────────────────────────────────────────────────
|
||||
|
||||
Q16_SCALE = 65536
|
||||
Q16_MAX = 2147483647
|
||||
Q16_MIN = -2147483648
|
||||
|
||||
|
||||
def _q16(value: float) -> int:
|
||||
"""Convert float to Q16_16 integer with clamping."""
|
||||
raw = int(value * Q16_SCALE)
|
||||
return max(Q16_MIN, min(Q16_MAX, raw))
|
||||
|
||||
|
||||
def _q16_to_float(q: int) -> float:
|
||||
"""Convert Q16_16 integer to float."""
|
||||
return q / Q16_SCALE
|
||||
|
||||
|
||||
# ── Particle Mass Table (PDG 2024) ───────────────────────────────────
|
||||
# Values in MeV, encoded as Q16_16 (so 938.272 → 938.272 * 65536)
|
||||
# Source: Particle Data Group, https://pdg.lbl.gov
|
||||
|
||||
PARTICLE_MASSES_MEV = {
|
||||
# Leptons
|
||||
'electron': 0.51099895, # ± 0.00000000015
|
||||
'muon': 105.6583755, # ± 0.0000023
|
||||
'tau': 1776.86, # ± 0.12
|
||||
|
||||
# Light quarks (MSbar masses)
|
||||
'up': 2.16, # ± 0.26 (at 2 GeV)
|
||||
'down': 4.67, # ± 0.29 (at 2 GeV)
|
||||
'strange': 93.4, # ± 3.4 (at 2 GeV)
|
||||
|
||||
# Heavy quarks
|
||||
'charm': 1270, # ± 20 (MSbar)
|
||||
'bottom': 4180, # ± 30 (MSbar)
|
||||
'top': 172690, # ± 300 (pole mass)
|
||||
|
||||
# Gauge bosons
|
||||
'photon': 0, # massless
|
||||
'W': 80379, # ± 12
|
||||
'Z': 91187.6, # ± 2.1
|
||||
'gluon': 0, # massless (confined)
|
||||
|
||||
# Higgs
|
||||
'higgs': 125250, # ± 110
|
||||
|
||||
# Light mesons
|
||||
'pion_charged': 139.57039, # ± 0.00018
|
||||
'pion_neutral': 134.9768, # ± 0.0005
|
||||
'kaon_charged': 493.677, # ± 0.016
|
||||
'kaon_neutral': 497.611, # ± 0.013
|
||||
'eta': 547.862, # ± 0.017
|
||||
'eta_prime': 957.78, # ± 0.06
|
||||
|
||||
# Light baryons
|
||||
'proton': 938.27208816, # ± 0.00000029
|
||||
'neutron': 939.56542052, # ± 0.00000054
|
||||
'lambda': 1115.683, # ± 0.006
|
||||
'sigma_plus': 1189.37, # ± 0.07
|
||||
'sigma_zero': 1192.642, # ± 0.024
|
||||
'sigma_minus': 1197.449, # ± 0.030
|
||||
'xi_zero': 1314.86, # ± 0.20
|
||||
'xi_minus': 1321.71, # ± 0.07
|
||||
'omega_minus': 1672.45, # ± 0.29
|
||||
|
||||
# Charmonium
|
||||
'J_psi': 3096.900, # ± 0.006
|
||||
'psi_2S': 3686.097, # ± 0.010
|
||||
|
||||
# Bottomonium
|
||||
'upsilon_1S': 9460.30, # ± 0.26
|
||||
'upsilon_2S': 10023.26, # ± 0.31
|
||||
'upsilon_3S': 10355.2, # ± 0.5
|
||||
}
|
||||
|
||||
# Encode as Q16_16 integers
|
||||
PARTICLE_MASSES_Q16 = {
|
||||
name: _q16(mass) for name, mass in PARTICLE_MASSES_MEV.items()
|
||||
}
|
||||
|
||||
# ── Decay Width Table (PDG 2024) ─────────────────────────────────────
|
||||
# Values in GeV, encoded as Q16_16
|
||||
|
||||
DECAY_WIDTHS_GEV = {
|
||||
'W': 2.085, # ± 0.042
|
||||
'Z': 2.4952, # ± 0.0023
|
||||
'higgs': 0.00407, # ± 0.00010 (SM prediction)
|
||||
'top': 1.42, # ± 0.51 (approximate)
|
||||
'J_psi': 0.0000929, # ± 0.0000017 (keV → GeV: 92.9 keV)
|
||||
'upsilon_1S': 0.00005402, # ± 0.00000125
|
||||
'tau': 2.267e-12, # lifetime → width (very small)
|
||||
'muon': 2.996e-19, # lifetime → width
|
||||
}
|
||||
|
||||
DECAY_WIDTHS_Q16 = {
|
||||
name: _q16(width) for name, width in DECAY_WIDTHS_GEV.items()
|
||||
}
|
||||
|
||||
# ── Cross-Section Table (representative, in pb) ──────────────────────
|
||||
# Q16_16 encoded cross-sections at various energies
|
||||
|
||||
CROSS_SECTIONS_PB = {
|
||||
# pp collisions at √s = 13 TeV (LHC Run 2)
|
||||
'ttbar_13TeV': 830, # ± 40 (top pair production)
|
||||
'W_incl_13TeV': 20500, # ± 500 (W boson inclusive)
|
||||
'Z_incl_13TeV': 1870, # ± 50 (Z boson inclusive)
|
||||
'H_gg_13TeV': 48.6, # ± 2.0 (Higgs via gluon fusion)
|
||||
'H_vbf_13TeV': 3.78, # ± 0.08 (Higgs via VBF)
|
||||
'jets_incl_13TeV': 80000000, # 80 Mb (inclusive jets, pT > 30 GeV)
|
||||
'dijet_13TeV': 10000000, # 10 Mb (dijet, pT > 50 GeV)
|
||||
|
||||
# ee collisions at √s = 91.2 GeV (LEP Z pole)
|
||||
'Z_ee': 2980, # ± 30 (Z → ee)
|
||||
'Z_mumu': 2980, # ± 30 (Z → μμ)
|
||||
'Z_tautau': 2980, # ± 30 (Z → ττ)
|
||||
'Z_had': 1720, # ± 20 (Z → hadrons)
|
||||
}
|
||||
|
||||
CROSS_SECTIONS_Q16 = {
|
||||
name: _q16(xs) for name, xs in CROSS_SECTIONS_PB.items()
|
||||
}
|
||||
|
||||
# ── Trigger Thresholds (LHC, in GeV) ─────────────────────────────────
|
||||
|
||||
TRIGGER_THRESHOLDS_GEV = {
|
||||
'single_electron': 27, # HLT_Ele27_WPTight_Gsf
|
||||
'single_muon': 24, # HLT_IsoMu24
|
||||
'single_photon': 150, # HLT_Photon150
|
||||
'jet_450': 450, # HLT_PFJet450
|
||||
'met_120': 120, # PFMET120
|
||||
'ht_500': 500, # HLT_PFHT500
|
||||
'dilepton_mll': 12, # M(ee/μμ) > 12 GeV
|
||||
'bjet_csv': 0.8484, # DeepJet medium WP
|
||||
}
|
||||
|
||||
TRIGGER_THRESHOLDS_Q16 = {
|
||||
name: _q16(thresh) for name, thresh in TRIGGER_THRESHOLDS_GEV.items()
|
||||
}
|
||||
|
||||
# ── Calorimeter Calibration Constants ────────────────────────────────
|
||||
# Energy scale factors for ECAL/HCAL (dimensionless, Q16_16)
|
||||
|
||||
CALIBRATION_CONSTANTS = {
|
||||
'ecal_eb': 1.000, # ECAL barrel
|
||||
'ecal_ee': 1.005, # ECAL endcap (slightly higher)
|
||||
'hcal_hb': 1.000, # HCAL barrel
|
||||
'hcal_he': 1.010, # HCAL endcap
|
||||
'hcal_hf': 1.050, # HF (forward, larger corrections)
|
||||
'muon_scale': 1.000, # Muon momentum scale
|
||||
'electron_scale': 1.002, # Electron energy scale
|
||||
'photon_scale': 1.003, # Photon energy scale
|
||||
}
|
||||
|
||||
CALIBRATION_Q16 = {
|
||||
name: _q16(cal) for name, cal in CALIBRATION_CONSTANTS.items()
|
||||
}
|
||||
|
||||
|
||||
# ── BRAM Layout for FPGA ─────────────────────────────────────────────
|
||||
|
||||
def generate_bram_layout() -> dict:
|
||||
"""Generate BRAM layout for particle physics LUTs.
|
||||
|
||||
Returns dict mapping BRAM bank to (name, entries, data).
|
||||
Each bank is 256 entries × 32-bit Q16_16.
|
||||
"""
|
||||
banks = {}
|
||||
|
||||
# Bank 0: Particle masses (up to 256 entries)
|
||||
mass_entries = list(PARTICLE_MASSES_Q16.items())[:256]
|
||||
bank0 = [0] * 256
|
||||
for i, (name, q16val) in enumerate(mass_entries):
|
||||
bank0[i] = q16val
|
||||
banks[0] = ('particle_masses', len(mass_entries), bank0)
|
||||
|
||||
# Bank 1: Decay widths
|
||||
width_entries = list(DECAY_WIDTHS_Q16.items())[:256]
|
||||
bank1 = [0] * 256
|
||||
for i, (name, q16val) in enumerate(width_entries):
|
||||
bank1[i] = q16val
|
||||
banks[1] = ('decay_widths', len(width_entries), bank1)
|
||||
|
||||
# Bank 2: Cross-sections
|
||||
xs_entries = list(CROSS_SECTIONS_Q16.items())[:256]
|
||||
bank2 = [0] * 256
|
||||
for i, (name, q16val) in enumerate(xs_entries):
|
||||
bank2[i] = q16val
|
||||
banks[2] = ('cross_sections', len(xs_entries), bank2)
|
||||
|
||||
# Bank 3: Trigger thresholds + calibration
|
||||
trigger_entries = list(TRIGGER_THRESHOLDS_Q16.items())
|
||||
cal_entries = list(CALIBRATION_Q16.items())
|
||||
bank3 = [0] * 256
|
||||
for i, (name, q16val) in enumerate(trigger_entries):
|
||||
bank3[i] = q16val
|
||||
for i, (name, q16val) in enumerate(cal_entries):
|
||||
bank3[len(trigger_entries) + i] = q16val
|
||||
banks[3] = ('triggers_calibration', len(trigger_entries) + len(cal_entries), bank3)
|
||||
|
||||
return banks
|
||||
|
||||
|
||||
def export_bram_init(bank_data: list[int], path: Path):
|
||||
"""Export BRAM initialization data as Verilog initial block."""
|
||||
with open(path, 'w') as f:
|
||||
f.write('// Auto-generated particle physics LUT\n')
|
||||
f.write('// Q16_16 fixed-point, 256 entries × 32-bit\n\n')
|
||||
f.write('initial begin\n')
|
||||
for i, val in enumerate(bank_data):
|
||||
# Format as 32-bit hex
|
||||
hexval = val & 0xFFFFFFFF
|
||||
f.write(f' memory[8\'h{i:02X}] = 32\'h{hexval:08X};\n')
|
||||
f.write('end\n')
|
||||
|
||||
|
||||
def export_all_bram(output_dir: Path):
|
||||
"""Export all BRAM initialization files."""
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
banks = generate_bram_layout()
|
||||
|
||||
for bank_id, (name, count, data) in banks.items():
|
||||
path = output_dir / f'particle_lut_bank{bank_id}_{name}.v'
|
||||
export_bram_init(data, path)
|
||||
print(f'Bank {bank_id}: {name} ({count} entries) → {path}')
|
||||
|
||||
|
||||
# ── Cross-Section Lookup (energy-dependent) ───────────────────────────
|
||||
|
||||
def interpolate_cross_section(energy_tev: float, process: str) -> int:
|
||||
"""Interpolate cross-section at given energy. Returns Q16_16.
|
||||
|
||||
Uses log-log interpolation between known energy points.
|
||||
For energies outside the table, uses power-law extrapolation.
|
||||
"""
|
||||
# Energy points and corresponding cross-sections (approximate)
|
||||
energy_points = {
|
||||
'ttbar': [(7, 165), (8, 230), (13, 830), (14, 930)],
|
||||
'W': [(7, 12200), (8, 14700), (13, 20500), (14, 22000)],
|
||||
'Z': [(7, 1200), (8, 1400), (13, 1870), (14, 2000)],
|
||||
'H_gg': [(7, 15.1), (8, 19.3), (13, 48.6), (14, 54.7)],
|
||||
}
|
||||
|
||||
if process not in energy_points:
|
||||
return _q16(0)
|
||||
|
||||
points = energy_points[process]
|
||||
if energy_tev <= points[0][0]:
|
||||
return _q16(points[0][1])
|
||||
if energy_tev >= points[-1][0]:
|
||||
return _q16(points[-1][1])
|
||||
|
||||
# Log-log interpolation
|
||||
import math
|
||||
for i in range(len(points) - 1):
|
||||
e0, xs0 = points[i]
|
||||
e1, xs1 = points[i + 1]
|
||||
if e0 <= energy_tev <= e1:
|
||||
t = (math.log(energy_tev) - math.log(e0)) / (math.log(e1) - math.log(e0))
|
||||
xs = xs0 * (xs1 / xs0) ** t
|
||||
return _q16(xs)
|
||||
|
||||
return _q16(0)
|
||||
|
||||
|
||||
# ── CLI ───────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('=== Particle Physics LUT (Q16_16) ===\n')
|
||||
|
||||
print('Particle Masses (MeV):')
|
||||
for name, mass in PARTICLE_MASSES_MEV.items():
|
||||
q16 = PARTICLE_MASSES_Q16[name]
|
||||
print(f' {name:<20} {mass:>12.3f} MeV → Q16: {q16:>12d} ({_q16_to_float(q16):.3f} MeV)')
|
||||
|
||||
print(f'\nDecay Widths (GeV):')
|
||||
for name, width in DECAY_WIDTHS_GEV.items():
|
||||
q16 = DECAY_WIDTHS_Q16[name]
|
||||
print(f' {name:<20} {width:>12.6f} GeV → Q16: {q16:>12d}')
|
||||
|
||||
print(f'\nCross-Sections at 13 TeV (pb):')
|
||||
for name, xs in CROSS_SECTIONS_PB.items():
|
||||
q16 = CROSS_SECTIONS_Q16[name]
|
||||
print(f' {name:<20} {xs:>12.1f} pb → Q16: {q16:>12d}')
|
||||
|
||||
print(f'\nTrigger Thresholds (GeV):')
|
||||
for name, thresh in TRIGGER_THRESHOLDS_GEV.items():
|
||||
q16 = TRIGGER_THRESHOLDS_Q16[name]
|
||||
print(f' {name:<20} {thresh:>12.1f} GeV → Q16: {q16:>12d}')
|
||||
|
||||
print(f'\nBRAM Layout:')
|
||||
banks = generate_bram_layout()
|
||||
for bank_id, (name, count, data) in banks.items():
|
||||
nonzero = sum(1 for v in data if v != 0)
|
||||
print(f' Bank {bank_id}: {name} ({count} entries, {nonzero} non-zero)')
|
||||
|
||||
# Export
|
||||
out_dir = Path('/tmp/particle_lut_bram')
|
||||
export_all_bram(out_dir)
|
||||
print(f'\nExported to {out_dir}/')
|
||||
Loading…
Add table
Reference in a new issue