From ffda837f064e3495958b338501ae6a1d669cec40 Mon Sep 17 00:00:00 2001 From: "Schwarz, Mario" Date: Thu, 21 Aug 2025 14:01:29 +0200 Subject: [PATCH 1/5] Option to set additional output parameters for the internal processing chain, useful for derived classes --- src/dspeed/vis/waveform_browser.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/dspeed/vis/waveform_browser.py b/src/dspeed/vis/waveform_browser.py index 86587e4..2b6ffbf 100644 --- a/src/dspeed/vis/waveform_browser.py +++ b/src/dspeed/vis/waveform_browser.py @@ -52,6 +52,7 @@ def __init__( align: str = None, buffer_len: int = 128, block_width: int = 8, + additional_outputs: list[str] | None = None ) -> None: """ Parameters @@ -149,6 +150,10 @@ def __init__( block_width block width for :class:`~.processing_chain.ProcessingChain`. + + additional_outputs + More output fields for the internal processing chain. + Useful for deriving classes. """ self.norm_par = norm @@ -260,6 +265,8 @@ def __init__( outputs += [self.norm_par] if isinstance(self.align_par, str): outputs += [self.align_par] + if additional_outputs is not None: + outputs += additional_outputs # Remove any values not found in aux_vals if self.aux_vals is not None: From b0b588a88975316d6094225b3a52238ee1649520 Mon Sep 17 00:00:00 2001 From: "Schwarz, Mario" Date: Thu, 21 Aug 2025 14:02:53 +0200 Subject: [PATCH 2/5] safe is actually used in draw_entry --- src/dspeed/vis/waveform_browser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dspeed/vis/waveform_browser.py b/src/dspeed/vis/waveform_browser.py index 2b6ffbf..7eb36e1 100644 --- a/src/dspeed/vis/waveform_browser.py +++ b/src/dspeed/vis/waveform_browser.py @@ -621,7 +621,7 @@ def draw_entry( safe if ``False``, throw an exception for out of range entries. """ - self.find_entry(entry, append) + self.find_entry(entry, append, safe) self.draw_current(clear) def find_next(self, n_wfs: int = None, append: bool = False) -> tuple[int, int]: From 5ba0be4f531c51f3ec8c5da06faba34a371f5457 Mon Sep 17 00:00:00 2001 From: "Schwarz, Mario" Date: Thu, 21 Aug 2025 14:14:56 +0200 Subject: [PATCH 3/5] added the WaveformAndHistBrowser to show histograms alongside of waveforms View the output histograms of dspeed.processors.histogram. Useful especially for investigating the SiPM dsp --- src/dspeed/vis/__init__.py | 3 +- src/dspeed/vis/waveform_and_hist_browser.py | 192 ++++++++++++++++++++ src/dspeed/vis/waveform_browser.py | 4 +- 3 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 src/dspeed/vis/waveform_and_hist_browser.py diff --git a/src/dspeed/vis/__init__.py b/src/dspeed/vis/__init__.py index 66ba7cf..438989a 100644 --- a/src/dspeed/vis/__init__.py +++ b/src/dspeed/vis/__init__.py @@ -2,6 +2,7 @@ This subpackage implements utilities to visualize data. """ +from .waveform_and_hist_browser import WaveformAndHistBrowser from .waveform_browser import WaveformBrowser -__all__ = ["WaveformBrowser"] +__all__ = ["WaveformBrowser", "WaveformAndHistBrowser"] diff --git a/src/dspeed/vis/waveform_and_hist_browser.py b/src/dspeed/vis/waveform_and_hist_browser.py new file mode 100644 index 0000000..1173807 --- /dev/null +++ b/src/dspeed/vis/waveform_and_hist_browser.py @@ -0,0 +1,192 @@ +from __future__ import annotations + +import itertools +import logging + +import lgdo +import matplotlib.pyplot as plt +from cycler import cycler +from matplotlib.axes import Axes +from matplotlib.figure import Figure + +from .waveform_browser import WaveformBrowser + +log = logging.getLogger(__name__) + + +class WaveformAndHistBrowser(WaveformBrowser): + """ + The :class:`WaveformAndHistBrowser` extends :class:`WaveformBrowser` to provide + interactive browsing and visualization of histograms in addition to waveforms. + It supports drawing waveforms, multiple histograms (with custom styles), and offers options + for vertical/horizontal orientation and logarithmic axes. Histogram data is specified via + value/edge pairs, and can be visualized alongside or instead of waveforms. + """ + + def __init__( + self, + *args, + hist_values_edges: tuple[str, str] | list[tuple[str, str]], + hist_styles: list[dict[str, list]] | None = None, + vertical_hist: bool = False, + hist_log: bool = False, + **kwargs, + ): + """ + Parameters + ---------- + hist_values_edges + Tuple or list of tuples specifying the names of histogram values and edges, + which are defined in the dsp_config (see :class:`WaveformBrowser`) + hist_styles + List of style dictionaries for histograms. Each dictionary should map style properties to lists of values. + vertical_hist + If ``True``, draw histograms vertically (only allowed if no lines are drawn). + hist_log + If ``True``, use logarithmic scale for histogram counts. + args, kwargs + Additional (keyword) arguments passed to :class:`WaveformBrowser`. + """ + self.values_edges_names = ( + hist_values_edges + if isinstance(hist_values_edges, list) + else [hist_values_edges] + ) + super().__init__( + *args, + additional_outputs=[x for y in self.values_edges_names for x in y], + **kwargs, + ) + self.values_edges_data = [([], [])] * len(self.values_edges_names) + + self.hist_styles = [None] * len(self.values_edges_names) + if hist_styles is not None: + assert isinstance(hist_styles, list) + for i, sty in enumerate(hist_styles): + if sty is None: + self.hist_styles[i] = None + else: + self.hist_styles[i] = itertools.cycle(cycler(**sty)) + + if vertical_hist and len(self.lines) > 0: + raise RuntimeError( + "Cannot draw vertical histograms when also " + "drawing waveforms. Use lines=[] in this case." + ) + self.vertical_hist = vertical_hist + self.hist_log = hist_log + + def new_figure(self, *args, **kwargs) -> None: + """ + Create a new figure and axis for drawing waveforms and histograms. + If vertical histograms are not requested, create a secondary xaxis for histograms. + """ + super().new_figure(*args, **kwargs) + if not self.vertical_hist: + self.ax2 = self.ax.twiny() + + def set_figure(self, fig: WaveformBrowser | Figure, ax: Axes = None) -> None: + """ + Use an existing figure and axis for drawing. + If vertical histograms are not requested, create a secondary axis for histograms. + + Parameters + ---------- + fig + Existing :class:`WaveformBrowser` or :class:`matplotlib.figure.Figure` to use. + ax + Existing :class:`matplotlib.axes.Axes` to use (optional). + """ + super().set_figure(fig, ax) + if not self.vertical_hist: + self.ax2 = self.ax.twiny() + + def clear_data(self) -> None: + """ + Reset the currently stored data. + Derived class data is reset before base class data. + """ + for val_edg in self.values_edges_data: + val_edg[0].clear() + val_edg[1].clear() + super().clear_data() + + def find_entry(self, entry: int | list[int], *args, **kwargs) -> None: + """ + Find the requested entry or entries and store associated waveform and histogram data internally. + For each entry, extract histogram values and edges and store them for later drawing. + + Parameters + ---------- + entry + Index or list of indices to find. + args, kwargs + Additional arguments passed to base class. + """ + super().find_entry(entry, *args, **kwargs) + if hasattr(entry, "__iter__"): + # super().find_entry() recurses in this case + return + assert isinstance(entry, int) + i_tb = entry - self.lh5_it.current_i_entry + assert len(self.lh5_out) > i_tb >= 0 + for i, (val_n, edg_n) in enumerate(self.values_edges_names): + val_data = self.lh5_out.get(val_n, None) + edg_data = self.lh5_out.get(edg_n, None) + if not isinstance(val_data, lgdo.ArrayOfEqualSizedArrays): + raise RuntimeError( + f"histogram values {val_n} has to be instance of lgdo.ArrayOfEqualSizedArrays" + ) + if not isinstance(edg_data, lgdo.ArrayOfEqualSizedArrays): + raise RuntimeError( + f"histogram edges {edg_n} has to be instance of lgdo.ArrayOfEqualSizedArrays" + ) + self.values_edges_data[i][0].append(val_data.view_as("ak")[i_tb].to_numpy()) + self.values_edges_data[i][1].append(edg_data.view_as("ak")[i_tb].to_numpy()) + + def draw_current(self, clear: bool = True, *args, **kwargs) -> None: + """ + Draw the currently stored waveforms and histograms in the figure. + If waveforms are present, draw them using the base class and draw histograms on a secondary axis. + If only histograms are present, draw them on the main axis, optionally vertically. + + Parameters + ---------- + clear + If ``True``, clear the axes before drawing. + args, kwargs + Additional arguments passed to base class. + """ + use_ax = None + orientation = "horizontal" + if len(self.lines) > 0: + super().draw_current(clear, *args, **kwargs) + if clear: + self.ax2.clear() + self.ax2.set_ylim(self.ax.get_ylim()) + use_ax = self.ax2 + else: + # No lines drawn by base class; only histograms requested + if not (self.ax and self.fig and plt.fignum_exists(self.fig.number)): + self.new_figure() + use_ax = self.ax + if self.vertical_hist: + orientation = "vertical" + assert use_ax is not None + + if self.hist_log: + if self.vertical_hist: + use_ax.set_yscale("log") + else: + use_ax.set_xscale("log") + + default_style = itertools.cycle(cycler(plt.rcParams["axes.prop_cycle"])) + for i, (values_list, edges_list) in enumerate(self.values_edges_data): + styles = self.hist_styles[i] + if styles is None: + styles = default_style + else: + styles = iter(styles) + for values, edges in zip(values_list, edges_list): + sty = next(styles) + use_ax.stairs(values, edges, orientation=orientation, **sty) diff --git a/src/dspeed/vis/waveform_browser.py b/src/dspeed/vis/waveform_browser.py index 7eb36e1..f0e213b 100644 --- a/src/dspeed/vis/waveform_browser.py +++ b/src/dspeed/vis/waveform_browser.py @@ -52,7 +52,7 @@ def __init__( align: str = None, buffer_len: int = 128, block_width: int = 8, - additional_outputs: list[str] | None = None + additional_outputs: list[str] | None = None, ) -> None: """ Parameters @@ -150,7 +150,7 @@ def __init__( block_width block width for :class:`~.processing_chain.ProcessingChain`. - + additional_outputs More output fields for the internal processing chain. Useful for deriving classes. From 46d5a86295e6ae0f5b1454f76a77df97f75f57bb Mon Sep 17 00:00:00 2001 From: "Schwarz, Mario" Date: Thu, 21 Aug 2025 15:54:53 +0200 Subject: [PATCH 4/5] tests for the WaveformAndHistBrowser --- tests/vis/configs/hpge-dsp-histo-config.yaml | 24 +++++++++++ tests/vis/test_waveform_and_hist_browser.py | 42 ++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 tests/vis/configs/hpge-dsp-histo-config.yaml create mode 100644 tests/vis/test_waveform_and_hist_browser.py diff --git a/tests/vis/configs/hpge-dsp-histo-config.yaml b/tests/vis/configs/hpge-dsp-histo-config.yaml new file mode 100644 index 0000000..b2515b4 --- /dev/null +++ b/tests/vis/configs/hpge-dsp-histo-config.yaml @@ -0,0 +1,24 @@ +processors: + wf_hist , wf_borders: + description: projection histogram of scaled waveform onto the adc-axis + function: histogram + module: dspeed.processors + args: + - waveform + - wf_hist(100) + - wf_borders(101) + unit: [ADC, ADC] + wf_fwhm, wf_idx_out, wf_mode: + description: + FWHM, mode, and index of projection histogram of scaled waveform onto the + adc-axis + function: histogram_stats + module: dspeed.processors + args: + - wf_hist + - wf_borders + - wf_idx_out + - wf_mode + - wf_fwhm + - np.nan + unit: [ADC, ADC, ""] \ No newline at end of file diff --git a/tests/vis/test_waveform_and_hist_browser.py b/tests/vis/test_waveform_and_hist_browser.py new file mode 100644 index 0000000..1003558 --- /dev/null +++ b/tests/vis/test_waveform_and_hist_browser.py @@ -0,0 +1,42 @@ +from pathlib import Path + +from dspeed.vis import WaveformAndHistBrowser + +config_dir = Path(__file__).parent / "configs" + + +def test_basics(lgnd_test_data): + wb = WaveformAndHistBrowser( + lgnd_test_data.get_path("lh5/LDQTA_r117_20200110T105115Z_cal_geds_raw.lh5"), + "/geds/raw", + dsp_config=f"{config_dir}/hpge-dsp-histo-config.yaml", + lines=["waveform", "wf_mode"], + styles="seaborn-v0.8", + hist_values_edges=("wf_hist", "wf_borders"), + hist_styles=[ + {"color": ["red","green"]} + ], + ) + + wb.draw_next() + wb.draw_entry(24) + wb.draw_entry([2, 24]) + +def test_solo_and_log(lgnd_test_data): + wb = WaveformAndHistBrowser( + lgnd_test_data.get_path("lh5/LDQTA_r117_20200110T105115Z_cal_geds_raw.lh5"), + "/geds/raw", + dsp_config=f"{config_dir}/hpge-dsp-histo-config.yaml", + lines=[], + hist_values_edges=("wf_hist", "wf_borders"), + hist_styles=[ + {"color": ["red","green"]} + ], + hist_log=True, + vertical_hist=True + ) + + wb.draw_next() + wb.draw_entry(24) + wb.draw_entry([2, 24]) + From a01d239cda98ffd28ae6f2c4ac9bae77d385b503 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 13:55:17 +0000 Subject: [PATCH 5/5] style: pre-commit fixes --- tests/vis/configs/hpge-dsp-histo-config.yaml | 2 +- tests/vis/test_waveform_and_hist_browser.py | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/vis/configs/hpge-dsp-histo-config.yaml b/tests/vis/configs/hpge-dsp-histo-config.yaml index b2515b4..420eafd 100644 --- a/tests/vis/configs/hpge-dsp-histo-config.yaml +++ b/tests/vis/configs/hpge-dsp-histo-config.yaml @@ -21,4 +21,4 @@ processors: - wf_mode - wf_fwhm - np.nan - unit: [ADC, ADC, ""] \ No newline at end of file + unit: [ADC, ADC, ""] diff --git a/tests/vis/test_waveform_and_hist_browser.py b/tests/vis/test_waveform_and_hist_browser.py index 1003558..7d7572b 100644 --- a/tests/vis/test_waveform_and_hist_browser.py +++ b/tests/vis/test_waveform_and_hist_browser.py @@ -13,15 +13,14 @@ def test_basics(lgnd_test_data): lines=["waveform", "wf_mode"], styles="seaborn-v0.8", hist_values_edges=("wf_hist", "wf_borders"), - hist_styles=[ - {"color": ["red","green"]} - ], + hist_styles=[{"color": ["red", "green"]}], ) wb.draw_next() wb.draw_entry(24) wb.draw_entry([2, 24]) + def test_solo_and_log(lgnd_test_data): wb = WaveformAndHistBrowser( lgnd_test_data.get_path("lh5/LDQTA_r117_20200110T105115Z_cal_geds_raw.lh5"), @@ -29,14 +28,11 @@ def test_solo_and_log(lgnd_test_data): dsp_config=f"{config_dir}/hpge-dsp-histo-config.yaml", lines=[], hist_values_edges=("wf_hist", "wf_borders"), - hist_styles=[ - {"color": ["red","green"]} - ], + hist_styles=[{"color": ["red", "green"]}], hist_log=True, - vertical_hist=True + vertical_hist=True, ) wb.draw_next() wb.draw_entry(24) wb.draw_entry([2, 24]) -