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_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 # ── configuration ──────────────────────────────────────────────────────────────
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"   # fixed pion smearing from K0S analysis
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)  # proton scan 0 → 60 %, step 0.25 %
0042 #N_AVG_SEEDS  = 5    # tile sim events this many times for stable σ in low-stat bins
0043 FIT_LO    = 1.30   # GeV
0044 FIT_HI    = 1.34   # GeV
0045 FIT_NBINS = 35
0046 PION_MASS   = 0.13957018   # GeV
0047 PROTON_MASS = 0.93827209   # GeV
0048 RNG_SEED  = 42
0049 
0050 
0051 # ── load pion fracs ─────────────────────────────────────────────────────────────
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 # ── physics helpers ─────────────────────────────────────────────────────────────
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 # ── main ────────────────────────────────────────────────────────────────────────
0149 def main():
0150     # ── pion fracs ─────────────────────────────────────────────────────────────
0151     pi_pts, pi_fracs = load_fracs(PION_FRACS)
0152     # Anchor to (pT=0, frac=0) so interpolation below the lowest calibration
0153     # point slopes linearly to zero rather than holding flat.
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     # ── read data ──────────────────────────────────────────────────────────────
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     # ── read sim ────────────────────────────────────────────────────────────────
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)   # lam pion track pT (computed from stored px,py)
0183     s_pt3 = np.sqrt(s_px3**2 + s_py3**2)   # bach pion track pT (computed from stored px,py)
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         # data sigma — Gaussian + linear background to account for combinatorics
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         # sim events in bin
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         # pion frac looked up by mean pion track pT in this bin
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         # generate random draws once; reuse for all proton fracs
0226         rng    = np.random.default_rng(RNG_SEED)
0227         z1     = rng.standard_normal(n_s)   # lam pion draws (fixed)
0228         z3     = rng.standard_normal(n_s)   # bach pion draws (fixed)
0229         z2_base= rng.standard_normal(n_s)   # proton draws (fixed, reused)
0230 
0231         # smear pion tracks once (frac is fixed for this bin)
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         # scan proton frac
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])  # sigma with pion smeared, proton unsmeared
0244 
0245         # interpolate to find optimal proton frac
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         # verify with independent draws
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     # ── write proton fracs file ────────────────────────────────────────────────
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     # ── ROOT macro ─────────────────────────────────────────────────────────────
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()