From 137861dcc68a8b2eac632d7861f2945b18c00b6f Mon Sep 17 00:00:00 2001 From: Anton Riedel Date: Fri, 25 Sep 2026 10:33:41 +0200 Subject: [PATCH] Feat: add capability to compute cuts from cutbit --- PWGCF/Femto/Macros/cutculator.py | 90 +++++++++++++++++++++++++++- PWGCF/Femto/Macros/cutculator_gui.py | 78 ++++++++++++++++++++++-- 2 files changed, 161 insertions(+), 7 deletions(-) diff --git a/PWGCF/Femto/Macros/cutculator.py b/PWGCF/Femto/Macros/cutculator.py index 7ce759182dc..e2d534b64e2 100644 --- a/PWGCF/Femto/Macros/cutculator.py +++ b/PWGCF/Femto/Macros/cutculator.py @@ -181,7 +181,77 @@ def ask_user_selection(group): return selected_bins -def main(rootfile_path, tdir_path="femto-producer"): +def parse_bitmask(text): + """Parse a bitmask given as decimal, hex (0x...) or binary (0b...).""" + text = text.strip() + try: + value = int(text, 0) + except ValueError: + # int(x, 0) rejects decimals with leading zeros, e.g. "010" + value = int(text, 10) + if value < 0: + raise ValueError("bitmask must be non-negative") + return value + + +def is_minimal_bin(b): + return b.get("MinimalCut", "0") == "1" and b.get("OptionalCut", "0") == "0" + + +def print_decoded_bitmask(groups, bitmask): + """ + Print all cuts selected by the given bitmask, grouped by selection. Minimal + cuts with BitPosition X (always applied) are listed as well for every + selection where no stricter minimal bit is set. + """ + print("\n=======================================") + print(f"Cuts selected by bitmask {bitmask} ({hex(bitmask)}):") + print("=======================================\n") + + known_bits = set() + for sel_name, group in groups.items(): + selected = [] + has_minimal_bit = False + for b in group: + pos = b.get("BitPosition", "X") + if pos.upper() == "X": + continue + known_bits.add(int(pos)) + if bitmask & (1 << int(pos)): + selected.append(b) + if is_minimal_bin(b): + has_minimal_bit = True + + entries = [] + if not has_minimal_bit: + for b in group: + if is_minimal_bin(b) and b.get("BitPosition", "X").upper() == "X": + entries.append(f"{format_value_with_comment(b)} [minimal, no bit]") + for b in selected: + if is_minimal_bin(b): + kind = "minimal" + elif b.get("OptionalCut", "0") == "1": + kind = "optional" + else: + kind = "neutral" + entries.append(f"{format_value_with_comment(b)} [{kind}, bit {b.get('BitPosition')}]") + + if entries: + print(f" {sel_name}:") + for e in entries: + print(f" {e}") + + unknown_bits = [i for i in range(bitmask.bit_length()) if bitmask & (1 << i) and i not in known_bits] + if unknown_bits: + print(f"\nWarning: bit(s) {', '.join(map(str, unknown_bits))} are set but not defined in this histogram!") + + print("\nBitmask:") + print(f" Decimal: {bitmask}") + print(f" Binary: {bin(bitmask)}") + print(f" Hex: {hex(bitmask)}") + + +def main(rootfile_path, tdir_path="femto-producer", decode=False): print(f"Opening ROOT file: {rootfile_path}") f = ROOT.TFile.Open(rootfile_path) if not f: @@ -243,6 +313,17 @@ def main(rootfile_path, tdir_path="femto-producer"): sel_name = b.get("SelectionName", f"unknown_{b['_bin_index']}") groups.setdefault(sel_name, []).append(b) + # decode an existing bitmask instead of building a new one + if decode: + while True: + try: + bitmask = parse_bitmask(input("\nEnter bitmask to decode (dec, 0x hex or 0b bin): ")) + break + except ValueError: + print("Invalid bitmask.") + print_decoded_bitmask(groups, bitmask) + return + selected_bins = [] for group in groups.values(): @@ -280,5 +361,10 @@ def main(rootfile_path, tdir_path="femto-producer"): parser = argparse.ArgumentParser() parser.add_argument("rootfile", help="Path to ROOT file") parser.add_argument("--dir", default="femto-producer", help="TDirectory path in ROOT file") + parser.add_argument( + "--bitmask", + action="store_true", + help="Ask for a bitmask after selecting the histogram and print the cuts it selects", + ) args = parser.parse_args() - main(args.rootfile, args.dir) + main(args.rootfile, args.dir, args.bitmask) diff --git a/PWGCF/Femto/Macros/cutculator_gui.py b/PWGCF/Femto/Macros/cutculator_gui.py index 039908fa158..9f0393895c6 100644 --- a/PWGCF/Femto/Macros/cutculator_gui.py +++ b/PWGCF/Femto/Macros/cutculator_gui.py @@ -231,6 +231,7 @@ def __init__(self, rootfile=None, tdir="femto-producer"): self._is_filter_hist = False self._vars = {} # (SelectionName, idx) → BooleanVar self._check_labels = {} # (SelectionName, idx) → Label (custom checkbox glyph) + self._setters = {} # (SelectionName, idx) → callable(bool) setting the checkbox state + glyph self._build_ui() @@ -264,6 +265,26 @@ def _build_ui(self): self._hist_combo.pack(side="left", padx=6) self._hist_combo.bind("<>", self._on_hist_selected) + # ── bitmask input: check all cuts corresponding to a given bitmask ── + tk.Label(bar, text="Bitmask:", font=FONT_BODY, bg=BG_CARD, fg=FG_DIM).pack(side="left", padx=(20, 0)) + self._mask_var = tk.StringVar() + mask_entry = tk.Entry( + bar, + textvariable=self._mask_var, + width=16, + font=FONT_BODY, + bg=BG, + fg=FG, + insertbackground=FG, + relief="flat", + highlightthickness=1, + highlightbackground=BORDER, + highlightcolor=ACCENT, + ) + mask_entry.pack(side="left", padx=6) + mask_entry.bind("", lambda _e: self._apply_bitmask()) + self._make_button(bar, "Set Bitmask", self._apply_bitmask, ACCENT).pack(side="left", padx=2) + self._style_combobox() # ── legend ── @@ -470,6 +491,7 @@ def _on_hist_selected(self, _e=None): self._hist = hist self._vars = {} self._check_labels = {} + self._setters = {} # Always start from a clean summary panel — forget()-ing the pane only # hides it, it does not destroy previously built rows. @@ -714,10 +736,15 @@ def _build_loosest_row(self, parent, sel_name, idx, b): self._vars[(sel_name, idx)] = var self._check_labels[(sel_name, idx)] = check_lbl + def set_state(state): + var.set(state) + check_lbl.config(text="[x]" if state else "[ ]", fg=ACCENT_ALWAYS if state else FG_DIM) + text_lbl.config(fg=FG if state else FG_DIM) + + self._setters[(sel_name, idx)] = set_state + def toggle(_e=None): - var.set(not var.get()) - check_lbl.config(text="[x]" if var.get() else "[ ]", fg=ACCENT_ALWAYS if var.get() else FG_DIM) - text_lbl.config(fg=FG if var.get() else FG_DIM) + set_state(not var.get()) self._update_bitmask() for w in (row, check_lbl, text_lbl): @@ -752,14 +779,55 @@ def _build_bin_row(self, parent, sel_name, idx, b, kind): if pos.upper() != "X": tk.Label(row, text=f"bit {pos}", font=FONT_SMALL, bg=BG_CARD, fg=FG_DIM, width=8).pack(side="right", padx=4) + def set_state(state): + var.set(state) + check_lbl.config(text="[x]" if state else "[ ]", fg=color if state else FG_DIM) + + self._setters[(sel_name, idx)] = set_state + def toggle(_e=None): - var.set(not var.get()) - check_lbl.config(text="[x]" if var.get() else "[ ]", fg=color if var.get() else FG_DIM) + set_state(not var.get()) self._update_bitmask() for w in (row, check_lbl, text_lbl): w.bind("", toggle) + # ── Bitmask input ───────────────────────────────────────────────────────── + def _apply_bitmask(self): + """Check exactly the cuts whose bit is set in the entered bitmask. The + always-applied minimal floors are added to the summary automatically.""" + if self._is_filter_hist or not self._groups: + messagebox.showinfo("Not applicable", "Load a selection histogram first.") + return + + text = self._mask_var.get().strip() + try: + bitmask = int(text, 0) + except ValueError: + try: + # int(x, 0) rejects decimals with leading zeros, e.g. "010" + bitmask = int(text, 10) + except ValueError: + bitmask = -1 + if bitmask < 0: + messagebox.showerror("Invalid bitmask", f"Cannot parse '{text}'.\nUse decimal, 0x hex or 0b binary.") + return + + known_bits = set() + for (sel_name, idx), set_state in self._setters.items(): + pos = bit_position_int(self._groups[sel_name][idx]) + if pos >= 0: + known_bits.add(pos) + set_state(pos >= 0 and bool(bitmask & (1 << pos))) + self._update_bitmask() + + unknown_bits = [i for i in range(bitmask.bit_length()) if bitmask & (1 << i) and i not in known_bits] + if unknown_bits: + messagebox.showwarning( + "Unknown bits", + f"Bit(s) {', '.join(map(str, unknown_bits))} are set but not defined in this histogram.", + ) + # ── Bitmask computation + summary update ────────────────────────────────── def _update_bitmask(self): if self._is_filter_hist: