File indexing completed on 2026-07-16 08:11:23
0001
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
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]
0042
0043 FRAC_SCAN = np.linspace(0.0, 0.20, 81)
0044 FIT_LO = 0.475
0045 FIT_HI = 0.520
0046 FIT_NBINS = 45
0047 PION_MASS = 0.13957018
0048 RNG_SEED = 42
0049
0050
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
0093 def main():
0094
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
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
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
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
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
0150 rng = np.random.default_rng(RNG_SEED)
0151 z1 = rng.standard_normal(n_s)
0152 z2 = rng.standard_normal(n_s)
0153
0154
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
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
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
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
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
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()