From 096a566aaf2ee706570eecdd8e9a24414f07cb6d Mon Sep 17 00:00:00 2001 From: Brandon Schneider Date: Thu, 28 May 2026 19:26:27 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20particle=20physics=20LUT=20=E2=80=94=20?= =?UTF-8?q?50=20years=20of=20PDG=20data=20as=20Q16=5F16=20BRAM=20tables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- 4-Infrastructure/shim/particle_physics_lut.py | 323 ++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 4-Infrastructure/shim/particle_physics_lut.py diff --git a/4-Infrastructure/shim/particle_physics_lut.py b/4-Infrastructure/shim/particle_physics_lut.py new file mode 100644 index 00000000..d65602d7 --- /dev/null +++ b/4-Infrastructure/shim/particle_physics_lut.py @@ -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}/')