SND@LHC Software
Loading...
Searching...
No Matches
reweighting_function.FluxReweighter Class Reference
Collaboration diagram for reweighting_function.FluxReweighter:

Public Member Functions

 __init__ (self, config_path="config.yaml")
 
 read_files (self, path)
 
 base_cut (self, df)
 
 compute_ratio (self)
 
 fit (self, degree)
 
 run (self)
 

Static Public Member Functions

 fit_poly_weighted (x, y, sigma, degree=1)
 

Public Attributes

 cfg
 
 bins_E
 
 E
 
 H_DPM
 
 s_DPM
 
 H_LC
 
 s_LC
 
 w
 
 sig
 

Static Public Attributes

dict PID = {"nue": 12, "numu": 14, "nutau": 16}
 
list COLS
 

Protected Member Functions

 _load_config (self, yaml_path)
 
 _report_selection (self, name, df, pid)
 
 _hist (self, df, mask, use_weight=True)
 
 _fit_mask (self, verbose=False)
 
 _make_plot (self, deg, poly, chi2red, fit_mask)
 

Static Protected Member Functions

 _require (d, key, section)
 
 _adaptive_edges (fine, dsw, dsw2, lsw, lsw2, min_neff)
 
 _poly_equation_text (poly, deg)
 

Detailed Description

Derive the reweighting function w(E) = fast-sim / DPMJET from flux files.

The class histograms the four input samples in neutrino energy, forms the
ratio bin by bin with propagated errors, and fits a polynomial to it. All
settings come from a YAML config passed to the constructor.

Typical use
-----------
    rw = FluxReweighter("Config_epos.yaml")
    rw.run()                       # diagnostic plot per degree
    poly, chi2red = rw.fit(2)      # or just the deg-2 weight function

Attributes populated by :meth:`compute_ratio`
---------------------------------------------
E      : bin centres [GeV]
w      : the ratio fast-sim / DPMJET per bin (this is w(E) sampled)
sig    : uncertainty on the ratio per bin
H_DPM  : summed DPMJET histogram (light + charm), s_DPM its error
H_LC   : summed fast-sim histogram (light + charm), s_LC its error

Definition at line 104 of file reweighting_function.py.

Constructor & Destructor Documentation

◆ __init__()

reweighting_function.FluxReweighter.__init__ (   self,
  config_path = "config.yaml" 
)
Load the YAML config and prepare the default (uniform) bin edges.

Adaptive edges, if requested, are computed later in
:meth:`compute_ratio` because they depend on the selected events.

Definition at line 141 of file reweighting_function.py.

141 def __init__(self, config_path="config.yaml"):
142 """Load the YAML config and prepare the default (uniform) bin edges.
143
144 Adaptive edges, if requested, are computed later in
145 :meth:`compute_ratio` because they depend on the selected events.
146 """
147 self.cfg = self._load_config(config_path)
148 self.bins_E = np.linspace(
149 self.cfg["energy_min"], self.cfg["energy_max"], num=self.cfg["bins"]
150 )
151 # filled in by compute_ratio()
152 self.E = self.w = self.sig = None
153 self.H_DPM = self.s_DPM = self.H_LC = self.s_LC = None
154

Member Function Documentation

◆ _adaptive_edges()

reweighting_function.FluxReweighter._adaptive_edges (   fine,
  dsw,
  dsw2,
  lsw,
  lsw2,
  min_neff 
)
staticprotected
Build variable-width bin edges with a minimum of effective entries.

Starting from a fine uniform grid, fine bins are accumulated left to
right and a bin is closed as soon as *both* the DPMJET and the fast-sim
side reach

    N_eff = (sum w)^2 / sum w^2  >=  min_neff

N_eff is the number of unweighted events that would give the same
statistical power, so this guarantees no bin is dominated by a handful
of large-weight events -- the usual failure mode at high energy, where
a single charm event with a huge weight otherwise makes the ratio jump
and its error explode.

Parameters
----------
fine : ndarray
    Edges of the fine starting grid.
dsw, dsw2 : ndarray
    Per-fine-bin sum of weights and sum of weights squared, DPMJET side.
lsw, lsw2 : ndarray
    The same for the fast-sim side.
min_neff : float
    Required effective entries per output bin.

Returns
-------
ndarray
    The merged bin edges (always closed at ``fine[-1]``).

Definition at line 163 of file reweighting_function.py.

163 def _adaptive_edges(fine, dsw, dsw2, lsw, lsw2, min_neff):
164 """Build variable-width bin edges with a minimum of effective entries.
165
166 Starting from a fine uniform grid, fine bins are accumulated left to
167 right and a bin is closed as soon as *both* the DPMJET and the fast-sim
168 side reach
169
170 N_eff = (sum w)^2 / sum w^2 >= min_neff
171
172 N_eff is the number of unweighted events that would give the same
173 statistical power, so this guarantees no bin is dominated by a handful
174 of large-weight events -- the usual failure mode at high energy, where
175 a single charm event with a huge weight otherwise makes the ratio jump
176 and its error explode.
177
178 Parameters
179 ----------
180 fine : ndarray
181 Edges of the fine starting grid.
182 dsw, dsw2 : ndarray
183 Per-fine-bin sum of weights and sum of weights squared, DPMJET side.
184 lsw, lsw2 : ndarray
185 The same for the fast-sim side.
186 min_neff : float
187 Required effective entries per output bin.
188
189 Returns
190 -------
191 ndarray
192 The merged bin edges (always closed at ``fine[-1]``).
193 """
194 edges = [fine[0]]
195 adsw = adsw2 = alsw = alsw2 = 0.0
196 for i in range(len(fine) - 1):
197 adsw += dsw[i]; adsw2 += dsw2[i]
198 alsw += lsw[i]; alsw2 += lsw2[i]
199 nd = (adsw * adsw / adsw2) if adsw2 > 0 else 0.0
200 nl = (alsw * alsw / alsw2) if alsw2 > 0 else 0.0
201 if min(nd, nl) >= min_neff:
202 edges.append(fine[i + 1]); adsw = adsw2 = alsw = alsw2 = 0.0
203 if edges[-1] != fine[-1]:
204 edges.append(fine[-1])
205 return np.array(edges)
206

◆ _fit_mask()

reweighting_function.FluxReweighter._fit_mask (   self,
  verbose = False 
)
protected
Boolean mask selecting which ratio bins enter the polynomial fit.

A bin is kept when it lies inside ``fit_range``, has a non-zero ratio
and error, passes the relative-error cap, and falls inside the optional
ratio window. With ``verbose`` the number of bins removed by the ratio
window is printed.

Definition at line 454 of file reweighting_function.py.

454 def _fit_mask(self, verbose=False):
455 """Boolean mask selecting which ratio bins enter the polynomial fit.
456
457 A bin is kept when it lies inside ``fit_range``, has a non-zero ratio
458 and error, passes the relative-error cap, and falls inside the optional
459 ratio window. With ``verbose`` the number of bins removed by the ratio
460 window is printed.
461 """
462 c = self.cfg
463 rel_err = np.zeros_like(self.w)
464 nz = (self.w > 0) & (self.sig > 0)
465 rel_err[nz] = self.sig[nz] / self.w[nz]
466
467 base = (
468 (self.E >= c["fit_min"]) & (self.E <= c["fit_max"]) &
469 (self.w > 0) & (self.sig > 0) &
470 (rel_err <= c["rel_err_max"])
471 )
472
473 rmin = -np.inf if c["ratio_min"] is None else c["ratio_min"]
474 rmax = np.inf if c["ratio_max"] is None else c["ratio_max"]
475 within = (self.w >= rmin) & (self.w <= rmax)
476
477 if verbose and (c["ratio_min"] is not None or c["ratio_max"] is not None):
478 dropped = int((base & ~within).sum())
479 print(f" ratio window [{rmin:g}, {rmax:g}]: dropped {dropped} of "
480 f"{int(base.sum())} otherwise-eligible bin(s)")
481 return base & within
482

◆ _hist()

reweighting_function.FluxReweighter._hist (   self,
  df,
  mask,
  use_weight = True 
)
protected
Weighted energy histogram of the selected rows, with its error.

Each event contributes its own ``weight in pb`` (or 1.0 when
``use_weight`` is false). The error is the standard weighted-histogram
one, sqrt(sum w^2) per bin.

Returns ``(counts, sigma)``.

Definition at line 347 of file reweighting_function.py.

347 def _hist(self, df, mask, use_weight=True):
348 """Weighted energy histogram of the selected rows, with its error.
349
350 Each event contributes its own ``weight in pb`` (or 1.0 when
351 ``use_weight`` is false). The error is the standard weighted-histogram
352 one, sqrt(sum w^2) per bin.
353
354 Returns ``(counts, sigma)``.
355 """
356 x = df.loc[mask, "E in GeV"]
357 w = df.loc[mask, "weight in pb"] if use_weight else pd.Series(1.0, index=x.index)
358 counts, _ = np.histogram(x, bins=self.bins_E, weights=w)
359 var, _ = np.histogram(x, bins=self.bins_E, weights=w ** 2)
360 return counts.astype(float), np.sqrt(var.astype(float))
361

◆ _load_config()

reweighting_function.FluxReweighter._load_config (   self,
  yaml_path 
)
protected
Parse the reweighter YAML into a flat dict of settings.

Only the four input paths are mandatory; every other key has a default
so a minimal config stays valid. See Config_epos.yaml for the meaning
of each field.

Definition at line 207 of file reweighting_function.py.

207 def _load_config(self, yaml_path):
208 """Parse the reweighter YAML into a flat dict of settings.
209
210 Only the four input paths are mandatory; every other key has a default
211 so a minimal config stays valid. See Config_epos.yaml for the meaning
212 of each field.
213 """
214 with open(yaml_path) as f:
215 raw = yaml.safe_load(f) or {}
216
217 inp = raw.get("inputs", {})
218 eb = raw.get("energy_binning", {})
219 fr = raw.get("fit_range", {})
220 cut = raw.get("cuts", {})
221 out = raw.get("output", {})
222 wt = raw.get("weights", {})
223
224 return {
225 # --- input flux files (mandatory) ---
226 "light_dpm": self._require(inp, "light_dpm", "inputs"),
227 "charm_dpm": self._require(inp, "charm_dpm", "inputs"),
228 "light_fastsim": self._require(inp, "light_fastsim", "inputs"),
229 "charm_fastsim": self._require(inp, "charm_fastsim", "inputs"),
230
231 # --- neutrino flavour entering the ratio ---
232 "flavour": raw.get("flavour", "mu"),
233
234 # --- histogram binning in energy ---
235 "energy_min": float(eb.get("min", 0.0)),
236 "energy_max": float(eb.get("max", 4000.0)),
237 "bins": int(eb.get("bins", 51)), # uniform mode: n edges
238 "bin_mode": eb.get("mode", "uniform"), # "uniform" | "adaptive"
239 "fine_bins": int(eb.get("fine_bins", 400)), # adaptive: start grid
240 "min_neff": float(eb.get("min_neff", 10.0)),# adaptive: entries/bin
241
242 # --- energy window the polynomial is fitted over ---
243 "fit_min": float(fr.get("min", 0.0)),
244 "fit_max": float(fr.get("max", 4000.0)),
245
246 # --- one diagnostic plot per degree ---
247 "degrees": [int(d) for d in raw.get("polynomial_degrees", [1, 2, 3])],
248
249 # --- quality gates on which bins enter the fit ---
250 # Relative-error cap: drops bins whose sigma/ratio is too large.
251 # This is the unbiased gate -- prefer it over the ratio window.
252 "rel_err_max": float(raw.get("rel_err_max", 3.0)),
253 # Optional window on the ratio VALUE. Keep it symmetric about the
254 # data: a one-sided cap removes up-fluctuations only and biases the
255 # fit low. None disables that side.
256 "ratio_min": (None if raw.get("ratio_min") is None
257 else float(raw.get("ratio_min"))),
258 "ratio_max": (None if raw.get("ratio_max") is None
259 else float(raw.get("ratio_max"))),
260
261 # --- Selection cut ---
262 "cut_energy_min": float(cut.get("energy_min", 10.0)),
263 "cut_x_col": cut.get("x_column", "x in m"),
264 "cut_y_col": cut.get("y_column", "y in m"),
265 "cut_x_min": float(cut.get("x_min", -46.0)),
266 "cut_x_max": float(cut.get("x_max", -7.0)),
267 "cut_y_min": float(cut.get("y_min", 15.0)),
268 "cut_y_max": float(cut.get("y_max", 54.0)),
269 # x/y are multiplied by this before the bounds are applied, so cm
270 # bounds can be used with metre columns (unit_scale: 100).
271 "cut_scale": float(cut.get("unit_scale", 1.0)),
272 # Set false when the inputs already passed the cut-based analysis.
273 "cut_enabled": bool(cut.get("enabled", True)),
274
275 # --- whether 'weight in pb' is applied when histogramming ---
276 # Both default to true. Disabling one side only puts numerator and
277 # denominator on different scales and breaks the ratio's meaning.
278 "weight_dpm": bool(wt.get("dpm", True)),
279 "weight_fastsim": bool(wt.get("fastsim", True)),
280
281 # --- output ---
282 "outpath": out.get("path", "./output"),
283 "label": out.get("label", "EPOS+POWHEG"),
284 }
285

◆ _make_plot()

reweighting_function.FluxReweighter._make_plot (   self,
  deg,
  poly,
  chi2red,
  fit_mask 
)
protected
Write the diagnostic ratio plot for one polynomial degree.

Shows every ratio bin faintly, the subset entering the fit with error
bars, the fitted curve inside ``fit_range`` and its extrapolation
outside, plus a statistics box. Comparing the two point sets shows at a
glance what the quality gates removed.

Returns the path of the saved PDF.

Definition at line 532 of file reweighting_function.py.

532 def _make_plot(self, deg, poly, chi2red, fit_mask):
533 """Write the diagnostic ratio plot for one polynomial degree.
534
535 Shows every ratio bin faintly, the subset entering the fit with error
536 bars, the fitted curve inside ``fit_range`` and its extrapolation
537 outside, plus a statistics box. Comparing the two point sets shows at a
538 glance what the quality gates removed.
539
540 Returns the path of the saved PDF.
541 """
542 c = self.cfg
543 E, w, sig = self.E, self.w, self.sig
544 E_fit, w_fit, sig_fit = E[fit_mask], w[fit_mask], sig[fit_mask]
545
546 E_plot_fit = np.linspace(c["fit_min"], c["fit_max"], 300)
547 E_plot_all = np.linspace(E.min(), E.max(), 400)
548
549 fig, ax = plt.subplots(figsize=(9, 5.8))
550 ax.errorbar(E, w, yerr=sig, fmt="o", ms=4, alpha=0.25, label="all data")
551 ax.errorbar(E_fit, w_fit, yerr=sig_fit, fmt="o", ms=5, capsize=3, label="used in fit")
552 ax.plot(E_plot_fit, poly(E_plot_fit), "-", lw=2.5, label=f"poly deg={deg} (fit range)")
553 ax.plot(E_plot_all, poly(E_plot_all), "--", lw=1.8, alpha=0.6, label="poly extrapolation")
554
555 ax.axvline(c["fit_min"], color="grey", ls=":", lw=1)
556 ax.axvline(c["fit_max"], color="grey", ls=":", lw=1)
557 ax.axhline(1.0, color="grey", ls="--", lw=1) # ratio = 1: models agree
558
559 ax.set_xlabel("Energy [GeV]")
560 ax.set_ylabel("ratio")
561 ax.set_title(c["label"])
562 ax.grid(True, which="both", alpha=0.3)
563 ax.legend(fontsize="small", loc="lower left")
564
565 stats_text = "\n".join([
566 rf"deg = {deg}",
567 rf"$\chi^2$/ndf = {chi2red:.2f}",
568 rf"$N_\mathrm{{points}}$ = {len(E_fit)}",
569 rf"$E_\mathrm{{fit}}$: {c['fit_min']:.0f}-{c['fit_max']:.0f} GeV",
570 self._poly_equation_text(poly, deg),
571 ])
572 ax.text(0.98, 0.95, stats_text, transform=ax.transAxes,
573 ha="right", va="top", fontsize=10,
574 bbox=dict(boxstyle="round", facecolor="white", alpha=0.85))
575
576 fig.tight_layout()
577 outfile = os.path.join(c["outpath"],
578 f"reweight_poly_deg{deg}_{c['flavour']}.pdf")
579 fig.savefig(outfile)
580 plt.close(fig)
581 return outfile
582

◆ _poly_equation_text()

reweighting_function.FluxReweighter._poly_equation_text (   poly,
  deg 
)
staticprotected
Format w(E) as a LaTeX string for the plot's statistics box.

Definition at line 520 of file reweighting_function.py.

520 def _poly_equation_text(poly, deg):
521 """Format w(E) as a LaTeX string for the plot's statistics box."""
522 terms = []
523 for p, coef in zip(range(deg, -1, -1), poly.coefficients):
524 if p == 0:
525 terms.append(rf"({coef:.3e})")
526 elif p == 1:
527 terms.append(rf"({coef:.3e})\,E")
528 else:
529 terms.append(rf"({coef:.3e})\,E^{p}")
530 return r"$w(E) = " + " + ".join(terms) + r"$"
531

◆ _report_selection()

reweighting_function.FluxReweighter._report_selection (   self,
  name,
  df,
  pid 
)
protected
Print how many rows survive each selection stage for one file.

Printed before the ratio is built. If a stage removes everything the
actual x/y data ranges are shown next to the cut bounds, which makes a
unit mismatch (cut in cm, data in metres) immediately obvious.

Returns the number of surviving rows.

Definition at line 316 of file reweighting_function.py.

316 def _report_selection(self, name, df, pid):
317 """Print how many rows survive each selection stage for one file.
318
319 Printed before the ratio is built. If a stage removes everything the
320 actual x/y data ranges are shown next to the cut bounds, which makes a
321 unit mismatch (cut in cm, data in metres) immediately obvious.
322
323 Returns the number of surviving rows.
324 """
325 c = self.cfg
326 fl = (df["Neutrino"].abs() == pid)
327 if not c["cut_enabled"]:
328 print(f" [{name:13s}] N={len(df):>7} | flavour={int(fl.sum()):>7} "
329 f"| cuts disabled")
330 return int(fl.sum())
331 e = fl & (df["E in GeV"] > c["cut_energy_min"])
332 xs = df[c["cut_x_col"]] * c["cut_scale"]
333 ys = df[c["cut_y_col"]] * c["cut_scale"]
334 x = e & xs.between(c["cut_x_min"], c["cut_x_max"])
335 y = x & ys.between(c["cut_y_min"], c["cut_y_max"])
336 n = int(y.sum())
337 print(f" [{name:13s}] N={len(df):>7} | flavour={int(fl.sum()):>7} "
338 f"| +E>{c['cut_energy_min']:g}={int(e.sum()):>7} "
339 f"| +x={int(x.sum()):>7} | +y={n:>7}")
340 if n == 0:
341 print(f" 0 survive -> {c['cut_x_col']}*{c['cut_scale']:g} range "
342 f"[{xs.min():.3g}, {xs.max():.3g}] vs cut [{c['cut_x_min']}, {c['cut_x_max']}]; "
343 f"{c['cut_y_col']}*{c['cut_scale']:g} range "
344 f"[{ys.min():.3g}, {ys.max():.3g}] vs cut [{c['cut_y_min']}, {c['cut_y_max']}]")
345 return n
346

◆ _require()

reweighting_function.FluxReweighter._require (   d,
  key,
  section 
)
staticprotected
Return a mandatory config value, or raise naming the missing field.

Definition at line 156 of file reweighting_function.py.

156 def _require(d, key, section):
157 """Return a mandatory config value, or raise naming the missing field."""
158 if d is None or key not in d or d[key] is None:
159 raise ValueError(f"Missing required config field: {section}.{key}")
160 return d[key]
161

◆ base_cut()

reweighting_function.FluxReweighter.base_cut (   self,
  df 
)
Boolean mask for the fiducial (detector acceptance) selection.

Applies a minimum energy and an x/y box, with the x/y columns scaled by
``cuts.unit_scale`` first. Returns all-true when ``cuts.enabled`` is
false, i.e. when the inputs already went through the cut-based analysis.

Definition at line 298 of file reweighting_function.py.

298 def base_cut(self, df):
299 """Boolean mask for the fiducial (detector acceptance) selection.
300
301 Applies a minimum energy and an x/y box, with the x/y columns scaled by
302 ``cuts.unit_scale`` first. Returns all-true when ``cuts.enabled`` is
303 false, i.e. when the inputs already went through the cut-based analysis.
304 """
305 c = self.cfg
306 if not c["cut_enabled"]:
307 return pd.Series(True, index=df.index)
308 xs = df[c["cut_x_col"]] * c["cut_scale"]
309 ys = df[c["cut_y_col"]] * c["cut_scale"]
310 return (
311 (df["E in GeV"] > c["cut_energy_min"]) &
312 xs.between(c["cut_x_min"], c["cut_x_max"]) &
313 ys.between(c["cut_y_min"], c["cut_y_max"])
314 )
315

◆ compute_ratio()

reweighting_function.FluxReweighter.compute_ratio (   self)
Build the fast-sim / DPMJET ratio versus energy.

Steps: read the four files, print the selection report, optionally
derive adaptive bin edges, histogram each sample, sum light + charm on
each side, and divide.

Light and charm are histogrammed separately and the histograms added,
which is equivalent to weighting every event individually and filling
one histogram -- each event always keeps its own ``weight in pb``. The
errors are combined in quadrature, sqrt(s_light^2 + s_charm^2).

Populates ``E``, ``w``, ``sig``, ``H_DPM``, ``s_DPM``, ``H_LC``,
``s_LC`` and returns ``self`` so calls can be chained.

Definition at line 365 of file reweighting_function.py.

365 def compute_ratio(self):
366 """Build the fast-sim / DPMJET ratio versus energy.
367
368 Steps: read the four files, print the selection report, optionally
369 derive adaptive bin edges, histogram each sample, sum light + charm on
370 each side, and divide.
371
372 Light and charm are histogrammed separately and the histograms added,
373 which is equivalent to weighting every event individually and filling
374 one histogram -- each event always keeps its own ``weight in pb``. The
375 errors are combined in quadrature, sqrt(s_light^2 + s_charm^2).
376
377 Populates ``E``, ``w``, ``sig``, ``H_DPM``, ``s_DPM``, ``H_LC``,
378 ``s_LC`` and returns ``self`` so calls can be chained.
379 """
380 pid = self.PID[self.cfg["flavour"]]
381
382 df_ld = self.read_files(self.cfg["light_dpm"])
383 df_cd = self.read_files(self.cfg["charm_dpm"])
384 df_lf = self.read_files(self.cfg["light_fastsim"])
385 df_cf = self.read_files(self.cfg["charm_fastsim"])
386
387 # Selection breakdown per file; also reveals unit mismatches.
388 print(f"Selection report ({self.cfg['label']}, flavour={self.cfg['flavour']}):")
389 n_ld = self._report_selection("light_dpm", df_ld, pid)
390 n_cd = self._report_selection("charm_dpm", df_cd, pid)
391 n_lf = self._report_selection("light_fastsim", df_lf, pid)
392 n_cf = self._report_selection("charm_fastsim", df_cf, pid)
393 if (n_ld + n_cd) == 0 or (n_lf + n_cf) == 0:
394 raise ValueError(
395 "Selection leaves no events on one side of the ratio "
396 f"(DPMJET={n_ld + n_cd}, fast-sim={n_lf + n_cf}). See the report "
397 "above: the most common cause is the x/y fiducial cut being in "
398 "different units than the data (cut in cm, 'x in m' column in "
399 "metres). Fix via cuts.unit_scale (e.g. 100 for m->cm) or by "
400 "putting the bounds in the data's units.")
401
402 # Optional adaptive binning. Uses the same flavour + fiducial selection
403 # as the histograms below, and overwrites self.bins_E before filling.
404 if self.cfg["bin_mode"] == "adaptive":
405 fine = np.linspace(self.cfg["energy_min"], self.cfg["energy_max"],
406 self.cfg["fine_bins"] + 1)
407
408 def _fine(df, use_w):
409 """Sum of w and w^2 per fine bin for one selected sample."""
410 m = (df["Neutrino"].abs() == pid) & self.base_cut(df)
411 E = df.loc[m, "E in GeV"].to_numpy()
412 w = (df.loc[m, "weight in pb"].to_numpy()
413 if use_w else np.ones(int(m.sum())))
414 sw, _ = np.histogram(E, bins=fine, weights=w)
415 sw2, _ = np.histogram(E, bins=fine, weights=w ** 2)
416 return sw.astype(float), sw2.astype(float)
417
418 wd, wf = self.cfg["weight_dpm"], self.cfg["weight_fastsim"]
419 aLd = _fine(df_ld, wd); aCd = _fine(df_cd, wd)
420 aLf = _fine(df_lf, wf); aCf = _fine(df_cf, wf)
421 self.bins_E = self._adaptive_edges(
422 fine, aLd[0] + aCd[0], aLd[1] + aCd[1],
423 aLf[0] + aCf[0], aLf[1] + aCf[1], self.cfg["min_neff"])
424 print(f"adaptive binning -> {len(self.bins_E) - 1} bins "
425 f"(min_neff={self.cfg['min_neff']:g})")
426
427 # Denominator: DPMJET total = light + charm.
428 H_L_dpm, s_L_dpm = self._hist(df_ld, (df_ld["Neutrino"].abs() == pid) & self.base_cut(df_ld), use_weight=self.cfg["weight_dpm"])
429 H_C_dpm, s_C_dpm = self._hist(df_cd, (df_cd["Neutrino"].abs() == pid) & self.base_cut(df_cd), use_weight=self.cfg["weight_dpm"])
430 self.H_DPM = H_L_dpm + H_C_dpm
431 self.s_DPM = np.sqrt(s_L_dpm ** 2 + s_C_dpm ** 2)
432
433 # Numerator: fast-sim total = light (EPOS) + charm (POWHEG).
434 H_L_fs, s_L_fs = self._hist(df_lf, (df_lf["Neutrino"].abs() == pid) & self.base_cut(df_lf), use_weight=self.cfg["weight_fastsim"])
435 H_C_fs, s_C_fs = self._hist(df_cf, (df_cf["Neutrino"].abs() == pid) & self.base_cut(df_cf), use_weight=self.cfg["weight_fastsim"])
436 self.H_LC = H_L_fs + H_C_fs
437 self.s_LC = np.sqrt(s_L_fs ** 2 + s_C_fs ** 2)
438
439 # Ratio with relative errors added in quadrature. Bins empty on either
440 # side are left at zero and are excluded from the fit by _fit_mask().
441 centers = 0.5 * (self.bins_E[:-1] + self.bins_E[1:])
442 R = np.zeros_like(self.H_DPM)
443 sR = np.zeros_like(self.H_DPM)
444
445 m = (self.H_DPM > 0) & (self.H_LC > 0)
446 R[m] = self.H_LC[m] / self.H_DPM[m]
447 rel2_num = (self.s_LC[m] / self.H_LC[m]) ** 2
448 rel2_den = (self.s_DPM[m] / self.H_DPM[m]) ** 2
449 sR[m] = R[m] * np.sqrt(rel2_num + rel2_den)
450
451 self.E, self.w, self.sig = centers, R, sR
452 return self
453

◆ fit()

reweighting_function.FluxReweighter.fit (   self,
  degree 
)
Return the reweighting polynomial w(E) for one degree.

Computes the ratio on first use. Evaluate the result per event as
``poly(E)``; it is only meaningful inside ``fit_range``.

Returns ``(poly, chi2red)``. Raises ``ValueError`` if the selection
leaves fewer points than the degree requires.

Definition at line 495 of file reweighting_function.py.

495 def fit(self, degree):
496 """Return the reweighting polynomial w(E) for one degree.
497
498 Computes the ratio on first use. Evaluate the result per event as
499 ``poly(E)``; it is only meaningful inside ``fit_range``.
500
501 Returns ``(poly, chi2red)``. Raises ``ValueError`` if the selection
502 leaves fewer points than the degree requires.
503 """
504 if self.E is None:
505 self.compute_ratio()
506 mask = self._fit_mask(verbose=True)
507 n_fit = int(mask.sum())
508 if n_fit < degree + 1:
509 raise ValueError(
510 f"Not enough fit points ({n_fit}) for degree {degree}")
511 poly, _chi2, chi2red = self.fit_poly_weighted(
512 self.E[mask], self.w[mask], self.sig[mask], degree=degree
513 )
514 return poly, chi2red
515

◆ fit_poly_weighted()

reweighting_function.FluxReweighter.fit_poly_weighted (   x,
  y,
  sigma,
  degree = 1 
)
static
Least-squares polynomial fit weighted by 1/sigma.

Returns ``(poly, chi2, chi2/ndf)`` where ``poly`` is a ``np.poly1d``
with coefficients ordered highest power first.

Definition at line 484 of file reweighting_function.py.

484 def fit_poly_weighted(x, y, sigma, degree=1):
485 """Least-squares polynomial fit weighted by 1/sigma.
486
487 Returns ``(poly, chi2, chi2/ndf)`` where ``poly`` is a ``np.poly1d``
488 with coefficients ordered highest power first.
489 """
490 poly = np.poly1d(np.polyfit(x, y, deg=degree, w=1.0 / sigma))
491 chi2 = np.sum(((y - poly(x)) / sigma) ** 2)
492 ndof = max(len(x) - (degree + 1), 1)
493 return poly, chi2, chi2 / ndof
494

◆ read_files()

reweighting_function.FluxReweighter.read_files (   self,
  path 
)
Read one flux text file into a DataFrame with named columns.

POWHEG samples carry an extra trailing ``iEvent`` column; this is
detected from the file name so the column names line up either way.

Definition at line 289 of file reweighting_function.py.

289 def read_files(self, path):
290 """Read one flux text file into a DataFrame with named columns.
291
292 POWHEG samples carry an extra trailing ``iEvent`` column; this is
293 detected from the file name so the column names line up either way.
294 """
295 cols = self.COLS + ["iEvent"] if "powheg" in path.lower() else self.COLS
296 return pd.read_csv(path, sep=r"\s+", header=None, names=cols)
297

◆ run()

reweighting_function.FluxReweighter.run (   self)
Fit every degree in ``polynomial_degrees`` and save one plot each.

This is the "reweight" mode entry point: it is the diagnostic pass used
to choose a degree and check the binning and quality gates. Degrees
with too few surviving points are skipped with a message. Returns
``self``.

Definition at line 586 of file reweighting_function.py.

586 def run(self):
587 """Fit every degree in ``polynomial_degrees`` and save one plot each.
588
589 This is the "reweight" mode entry point: it is the diagnostic pass used
590 to choose a degree and check the binning and quality gates. Degrees
591 with too few surviving points are skipped with a message. Returns
592 ``self``.
593 """
594 os.makedirs(self.cfg["outpath"], exist_ok=True) # create BEFORE saving
595 if self.E is None:
596 self.compute_ratio()
597
598 fit_mask = self._fit_mask(verbose=True)
599 n_fit = int(fit_mask.sum())
600
601 for deg in self.cfg["degrees"]:
602 if n_fit < deg + 1:
603 print(f"[skip] deg={deg}: only {n_fit} fit points (need {deg + 1})")
604 continue
605 poly, chi2, chi2red = self.fit_poly_weighted(
606 self.E[fit_mask], self.w[fit_mask], self.sig[fit_mask], degree=deg
607 )
608 print("Saved:", self._make_plot(deg, poly, chi2red, fit_mask))
609
610 print("Output dir:", self.cfg["outpath"])
611 return self
612
613
614# ==========================================================================
615# Run-level configuration (Analysis.yaml)
616# ==========================================================================
617
618#: PDG codes used when selecting the incoming neutrino in the ROOT files.

Member Data Documentation

◆ bins_E

reweighting_function.FluxReweighter.bins_E

Definition at line 148 of file reweighting_function.py.

◆ cfg

reweighting_function.FluxReweighter.cfg

Definition at line 147 of file reweighting_function.py.

◆ COLS

list reweighting_function.FluxReweighter.COLS
static
Initial value:
= [
"Neutrino", "Parent",
"x in m", "y in m", "z in m",
"x angle in rad", "y angle in rad",
"E in GeV", "weight in pb",
]

Definition at line 131 of file reweighting_function.py.

◆ E

reweighting_function.FluxReweighter.E

Definition at line 152 of file reweighting_function.py.

◆ H_DPM

reweighting_function.FluxReweighter.H_DPM

Definition at line 153 of file reweighting_function.py.

◆ H_LC

reweighting_function.FluxReweighter.H_LC

Definition at line 436 of file reweighting_function.py.

◆ PID

dict reweighting_function.FluxReweighter.PID = {"nue": 12, "numu": 14, "nutau": 16}
static

Definition at line 127 of file reweighting_function.py.

◆ s_DPM

reweighting_function.FluxReweighter.s_DPM

Definition at line 431 of file reweighting_function.py.

◆ s_LC

reweighting_function.FluxReweighter.s_LC

Definition at line 437 of file reweighting_function.py.

◆ sig

reweighting_function.FluxReweighter.sig

Definition at line 451 of file reweighting_function.py.

◆ w

reweighting_function.FluxReweighter.w

Definition at line 451 of file reweighting_function.py.


The documentation for this class was generated from the following file: