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 find_mass_smear_fracs_ks.py
0004 ---------------------------
0005 Finds the per-pT-bin smearing fraction that makes the Gaussian sigma of the
0006 K0S invariant mass peak in smeared SV simulation match the data.
0007 
0008 Method (per parent K0S pT bin):
0009   1. Fit Gaussian to data K0S mass in FIT window → sigma_data
0010   2. Generate one set of N(0,1) draws (fixed seed) for the sim events in
0011      the bin; reuse the same draws for every fraction value so sigma(f)
0012      is a smooth, deterministic curve.
0013   3. Scan frac from 0 to FRAC_MAX; for each f smear both pion tracks
0014      (dpT = f * pT * z), recompute K0S mass, fit Gaussian → sigma(f)
0015   4. Interpolate sigma(f) to find f* where sigma(f*) = sigma_data.
0016   5. Verify with an independent random seed.
0017 
0018 Output:
0019   mass_smear_fracs_ks.txt    — fracs table (compatible with smear_sv_ks.py --fracs)
0020   plot_mass_smear_fracs_ks.C — ROOT macro: sigma comparison + frac vs pT
0021 
0022 Usage:
0023     python3 find_mass_smear_fracs_ks.py
0024 
0025 Notes:
0026   - avg_pt in the output is the mean K0S pT within each bin; smear_sv_ks.py
0027     uses avg_pt as a track-pT control point, so treat these fracs as an
0028     approximate pT-dependent calibration.
0029   - To use different bins change PT_EDGES below.
0030 """
0031 
0032 import sys
0033 import numpy as np
0034 import uproot
0035 from scipy.optimize import curve_fit
0036 
0037 # ── configuration ──────────────────────────────────────────────────────────────
0038 DATA_FILE = "KShort6RunCombined.root"
0039 SIM_FILE  = "outputKFParticleKShortRecoSV_filtered.root"
0040 
0041 PT_EDGES  = [0.5, 0.8, 1.1, 1.4, 1.8, 2.2, 3.0, 4.0]   # change freely
0042 
0043 FRAC_SCAN = np.linspace(0.0, 0.20, 81)   # 0 → 20 %, step 0.25 %
0044 FIT_LO    = 0.475   # GeV — Gaussian fit window
0045 FIT_HI    = 0.520   # GeV
0046 FIT_NBINS = 45
0047 PION_MASS = 0.13957018  # GeV
0048 RNG_SEED  = 42
0049 
0050 # ── physics helpers ─────────────────────────────────────────────────────────────
0051 def ks_mass_from_p3(px1, py1, pz1, px2, py2, pz2):
0052     """K0S invariant mass from two pion 3-momenta."""
0053     m = PION_MASS
0054     e1 = np.sqrt(px1**2 + py1**2 + pz1**2 + m**2)
0055     e2 = np.sqrt(px2**2 + py2**2 + pz2**2 + m**2)
0056     m2 = (e1+e2)**2 - (px1+px2)**2 - (py1+py2)**2 - (pz1+pz2)**2
0057     return np.sqrt(np.maximum(m2, 0.0))
0058 
0059 
0060 def smear_p3(px, py, pz, f, z):
0061     """Scale transverse momentum by (1 + f*z), preserve pz (i.e. preserve eta)."""
0062     scale = 1.0 + f * z
0063     return px * scale, py * scale, pz
0064 
0065 
0066 def _gauss(x, A, mu, sigma):
0067     return A * np.exp(-0.5 * ((x - mu) / sigma)**2)
0068 
0069 
0070 def fit_sigma(masses_gev):
0071     """Gaussian fit to mass histogram in [FIT_LO, FIT_HI]. Returns (sigma_MeV, err_MeV)."""
0072     sel = masses_gev[(masses_gev >= FIT_LO) & (masses_gev < FIT_HI)]
0073     if len(sel) < 20:
0074         return np.nan, np.nan
0075     counts, edges = np.histogram(sel, bins=FIT_NBINS, range=(FIT_LO, FIT_HI))
0076     cx = 0.5 * (edges[:-1] + edges[1:])
0077     try:
0078         imax = int(np.argmax(counts))
0079         popt, pcov = curve_fit(
0080             _gauss, cx, counts.astype(float),
0081             p0=[float(counts[imax]), float(cx[imax]), 0.010],
0082             bounds=([0, FIT_LO, 0.001], [np.inf, FIT_HI, 0.050]),
0083             maxfev=10000,
0084         )
0085         s     = abs(popt[2]) * 1000.0
0086         s_err = np.sqrt(pcov[2, 2]) * 1000.0 if pcov[2, 2] >= 0 else np.nan
0087         return s, s_err
0088     except Exception:
0089         return np.nan, np.nan
0090 
0091 
0092 # ── main ────────────────────────────────────────────────────────────────────────
0093 def main():
0094     # ── read data ──────────────────────────────────────────────────────────────
0095     print(f"Reading {DATA_FILE} ...")
0096     with uproot.open(DATA_FILE) as f:
0097         t = f["DecayTree"]
0098         d_kspt   = t["K_S0_pT"].array(library="np")
0099         d_ksmass = t["K_S0_mass"].array(library="np")
0100     print(f"  {len(d_kspt)} data events")
0101 
0102     # ── read sim ────────────────────────────────────────────────────────────────
0103     print(f"Reading {SIM_FILE} ...")
0104     with uproot.open(SIM_FILE) as f:
0105         t = f["DecayTree"]
0106         s_kspt = t["K_S0_pT"].array(library="np")
0107         s_px1  = t["track_1_px"].array(library="np")
0108         s_py1  = t["track_1_py"].array(library="np")
0109         s_pz1  = t["track_1_pz"].array(library="np")
0110         s_px2  = t["track_2_px"].array(library="np")
0111         s_py2  = t["track_2_py"].array(library="np")
0112         s_pz2  = t["track_2_pz"].array(library="np")
0113     print(f"  {len(s_kspt)} sim events\n")
0114 
0115     # ── per-bin optimisation ────────────────────────────────────────────────────
0116     results = []
0117     nbins   = len(PT_EDGES) - 1
0118 
0119     hdr = (f"{'pT bin':>16}  {'N_data':>7}  {'N_sim':>6}  "
0120            f"{'σ_data':>8}  {'σ_sim':>7}  {'frac%*':>7}  {'σ_verify':>9}")
0121     print(hdr)
0122     print("─" * len(hdr))
0123 
0124     for b in range(nbins):
0125         lo, hi = PT_EDGES[b], PT_EDGES[b+1]
0126 
0127         # data sigma
0128         dmask        = (d_kspt >= lo) & (d_kspt < hi)
0129         sig_d, sig_d_err = fit_sigma(d_ksmass[dmask])
0130         n_d          = int(dmask.sum())
0131 
0132         # sim events in this bin
0133         smask = (s_kspt >= lo) & (s_kspt < hi)
0134         n_s   = int(smask.sum())
0135         avg_pt = float(s_kspt[smask].mean()) if n_s > 0 else 0.5*(lo+hi)
0136 
0137         if n_d < 20 or n_s < 20 or np.isnan(sig_d):
0138             print(f"  [{lo:.1f},{hi:.1f})  skipped (data={n_d}, sim={n_s})")
0139             results.append(dict(lo=lo, hi=hi, avg_pt=avg_pt,
0140                                 sig_d=np.nan, sig_d_err=np.nan,
0141                                 sig_s0=np.nan, frac_pct=np.nan,
0142                                 sig_verify=np.nan, n_d=n_d, n_s=n_s,
0143                                 scan_f=None, scan_s=None, note="skipped"))
0144             continue
0145 
0146         px1 = s_px1[smask]; py1 = s_py1[smask]; pz1 = s_pz1[smask]
0147         px2 = s_px2[smask]; py2 = s_py2[smask]; pz2 = s_pz2[smask]
0148 
0149         # generate one set of draws; reuse for all fracs → smooth sigma(f) curve
0150         rng = np.random.default_rng(RNG_SEED)
0151         z1  = rng.standard_normal(n_s)
0152         z2  = rng.standard_normal(n_s)
0153 
0154         # scan
0155         scan_s = []
0156         for f in FRAC_SCAN:
0157             px1s, py1s, pz1s = smear_p3(px1, py1, pz1, f, z1)
0158             px2s, py2s, pz2s = smear_p3(px2, py2, pz2, f, z2)
0159             ms   = ks_mass_from_p3(px1s, py1s, pz1s, px2s, py2s, pz2s)
0160             s, _ = fit_sigma(ms)
0161             scan_s.append(s)
0162         scan_s = np.array(scan_s)
0163         sig_s0 = float(scan_s[0])
0164 
0165         # interpolate to find optimal fraction
0166         valid = np.isfinite(scan_s)
0167         if not valid.any():
0168             frac_opt = np.nan
0169             note = "scan_failed"
0170         elif sig_d <= scan_s[valid][0]:
0171             frac_opt = 0.0
0172             note = "no_smear_needed"
0173         elif sig_d >= scan_s[valid][-1]:
0174             frac_opt = float(FRAC_SCAN[valid][-1])
0175             note = "exceeded_range"
0176             print(f"  WARNING [{lo:.1f},{hi:.1f}): σ_data={sig_d:.1f} > "
0177                   f"σ_max={scan_s[valid][-1]:.1f}; try increasing FRAC_MAX")
0178         else:
0179             frac_opt = float(np.interp(sig_d, scan_s[valid], FRAC_SCAN[valid]))
0180             note = "ok"
0181 
0182         # verify with independent random draws
0183         if not np.isnan(frac_opt):
0184             rng2 = np.random.default_rng(RNG_SEED + 1)
0185             z1v  = rng2.standard_normal(n_s)
0186             z2v  = rng2.standard_normal(n_s)
0187             px1v, py1v, pz1v = smear_p3(px1, py1, pz1, frac_opt, z1v)
0188             px2v, py2v, pz2v = smear_p3(px2, py2, pz2, frac_opt, z2v)
0189             mv   = ks_mass_from_p3(px1v, py1v, pz1v, px2v, py2v, pz2v)
0190             sig_verify, _ = fit_sigma(mv)
0191         else:
0192             sig_verify = np.nan
0193 
0194         frac_pct = frac_opt * 100.0 if not np.isnan(frac_opt) else np.nan
0195 
0196         results.append(dict(
0197             lo=lo, hi=hi, avg_pt=avg_pt,
0198             sig_d=sig_d, sig_d_err=sig_d_err,
0199             sig_s0=sig_s0, frac_pct=frac_pct,
0200             sig_verify=sig_verify, n_d=n_d, n_s=n_s,
0201             scan_f=FRAC_SCAN, scan_s=scan_s, note=note,
0202         ))
0203 
0204         v_str = f"{sig_verify:>8.2f}" if not np.isnan(sig_verify) else "     n/a"
0205         print(f"  [{lo:.1f},{hi:.1f})  {n_d:>7}  {n_s:>6}  "
0206               f"{sig_d:>6.2f}±{sig_d_err:<4.2f}  {sig_s0:>6.2f}  "
0207               f"{frac_pct:>6.3f}%  {v_str}")
0208 
0209     # ── write fracs file (smear_sv_ks.py compatible) ───────────────────────────
0210     valid_r = [r for r in results if not np.isnan(r["frac_pct"])]
0211     out_fracs = "mass_smear_fracs_ks.txt"
0212     with open(out_fracs, "w") as fh:
0213         fh.write("# Auto-generated by find_mass_smear_fracs_ks.py\n")
0214         fh.write("# Fracs chosen to match K0S mass Gaussian sigma between smeared sim and data.\n")
0215         fh.write("# avg_pt is mean K0S pT in the bin; fracs are applied per-track in smear_sv_ks.py.\n")
0216         fh.write("bin_lo,bin_hi,avg_pt,best_frac_pct,sig_data_mev,sig_data_err_mev,sig_sim0_mev,sig_verify_mev,n_data,n_sim,note\n")
0217         for r in valid_r:
0218             fh.write(f"{r['lo']:.2f},{r['hi']:.2f},{r['avg_pt']:.4f},"
0219                      f"{r['frac_pct']:.4f},{r['sig_d']:.4f},{r['sig_d_err']:.4f},{r['sig_s0']:.4f},"
0220                      f"{r['sig_verify']:.4f},{r['n_d']},{r['n_s']},{r['note']}\n")
0221     print(f"\nFracs → {out_fracs}")
0222 
0223     # ── ROOT macro ─────────────────────────────────────────────────────────────
0224     n        = len(valid_r)
0225     pts      = ", ".join(f"{r['avg_pt']:.4f}"    for r in valid_r)
0226     sig_d    = ", ".join(f"{r['sig_d']:.4f}"     for r in valid_r)
0227     sig_d_err= ", ".join(f"{r['sig_d_err']:.4f}" for r in valid_r)
0228     sig_s0   = ", ".join(f"{r['sig_s0']:.4f}"    for r in valid_r)
0229     sig_ver  = ", ".join(f"{r['sig_verify']:.4f}"for r in valid_r)
0230     fracs    = ", ".join(f"{r['frac_pct']:.4f}"  for r in valid_r)
0231     xerrs    = ", ".join(f"{(r['hi']-r['lo'])/2:.3f}" for r in valid_r)
0232     zeros    = ", ".join("0" for _ in valid_r)
0233     pt_lo    = PT_EDGES[0]
0234     pt_hi    = PT_EDGES[-1]
0235 
0236     macro = f"""// plot_mass_smear_fracs_ks.C
0237 // Visualises mass-width-matched K0S smearing fractions.
0238 // Run with:  root -l -b -q plot_mass_smear_fracs_ks.C
0239 
0240 #include <ctime>
0241 #include <sstream>
0242 #include <algorithm>
0243 #include <cmath>
0244 
0245 std::string _getDate(){{
0246     std::time_t t=std::time(0); std::tm* n=std::localtime(&t);
0247     std::stringstream s;
0248     s<<(n->tm_mon+1)<<'/'<<n->tm_mday<<'/'<<(n->tm_year+1900);
0249     return s.str();
0250 }}
0251 void _label(double x1,double y1,double x2,double y2){{
0252     TPaveText *p=new TPaveText(x1,y1,x2,y2,"NDC");
0253     p->SetFillStyle(0); p->SetBorderSize(0); p->SetTextFont(42);
0254     p->AddText("#it{{#bf{{sPHENIX}}}} Internal,  #it{{p}}+#it{{p}}  #sqrt{{s}} = 200 GeV");
0255     p->Draw();
0256 }}
0257 void _date(double x1,double y1){{
0258     TLatex l; l.SetNDC(); l.SetTextFont(42); l.SetTextSize(0.035);
0259     l.SetTextColor(kGray+2); l.DrawLatex(x1,y1,_getDate().c_str());
0260 }}
0261 
0262 void plot_mass_smear_fracs_ks() {{
0263     gStyle->SetOptStat(0); gStyle->SetOptTitle(0);
0264 
0265     const int N = {n};
0266     double avg_pt[]    = {{{pts}}};
0267     double sig_data[]  = {{{sig_d}}};
0268     double sig_derr[]  = {{{sig_d_err}}};
0269     double sig_sim0[]  = {{{sig_s0}}};
0270     double sig_check[] = {{{sig_ver}}};
0271     double frac_pct[]  = {{{fracs}}};
0272     double xerr[]      = {{{xerrs}}};
0273     double zero[]      = {{{zeros}}};
0274 
0275     // ── canvas 1: sigma comparison ─────────────────────────────────────────────
0276     TCanvas *c1 = new TCanvas("c1","K0S mass sigma comparison",900,650);
0277     c1->SetLeftMargin(0.13); c1->SetBottomMargin(0.13); c1->SetRightMargin(0.06);
0278 
0279     TGraphErrors *gD = new TGraphErrors(N, avg_pt, sig_data, xerr, sig_derr);
0280     TGraph       *gS = new TGraph(N, avg_pt, sig_sim0);
0281     TGraph       *gC = new TGraph(N, avg_pt, sig_check);
0282 
0283     gD->SetMarkerStyle(20); gD->SetMarkerSize(1.3);
0284     gD->SetMarkerColor(kBlack);  gD->SetLineColor(kBlack);  gD->SetLineWidth(2);
0285     gS->SetMarkerStyle(24); gS->SetMarkerSize(1.3);
0286     gS->SetMarkerColor(kAzure+7); gS->SetLineColor(kAzure+7); gS->SetLineWidth(2);
0287     gC->SetMarkerStyle(21); gC->SetMarkerSize(1.3);
0288     gC->SetMarkerColor(kRed+1);  gC->SetLineColor(kRed+1);  gC->SetLineWidth(2);
0289 
0290     double ymax = 0;
0291     for(int i=0;i<N;++i) ymax = std::max(ymax, std::max({{sig_data[i]+sig_derr[i], sig_sim0[i]}}));
0292     ymax *= 1.35;
0293 
0294     TMultiGraph *mg1 = new TMultiGraph();
0295     mg1->Add(gS,"PL"); mg1->Add(gD,"PE"); mg1->Add(gC,"PL");
0296     mg1->Draw("A");
0297     mg1->GetXaxis()->SetTitle("#it{{p}}_{{T}}^{{K_{{S}}^{{0}}}} (GeV/#it{{c}})");
0298     mg1->GetYaxis()->SetTitle("Gaussian #sigma of K_{{S}}^{{0}} mass (MeV/#it{{c}}^{{2}})");
0299     mg1->GetYaxis()->SetRangeUser(0, ymax);
0300     mg1->GetXaxis()->SetTitleSize(0.05); mg1->GetYaxis()->SetTitleSize(0.047);
0301     mg1->GetYaxis()->SetTitleOffset(1.25);
0302 
0303     TLegend *leg1 = new TLegend(0.14,0.72,0.65,0.87);
0304     leg1->SetBorderSize(0); leg1->SetFillStyle(0); leg1->SetTextSize(0.033);
0305     leg1->AddEntry(gD,"Data",                       "lpe");
0306     leg1->AddEntry(gS,"SV sim (unsmeared)",          "lp");
0307     leg1->AddEntry(gC,"SV sim (mass-width smeared)","lp");
0308     leg1->Draw();
0309     _label(0.14,0.88,0.68,0.96);
0310     _date(0.72,0.90);
0311 
0312     c1->SaveAs("plot_mass_smear_sigma_comparison.pdf");
0313     c1->SaveAs("plot_mass_smear_sigma_comparison.png");
0314     std::cout << "Saved plot_mass_smear_sigma_comparison.pdf/.png\\n";
0315 
0316     // ── canvas 2: optimal fracs vs pT ──────────────────────────────────────────
0317     TCanvas *c2 = new TCanvas("c2","Mass-width smearing fractions",900,650);
0318     c2->SetLeftMargin(0.13); c2->SetBottomMargin(0.13); c2->SetRightMargin(0.06);
0319 
0320     TGraphErrors *gF = new TGraphErrors(N, avg_pt, frac_pct, xerr, zero);
0321     gF->SetMarkerStyle(20); gF->SetMarkerSize(1.4);
0322     gF->SetMarkerColor(kViolet+1); gF->SetLineColor(kViolet+1); gF->SetLineWidth(2);
0323     gF->Draw("APE");
0324     gF->GetXaxis()->SetTitle("#it{{p}}_{{T}}^{{K_{{S}}^{{0}}}} (GeV/#it{{c}})");
0325     gF->GetYaxis()->SetTitle("Smearing fraction (%)");
0326     double fmax = *std::max_element(frac_pct,frac_pct+N)*1.4 + 0.2;
0327     gF->GetYaxis()->SetRangeUser(0, fmax);
0328     gF->GetXaxis()->SetTitleSize(0.05); gF->GetYaxis()->SetTitleSize(0.05);
0329     gF->GetYaxis()->SetTitleOffset(1.2);
0330 
0331     _label(0.14,0.88,0.68,0.96);
0332     _date(0.72,0.90);
0333 
0334     TLegend *leg2 = new TLegend(0.14,0.74,0.60,0.85);
0335     leg2->SetBorderSize(0); leg2->SetFillStyle(0); leg2->SetTextSize(0.033);
0336     leg2->AddEntry(gF,"Frac needed to match K_{{S}}^{{0}} mass #sigma","lp");
0337     leg2->Draw();
0338 
0339     c2->SaveAs("plot_mass_smear_fracs_ks.pdf");
0340     c2->SaveAs("plot_mass_smear_fracs_ks.png");
0341     std::cout << "Saved plot_mass_smear_fracs_ks.pdf/.png\\n";
0342 }}
0343 """
0344     out_mac = "plot_mass_smear_fracs_ks.C"
0345     with open(out_mac, "w") as fh:
0346         fh.write(macro)
0347     print(f"Macro  → {out_mac}")
0348     print(f"\nRun:  root -l -b -q {out_mac}")
0349 
0350     # ── summary stats ──────────────────────────────────────────────────────────
0351     fp = [r["frac_pct"] for r in valid_r]
0352     if fp:
0353         print(f"\n  Frac range: {min(fp):.3f}% – {max(fp):.3f}%")
0354         print(f"  Mean frac:  {np.mean(fp):.3f}%")
0355 
0356 
0357 if __name__ == "__main__":
0358     main()