Skip to content

Commit b1aa3c5

Browse files
committed
Feat: add summary table
1 parent eeb2f2a commit b1aa3c5

1 file changed

Lines changed: 216 additions & 39 deletions

File tree

PWGCF/Femto/Macros/cutculatorGui.py

Lines changed: 216 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
ACCENT = "#89b4fa" # blue – minimal
3838
ACCENT_OPT = "#a6e3a1" # green – optional
3939
ACCENT_REJ = "#f38ba8" # red – rejection / neutral
40+
ACCENT_ALWAYS = "#cba6f7" # purple – loosest/always-true (no bit set)
4041
FG = "#cdd6f4"
4142
FG_DIM = "#6c7086"
4243
BORDER = "#45475a"
@@ -81,6 +82,17 @@ def bin_type(b):
8182
return "neutral"
8283

8384

85+
def has_only_skipped_minimal_bins(group):
86+
"""
87+
Returns True when a group has at least one minimal bin but ALL of them have
88+
BitPosition=="X" — meaning the cut is the loosest (always-true) selection
89+
and no bit needs to be set. The group should still be displayed so the
90+
user knows the cut exists.
91+
"""
92+
minimal = [b for b in group if b.get("MinimalCut", "0") == "1" and b.get("OptionalCut", "0") == "0"]
93+
return bool(minimal) and all(b.get("BitPosition", "X").upper() == "X" for b in minimal)
94+
95+
8496
def load_bins_from_hist(hist):
8597
nbins = hist.GetNbinsX()
8698
bins = []
@@ -105,8 +117,8 @@ def __init__(self, rootfile=None, tdir="femto-producer"):
105117
super().__init__()
106118
self.title("CutCulator")
107119
self.configure(bg=BG)
108-
self.geometry("920x720")
109-
self.minsize(780, 560)
120+
self.geometry("980x820")
121+
self.minsize(780, 600)
110122
self.resizable(True, True)
111123

112124
self._rootfile_path = rootfile
@@ -153,14 +165,23 @@ def _build_ui(self):
153165
# ── legend ──
154166
legend = tk.Frame(self, bg=BG, pady=4, padx=18)
155167
legend.pack(fill="x")
156-
for color, label in [(ACCENT, "Minimal"), (ACCENT_OPT, "Optional"), (ACCENT_REJ, "Neutral")]:
168+
for color, label in [
169+
(ACCENT, "Minimal"),
170+
(ACCENT_OPT, "Optional"),
171+
(ACCENT_REJ, "Neutral"),
172+
(ACCENT_ALWAYS, "Loosest (no bit set)"),
173+
]:
157174
dot = tk.Label(legend, text="●", font=FONT_BODY, bg=BG, fg=color)
158175
dot.pack(side="left")
159176
tk.Label(legend, text=label, font=FONT_SMALL, bg=BG, fg=FG_DIM).pack(side="left", padx=(2, 12))
160177

178+
# ── main paned area: selection list (top) + summary (bottom) ──
179+
paned = tk.PanedWindow(self, orient="vertical", bg=BORDER, sashwidth=4, sashrelief="flat")
180+
paned.pack(fill="both", expand=True, padx=12, pady=4)
181+
161182
# ── scrollable selection area ──
162-
outer = tk.Frame(self, bg=BG)
163-
outer.pack(fill="both", expand=True, padx=12, pady=4)
183+
outer = tk.Frame(paned, bg=BG)
184+
paned.add(outer, stretch="always", minsize=200)
164185

165186
self._canvas = tk.Canvas(outer, bg=BG, highlightthickness=0, bd=0)
166187
vsb = ttk.Scrollbar(outer, orient="vertical", command=self._canvas.yview)
@@ -176,6 +197,32 @@ def _build_ui(self):
176197
self._canvas.bind_all("<Button-4>", self._on_mousewheel)
177198
self._canvas.bind_all("<Button-5>", self._on_mousewheel)
178199

200+
# ── summary panel ──
201+
summary_outer = tk.Frame(paned, bg=BG_CARD)
202+
paned.add(summary_outer, stretch="never", minsize=120)
203+
204+
summary_hdr = tk.Frame(summary_outer, bg=BG_CARD, pady=5, padx=10)
205+
summary_hdr.pack(fill="x")
206+
tk.Label(summary_hdr, text="Selected cuts", font=FONT_HEAD, bg=BG_CARD, fg=FG).pack(side="left")
207+
self._lbl_summary_empty = tk.Label(summary_hdr, text="(none)", font=FONT_SMALL, bg=BG_CARD, fg=FG_DIM)
208+
self._lbl_summary_empty.pack(side="left", padx=8)
209+
210+
tk.Frame(summary_outer, bg=BORDER, height=1).pack(fill="x")
211+
212+
summary_scroll_outer = tk.Frame(summary_outer, bg=BG_CARD)
213+
summary_scroll_outer.pack(fill="both", expand=True)
214+
215+
self._summary_canvas = tk.Canvas(summary_scroll_outer, bg=BG_CARD, highlightthickness=0, bd=0, height=100)
216+
summary_vsb = ttk.Scrollbar(summary_scroll_outer, orient="vertical", command=self._summary_canvas.yview)
217+
self._summary_canvas.configure(yscrollcommand=summary_vsb.set)
218+
summary_vsb.pack(side="right", fill="y")
219+
self._summary_canvas.pack(side="left", fill="both", expand=True)
220+
221+
self._summary_inner = tk.Frame(self._summary_canvas, bg=BG_CARD)
222+
self._summary_canvas_win = self._summary_canvas.create_window((0, 0), window=self._summary_inner, anchor="nw")
223+
self._summary_inner.bind("<Configure>", self._on_summary_inner_configure)
224+
self._summary_canvas.bind("<Configure>", self._on_summary_canvas_configure)
225+
179226
# ── bottom result bar ──
180227
bottom = tk.Frame(self, bg=BG_CARD, pady=10, padx=18)
181228
bottom.pack(fill="x", side="bottom")
@@ -224,6 +271,12 @@ def _on_inner_configure(self, _e=None):
224271
def _on_canvas_configure(self, e):
225272
self._canvas.itemconfig(self._canvas_win, width=e.width)
226273

274+
def _on_summary_inner_configure(self, _e=None):
275+
self._summary_canvas.configure(scrollregion=self._summary_canvas.bbox("all"))
276+
277+
def _on_summary_canvas_configure(self, e):
278+
self._summary_canvas.itemconfig(self._summary_canvas_win, width=e.width)
279+
227280
def _on_mousewheel(self, e):
228281
if e.num == 4:
229282
self._canvas.yview_scroll(-1, "units")
@@ -292,13 +345,17 @@ def _build_selections(self):
292345
self._on_inner_configure()
293346

294347
def _build_group_card(self, sel_name, group):
295-
# categorise
348+
# categorise into selectable bins
296349
minimal = [(i, b) for i, b in enumerate(group) if bin_type(b) == "minimal"]
297350
optional = [(i, b) for i, b in enumerate(group) if bin_type(b) == "optional"]
298351
neutral = [(i, b) for i, b in enumerate(group) if bin_type(b) == "neutral"]
299352

300-
if not (minimal or optional or neutral):
301-
return # nothing to show (all "skip")
353+
# detect loosest/always-true: minimal bins exist but all have BitPosition==X
354+
loosest_only = has_only_skipped_minimal_bins(group)
355+
356+
# skip entirely if there is truly nothing to show
357+
if not (minimal or optional or neutral or loosest_only):
358+
return
302359

303360
card = tk.Frame(self._inner, bg=BG_CARD, bd=0, highlightthickness=1, highlightbackground=BORDER)
304361
card.pack(fill="x", padx=10, pady=5, ipadx=8, ipady=6)
@@ -308,27 +365,42 @@ def _build_group_card(self, sel_name, group):
308365
hdr.pack(fill="x", padx=6, pady=(4, 2))
309366
tk.Label(hdr, text=sel_name, font=FONT_HEAD, bg=BG_CARD, fg=FG).pack(side="left")
310367

311-
# show the loosest (most permissive) minimal threshold as a hint.
312-
# the truly loosest threshold has mSkipMostPermissiveBit=true so its
313-
# BitPosition is "X" — it lands in the "skip" category. check there first,
314-
# then fall back to the loosest bit-carrying minimal bin.
315-
skipped_minimal = [
316-
b for b in group if b.get("MinimalCut", "0") == "1" and b.get("BitPosition", "X").upper() == "X"
317-
]
318-
if skipped_minimal:
319-
loosest_val = format_value_with_comment(skipped_minimal[0])
368+
if loosest_only:
369+
# All minimal bins are BitPosition==X → loosest cut, no bit set
320370
tk.Label(
321-
hdr, text=f"minimal cut → loosest selection: {loosest_val}", font=FONT_SMALL, bg=BG_CARD, fg=FG_DIM
371+
hdr,
372+
text="loosest selection — no bit set",
373+
font=FONT_SMALL,
374+
bg=BG_CARD,
375+
fg=ACCENT_ALWAYS,
322376
).pack(side="left", padx=10)
323-
elif minimal:
324-
loosest_val = format_value_with_comment(minimal[0][1])
325-
tk.Label(
326-
hdr, text=f"minimal cut → loosest selection: {loosest_val}", font=FONT_SMALL, bg=BG_CARD, fg=FG_DIM
327-
).pack(side="left", padx=10)
328-
elif optional:
329-
tk.Label(hdr, text="optional", font=FONT_SMALL, bg=BG_CARD, fg=ACCENT_OPT).pack(side="left", padx=10)
330-
elif neutral:
331-
tk.Label(hdr, text="neutral", font=FONT_SMALL, bg=BG_CARD, fg=ACCENT_REJ).pack(side="left", padx=10)
377+
else:
378+
# show the loosest (most permissive) minimal threshold as a hint
379+
skipped_minimal = [
380+
b for b in group if b.get("MinimalCut", "0") == "1" and b.get("BitPosition", "X").upper() == "X"
381+
]
382+
if skipped_minimal:
383+
loosest_val = format_value_with_comment(skipped_minimal[0])
384+
tk.Label(
385+
hdr,
386+
text=f"minimal cut → loosest selection: {loosest_val}",
387+
font=FONT_SMALL,
388+
bg=BG_CARD,
389+
fg=FG_DIM,
390+
).pack(side="left", padx=10)
391+
elif minimal:
392+
loosest_val = format_value_with_comment(minimal[0][1])
393+
tk.Label(
394+
hdr,
395+
text=f"minimal cut → loosest selection: {loosest_val}",
396+
font=FONT_SMALL,
397+
bg=BG_CARD,
398+
fg=FG_DIM,
399+
).pack(side="left", padx=10)
400+
elif optional:
401+
tk.Label(hdr, text="optional", font=FONT_SMALL, bg=BG_CARD, fg=ACCENT_OPT).pack(side="left", padx=10)
402+
elif neutral:
403+
tk.Label(hdr, text="neutral", font=FONT_SMALL, bg=BG_CARD, fg=ACCENT_REJ).pack(side="left", padx=10)
332404

333405
# separator
334406
tk.Frame(card, bg=BORDER, height=1).pack(fill="x", padx=6, pady=2)
@@ -337,19 +409,41 @@ def _build_group_card(self, sel_name, group):
337409
bins_frame = tk.Frame(card, bg=BG_CARD)
338410
bins_frame.pack(fill="x", padx=6, pady=4)
339411

340-
for i, b in minimal:
341-
self._build_bin_row(bins_frame, sel_name, i, b, "minimal")
342-
for i, b in optional:
343-
self._build_bin_row(bins_frame, sel_name, i, b, "optional")
344-
for i, b in neutral:
345-
self._build_bin_row(bins_frame, sel_name, i, b, "neutral")
412+
if loosest_only:
413+
# Display loosest minimal bins as informational rows — no checkbox, no bit
414+
for b in group:
415+
if b.get("MinimalCut", "0") == "1" and b.get("OptionalCut", "0") == "0":
416+
self._build_loosest_row(bins_frame, b)
417+
else:
418+
for i, b in minimal:
419+
self._build_bin_row(bins_frame, sel_name, i, b, "minimal")
420+
for i, b in optional:
421+
self._build_bin_row(bins_frame, sel_name, i, b, "optional")
422+
for i, b in neutral:
423+
self._build_bin_row(bins_frame, sel_name, i, b, "neutral")
424+
425+
def _build_loosest_row(self, parent, b):
426+
"""Informational row for a loosest/always-true cut — no checkbox, no bit."""
427+
label_text = format_value_with_comment(b)
428+
429+
row = tk.Frame(parent, bg=BG_CARD)
430+
row.pack(fill="x", pady=1)
431+
432+
tk.Label(row, text="●", font=FONT_BODY, bg=BG_CARD, fg=ACCENT_ALWAYS).pack(side="left", padx=(0, 4))
433+
tk.Label(
434+
row,
435+
text=label_text,
436+
font=FONT_BODY,
437+
bg=BG_CARD,
438+
fg=FG_DIM, # dimmed to signal non-interactive
439+
).pack(side="left", fill="x", expand=True)
440+
# no bit-position badge since it's "X"
346441

347442
def _build_bin_row(self, parent, sel_name, idx, b, kind):
348443
color = {"minimal": ACCENT, "optional": ACCENT_OPT, "neutral": ACCENT_REJ}[kind]
349444
label_text = format_value_with_comment(b)
350445

351446
var = tk.BooleanVar(value=False)
352-
353447
self._vars[(sel_name, idx)] = var
354448

355449
row = tk.Frame(parent, bg=BG_CARD)
@@ -358,7 +452,7 @@ def _build_bin_row(self, parent, sel_name, idx, b, kind):
358452
# coloured dot
359453
tk.Label(row, text="●", font=FONT_BODY, bg=BG_CARD, fg=color).pack(side="left", padx=(0, 4))
360454

361-
# checkbox styled as a toggle button
455+
# checkbox
362456
cb = tk.Checkbutton(
363457
row,
364458
text=label_text,
@@ -382,22 +476,105 @@ def _build_bin_row(self, parent, sel_name, idx, b, kind):
382476
if pos.upper() != "X":
383477
tk.Label(row, text=f"bit {pos}", font=FONT_SMALL, bg=BG_CARD, fg=FG_DIM, width=8).pack(side="right", padx=4)
384478

385-
# ── Bitmask computation ───────────────────────────────────────────────────
479+
# ── Bitmask computation + summary update ──────────────────────────────────
386480
def _update_bitmask(self):
387481
bitmask = 0
482+
selected_entries = [] # list of (sel_name, label_text, bit_pos, color, is_loosest)
483+
484+
# User-selected cuts (from checkboxes)
388485
for (sel_name, idx), var in self._vars.items():
389486
if not var.get():
390487
continue
391488
b = self._groups[sel_name][idx]
392489
pos = b.get("BitPosition", "X")
393-
if pos.upper() == "X":
394-
continue
395-
bitmask |= 1 << int(pos)
490+
kind = bin_type(b)
491+
color = {"minimal": ACCENT, "optional": ACCENT_OPT, "neutral": ACCENT_REJ}.get(kind, FG_DIM)
492+
493+
if pos.upper() != "X":
494+
bitmask |= 1 << int(pos)
495+
label_text = format_value_with_comment(b)
496+
selected_entries.append((sel_name, label_text, pos, color, False))
497+
498+
# Loosest-only cuts: always shown in summary, never set a bit
499+
for sel_name, group in self._groups.items():
500+
if has_only_skipped_minimal_bins(group):
501+
for b in group:
502+
if b.get("MinimalCut", "0") == "1" and b.get("OptionalCut", "0") == "0":
503+
label_text = format_value_with_comment(b)
504+
selected_entries.append((sel_name, label_text, "X", ACCENT_ALWAYS, True))
396505

397506
self._lbl_dec.config(text=str(bitmask))
398507
self._lbl_hex.config(text=hex(bitmask))
399508
self._lbl_bin.config(text=bin(bitmask))
400509

510+
self._rebuild_summary(selected_entries)
511+
512+
def _rebuild_summary(self, entries):
513+
"""Rebuild the selected-cuts summary panel."""
514+
for w in self._summary_inner.winfo_children():
515+
w.destroy()
516+
517+
if not entries:
518+
self._lbl_summary_empty.config(text="(none)")
519+
self._on_summary_inner_configure()
520+
return
521+
522+
self._lbl_summary_empty.config(text="")
523+
524+
# group by selection name, preserving is_loosest flag
525+
by_name = {}
526+
for sel_name, label_text, pos, color, is_loosest in entries:
527+
by_name.setdefault(sel_name, []).append((label_text, pos, color, is_loosest))
528+
529+
for sel_name, items in by_name.items():
530+
block = tk.Frame(self._summary_inner, bg=BG_CARD)
531+
block.pack(fill="x", padx=10, pady=(3, 0))
532+
533+
# name on its own line — no truncation
534+
tk.Label(
535+
block,
536+
text=f"{sel_name}:",
537+
font=FONT_BODY,
538+
bg=BG_CARD,
539+
fg=FG,
540+
anchor="w",
541+
).pack(fill="x")
542+
543+
# values indented below the name
544+
values_row = tk.Frame(block, bg=BG_CARD)
545+
values_row.pack(fill="x", padx=(16, 0), pady=(0, 2))
546+
547+
for label_text, pos, color, is_loosest in items:
548+
entry_frame = tk.Frame(values_row, bg=BG_CARD)
549+
entry_frame.pack(side="left", padx=(0, 12))
550+
551+
tk.Label(entry_frame, text="●", font=FONT_SMALL, bg=BG_CARD, fg=color).pack(side="left")
552+
tk.Label(
553+
entry_frame,
554+
text=label_text,
555+
font=FONT_SMALL,
556+
bg=BG_CARD,
557+
fg=FG_DIM if is_loosest else FG,
558+
).pack(side="left", padx=(2, 0))
559+
if is_loosest:
560+
tk.Label(
561+
entry_frame,
562+
text="(no bit)",
563+
font=FONT_SMALL,
564+
bg=BG_CARD,
565+
fg=ACCENT_ALWAYS,
566+
).pack(side="left", padx=(4, 0))
567+
elif pos.upper() != "X":
568+
tk.Label(
569+
entry_frame,
570+
text=f"[bit {pos}]",
571+
font=FONT_SMALL,
572+
bg=BG_CARD,
573+
fg=FG_DIM,
574+
).pack(side="left", padx=(4, 0))
575+
576+
self._on_summary_inner_configure()
577+
401578
# ── Utilities ─────────────────────────────────────────────────────────────
402579
def _copy(self, text):
403580
self.clipboard_clear()

0 commit comments

Comments
 (0)