12 obj = root_file.Get(path)
15 raise RuntimeError(f
"Object not found at path: {path}")
17 logging.warning(f
"Object not found at path: {path}")
21 path_parts = path.strip(
"/").
split(
"/")
22 *dirs, name = path_parts
23 current_dir = root_file
25 current_dir = current_dir.Get(d)
28 raise RuntimeError(f
"Directory '{d}' not found in path '{'/'.join(dirs)}' for deletion.")
30 logging.warning(f
"Directory '{d}' not found in path '{'/'.join(dirs)}' for deletion.")
33 current_dir.Delete(f
"{name};*")
40 for part
in path.strip(
"/").
split(
"/"):
41 next_dir = current.Get(part)
43 logging.info(f
"Saving directory not exist. Creating directory '{part}' in '{current.GetName()}'")
46 current = ROOT.gDirectory
49_FIELD = re.compile(
r"\{(\w+)\}")
52 """Return the path of every histogram below a directory."""
54 for key
in directory.GetListOfKeys():
56 path = f
"{prefix}/{name}" if prefix
else name
58 stored = ROOT.TClass.GetClass(key.GetClassName())
59 if stored.InheritsFrom(ROOT.TDirectory.Class()):
60 subdir = directory.Get(name)
63 elif stored.InheritsFrom(ROOT.TH1.Class()):
68 """Turn a pattern with {field} placeholders into a regex with one group per field."""
69 regex, seen, pos =
"",
set(), 0
70 for field
in _FIELD.finditer(pattern):
72 regex += re.escape(pattern[pos:field.start()])
73 regex += f
"(?P={name})" if name
in seen
else f
"(?P<{name}>[^/]+)"
76 return re.compile(regex + re.escape(pattern[pos:]))
79 """Substitute the {field} placeholders of one fragment value."""
80 if not isinstance(value, str):
82 return _FIELD.sub(
lambda field: fields[field.group(1)], value)
85 """Return the first of the named fragment inputs that exists but has no entries."""
86 for field
in [fields]
if isinstance(fields, str)
else fields:
87 histo = root_file.Get(fragment[field])
88 if histo
and histo.InheritsFrom(ROOT.TH1.Class())
and not histo.GetEntries():
89 return fragment[field]
93 """Resolve the for_each patterns of a domain against the histograms in a file.
95 A fragment without for_each is kept as it is. One with for_each is repeated
96 once per histogram matching the pattern, with its {field} placeholders filled
97 in from the match. skip_if_empty names the inputs that a repetition needs to
98 be non-empty, which is how the selections a sample does not fill are dropped.
102 for fragment
in fragments:
103 pattern = fragment.get(
"for_each")
105 expanded.append(fragment)
109 matches = [match.groupdict()
for match
in map(
pattern_to_regex(pattern).fullmatch, paths)
if match]
111 logging.info(f
"No histogram matches '{pattern}', nothing to expand")
112 for fields
in sorted(matches, key=
lambda match: sorted(match.items())):
113 entry = {key:
fill_fields(value, fields)
for key, value
in fragment.items()
114 if key
not in (
"for_each",
"skip_if_empty")}
115 required = fragment.get(
"skip_if_empty")
116 empty =
empty_input(root_file, entry, required)
if required
else None
118 logging.info(f
"'{empty}' is empty, skipping '{entry['output']}'")
120 expanded.append(entry)
128 with open(config_file,
"r")
as f:
129 config_data = yaml.safe_load(f)
130 self.
configs[name] = config_data
131 logging.info(f
"Configuration for '{name}' loaded from {config_file}")
134 """Returns the config for a specific domain (e.g., 'Electron')"""
138 """Returns all configs as (name, config) pairs"""
142 def __init__(self, input, output, rebin_factor, delete_original=False):
152 raise RuntimeError(f
"Histogram '{self.input}' not found for rebinning.")
154 logging.error(f
"Histogram '{self.input}' not found for rebinning.")
158 *dir_parts, output_name = path_parts
159 directory_path =
"/".join(dir_parts)
161 root_file.cd(directory_path)
163 if hist.GetDimension() == 1:
165 rebinned.Write(output_name, ROOT.TObject.kOverwrite)
166 logging.info(f
"Rebinned 1D histogram and saved as '{output_name}' in '{directory_path}'.")
167 elif hist.GetDimension() == 2:
170 hist.SetName(output_name)
171 hist.Write(output_name, ROOT.TObject.kOverwrite)
172 logging.info(f
"Rebinned 2D histogram and saved as '{output_name}' in '{directory_path}'.")
179 input=fragment[
"input"],
180 output=fragment[
"output"],
181 rebin_factor=fragment[
"rebin"],
182 delete_original=fragment.get(
"delete_original",
False)
186 def __init__(self, numerator, denominator, output):
194 if not num
or not den:
196 raise RuntimeError(f
"Missing histogram: {self.numerator} or {self.denominator}")
198 logging.error(f
"Missing histogram: {self.numerator} or {self.denominator}")
201 if not ROOT.TEfficiency.CheckConsistency(num, den):
202 logging.warning(
"Inconsistency detected between numerator and denominator histograms.")
203 logging.warning(f
"num={self.numerator} denom={self.denominator}")
204 logging.info(
"Attempting to fix...")
205 for i
in range(1, num.GetNbinsX() + 1):
206 num_val = num.GetBinContent(i)
207 den_val = den.GetBinContent(i)
209 den.SetBinContent(i, 1e-6)
211 logging.info(f
"Setting denominator to a smaller value for bin {i} as it is zero. Any other options?")
212 if num_val > den_val:
213 logging.info(f
"For Bin {i}: Num ({num_val}) > Den ({den_val})! Adjusting by setting Den to Num. Any other options?")
214 den.SetBinContent(i, num_val)
215 if not ROOT.TEfficiency.CheckConsistency(num, den):
216 logging.error(
"Unable to fix histogram inconsistencies. Aborting efficiency calculation.")
219 eff = ROOT.TEfficiency(num, den)
221 *dir_path, obj_name = path_parts
222 directory_path =
"/".join(dir_path)
223 eff.SetName(obj_name)
224 eff.SetTitle(f
"{self.numerator}/{self.denominator};{num.GetXaxis().GetTitle()};Efficiency")
226 root_file.cd(directory_path)
228 logging.info(f
"Saved efficiency '{obj_name}' in '{directory_path}'.")
233 numerator=fragment[
"numerator"],
234 denominator=fragment[
"denominator"],
235 output=fragment[
"output"]
247 raise RuntimeError(f
"Histogram '{self.histogram_name}' not found.")
249 logging.error(f
"Histogram '{self.histogram_name}' not found.")
253 cum = [hist.GetBinContent(i)
for i
in range(0, hist.GetNbinsX() + 2)]
254 for i
in range(1, len(cum)):
256 total = cum[-1]
or 1.0
257 cum = [x / total
for x
in cum]
263 logging.info(f
"Resolution widths for {self.histogram_name} -> s68: {s68:.4f}, s90: {s90:.4f}")
264 logging.info(
"Todo: Take a 2D histo, take the quantity say xaxis, make its small intervals %s",
"and then compute the Resolution widths and then save as a 1D histo with quantity on xaxis and Resolution widths on yaxis")
268 min_width = float(
"inf")
270 for i
in range(len(cum)):
271 target = cum[i] + prob
274 for j
in range(i + 1, len(cum)):
276 width = hist.GetBinCenter(j) - hist.GetBinCenter(i)
277 if width < min_width:
281 return 0.5 * (hist.GetBinCenter(best[1]) - hist.GetBinCenter(best[0]))
286 histogram_name=fragment[
"histogram"]
290 def __init__(self, signal_path, background_path, output):
299 if not sig
or not bkg:
301 raise RuntimeError(f
"Missing histogram: {self.signal_path} or {self.background_path}")
303 logging.error(f
"Missing histogram: {self.signal_path} or {self.background_path}")
308 logging.error(
"ROC graph is empty or could not be constructed.")
312 *dir_path, obj_name = path_parts
313 directory_path =
"/".join(dir_path)
315 graph.SetName(obj_name)
316 graph.SetTitle(f
"{obj_name};Signal Efficiency;Background Rejection")
319 root_file.cd(directory_path)
321 logging.info(f
"Saved ROC curve '{obj_name}' in '{directory_path}'.")
324 n_bins = sig.GetNbinsX()
325 total_sig = sig.Integral()
326 total_bkg = bkg.Integral()
328 x_vals, y_vals = [], []
329 if total_sig == 0
or total_bkg == 0:
330 logging.error(
"Zero total signal or background counts; cannot compute ROC.")
333 for cut_bin
in range(1, n_bins + 1):
334 sig_pass = sig.Integral(cut_bin, n_bins)
335 bkg_pass = bkg.Integral(cut_bin, n_bins)
337 eff = sig_pass / total_sig
343 rej = total_bkg / bkg_pass
351 graph = ROOT.TGraphErrors(len(x_vals))
352 for i, (x, y)
in enumerate(zip(x_vals, y_vals)):
353 graph.SetPoint(i, x, y)
360 signal_path=fragment[
"signal"],
361 background_path=fragment[
"background"],
362 output=fragment[
"output"]
366 def __init__(self, input_hist, bins_x, bins_y, projection_axis, output=None):
378 raise RuntimeError(f
"Histogram '{self.input_hist}' not found.")
380 logging.error(f
"Histogram '{self.input_hist}' not found.")
383 b1, b2 = hist.GetYaxis().FindBin(self.
bins_x[0]), hist.GetYaxis().FindBin(self.
bins_x[1])
384 b3, b4 = hist.GetXaxis().FindBin(self.
bins_y[0]), hist.GetXaxis().FindBin(self.
bins_y[1])
387 *dir_path, output_name = path_parts
388 directory_path =
"/".join(dir_path)
390 root_file.cd(directory_path)
393 proj_x = hist.ProjectionX(output_name, b1, b2).Clone(output_name)
394 self.
project(proj_x, hist.GetXaxis().GetTitle(),
"X")
396 proj_y = hist.ProjectionY(output_name, b3, b4).Clone(output_name)
397 self.
project(proj_y, hist.GetYaxis().GetTitle(),
"Y")
399 def project(self, projection, axis_title, axis):
400 projection.SetTitle(f
";{axis_title};Entries")
401 projection.SetStats(
False)
403 logging.info(f
"Saved projection by integrating over {axis} axis as '{projection.GetName()}'.")
408 input_hist=fragment[
"hist"],
409 bins_x=fragment.get(
"bins_projection_x", [10, 200]),
410 bins_y=fragment.get(
"bins_projection_y", [-2.47, 2.47]),
411 projection_axis=fragment.get(
"projection_axis",
"x"),
412 output=fragment.get(
"output")
416 def __init__(self, histo1, histo2, output, delete_inputs=False):
425 if not histo1
or not histo2:
427 raise RuntimeError(f
"Missing histograms: {self.histo1} or {self.histo2}")
429 logging.error(f
"Missing histograms: {self.histo1} or {self.histo2}")
433 *dir_path, output_name = path_parts
434 directory_path =
"/".join(dir_path)
435 summed = histo1.Clone(output_name)
438 root_file.cd(directory_path)
439 summed.Write(output_name, ROOT.TObject.kOverwrite)
440 logging.info(f
"Saved added histogram '{output_name}' in '{directory_path}'.")
449 histo1=fragment[
"histo1_name"],
450 histo2=fragment[
"histo2_name"],
451 output=fragment[
"output"],
452 delete_inputs=fragment.get(
"delete_inputs",
False)
456 def __init__(self, input_hist, new_path, x_label=None, y_label=None, xlow=None, xhigh=None):
465 ROOT.TH1.AddDirectory(
False)
469 raise RuntimeError(f
"Histogram '{self.input_hist}' not found.")
471 logging.error(f
"Histogram '{self.input_hist}' not found.")
475 *dir_path, new_name = path_parts
476 directory_path =
"/".join(dir_path)
478 root_file.cd(directory_path)
480 if self.
xlow is None or self.
xhigh is None:
481 h_out = hist.Clone(new_name)
484 bin_low =
max(1,
min(ax.FindBin(self.
xlow), ax.GetNbins()))
485 bin_high =
max(1,
min(ax.FindBin(self.
xhigh), ax.GetNbins()))
486 nbins = bin_high - bin_low
487 h_out = ROOT.TH1D(new_name, hist.GetTitle(), nbins, float(self.
xlow), float(self.
xhigh))
488 for i
in range(nbins + 1):
489 h_out.SetBinContent(i + 1, hist.GetBinContent(i + bin_low))
490 h_out.SetBinError(i + 1, hist.GetBinError(i + bin_low))
492 h_out.SetEntries(hist.GetEntries())
495 h_out.GetXaxis().SetTitle(self.
x_label)
497 h_out.GetYaxis().SetTitle(self.
y_label)
498 h_out.Write(new_name, ROOT.TObject.kOverwrite)
499 logging.info(f
"Renamed and updated histogram '{self.input_hist}' → '{self.new_path}' in '{directory_path}' | X-axis: '{self.x_label}' | Y-axis: '{self.y_label}' | Range: ({self.xlow}, {self.xhigh})")
504 input_hist=fragment[
"hist"],
505 new_path=fragment.get(
"final_histo_name", fragment[
"hist"]),
506 x_label=fragment.get(
"x_axis_label"),
507 y_label=fragment.get(
"y_axis_label"),
508 xlow=fragment.get(
"xlow"),
509 xhigh=fragment.get(
"xhigh")
522 toDelete=fragment[
"toDelete"]
add_configuration(self, name, config_file)
__init__(self, numerator, denominator, output)
__call__(self, root_file)
__init__(self, histo1, histo2, output, delete_inputs=False)
__call__(self, root_file)
__init__(self, input_hist, new_path, x_label=None, y_label=None, xlow=None, xhigh=None)
__call__(self, root_file)
__call__(self, root_file)
project(self, projection, axis_title, axis)
__call__(self, root_file)
__init__(self, input_hist, bins_x, bins_y, projection_axis, output=None)
compute_roc(self, sig, bkg)
__init__(self, signal_path, background_path, output)
__call__(self, root_file)
__init__(self, input, output, rebin_factor, delete_original=False)
__call__(self, root_file)
_compute_s(self, hist, cum, prob)
__call__(self, root_file)
__init__(self, histogram_name)
T * get(TKey *tobj)
get a TObject* from a TKey* (why can't a TObject be a TKey?)
std::vector< std::string > split(const std::string &s, const std::string &t=":")
expand_fragments(fragments, root_file)
empty_input(root_file, fragment, fields)
find_histo(root_file, path, should_delete=False)
— HELPER — ###
pattern_to_regex(pattern)
fill_fields(value, fields)
ensure_directory_exists(root_file, path)
— HELPER — ###
histogram_paths(directory, prefix="")