Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 88 additions & 2 deletions PWGCF/Femto/Macros/cutculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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)
78 changes: 73 additions & 5 deletions PWGCF/Femto/Macros/cutculator_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -264,6 +265,26 @@ def _build_ui(self):
self._hist_combo.pack(side="left", padx=6)
self._hist_combo.bind("<<ComboboxSelected>>", 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("<Return>", lambda _e: self._apply_bitmask())
self._make_button(bar, "Set Bitmask", self._apply_bitmask, ACCENT).pack(side="left", padx=2)

self._style_combobox()

# ── legend ──
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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("<Button-1>", 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:
Expand Down
Loading