File indexing completed on 2026-07-16 08:11:23
0001
0002 """
0003 find_mass_smear_fracs_xi.py
0004 --------------------------------
0005 Finds the per-pT-bin proton smearing fraction that makes the Gaussian sigma
0006 of the Ximinus invariant mass peak in smeared SV simulation match the data.
0007
0008 The pion (track_1) smearing is fixed using the K0S mass-matched fracs from
0009 mass_smear_fracs_ks.txt, looked up by parent Xi pT. Only the proton
0010 (track_2) fraction is scanned.
0011
0012 Method (per Xi pT bin):
0013 1. Load pion fracs from KS fracs file; fix f_pi = interp(Ximinus_pT).
0014 2. Fit Gaussian + linear background to data Xi mass → sigma_data (signal only).
0015 3. Generate one set of N(0,1) draws (fixed seed), reused for all proton
0016 fracs so sigma(f_pro) is a smooth deterministic curve.
0017 4. Scan f_pro; for each: smear pion with f_pi, proton with f_pro, recompute
0018 Xi mass, fit Gaussian → sigma(f_pro).
0019 5. Interpolate to find f_pro* where sigma(f_pro*) = sigma_data.
0020
0021 Output:
0022 mass_smear_fracs_xi_proton.txt — proton fracs (smear_sv_xi.py --proton_fracs)
0023 plot_mass_smear_fracs_xi.C — ROOT macro: sigma comparison + frac vs pT
0024
0025 Usage:
0026 python3 find_mass_smear_fracs_xi.py
0027 """
0028
0029 import sys
0030 import numpy as np
0031 import uproot
0032 from scipy.optimize import curve_fit
0033
0034
0035 DATA_FILE = "Ximinus_fullDataset_finalCuts_0p2pTCut_rapidity1p0Cut_BCOcut_charge_.root"
0036 SIM_FILE = "outputKFParticleXiminusSV_filtered.root"
0037 PION_FRACS = "mass_smear_fracs_ks.txt"
0038
0039 PT_EDGES = [0.5, 0.8, 1.1, 1.4, 1.8, 2.2, 3.0]
0040
0041 PRO_FRAC_SCAN = np.linspace(0.0, 0.60, 241)
0042
0043 FIT_LO = 1.30
0044 FIT_HI = 1.34
0045 FIT_NBINS = 35
0046 PION_MASS = 0.13957018
0047 PROTON_MASS = 0.93827209
0048 RNG_SEED = 42
0049
0050
0051
0052 def load_fracs(csv_path):
0053 """Parse fracs CSV (same format as smear_sv_ks.py); return (avg_pt, frac) arrays."""
0054 pts, fracs = [], []
0055 avg_col = frc_col = None
0056 with open(csv_path) as fh:
0057 for line in fh:
0058 line = line.strip()
0059 if not line or line.startswith("#"):
0060 continue
0061 if line.startswith("bin_lo"):
0062 cols = line.split(",")
0063 avg_col = cols.index("avg_pt")
0064 frc_col = cols.index("best_frac_pct")
0065 continue
0066 parts = line.split(",")
0067 frc = parts[frc_col]
0068 if frc == "nan":
0069 continue
0070 pts.append(float(parts[avg_col]))
0071 fracs.append(float(frc) / 100.0)
0072 return np.array(pts, dtype=np.float64), np.array(fracs, dtype=np.float64)
0073
0074
0075
0076 def xi_mass(px1, py1, pz1, px2, py2, pz2, px3, py3, pz3):
0077 """Xi invariant mass: track1 = pion, track2 = proton, track3 = pion."""
0078 E1 = np.sqrt(px1**2 + py1**2 + pz1**2 + PION_MASS**2)
0079 E2 = np.sqrt(px2**2 + py2**2 + pz2**2 + PROTON_MASS**2)
0080 E3 = np.sqrt(px3**2 + py3**2 + pz3**2 + PION_MASS**2)
0081 m2 = (E1+E2+E3)**2 - (px1+px2+px3)**2 - (py1+py2+py3)**2 - (pz1+pz2+pz3)**2
0082 return np.sqrt(np.maximum(m2, 0.0))
0083
0084
0085 def smear_p3(px, py, pz, f, z):
0086 """Scale transverse momentum by (1 + f*z); preserve pz (eta preserved)."""
0087 scale = 1.0 + f * z
0088 return px * scale, py * scale, pz
0089
0090
0091 def _gauss(x, A, mu, sigma):
0092 return A * np.exp(-0.5 * ((x - mu) / sigma)**2)
0093
0094 def _gauss_plus_pol1(x, A, mu, sigma, C, D):
0095 return A * np.exp(-0.5 * ((x - mu) / sigma)**2) + C + D * x
0096
0097
0098 def fit_sigma_data(masses_gev):
0099 """Gaussian + linear background fit for data; return (sigma_MeV, err_MeV).
0100 Uses the full [FIT_LO, FIT_HI] range so the polynomial absorbs combinatoric
0101 background while the Gaussian component gives the signal width."""
0102 sel = masses_gev[(masses_gev >= FIT_LO) & (masses_gev < FIT_HI)]
0103 if len(sel) < 30:
0104 return np.nan, np.nan
0105 counts, edges = np.histogram(sel, bins=FIT_NBINS, range=(FIT_LO, FIT_HI))
0106 cx = 0.5 * (edges[:-1] + edges[1:])
0107 try:
0108 imax = int(np.argmax(counts))
0109 bg_lvl = 0.5 * (float(counts[0]) + float(counts[-1]))
0110 bg_slp = (float(counts[-1]) - float(counts[0])) / (cx[-1] - cx[0])
0111 p0 = [float(counts[imax]) - bg_lvl, float(cx[imax]), 0.004,
0112 bg_lvl, bg_slp]
0113 popt, pcov = curve_fit(
0114 _gauss_plus_pol1, cx, counts.astype(float), p0=p0,
0115 bounds=([0, FIT_LO, 0.0003, -np.inf, -np.inf],
0116 [np.inf, FIT_HI, 0.020, np.inf, np.inf]),
0117 maxfev=10000,
0118 )
0119 s = abs(popt[2]) * 1000.0
0120 s_err = np.sqrt(pcov[2, 2]) * 1000.0 if pcov[2, 2] >= 0 else np.nan
0121 return s, s_err
0122 except Exception:
0123 return np.nan, np.nan
0124
0125
0126 def fit_sigma(masses_gev):
0127 """Pure Gaussian fit for simulation (no background); return (sigma_MeV, err_MeV)."""
0128 sel = masses_gev[(masses_gev >= FIT_LO) & (masses_gev < FIT_HI)]
0129 if len(sel) < 20:
0130 return np.nan, np.nan
0131 counts, edges = np.histogram(sel, bins=FIT_NBINS, range=(FIT_LO, FIT_HI))
0132 cx = 0.5 * (edges[:-1] + edges[1:])
0133 try:
0134 imax = int(np.argmax(counts))
0135 popt, pcov = curve_fit(
0136 _gauss, cx, counts.astype(float),
0137 p0=[float(counts[imax]), float(cx[imax]), 0.004],
0138 bounds=([0, FIT_LO, 0.0005], [np.inf, FIT_HI, 0.030]),
0139 maxfev=10000,
0140 )
0141 s = abs(popt[2]) * 1000.0
0142 s_err = np.sqrt(pcov[2, 2]) * 1000.0 if pcov[2, 2] >= 0 else np.nan
0143 return s, s_err
0144 except Exception:
0145 return np.nan, np.nan
0146
0147
0148
0149 def main():
0150
0151 pi_pts, pi_fracs = load_fracs(PION_FRACS)
0152
0153
0154 pi_pts = np.concatenate([[0.0], pi_pts])
0155 pi_fracs = np.concatenate([[0.0], pi_fracs])
0156 print(f"Pion fracs from {PION_FRACS} (with (0,0) anchor):")
0157 for p, f in zip(pi_pts, pi_fracs):
0158 print(f" avg_pT={p:.4f} GeV → {f*100:.4f}%")
0159
0160
0161 print(f"\nReading {DATA_FILE} ...")
0162 with uproot.open(DATA_FILE) as f:
0163 t = f["DecayTree"]
0164 d_xipt = t["Ximinus_pT"].array(library="np")
0165 d_ximass = t["Ximinus_mass"].array(library="np")
0166 print(f" {len(d_xipt)} data events")
0167
0168
0169 print(f"Reading {SIM_FILE} ...")
0170 with uproot.open(SIM_FILE) as f:
0171 t = f["DecayTree"]
0172 s_xipt = t["Ximinus_pT"].array(library="np")
0173 s_px1 = t["Lambda0_track_1_px"].array(library="np")
0174 s_py1 = t["Lambda0_track_1_py"].array(library="np")
0175 s_pz1 = t["Lambda0_track_1_pz"].array(library="np")
0176 s_px2 = t["Lambda0_track_2_px"].array(library="np")
0177 s_py2 = t["Lambda0_track_2_py"].array(library="np")
0178 s_pz2 = t["Lambda0_track_2_pz"].array(library="np")
0179 s_px3 = t["track_3_px"].array(library="np")
0180 s_py3 = t["track_3_py"].array(library="np")
0181 s_pz3 = t["track_3_pz"].array(library="np")
0182 s_pt1 = np.sqrt(s_px1**2 + s_py1**2)
0183 s_pt3 = np.sqrt(s_px3**2 + s_py3**2)
0184 print(f" {len(s_xipt)} sim events\n")
0185
0186 results = []
0187 nbins = len(PT_EDGES) - 1
0188
0189 hdr = (f"{'pT bin':>16} {'N_data':>7} {'N_sim':>6} "
0190 f"{'σ_data':>8} {'σ_sim':>7} {'<pT_π>':>7} {'f_pi%':>6} {'f_pro%*':>8} {'σ_verify':>9}")
0191 print(hdr)
0192 print("─" * len(hdr))
0193
0194 for b in range(nbins):
0195 lo, hi = PT_EDGES[b], PT_EDGES[b+1]
0196
0197
0198 dmask = (d_xipt >= lo) & (d_xipt < hi)
0199 sig_d, sig_d_err = fit_sigma_data(d_ximass[dmask])
0200 n_d = int(dmask.sum())
0201
0202
0203 smask = (s_xipt >= lo) & (s_xipt < hi)
0204 n_s = int(smask.sum())
0205 avg_pt = float(s_xipt[smask].mean()) if n_s > 0 else 0.5*(lo+hi)
0206 avg_pi1_pt = float(s_pt1[smask].mean()) if n_s > 0 else 0.0
0207 avg_pi3_pt = float(s_pt3[smask].mean()) if n_s > 0 else 0.0
0208
0209 if n_d < 20 or n_s < 20 or np.isnan(sig_d):
0210 print(f" [{lo:.1f},{hi:.1f}) skipped (data={n_d}, sim={n_s})")
0211 results.append(dict(lo=lo, hi=hi, avg_pt=avg_pt,
0212 sig_d=np.nan, sig_d_err=np.nan, sig_s0=np.nan,
0213 f_pi1_pct=np.nan, f_pi3_pct=np.nan, frac_pct=np.nan,
0214 sig_verify=np.nan, n_d=n_d, n_s=n_s, note="skipped"))
0215 continue
0216
0217 px1 = s_px1[smask]; py1 = s_py1[smask]; pz1 = s_pz1[smask]
0218 px2 = s_px2[smask]; py2 = s_py2[smask]; pz2 = s_pz2[smask]
0219 px3 = s_px3[smask]; py3 = s_py3[smask]; pz3 = s_pz3[smask]
0220
0221
0222 f_pi1 = float(np.interp(avg_pi1_pt, pi_pts, pi_fracs))
0223 f_pi3 = float(np.interp(avg_pi3_pt, pi_pts, pi_fracs))
0224
0225
0226 rng = np.random.default_rng(RNG_SEED)
0227 z1 = rng.standard_normal(n_s)
0228 z3 = rng.standard_normal(n_s)
0229 z2_base= rng.standard_normal(n_s)
0230
0231
0232 px1s, py1s, pz1s = smear_p3(px1, py1, pz1, f_pi1, z1)
0233 px3s, py3s, pz3s = smear_p3(px3, py3, pz3, f_pi3, z3)
0234
0235
0236 scan_s = []
0237 for f_pro in PRO_FRAC_SCAN:
0238 px2s, py2s, pz2s = smear_p3(px2, py2, pz2, f_pro, z2_base)
0239 ms = xi_mass(px1s, py1s, pz1s, px2s, py2s, pz2s, px3s, py3s, pz3s)
0240 s, _ = fit_sigma(ms)
0241 scan_s.append(s)
0242 scan_s = np.array(scan_s)
0243 sig_s0 = float(scan_s[0])
0244
0245
0246 valid = np.isfinite(scan_s)
0247 if not valid.any():
0248 frac_opt = np.nan
0249 note = "scan_failed"
0250 elif sig_d <= scan_s[valid][0]:
0251 frac_opt = 0.0
0252 note = "no_proton_smear_needed"
0253 elif sig_d >= scan_s[valid][-1]:
0254 frac_opt = float(PRO_FRAC_SCAN[valid][-1])
0255 note = "exceeded_range"
0256 print(f" WARNING [{lo:.1f},{hi:.1f}): σ_data={sig_d:.1f} > "
0257 f"σ_max={scan_s[valid][-1]:.1f}; try increasing PRO_FRAC_SCAN max")
0258 else:
0259 frac_opt = float(np.interp(sig_d, scan_s[valid], PRO_FRAC_SCAN[valid]))
0260 note = "ok"
0261
0262
0263 if not np.isnan(frac_opt):
0264 rng2 = np.random.default_rng(RNG_SEED + 1)
0265 z1v = rng2.standard_normal(n_s)
0266 z2v = rng2.standard_normal(n_s)
0267 z3v = rng2.standard_normal(n_s)
0268 px1v, py1v, pz1v = smear_p3(px1, py1, pz1, f_pi1, z1v)
0269 px2v, py2v, pz2v = smear_p3(px2, py2, pz2, frac_opt, z2v)
0270 px3v, py3v, pz3v = smear_p3(px3, py3, pz3, f_pi3, z3v)
0271 mv = xi_mass(px1v, py1v, pz1v, px2v, py2v, pz2v, px3v, py3v, pz3v)
0272 sig_verify, _ = fit_sigma(mv)
0273 else:
0274 sig_verify = np.nan
0275
0276 frac_pct = frac_opt * 100.0 if not np.isnan(frac_opt) else np.nan
0277
0278 results.append(dict(
0279 lo=lo, hi=hi, avg_pt=avg_pt, avg_pi1_pt=avg_pi1_pt, avg_pi3_pt=avg_pi3_pt,
0280 sig_d=sig_d, sig_d_err=sig_d_err,
0281 sig_s0=sig_s0, f_pi1_pct=f_pi1*100, f_pi3_pct=f_pi3*100,
0282 frac_pct=frac_pct, sig_verify=sig_verify,
0283 n_d=n_d, n_s=n_s, note=note,
0284 ))
0285
0286 v_str = f"{sig_verify:>8.2f}" if not np.isnan(sig_verify) else " n/a"
0287 print(f" [{lo:.1f},{hi:.1f}) {n_d:>7} {n_s:>6} "
0288 f"{sig_d:>6.2f}±{sig_d_err:<4.2f} {sig_s0:>6.2f} "
0289 f"{avg_pi1_pt:>6.3f} {avg_pi3_pt:>6.3f} {f_pi1*100:>5.2f}% {f_pi3*100:>5.2f}% {frac_pct:>7.3f}% {v_str}")
0290
0291
0292 valid_r = [r for r in results if not np.isnan(r["frac_pct"])]
0293 out_fracs = "mass_smear_fracs_xi_proton.txt"
0294 with open(out_fracs, "w") as fh:
0295 fh.write("# Auto-generated by find_mass_smear_fracs_xi.py\n")
0296 fh.write(f"# Proton fracs from Xi mass-width matching; pion fracs fixed from {PION_FRACS}\n")
0297 fh.write("bin_lo,bin_hi,avg_pt,avg_pi1_pt,avg_pi3_pt,best_frac_pct,sig_data_mev,sig_data_err_mev,sig_sim0_mev,sig_verify_mev,f_pi1_pct,f_pi3_pct,n_data,n_sim,note\n")
0298 for r in valid_r:
0299 fh.write(f"{r['lo']:.2f},{r['hi']:.2f},{r['avg_pt']:.4f},{r['avg_pi1_pt']:.4f},{r['avg_pi3_pt']:.4f},"
0300 f"{r['frac_pct']:.4f},{r['sig_d']:.4f},{r['sig_d_err']:.4f},{r['sig_s0']:.4f},"
0301 f"{r['sig_verify']:.4f},{r['f_pi1_pct']:.4f},{r['f_pi3_pct']:.4f},"
0302 f"{r['n_d']},{r['n_s']},{r['note']}\n")
0303 print(f"\nProton fracs → {out_fracs}")
0304
0305
0306 n = len(valid_r)
0307 pts = ", ".join(f"{r['avg_pt']:.4f}" for r in valid_r)
0308 sig_d_v = ", ".join(f"{r['sig_d']:.4f}" for r in valid_r)
0309 sig_derr = ", ".join(f"{r['sig_d_err']:.4f}" for r in valid_r)
0310 sig_s0_v = ", ".join(f"{r['sig_s0']:.4f}" for r in valid_r)
0311 sig_ver = ", ".join(f"{r['sig_verify']:.4f}" for r in valid_r)
0312 fracs = ", ".join(f"{r['frac_pct']:.4f}" for r in valid_r)
0313 xerrs = ", ".join(f"{(r['hi']-r['lo'])/2:.3f}" for r in valid_r)
0314 zeros = ", ".join("0" for _ in valid_r)
0315 pt_lo = PT_EDGES[0]; pt_hi = PT_EDGES[-1]
0316
0317 macro = f"""// plot_mass_smear_fracs_xi.C
0318 // Xi mass-width-matched proton smearing fractions.
0319 // Pion fracs fixed from K0S analysis; only proton frac was scanned.
0320 // Run with: root -l -b -q plot_mass_smear_fracs_xi.C
0321
0322 #include <ctime>
0323 #include <sstream>
0324 #include <algorithm>
0325 #include <cmath>
0326
0327 std::string _getDate(){{
0328 std::time_t t=std::time(0); std::tm* n=std::localtime(&t);
0329 std::stringstream s;
0330 s<<(n->tm_mon+1)<<'/'<<n->tm_mday<<'/'<<(n->tm_year+1900);
0331 return s.str();
0332 }}
0333 void _label(double x1,double y1,double x2,double y2){{
0334 TPaveText *p=new TPaveText(x1,y1,x2,y2,"NDC");
0335 p->SetFillStyle(0); p->SetBorderSize(0); p->SetTextFont(42);
0336 p->AddText("#it{{#bf{{sPHENIX}}}} Internal, #it{{p}}+#it{{p}} #sqrt{{s}} = 200 GeV");
0337 p->Draw();
0338 }}
0339 void _date(double x1,double y1){{
0340 TLatex l; l.SetNDC(); l.SetTextFont(42); l.SetTextSize(0.035);
0341 l.SetTextColor(kGray+2); l.DrawLatex(x1,y1,_getDate().c_str());
0342 }}
0343
0344 void plot_mass_smear_fracs_xi() {{
0345 gStyle->SetOptStat(0); gStyle->SetOptTitle(0);
0346
0347 const int N = {n};
0348 double avg_pt[] = {{{pts}}};
0349 double sig_data[] = {{{sig_d_v}}};
0350 double sig_derr[] = {{{sig_derr}}};
0351 double sig_sim0[] = {{{sig_s0_v}}};
0352 double sig_check[] = {{{sig_ver}}};
0353 double frac_pct[] = {{{fracs}}};
0354 double xerr[] = {{{xerrs}}};
0355 double zero[] = {{{zeros}}};
0356
0357 // ── canvas 1: sigma comparison ─────────────────────────────────────────────
0358 TCanvas *c1 = new TCanvas("c1","Xi mass sigma comparison",900,650);
0359 c1->SetLeftMargin(0.13); c1->SetBottomMargin(0.13); c1->SetRightMargin(0.06);
0360
0361 TGraphErrors *gD = new TGraphErrors(N, avg_pt, sig_data, xerr, sig_derr);
0362 TGraph *gS = new TGraph(N, avg_pt, sig_sim0);
0363 TGraph *gC = new TGraph(N, avg_pt, sig_check);
0364
0365 gD->SetMarkerStyle(20); gD->SetMarkerSize(1.3);
0366 gD->SetMarkerColor(kBlack); gD->SetLineColor(kBlack); gD->SetLineWidth(2);
0367 gS->SetMarkerStyle(24); gS->SetMarkerSize(1.3);
0368 gS->SetMarkerColor(kAzure+7); gS->SetLineColor(kAzure+7); gS->SetLineWidth(2);
0369 gC->SetMarkerStyle(21); gC->SetMarkerSize(1.3);
0370 gC->SetMarkerColor(kRed+1); gC->SetLineColor(kRed+1); gC->SetLineWidth(2);
0371
0372 double ymax = 0;
0373 for(int i=0;i<N;++i) ymax=std::max(ymax,std::max(sig_data[i]+sig_derr[i],sig_sim0[i]));
0374 ymax *= 1.35;
0375
0376 TMultiGraph *mg1 = new TMultiGraph();
0377 mg1->Add(gS,"PL"); mg1->Add(gD,"PE"); mg1->Add(gC,"PL");
0378 mg1->Draw("A");
0379 mg1->GetXaxis()->SetTitle("#it{{p}}_{{T}}^{{#Xi^{{-}}}} (GeV/#it{{c}})");
0380 mg1->GetYaxis()->SetTitle("Gaussian #sigma of #Xi^{{-}} mass (MeV/#it{{c}}^{{2}})");
0381 mg1->GetYaxis()->SetRangeUser(0, ymax);
0382 mg1->GetXaxis()->SetTitleSize(0.05); mg1->GetYaxis()->SetTitleSize(0.045);
0383 mg1->GetYaxis()->SetTitleOffset(1.30);
0384
0385 TLegend *leg1 = new TLegend(0.14,0.72,0.70,0.87);
0386 leg1->SetBorderSize(0); leg1->SetFillStyle(0); leg1->SetTextSize(0.033);
0387 leg1->AddEntry(gD,"Data", "lpe");
0388 leg1->AddEntry(gS,"SV sim (pion smeared only)", "lp");
0389 leg1->AddEntry(gC,"SV sim (pion + proton smeared)", "lp");
0390 leg1->Draw();
0391 _label(0.14,0.88,0.68,0.96);
0392 _date(0.72,0.90);
0393
0394 c1->SaveAs("plot_mass_smear_sigma_xi.pdf");
0395 c1->SaveAs("plot_mass_smear_sigma_xi.png");
0396 std::cout << "Saved plot_mass_smear_sigma_xi.pdf/.png\\n";
0397
0398 // ── canvas 2: proton fracs vs pT ───────────────────────────────────────────
0399 TCanvas *c2 = new TCanvas("c2","Xi proton smearing fractions",900,650);
0400 c2->SetLeftMargin(0.13); c2->SetBottomMargin(0.13); c2->SetRightMargin(0.06);
0401
0402 TGraphErrors *gF = new TGraphErrors(N, avg_pt, frac_pct, xerr, zero);
0403 gF->SetMarkerStyle(20); gF->SetMarkerSize(1.4);
0404 gF->SetMarkerColor(kOrange+1); gF->SetLineColor(kOrange+1); gF->SetLineWidth(2);
0405 gF->Draw("APE");
0406 gF->GetXaxis()->SetTitle("#it{{p}}_{{T}}^{{#Xi^{{-}}}} (GeV/#it{{c}})");
0407 gF->GetYaxis()->SetTitle("Proton smearing fraction (%)");
0408 double fmax = *std::max_element(frac_pct,frac_pct+N)*1.4 + 0.5;
0409 gF->GetYaxis()->SetRangeUser(0, fmax);
0410 gF->GetXaxis()->SetTitleSize(0.05); gF->GetYaxis()->SetTitleSize(0.05);
0411 gF->GetYaxis()->SetTitleOffset(1.2);
0412
0413 _label(0.14,0.88,0.68,0.96);
0414 _date(0.72,0.90);
0415
0416 TLegend *leg2 = new TLegend(0.14,0.74,0.65,0.85);
0417 leg2->SetBorderSize(0); leg2->SetFillStyle(0); leg2->SetTextSize(0.033);
0418 leg2->AddEntry(gF,"Proton frac to match #Xi^{{-}} mass #sigma (pion fixed from K_{{S}}^{{0}})","lp");
0419 leg2->Draw();
0420
0421 c2->SaveAs("plot_mass_smear_fracs_xi.pdf");
0422 c2->SaveAs("plot_mass_smear_fracs_xi.png");
0423 std::cout << "Saved plot_mass_smear_fracs_xi.pdf/.png\\n";
0424 }}
0425 """
0426 out_mac = "plot_mass_smear_fracs_xi.C"
0427 with open(out_mac, "w") as fh:
0428 fh.write(macro)
0429 print(f"Macro → {out_mac}")
0430 print(f"\nRun: root -l -b -q {out_mac}")
0431
0432 fp = [r["frac_pct"] for r in valid_r]
0433 if fp:
0434 print(f"\n Proton frac range: {min(fp):.3f}% – {max(fp):.3f}%")
0435 print(f" Mean proton frac: {np.mean(fp):.3f}%")
0436
0437
0438 if __name__ == "__main__":
0439 main()