Skip to content

Commit 49d45a3

Browse files
authored
[PWGCF] Update cutculator in femto framework (#18056)
1 parent 3a2e6b6 commit 49d45a3

2 files changed

Lines changed: 161 additions & 7 deletions

File tree

‎PWGCF/Femto/Macros/cutculator.py‎

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,77 @@ def ask_user_selection(group):
181181
return selected_bins
182182

183183

184-
def main(rootfile_path, tdir_path="femto-producer"):
184+
def parse_bitmask(text):
185+
"""Parse a bitmask given as decimal, hex (0x...) or binary (0b...)."""
186+
text = text.strip()
187+
try:
188+
value = int(text, 0)
189+
except ValueError:
190+
# int(x, 0) rejects decimals with leading zeros, e.g. "010"
191+
value = int(text, 10)
192+
if value < 0:
193+
raise ValueError("bitmask must be non-negative")
194+
return value
195+
196+
197+
def is_minimal_bin(b):
198+
return b.get("MinimalCut", "0") == "1" and b.get("OptionalCut", "0") == "0"
199+
200+
201+
def print_decoded_bitmask(groups, bitmask):
202+
"""
203+
Print all cuts selected by the given bitmask, grouped by selection. Minimal
204+
cuts with BitPosition X (always applied) are listed as well for every
205+
selection where no stricter minimal bit is set.
206+
"""
207+
print("\n=======================================")
208+
print(f"Cuts selected by bitmask {bitmask} ({hex(bitmask)}):")
209+
print("=======================================\n")
210+
211+
known_bits = set()
212+
for sel_name, group in groups.items():
213+
selected = []
214+
has_minimal_bit = False
215+
for b in group:
216+
pos = b.get("BitPosition", "X")
217+
if pos.upper() == "X":
218+
continue
219+
known_bits.add(int(pos))
220+
if bitmask & (1 << int(pos)):
221+
selected.append(b)
222+
if is_minimal_bin(b):
223+
has_minimal_bit = True
224+
225+
entries = []
226+
if not has_minimal_bit:
227+
for b in group:
228+
if is_minimal_bin(b) and b.get("BitPosition", "X").upper() == "X":
229+
entries.append(f"{format_value_with_comment(b)} [minimal, no bit]")
230+
for b in selected:
231+
if is_minimal_bin(b):
232+
kind = "minimal"
233+
elif b.get("OptionalCut", "0") == "1":
234+
kind = "optional"
235+
else:
236+
kind = "neutral"
237+
entries.append(f"{format_value_with_comment(b)} [{kind}, bit {b.get('BitPosition')}]")
238+
239+
if entries:
240+
print(f" {sel_name}:")
241+
for e in entries:
242+
print(f" {e}")
243+
244+
unknown_bits = [i for i in range(bitmask.bit_length()) if bitmask & (1 << i) and i not in known_bits]
245+
if unknown_bits:
246+
print(f"\nWarning: bit(s) {', '.join(map(str, unknown_bits))} are set but not defined in this histogram!")
247+
248+
print("\nBitmask:")
249+
print(f" Decimal: {bitmask}")
250+
print(f" Binary: {bin(bitmask)}")
251+
print(f" Hex: {hex(bitmask)}")
252+
253+
254+
def main(rootfile_path, tdir_path="femto-producer", decode=False):
185255
print(f"Opening ROOT file: {rootfile_path}")
186256
f = ROOT.TFile.Open(rootfile_path)
187257
if not f:
@@ -243,6 +313,17 @@ def main(rootfile_path, tdir_path="femto-producer"):
243313
sel_name = b.get("SelectionName", f"unknown_{b['_bin_index']}")
244314
groups.setdefault(sel_name, []).append(b)
245315

316+
# decode an existing bitmask instead of building a new one
317+
if decode:
318+
while True:
319+
try:
320+
bitmask = parse_bitmask(input("\nEnter bitmask to decode (dec, 0x hex or 0b bin): "))
321+
break
322+
except ValueError:
323+
print("Invalid bitmask.")
324+
print_decoded_bitmask(groups, bitmask)
325+
return
326+
246327
selected_bins = []
247328

248329
for group in groups.values():
@@ -280,5 +361,10 @@ def main(rootfile_path, tdir_path="femto-producer"):
280361
parser = argparse.ArgumentParser()
281362
parser.add_argument("rootfile", help="Path to ROOT file")
282363
parser.add_argument("--dir", default="femto-producer", help="TDirectory path in ROOT file")
364+
parser.add_argument(
365+
"--bitmask",
366+
action="store_true",
367+
help="Ask for a bitmask after selecting the histogram and print the cuts it selects",
368+
)
283369
args = parser.parse_args()
284-
main(args.rootfile, args.dir)
370+
main(args.rootfile, args.dir, args.bitmask)

‎PWGCF/Femto/Macros/cutculator_gui.py‎

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ def __init__(self, rootfile=None, tdir="femto-producer"):
231231
self._is_filter_hist = False
232232
self._vars = {} # (SelectionName, idx) → BooleanVar
233233
self._check_labels = {} # (SelectionName, idx) → Label (custom checkbox glyph)
234+
self._setters = {} # (SelectionName, idx) → callable(bool) setting the checkbox state + glyph
234235

235236
self._build_ui()
236237

@@ -264,6 +265,26 @@ def _build_ui(self):
264265
self._hist_combo.pack(side="left", padx=6)
265266
self._hist_combo.bind("<<ComboboxSelected>>", self._on_hist_selected)
266267

268+
# ── bitmask input: check all cuts corresponding to a given bitmask ──
269+
tk.Label(bar, text="Bitmask:", font=FONT_BODY, bg=BG_CARD, fg=FG_DIM).pack(side="left", padx=(20, 0))
270+
self._mask_var = tk.StringVar()
271+
mask_entry = tk.Entry(
272+
bar,
273+
textvariable=self._mask_var,
274+
width=16,
275+
font=FONT_BODY,
276+
bg=BG,
277+
fg=FG,
278+
insertbackground=FG,
279+
relief="flat",
280+
highlightthickness=1,
281+
highlightbackground=BORDER,
282+
highlightcolor=ACCENT,
283+
)
284+
mask_entry.pack(side="left", padx=6)
285+
mask_entry.bind("<Return>", lambda _e: self._apply_bitmask())
286+
self._make_button(bar, "Set Bitmask", self._apply_bitmask, ACCENT).pack(side="left", padx=2)
287+
267288
self._style_combobox()
268289

269290
# ── legend ──
@@ -470,6 +491,7 @@ def _on_hist_selected(self, _e=None):
470491
self._hist = hist
471492
self._vars = {}
472493
self._check_labels = {}
494+
self._setters = {}
473495

474496
# Always start from a clean summary panel — forget()-ing the pane only
475497
# hides it, it does not destroy previously built rows.
@@ -714,10 +736,15 @@ def _build_loosest_row(self, parent, sel_name, idx, b):
714736
self._vars[(sel_name, idx)] = var
715737
self._check_labels[(sel_name, idx)] = check_lbl
716738

739+
def set_state(state):
740+
var.set(state)
741+
check_lbl.config(text="[x]" if state else "[ ]", fg=ACCENT_ALWAYS if state else FG_DIM)
742+
text_lbl.config(fg=FG if state else FG_DIM)
743+
744+
self._setters[(sel_name, idx)] = set_state
745+
717746
def toggle(_e=None):
718-
var.set(not var.get())
719-
check_lbl.config(text="[x]" if var.get() else "[ ]", fg=ACCENT_ALWAYS if var.get() else FG_DIM)
720-
text_lbl.config(fg=FG if var.get() else FG_DIM)
747+
set_state(not var.get())
721748
self._update_bitmask()
722749

723750
for w in (row, check_lbl, text_lbl):
@@ -752,14 +779,55 @@ def _build_bin_row(self, parent, sel_name, idx, b, kind):
752779
if pos.upper() != "X":
753780
tk.Label(row, text=f"bit {pos}", font=FONT_SMALL, bg=BG_CARD, fg=FG_DIM, width=8).pack(side="right", padx=4)
754781

782+
def set_state(state):
783+
var.set(state)
784+
check_lbl.config(text="[x]" if state else "[ ]", fg=color if state else FG_DIM)
785+
786+
self._setters[(sel_name, idx)] = set_state
787+
755788
def toggle(_e=None):
756-
var.set(not var.get())
757-
check_lbl.config(text="[x]" if var.get() else "[ ]", fg=color if var.get() else FG_DIM)
789+
set_state(not var.get())
758790
self._update_bitmask()
759791

760792
for w in (row, check_lbl, text_lbl):
761793
w.bind("<Button-1>", toggle)
762794

795+
# ── Bitmask input ─────────────────────────────────────────────────────────
796+
def _apply_bitmask(self):
797+
"""Check exactly the cuts whose bit is set in the entered bitmask. The
798+
always-applied minimal floors are added to the summary automatically."""
799+
if self._is_filter_hist or not self._groups:
800+
messagebox.showinfo("Not applicable", "Load a selection histogram first.")
801+
return
802+
803+
text = self._mask_var.get().strip()
804+
try:
805+
bitmask = int(text, 0)
806+
except ValueError:
807+
try:
808+
# int(x, 0) rejects decimals with leading zeros, e.g. "010"
809+
bitmask = int(text, 10)
810+
except ValueError:
811+
bitmask = -1
812+
if bitmask < 0:
813+
messagebox.showerror("Invalid bitmask", f"Cannot parse '{text}'.\nUse decimal, 0x hex or 0b binary.")
814+
return
815+
816+
known_bits = set()
817+
for (sel_name, idx), set_state in self._setters.items():
818+
pos = bit_position_int(self._groups[sel_name][idx])
819+
if pos >= 0:
820+
known_bits.add(pos)
821+
set_state(pos >= 0 and bool(bitmask & (1 << pos)))
822+
self._update_bitmask()
823+
824+
unknown_bits = [i for i in range(bitmask.bit_length()) if bitmask & (1 << i) and i not in known_bits]
825+
if unknown_bits:
826+
messagebox.showwarning(
827+
"Unknown bits",
828+
f"Bit(s) {', '.join(map(str, unknown_bits))} are set but not defined in this histogram.",
829+
)
830+
763831
# ── Bitmask computation + summary update ──────────────────────────────────
764832
def _update_bitmask(self):
765833
if self._is_filter_hist:

0 commit comments

Comments
 (0)