Back to home page

sPhenix code displayed by LXR

 
 

    


File indexing completed on 2026-07-16 08:11:23

0001 #!/usr/bin/env python3
0002 """
0003 smear_sv_ks.py
0004 --------------
0005 Reads outputKFParticleKShortRecoSV.root (or any KFParticle K0S SV file),
0006 applies per-pion Gaussian pT smearing using the calibrated fracs from
0007 optimal_sv_smear_fracs.txt, and writes a new ROOT file with updated
0008 track and K0S kinematics.
0009 
0010 Smearing model
0011 --------------
0012   δpT = frac(pT) × pT × z,   z ~ N(0,1)
0013 
0014   frac(pT) is **linearly interpolated** between the calibrated (avg_pt, frac)
0015   control points, with flat extrapolation outside the range.  Each pion is
0016   smeared independently using its own pT.  eta is preserved: only pT is
0017   scaled, pz follows via  pz_new = pT_new × sinh(eta).
0018 
0019 Branches updated
0020 ----------------
0021   track_{1,2}_px, _py, _pz          smeared momenta
0022   track_{1,2}_pT, _p, _pE           recomputed from smeared 3-momentum
0023   track_{1,2}_phi                   recomputed
0024   track_{1,2}_pseudorapidity        recomputed (= eta)
0025   track_{1,2}_rapidity              recomputed from smeared E, pz
0026   track_{1,2}_theta                 recomputed
0027   K_S0_px, _py, _pz                 sum of smeared daughter momenta
0028   K_S0_pT, _p, _pE                  recomputed
0029   K_S0_phi, _pseudorapidity,        recomputed
0030     _rapidity, _theta
0031   K_S0_mass                         invariant mass from smeared daughters (GeV)
0032   secondary_vertex_mass_pionPID     same as K_S0_mass (pion-PID assumption)
0033 
0034 Branches NOT updated (KFP-fitter outputs / vertex geometry)
0035 -----------------------------------------------------------
0036   *_chi2, *_nDoF, *_Covariance      KFP fitter quantities
0037   *_massErr, *_pTErr, *_pErr        KFP uncertainties
0038   *_IP*, *_DCA, *_DIRA              require re-propagating track to vertex
0039   *_decayLength*, *_decayTime*      vertex geometry
0040   *_x, *_y, *_z (positions)        vertex positions unchanged
0041   true_* branches                   MC truth unchanged
0042 
0043 Note on vector branches
0044 -----------------------
0045   Variable-length branches (std::vector<T>: hit IDs, residuals, IP_allPV,
0046   track history, etc.) are NOT included in the output.  None of these are
0047   used by the standard quality cuts or kinematic analysis.  If you need them,
0048   use ROOT's hadd or TTree::AddFriend to merge with the original file.
0049 
0050 Usage
0051 -----
0052   python3 smear_sv_ks.py                           # defaults
0053   python3 smear_sv_ks.py --input foo.root --output foo_smeared.root
0054   python3 smear_sv_ks.py --seed 123
0055 """
0056 
0057 import argparse, sys
0058 import numpy as np
0059 import uproot
0060 
0061 # ── physical constants ─────────────────────────────────────────────────────
0062 PION_MASS   = 0.13957018   # GeV
0063 TREE_NAME   = "DecayTree"
0064 RNG_SEED    = 42
0065 
0066 
0067 # ── helpers ────────────────────────────────────────────────────────────────
0068 
0069 def load_fracs(csv_path):
0070     """
0071     Parse optimal_sv_smear_fracs.txt.
0072     Returns (avg_pt_array, frac_array) suitable for np.interp.
0073     Skips rows with nan frac.
0074     """
0075     pts, fracs = [], []
0076     avg_col = frc_col = None
0077     with open(csv_path) as fh:
0078         for line in fh:
0079             line = line.strip()
0080             if not line or line.startswith("#"):
0081                 continue
0082             if line.startswith("bin_lo"):
0083                 cols = line.split(",")
0084                 avg_col = cols.index("avg_pt")
0085                 frc_col = cols.index("best_frac_pct")
0086                 continue
0087             parts = line.split(",")
0088             frc = parts[frc_col]
0089             if frc == "nan":
0090                 continue
0091             pts.append(float(parts[avg_col]))
0092             fracs.append(float(frc) / 100.0)
0093     return np.array(pts, dtype=np.float64), np.array(fracs, dtype=np.float64)
0094 
0095 
0096 def interp_frac(pt_arr, pts, fracs):
0097     """
0098     Linearly interpolate smearing frac at each pT value.
0099     Flat extrapolation outside [pts[0], pts[-1]].
0100     """
0101     return np.interp(pt_arr.astype(np.float64), pts, fracs)
0102 
0103 
0104 def smear_track(px, py, pz, frac_arr, z):
0105     """
0106     Smear pT of a track array; preserve eta.
0107     pT_new = pT * (1 + frac * z)
0108     Returns (px_new, py_new, pz_new) as float64.
0109     """
0110     px, py, pz = (np.asarray(a, dtype=np.float64) for a in (px, py, pz))
0111     pT   = np.sqrt(px**2 + py**2)
0112     phi  = np.arctan2(py, px)
0113     ptot = np.sqrt(pT**2 + pz**2)
0114     eta  = np.arctanh(np.clip(pz / np.where(ptot > 0, ptot, 1e-12),
0115                                -1 + 1e-9, 1 - 1e-9))
0116     pT_s = np.maximum(pT * (1.0 + frac_arr * z), 0.0)
0117     return pT_s * np.cos(phi), pT_s * np.sin(phi), pT_s * np.sinh(eta)
0118 
0119 
0120 def derived(px, py, pz, mass_gev):
0121     """
0122     Compute (pT, p, pE, phi, eta, rapidity, theta) from 3-momentum + mass.
0123     All inputs/outputs float64.
0124     """
0125     px, py, pz = (np.asarray(a, dtype=np.float64) for a in (px, py, pz))
0126     pT    = np.sqrt(px**2 + py**2)
0127     p     = np.sqrt(px**2 + py**2 + pz**2)
0128     pE    = np.sqrt(p**2 + mass_gev**2)
0129     phi   = np.arctan2(py, px)
0130     p_s   = np.where(p > 0, p, 1e-12)
0131     eta   = np.arctanh(np.clip(pz / p_s, -1 + 1e-9, 1 - 1e-9))
0132     # rapidity = 0.5 * ln((E+pz)/(E-pz))
0133     denom = np.abs(pE - np.abs(pz))
0134     rap   = np.where(denom > 1e-12,
0135                      0.5 * np.log((pE + pz) / np.where(denom > 1e-12, np.abs(pE - pz), 1e-12)),
0136                      np.sign(pz) * 1e6)
0137     theta = np.arctan2(pT, pz)
0138     return pT, p, pE, phi, eta, rap, theta
0139 
0140 
0141 def inv_mass(px1, py1, pz1, px2, py2, pz2, m1=PION_MASS, m2=PION_MASS):
0142     """K0S invariant mass in GeV from two daughters."""
0143     E1 = np.sqrt(px1**2 + py1**2 + pz1**2 + m1**2)
0144     E2 = np.sqrt(px2**2 + py2**2 + pz2**2 + m2**2)
0145     m2v = (E1+E2)**2 - (px1+px2)**2 - (py1+py2)**2 - (pz1+pz2)**2
0146     return np.sqrt(np.maximum(m2v, 0.0))
0147 
0148 
0149 # ── main ───────────────────────────────────────────────────────────────────
0150 
0151 def main():
0152     parser = argparse.ArgumentParser(
0153         description="Apply SV K0S pion smearing and write updated ROOT file.")
0154     parser.add_argument("--input",  default="outputKFParticleKShortRecoSV_filtered.root")
0155     parser.add_argument("--fracs",  default="mass_smear_fracs_ks.txt")
0156     parser.add_argument("--output", default="outputKFParticleKShortRecoSV_smeared.root")
0157     parser.add_argument("--seed",   type=int, default=RNG_SEED)
0158     args = parser.parse_args()
0159 
0160     # ── calibration ────────────────────────────────────────────────────────
0161     pts, fracs = load_fracs(args.fracs)
0162     print(f"Loaded {len(fracs)} calibration points from {args.fracs}:")
0163     for p, f in zip(pts, fracs):
0164         print(f"  avg_pT = {p:.4f} GeV  →  frac = {f*100:.4f}%")
0165     print(f"  (linear interp; flat extrapolation outside [{pts[0]:.3f}, {pts[-1]:.3f}] GeV)\n")
0166 
0167     # ── identify branches to load ──────────────────────────────────────────
0168     # Skip std::vector<T> branches — none are used by quality cuts or
0169     # kinematic analysis.  Write a note in the output.
0170     print(f"Reading branch list from {args.input} ...")
0171     with uproot.open(args.input) as fin:
0172         tree = fin[TREE_NAME]
0173         all_branches = tree.keys()
0174         skip_vector  = [b for b in all_branches
0175                         if "vector" in tree[b].typename.lower()
0176                         or "std::" in tree[b].typename.lower()]
0177         load_branches = [b for b in all_branches if b not in skip_vector]
0178 
0179     print(f"  Loading {len(load_branches)} scalar/fixed-array branches "
0180           f"(skipping {len(skip_vector)} vector branches)")
0181     if skip_vector:
0182         print(f"  Skipped: {', '.join(skip_vector[:6])}"
0183               + (" ..." if len(skip_vector) > 6 else ""))
0184 
0185     # ── load data ──────────────────────────────────────────────────────────
0186     print(f"\nReading events ...")
0187     with uproot.open(args.input) as fin:
0188         d = fin[TREE_NAME].arrays(load_branches, library="np")
0189     n = len(d[load_branches[0]])
0190     print(f"  {n:,} events read\n")
0191 
0192     # ── smear ──────────────────────────────────────────────────────────────
0193     rng = np.random.default_rng(args.seed)
0194     z1  = rng.standard_normal(n)
0195     z2  = rng.standard_normal(n)
0196 
0197     pT1 = d["track_1_pT"].astype(np.float64)
0198     pT2 = d["track_2_pT"].astype(np.float64)
0199 
0200     f1  = interp_frac(pT1, pts, fracs)
0201     f2  = interp_frac(pT2, pts, fracs)
0202 
0203     print("Smearing tracks ...")
0204     px1s, py1s, pz1s = smear_track(d["track_1_px"], d["track_1_py"], d["track_1_pz"], f1, z1)
0205     px2s, py2s, pz2s = smear_track(d["track_2_px"], d["track_2_py"], d["track_2_pz"], f2, z2)
0206 
0207     # ── derived kinematics ─────────────────────────────────────────────────
0208     pT1s, p1s, pE1s, phi1s, eta1s, rap1s, th1s = derived(px1s, py1s, pz1s, PION_MASS)
0209     pT2s, p2s, pE2s, phi2s, eta2s, rap2s, th2s = derived(px2s, py2s, pz2s, PION_MASS)
0210 
0211     ks_mass = inv_mass(px1s, py1s, pz1s, px2s, py2s, pz2s)
0212     ks_px, ks_py, ks_pz = px1s+px2s, py1s+py2s, pz1s+pz2s
0213     ks_pTs, ks_ps, ks_pEs, ks_phis, ks_etas, ks_raps, ks_ths = derived(
0214         ks_px, ks_py, ks_pz, ks_mass)
0215 
0216     # ── sanity check (signal-window mean & sigma) ──────────────────────────
0217     win = (d["K_S0_mass"]*1000 > 480) & (d["K_S0_mass"]*1000 < 516)
0218     before = d["K_S0_mass"][win].astype(np.float64)*1000
0219     after  = ks_mass[win]*1000
0220     print(f"Signal-window mass (MeV):")
0221     print(f"  Before smearing: mean={before.mean():.2f}  σ_rms={before.std():.2f}")
0222     print(f"  After  smearing: mean={after.mean():.2f}  σ_rms={after.std():.2f}\n")
0223 
0224     # ── replacement map: branch → new float64 array ───────────────────────
0225     replacements = {
0226         "track_1_px":            px1s,
0227         "track_1_py":            py1s,
0228         "track_1_pz":            pz1s,
0229         "track_1_pT":            pT1s,
0230         "track_1_p":             p1s,
0231         "track_1_pE":            pE1s,
0232         "track_1_phi":           phi1s,
0233         "track_1_pseudorapidity": eta1s,
0234         "track_1_rapidity":      rap1s,
0235         "track_1_theta":         th1s,
0236         "track_2_px":            px2s,
0237         "track_2_py":            py2s,
0238         "track_2_pz":            pz2s,
0239         "track_2_pT":            pT2s,
0240         "track_2_p":             p2s,
0241         "track_2_pE":            pE2s,
0242         "track_2_phi":           phi2s,
0243         "track_2_pseudorapidity": eta2s,
0244         "track_2_rapidity":      rap2s,
0245         "track_2_theta":         th2s,
0246         "K_S0_px":               ks_px,
0247         "K_S0_py":               ks_py,
0248         "K_S0_pz":               ks_pz,
0249         "K_S0_pT":               ks_pTs,
0250         "K_S0_p":                ks_ps,
0251         "K_S0_pE":               ks_pEs,
0252         "K_S0_phi":              ks_phis,
0253         "K_S0_pseudorapidity":   ks_etas,
0254         "K_S0_rapidity":         ks_raps,
0255         "K_S0_theta":            ks_ths,
0256         "K_S0_mass":             ks_mass,
0257         "secondary_vertex_mass_pionPID": ks_mass,
0258     }
0259 
0260     # ── build output dict (all numpy, preserve original dtypes) ───────────
0261     out = {}
0262     for b in load_branches:
0263         if b in replacements:
0264             out[b] = replacements[b].astype(d[b].dtype)
0265         else:
0266             out[b] = d[b]
0267 
0268     not_found = [b for b in replacements if b not in load_branches]
0269     if not_found:
0270         print(f"WARNING: replacement branches not in file: {not_found}")
0271 
0272     # ── write ──────────────────────────────────────────────────────────────
0273     print(f"Writing {args.output} ...")
0274     with uproot.recreate(args.output) as fout:
0275         fout[TREE_NAME] = out
0276 
0277     import os
0278     sz = os.path.getsize(args.output) / 1e6
0279     print(f"Done — {n:,} events, {len(out)} branches → {args.output}  ({sz:.1f} MB)")
0280     if skip_vector:
0281         print(f"\nNote: {len(skip_vector)} vector branches omitted (hit IDs, "
0282               f"residuals, IP_allPV, track history).")
0283         print(f"To recover them use ROOT:\n"
0284               f"  new_tree->AddFriend(original_tree)  # or hadd after friend setup")
0285 
0286 
0287 if __name__ == "__main__":
0288     main()