|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import awkward as ak |
| 4 | +import numpy as np |
| 5 | + |
| 6 | + |
| 7 | +# 3 events with different numbers of electrons and muons in each |
| 8 | +# (numerical values are made up and aren't physically meaningful) |
| 9 | +# --- |
| 10 | +# legend: |
| 11 | +# - 'pt': transverse momentum |
| 12 | +# - 'eta': pseudorapidity (collider detector angle, e.g. CMS detector) |
| 13 | +# - 'phi': azimuthal angle (collider detector angle, e.g. CMS detector) |
| 14 | +electrons = ak.Array( |
| 15 | + [ |
| 16 | + # 2 electrons |
| 17 | + {"pt": [50.0, 60.0], "eta": [2.1, 2.2], "phi": [0.6, 0.7]}, |
| 18 | + # 1 electron |
| 19 | + {"pt": [30.0], "eta": [-1.5], "phi": [0.3]}, |
| 20 | + # 0 electrons |
| 21 | + {"pt": [], "eta": [], "phi": []}, |
| 22 | + ] |
| 23 | +) |
| 24 | +muons = ak.Array( |
| 25 | + [ |
| 26 | + # 1 muon |
| 27 | + {"pt": [45.0], "eta": [2.5], "phi": [0.4]}, |
| 28 | + # 2 muons |
| 29 | + {"pt": [25.0, 35.0], "eta": [-2.0, 1.0], "phi": [0.5, 0.6]}, |
| 30 | + # 1 muon |
| 31 | + {"pt": [15.0], "eta": [0.0], "phi": [0.7]}, |
| 32 | + ] |
| 33 | +) |
| 34 | + |
| 35 | +events = ak.zip({"electrons": electrons, "muons": muons}, depth_limit=1) |
| 36 | + |
| 37 | + |
| 38 | +def physics_analysis(events: ak.Array) -> ak.Array: |
| 39 | + """ |
| 40 | + A oversimplified physics analysis selecting events with exactly 2 leptons (electrons or muons) |
| 41 | + and computing their invariant mass. |
| 42 | + """ |
| 43 | + # select only electrons with pt > 40 |
| 44 | + selected_electrons = events.electrons[events.electrons.pt > 40.0] |
| 45 | + # select only muons with pt > 20 and abs(eta) < 2.4 |
| 46 | + selected_muons = events.muons[ |
| 47 | + (events.muons.pt > 20.0) & (abs(events.muons.eta) < 2.4) |
| 48 | + ] |
| 49 | + |
| 50 | + # choose exactly 2 leptons (electrons or muons) |
| 51 | + two_electrons = selected_electrons[ak.num(selected_electrons.pt, axis=-1) == 2] |
| 52 | + two_muons = selected_muons[ak.num(selected_muons.pt, axis=-1) == 2] |
| 53 | + |
| 54 | + return ak.zip( |
| 55 | + { |
| 56 | + "electron": invariant_mass(two_electrons), |
| 57 | + "muon": invariant_mass(two_muons), |
| 58 | + } |
| 59 | + ) |
| 60 | + |
| 61 | + |
| 62 | +def invariant_mass(two_particles: ak.Array) -> ak.Array: |
| 63 | + """Compute invariant mass of two particles given their pt, eta, phi.""" |
| 64 | + pt1, eta1, phi1 = ( |
| 65 | + two_particles[:, 0].pt, |
| 66 | + two_particles[:, 0].eta, |
| 67 | + two_particles[:, 0].phi, |
| 68 | + ) |
| 69 | + pt2, eta2, phi2 = ( |
| 70 | + two_particles[:, 1].pt, |
| 71 | + two_particles[:, 1].eta, |
| 72 | + two_particles[:, 1].phi, |
| 73 | + ) |
| 74 | + |
| 75 | + # https://en.wikipedia.org/wiki/Invariant_mass#Collider_experiments |
| 76 | + m2 = 2 * pt1 * pt2 * (np.cosh(eta1 - eta2) - np.cos(phi1 - phi2)) |
| 77 | + return np.sqrt(m2) |
| 78 | + |
| 79 | + |
| 80 | +if __name__ == "__main__": |
| 81 | + # ipython -i studies/cccl/playground.py to play around with `events` and `physics_analysis` |
| 82 | + inv_mass = physics_analysis(events) |
| 83 | + |
| 84 | + print("Electron invariant masses (in GeV):", inv_mass.electron) |
| 85 | + print("Muon invariant masses (in GeV):", inv_mass.muon) |
0 commit comments