From 2ae2a4d4ad8419bfc328fd642635ee1bde3739f1 Mon Sep 17 00:00:00 2001 From: Luca Baldini Date: Wed, 4 Feb 2026 16:46:59 +0100 Subject: [PATCH 1/9] Event display tweaked to support circular event readout. --- src/hexsample/cli.py | 14 ++++++--- src/hexsample/digi.py | 1 + src/hexsample/display.py | 61 ++++++++++++++++++++++++++++++++++++++-- src/hexsample/tasks.py | 35 +++++++++++++++++++---- 4 files changed, 100 insertions(+), 11 deletions(-) diff --git a/src/hexsample/cli.py b/src/hexsample/cli.py index da57e678..7554d2fa 100644 --- a/src/hexsample/cli.py +++ b/src/hexsample/cli.py @@ -103,6 +103,7 @@ def __init__(self) -> None: formatter_class=self._FORMATTER_CLASS) self.add_input_file(display) self.add_logging_level(display) + self.add_zero_sup_threshold(display, default=tasks.DisplayDefaults.zero_sup_threshold) display.set_defaults(runner=pipeline.display) # Run the quicklook? @@ -162,6 +163,13 @@ def add_suffix(parser: argparse.ArgumentParser, default: str) -> None: parser.add_argument("--suffix", type=str, default=default, help="suffix for the output file") + @staticmethod + def add_zero_sup_threshold(parser: argparse.ArgumentParser, default: int) -> None: + """Add an option for the zero-suppression threshold. + """ + parser.add_argument("--zero_sup_threshold", type=int, default=default, + help="zero-suppression threshold in ADC counts") + @staticmethod def add_source_options(parser: argparse.ArgumentParser) -> None: """Add an option group for to a given (sub-)parser to define the basic @@ -246,10 +254,8 @@ def add_readout_options(parser: argparse.ArgumentParser) -> None: group.add_argument("--trg_threshold", type=float, default=readout.HexagonalReadoutBase.trg_threshold, help="trigger threshold in electron equivalent") - group.add_argument("--zero_sup_threshold", type=int, - default=readout.HexagonalReadoutBase.zero_sup_threshold, - help="zero suppression threshold in ADC counts") - + CliArgumentParser.add_zero_sup_threshold(group, + default=readout.HexagonalReadoutBase.zero_sup_threshold) def add_recon_options(self, parser: argparse.ArgumentParser) -> None: """Add an option group for the reconstruction properties. diff --git a/src/hexsample/digi.py b/src/hexsample/digi.py index 8880d780..067e4b85 100644 --- a/src/hexsample/digi.py +++ b/src/hexsample/digi.py @@ -238,6 +238,7 @@ def from_digi(cls, file_row: np.ndarray): def ascii(self, pha_width: int = 5) -> str: """Ascii representation. + In the specific case of this class, the ascii representation is simply a px (that is the highest PHA pixel), because the neighbor position is not accessible by the DigiEvent. diff --git a/src/hexsample/display.py b/src/hexsample/display.py index cf9f6f3d..a149d65c 100644 --- a/src/hexsample/display.py +++ b/src/hexsample/display.py @@ -28,7 +28,7 @@ from matplotlib.collections import PatchCollection from matplotlib.patches import RegularPolygon -from .digi import DigiEventRectangular +from .digi import DigiEventCircular, DigiEventRectangular from .hexagon import HexagonalGrid from .roi import RegionOfInterest @@ -169,7 +169,8 @@ def draw_roi(self, roi: RegionOfInterest, offset: Tuple[float, float] = (0., 0.) plt.text(x + dx - self._grid.pitch, y + dy, f"{row}", **fmt) return collection - def draw_digi_event(self, event: DigiEventRectangular, offset: Tuple[float, float] = (0., 0.), + def draw_digi_event_rectangular(self, event: DigiEventRectangular, + offset: Tuple[float, float] = (0., 0.), indices: bool = True, padding: bool = True, zero_sup_threshold: float = 0, values: bool = True, **kwargs) -> HexagonCollection: """Draw an actual event int the parent hexagonal grid. @@ -194,3 +195,59 @@ def draw_digi_event(self, event: DigiEventRectangular, offset: Tuple[float, floa if value > zero_sup_threshold: plt.text(x, y, f"{value}", color=color, **fmt) return collection + + def draw_digi_event_circular(self, event: DigiEventCircular, + offset: Tuple[float, float] = (0., 0.), zero_sup_threshold: float = 0, + values: bool = True, **kwargs) -> HexagonCollection: + """Display a digi event with circular readout. + """ + dx, dy = offset + # This is shamelessly copied from clustering.py, and we should really + # have a function in event that is returning the physical coordinates + # and the pha values of all the pixels involved in the event. + col = [event.column] + row = [event.row] + adc_channel_order = [self._grid.adc_channel(event.column, event.row)] + # Taking the NN in logical coordinates ... + for _col, _row in self._grid.neighbors(event.column, event.row): + col.append(_col) + row.append(_row) + # ... transforming the coordinates of the NN in its corresponding ADC channel ... + adc_channel_order.append(self. _grid.adc_channel(_col, _row)) + # ... reordering the pha array for the correspondence (col[i], row[i]) with pha[i]. + pha = event.pha[adc_channel_order] + # Converting lists into numpy arrays + cols = np.array(col) + rows = np.array(row) + pha = np.array(pha) + x, y = self._grid.pixel_to_world(cols, rows) + args = x + dx, y + dy, 0.5 * self._grid.pitch, self._grid.hexagon_orientation() + collection = HexagonCollection(*args, **kwargs) + face_color = self.pha_to_colors(pha, zero_sup_threshold) + collection.set_facecolor(face_color) + if values: + # Draw the pixel values---note that we use black or white for the text + # color depending on the brightness of the pixel. + black = np.array([0., 0., 0., 1.]) + white = np.array([1., 1., 1., 1.]) + text_color = np.tile(black, len(face_color)).reshape(face_color.shape) + text_color[self.brightness(face_color) < 0.5] = white + fmt = dict(ha="center", va="center", fontsize="xx-small") + for x, y, value, color in zip(collection.x, collection.y,\ + pha.flatten(), text_color): + if value > zero_sup_threshold: + plt.text(x, y, f"{value}", color=color, **fmt) + plt.gca().add_collection(collection) + return collection + + def draw_digi_event(self, event, zero_sup_threshold) -> HexagonCollection: + """Draw a digi event. + + This is just dispatching the call to the proper method depending + on the event type. + """ + if isinstance(event, DigiEventRectangular): + return self.draw_digi_event_rectangular(event, zero_sup_threshold=zero_sup_threshold) + if isinstance(event, DigiEventCircular): + return self.draw_digi_event_circular(event, zero_sup_threshold=zero_sup_threshold) + raise NotImplementedError(f"Cannot draw event of type {type(event)}.") diff --git a/src/hexsample/tasks.py b/src/hexsample/tasks.py index 5b012c80..0e1739ce 100644 --- a/src/hexsample/tasks.py +++ b/src/hexsample/tasks.py @@ -35,7 +35,6 @@ from .clustering import ClusteringNN from .display import HexagonalGridDisplay from .fileio import ( - DigiInputFileRectangular, ReconInputFile, ReconOutputFile, digi_input_file_class, @@ -154,11 +153,13 @@ def simulate( @dataclass(frozen=True) class ReconstructionDefaults: + """Default parameters for the reconstruction task. This is a small helper dataclass to help ensure consistency between the main task definition in this Python module and the command-line interface. """ + suffix: str = "recon" zero_sup_threshold: int = 0 num_neighbors: int = 2 @@ -258,14 +259,20 @@ def reconstruct( class DisplayDefaults: + """Default parameters for the display task. This is a small helper dataclass to help ensure consistency between the main task definition in this Python module and the command-line interface. """ + zero_sup_threshold: int = 30 + -def display(input_file_path: str) -> None: +def display( + input_file_path: str, + zero_sup_threshold: int = DisplayDefaults.zero_sup_threshold, + ) -> None: """Display events from a digi file. Arguments @@ -275,16 +282,34 @@ def display(input_file_path: str) -> None: """ name, args = current_call() logger.info(f"Running {__name__}.{name} with arguments {args}...") - input_file = DigiInputFileRectangular(input_file_path) + + # Note we cast the input file to string, in case it happens to be a pathlib.Path object. + input_file_path = str(input_file_path) + if not input_file_path.endswith(".h5"): + raise RuntimeError("Input file {input_file_path} does not look like a HDF5 file") + + # It is necessary to extract the reaodut type because every readout type + # corresponds to a different DigiEvent type. + readout_mode = peek_readout_type(input_file_path) + # And we should get rid of all this crap when we store the readout type and all the + # relevant metadata in the hdf5 file in a sensible way. + file_type = digi_input_file_class(readout_mode) + input_file = file_type(input_file_path) header = input_file.header args = HexagonalLayout(header["layout"]), header["num_cols"], header["num_rows"],\ header["pitch"], header["enc"], header["gain"] - readout = HexagonalReadoutRectangular(*args) + if readout_mode is HexagonalReadoutMode.RECTANGULAR: + readout = HexagonalReadoutRectangular(*args, padding=header["padding"]) + elif readout_mode is HexagonalReadoutMode.CIRCULAR: + readout = HexagonalReadoutCircular(*args) + else: + raise RuntimeError(f"Unsupported readout mode: {readout_mode}") logger.info(f"Readout chip: {readout}") + grid_display = HexagonalGridDisplay(readout) for event in input_file: print(event.ascii()) - grid_display.draw_digi_event(event, zero_sup_threshold=0) + grid_display.draw_digi_event(event, zero_sup_threshold=zero_sup_threshold) grid_display.show() input_file.close() From 0b0fb3942cfeb129ff999a88d875dba312ed3328 Mon Sep 17 00:00:00 2001 From: Luca Baldini Date: Wed, 4 Feb 2026 16:48:03 +0100 Subject: [PATCH 2/9] Updated release notes. --- docs/release_notes.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release_notes.rst b/docs/release_notes.rst index 06d98703..25e773da 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -3,11 +3,12 @@ Release notes ============= +* Added rough support for DigiEventCircular objects in the event display. + Version 0.13.2 (2026-02-04) ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - * Modified the model to fit the angular coordinate for three-pixel events eta reconstruction. * Pull requests merged and issues closed: From b3e754b585fa01a773320ed663702b9c54f41e40 Mon Sep 17 00:00:00 2001 From: Luca Baldini Date: Wed, 4 Feb 2026 16:53:21 +0100 Subject: [PATCH 3/9] Minor. --- docs/release_notes.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/release_notes.rst b/docs/release_notes.rst index 25e773da..fa736772 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -15,7 +15,6 @@ Version 0.13.2 (2026-02-04) - https://github.com/lucabaldini/hexsample/pull/91 - Version 0.13.1 (2026-01-30) ~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 22e25b2d259ed9119ae12cafe099f234c25ca1f8 Mon Sep 17 00:00:00 2001 From: Augusto Cattafesta Date: Wed, 4 Feb 2026 18:40:42 +0100 Subject: [PATCH 4/9] Reconstructed position drawing added. --- src/hexsample/cli.py | 9 +++++++++ src/hexsample/display.py | 40 ++++++++++++++++++++++++++++++++++++++- src/hexsample/pipeline.py | 4 +++- src/hexsample/tasks.py | 15 +++++++++++---- 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/hexsample/cli.py b/src/hexsample/cli.py index 7554d2fa..dcd4e8b5 100644 --- a/src/hexsample/cli.py +++ b/src/hexsample/cli.py @@ -104,6 +104,7 @@ def __init__(self) -> None: self.add_input_file(display) self.add_logging_level(display) self.add_zero_sup_threshold(display, default=tasks.DisplayDefaults.zero_sup_threshold) + self.add_display_options(display) display.set_defaults(runner=pipeline.display) # Run the quicklook? @@ -292,6 +293,14 @@ def add_recon_options(self, parser: argparse.ArgumentParser) -> None: help="model to use for neural network reconstruction") group.add_argument("--model_path", type=str, help="path of the model to use, in case of custom model") + + def add_display_options(self, parser: argparse.ArgumentParser) -> None: + """Add an option group for the event display properties. + """ + group = parser.add_argument_group("display", "Event display configuration") + group.add_argument("--num_neighbors", type=int, + default=tasks.DisplayDefaults.num_neighbors, + help="number of neighbors to be considered (0--6)") def run(self) -> None: """Run the actual command tied to the specific options. diff --git a/src/hexsample/display.py b/src/hexsample/display.py index a149d65c..e9389b28 100644 --- a/src/hexsample/display.py +++ b/src/hexsample/display.py @@ -28,8 +28,12 @@ from matplotlib.collections import PatchCollection from matplotlib.patches import RegularPolygon -from .digi import DigiEventCircular, DigiEventRectangular + +from .clustering import ClusteringNN +from .digi import DigiEventCircular, DigiEventRectangular, DigiEventBase from .hexagon import HexagonalGrid +from .mc import MonteCarloEvent +from .readout import HexagonalReadoutBase from .roi import RegionOfInterest @@ -251,3 +255,37 @@ def draw_digi_event(self, event, zero_sup_threshold) -> HexagonCollection: if isinstance(event, DigiEventCircular): return self.draw_digi_event_circular(event, zero_sup_threshold=zero_sup_threshold) raise NotImplementedError(f"Cannot draw event of type {type(event)}.") + + def draw_positions(self, mc_event: MonteCarloEvent, digi_event: DigiEventBase, + readout: HexagonalReadoutBase, recon_defaults: "ReconstructionDefaults", + zero_sup_threshold: int, num_neighbors: int) -> None: + """Draw the Monte Carlo truth position and the reconstructed positions on top of the digi + event. + """ + # WARNING: currently it crashes if the cluster has size != 2 or 3, because of how eta is + # implemented in this branch, but in the very near future this will be fixed. + marker = dict(marker="x", s=100) + # Plot the Monte Carlo truth position. + plt.scatter(mc_event.absx, mc_event.absy, + **marker, color="green", label="Monte Carlo") + # Calculate the cluster from the digi event. + cluster = ClusteringNN(readout, zero_sup_threshold, num_neighbors=num_neighbors).run(digi_event) + # Calculate and plot centroid position. + centroid_position = cluster.centroid() + plt.scatter(*centroid_position, + **marker, color="yellow", + label="Centroid") + # Calculate and plot eta reconstructed position. + eta_recon_args = (recon_defaults.eta_2pix_rad, recon_defaults.eta_3pix_rad0, + recon_defaults.eta_3pix_rad1, recon_defaults.eta_3pix_theta0) + try: + eta_position = cluster.eta(*eta_recon_args, pitch=readout.pitch) + # If cluster size is not 2 or 3, eta returns the centroid position, so we only + # plot it if it's different from the centroid. + if not np.array_equal(eta_position, centroid_position): + plt.scatter(*eta_position, + **marker, color="blue", + label=r"$\eta$") + except RuntimeError: + pass + plt.legend() \ No newline at end of file diff --git a/src/hexsample/pipeline.py b/src/hexsample/pipeline.py index 9a580f48..d103996d 100644 --- a/src/hexsample/pipeline.py +++ b/src/hexsample/pipeline.py @@ -62,7 +62,9 @@ def display(**kwargs) -> None: """Display events from a digi or recon file. """ input_file_path = kwargs["input_file"] - return tasks.display(input_file_path) + num_neighbors = kwargs.get("num_neighbors", tasks.DisplayDefaults.num_neighbors) + args = input_file_path, num_neighbors + return tasks.display(*args, kwargs) def quicklook(**kwargs) -> None: diff --git a/src/hexsample/tasks.py b/src/hexsample/tasks.py index 0e1739ce..0538ce58 100644 --- a/src/hexsample/tasks.py +++ b/src/hexsample/tasks.py @@ -267,11 +267,13 @@ class DisplayDefaults: """ zero_sup_threshold: int = 30 + num_neighbors: int = 6 def display( input_file_path: str, zero_sup_threshold: int = DisplayDefaults.zero_sup_threshold, + num_neighbors: int = DisplayDefaults.num_neighbors, ) -> None: """Display events from a digi file. @@ -305,11 +307,16 @@ def display( else: raise RuntimeError(f"Unsupported readout mode: {readout_mode}") logger.info(f"Readout chip: {readout}") - grid_display = HexagonalGridDisplay(readout) - for event in input_file: - print(event.ascii()) - grid_display.draw_digi_event(event, zero_sup_threshold=zero_sup_threshold) + for i, event in enumerate(input_file): + # We cannot import this in display.py due to circular imports, so we are passing + # the default parameters for the reconstruction as arguments to the recon display function. + # We should decide if we want to add the options in the CLI to tweak the parameters on + # the fly. + recon_defaults = ReconstructionDefaults + mc_event = input_file.mc_event(i) + grid_display.draw_digi_event(event, zero_sup_threshold=zero_sup_threshold) + grid_display.draw_positions(mc_event, event, readout, recon_defaults, zero_sup_threshold, num_neighbors=num_neighbors) grid_display.show() input_file.close() From 4591c5471a9c7ef966f6fb1e11fb64d06c486a92 Mon Sep 17 00:00:00 2001 From: Augusto Cattafesta Date: Wed, 4 Feb 2026 18:44:26 +0100 Subject: [PATCH 5/9] Linting. --- src/hexsample/cli.py | 2 +- src/hexsample/display.py | 12 ++++++------ src/hexsample/tasks.py | 5 +++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/hexsample/cli.py b/src/hexsample/cli.py index dcd4e8b5..8fe8667c 100644 --- a/src/hexsample/cli.py +++ b/src/hexsample/cli.py @@ -293,7 +293,7 @@ def add_recon_options(self, parser: argparse.ArgumentParser) -> None: help="model to use for neural network reconstruction") group.add_argument("--model_path", type=str, help="path of the model to use, in case of custom model") - + def add_display_options(self, parser: argparse.ArgumentParser) -> None: """Add an option group for the event display properties. """ diff --git a/src/hexsample/display.py b/src/hexsample/display.py index e9389b28..4760d9ed 100644 --- a/src/hexsample/display.py +++ b/src/hexsample/display.py @@ -28,9 +28,8 @@ from matplotlib.collections import PatchCollection from matplotlib.patches import RegularPolygon - from .clustering import ClusteringNN -from .digi import DigiEventCircular, DigiEventRectangular, DigiEventBase +from .digi import DigiEventBase, DigiEventCircular, DigiEventRectangular from .hexagon import HexagonalGrid from .mc import MonteCarloEvent from .readout import HexagonalReadoutBase @@ -257,7 +256,7 @@ def draw_digi_event(self, event, zero_sup_threshold) -> HexagonCollection: raise NotImplementedError(f"Cannot draw event of type {type(event)}.") def draw_positions(self, mc_event: MonteCarloEvent, digi_event: DigiEventBase, - readout: HexagonalReadoutBase, recon_defaults: "ReconstructionDefaults", + readout: HexagonalReadoutBase, recon_defaults: object, zero_sup_threshold: int, num_neighbors: int) -> None: """Draw the Monte Carlo truth position and the reconstructed positions on top of the digi event. @@ -268,8 +267,9 @@ def draw_positions(self, mc_event: MonteCarloEvent, digi_event: DigiEventBase, # Plot the Monte Carlo truth position. plt.scatter(mc_event.absx, mc_event.absy, **marker, color="green", label="Monte Carlo") - # Calculate the cluster from the digi event. - cluster = ClusteringNN(readout, zero_sup_threshold, num_neighbors=num_neighbors).run(digi_event) + # Calculate the cluster from the digi event. + cluster = ClusteringNN(readout, zero_sup_threshold, + num_neighbors=num_neighbors).run(digi_event) # Calculate and plot centroid position. centroid_position = cluster.centroid() plt.scatter(*centroid_position, @@ -288,4 +288,4 @@ def draw_positions(self, mc_event: MonteCarloEvent, digi_event: DigiEventBase, label=r"$\eta$") except RuntimeError: pass - plt.legend() \ No newline at end of file + plt.legend() diff --git a/src/hexsample/tasks.py b/src/hexsample/tasks.py index 0538ce58..a1a67418 100644 --- a/src/hexsample/tasks.py +++ b/src/hexsample/tasks.py @@ -315,8 +315,9 @@ def display( # the fly. recon_defaults = ReconstructionDefaults mc_event = input_file.mc_event(i) - grid_display.draw_digi_event(event, zero_sup_threshold=zero_sup_threshold) - grid_display.draw_positions(mc_event, event, readout, recon_defaults, zero_sup_threshold, num_neighbors=num_neighbors) + grid_display.draw_digi_event(event, zero_sup_threshold=zero_sup_threshold) + grid_display.draw_positions(mc_event, event, readout, recon_defaults, zero_sup_threshold, + num_neighbors=num_neighbors) grid_display.show() input_file.close() From 6178808987e71b9532a83eac762f842847764e63 Mon Sep 17 00:00:00 2001 From: Augusto Cattafesta Date: Thu, 5 Feb 2026 11:20:43 +0100 Subject: [PATCH 6/9] Minor. --- src/hexsample/cli.py | 9 --------- src/hexsample/display.py | 6 ++---- src/hexsample/pipeline.py | 4 +--- src/hexsample/tasks.py | 8 +------- 4 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/hexsample/cli.py b/src/hexsample/cli.py index 8fe8667c..7554d2fa 100644 --- a/src/hexsample/cli.py +++ b/src/hexsample/cli.py @@ -104,7 +104,6 @@ def __init__(self) -> None: self.add_input_file(display) self.add_logging_level(display) self.add_zero_sup_threshold(display, default=tasks.DisplayDefaults.zero_sup_threshold) - self.add_display_options(display) display.set_defaults(runner=pipeline.display) # Run the quicklook? @@ -294,14 +293,6 @@ def add_recon_options(self, parser: argparse.ArgumentParser) -> None: group.add_argument("--model_path", type=str, help="path of the model to use, in case of custom model") - def add_display_options(self, parser: argparse.ArgumentParser) -> None: - """Add an option group for the event display properties. - """ - group = parser.add_argument_group("display", "Event display configuration") - group.add_argument("--num_neighbors", type=int, - default=tasks.DisplayDefaults.num_neighbors, - help="number of neighbors to be considered (0--6)") - def run(self) -> None: """Run the actual command tied to the specific options. """ diff --git a/src/hexsample/display.py b/src/hexsample/display.py index 4760d9ed..f45bcb68 100644 --- a/src/hexsample/display.py +++ b/src/hexsample/display.py @@ -257,19 +257,17 @@ def draw_digi_event(self, event, zero_sup_threshold) -> HexagonCollection: def draw_positions(self, mc_event: MonteCarloEvent, digi_event: DigiEventBase, readout: HexagonalReadoutBase, recon_defaults: object, - zero_sup_threshold: int, num_neighbors: int) -> None: + zero_sup_threshold: int) -> None: """Draw the Monte Carlo truth position and the reconstructed positions on top of the digi event. """ - # WARNING: currently it crashes if the cluster has size != 2 or 3, because of how eta is - # implemented in this branch, but in the very near future this will be fixed. marker = dict(marker="x", s=100) # Plot the Monte Carlo truth position. plt.scatter(mc_event.absx, mc_event.absy, **marker, color="green", label="Monte Carlo") # Calculate the cluster from the digi event. cluster = ClusteringNN(readout, zero_sup_threshold, - num_neighbors=num_neighbors).run(digi_event) + num_neighbors=6).run(digi_event) # Calculate and plot centroid position. centroid_position = cluster.centroid() plt.scatter(*centroid_position, diff --git a/src/hexsample/pipeline.py b/src/hexsample/pipeline.py index d103996d..9a580f48 100644 --- a/src/hexsample/pipeline.py +++ b/src/hexsample/pipeline.py @@ -62,9 +62,7 @@ def display(**kwargs) -> None: """Display events from a digi or recon file. """ input_file_path = kwargs["input_file"] - num_neighbors = kwargs.get("num_neighbors", tasks.DisplayDefaults.num_neighbors) - args = input_file_path, num_neighbors - return tasks.display(*args, kwargs) + return tasks.display(input_file_path) def quicklook(**kwargs) -> None: diff --git a/src/hexsample/tasks.py b/src/hexsample/tasks.py index a1a67418..9c92bddd 100644 --- a/src/hexsample/tasks.py +++ b/src/hexsample/tasks.py @@ -273,7 +273,6 @@ class DisplayDefaults: def display( input_file_path: str, zero_sup_threshold: int = DisplayDefaults.zero_sup_threshold, - num_neighbors: int = DisplayDefaults.num_neighbors, ) -> None: """Display events from a digi file. @@ -309,15 +308,10 @@ def display( logger.info(f"Readout chip: {readout}") grid_display = HexagonalGridDisplay(readout) for i, event in enumerate(input_file): - # We cannot import this in display.py due to circular imports, so we are passing - # the default parameters for the reconstruction as arguments to the recon display function. - # We should decide if we want to add the options in the CLI to tweak the parameters on - # the fly. recon_defaults = ReconstructionDefaults mc_event = input_file.mc_event(i) grid_display.draw_digi_event(event, zero_sup_threshold=zero_sup_threshold) - grid_display.draw_positions(mc_event, event, readout, recon_defaults, zero_sup_threshold, - num_neighbors=num_neighbors) + grid_display.draw_positions(mc_event, event, readout, recon_defaults, zero_sup_threshold) grid_display.show() input_file.close() From be7f2f75fad3ac94b395e4a6d51cb7768cb19762 Mon Sep 17 00:00:00 2001 From: Augusto Cattafesta Date: Thu, 5 Feb 2026 11:50:06 +0100 Subject: [PATCH 7/9] Facecolor removed. --- src/hexsample/display.py | 68 +++++++--------------------------------- 1 file changed, 11 insertions(+), 57 deletions(-) diff --git a/src/hexsample/display.py b/src/hexsample/display.py index f45bcb68..cb73cb77 100644 --- a/src/hexsample/display.py +++ b/src/hexsample/display.py @@ -101,29 +101,6 @@ def show(): HexagonalGridDisplay.setup_gca() plt.show() - def pha_to_colors(self, pha: np.array, zero_sup_threshold: float = None) -> np.array: - """Convert the pha values to colors for display purposes. - """ - values = pha.flatten() - values += self.color_map_offset - if zero_sup_threshold is not None: - values[values <= zero_sup_threshold + self.color_map_offset] = -1. - values = values / float(values.max()) - return self.color_map(values) - - @staticmethod - def brightness(color: np.array) -> np.array: - """Quick and dirty proxy for the brighness of a given array of colors. - - See https://stackoverflow.com/questions/9733288 - and also - https://stackoverflow.com/questions/30820962 - for how to split in columns the array of colors. - """ - # pylint: disable = invalid-name - r, g, b, _ = color.T - return (299 * r + 587 * g + 114 * b) / 1000 - def draw(self, offset: Tuple[float, float] = (0., 0.), pixel_labels: bool = False, **kwargs) -> HexagonCollection: """Draw the full grid display. @@ -183,20 +160,12 @@ def draw_digi_event_rectangular(self, event: DigiEventRectangular, """ # pylint: disable = invalid-name, too-many-arguments, too-many-locals collection = self.draw_roi(event.roi, offset, indices, padding, **kwargs) - face_color = self.pha_to_colors(event.pha, zero_sup_threshold) - collection.set_facecolor(face_color) if values: - # Draw the pixel values---note that we use black or white for the text - # color depending on the brightness of the pixel. - black = np.array([0., 0., 0., 1.]) - white = np.array([1., 1., 1., 1.]) - text_color = np.tile(black, len(face_color)).reshape(face_color.shape) - text_color[self.brightness(face_color) < 0.5] = white - fmt = dict(ha="center", va="center", fontsize="xx-small") - for x, y, value, color in zip(collection.x, collection.y,\ - event.pha.flatten(), text_color): + # Draw the pixel values + fmt = dict(ha="center", va="center", fontsize="small") + for x, y, value in zip(collection.x, collection.y, event.pha.flatten()): if value > zero_sup_threshold: - plt.text(x, y, f"{value}", color=color, **fmt) + plt.text(x, y, f"{value}", color="black", **fmt) return collection def draw_digi_event_circular(self, event: DigiEventCircular, @@ -226,20 +195,12 @@ def draw_digi_event_circular(self, event: DigiEventCircular, x, y = self._grid.pixel_to_world(cols, rows) args = x + dx, y + dy, 0.5 * self._grid.pitch, self._grid.hexagon_orientation() collection = HexagonCollection(*args, **kwargs) - face_color = self.pha_to_colors(pha, zero_sup_threshold) - collection.set_facecolor(face_color) if values: - # Draw the pixel values---note that we use black or white for the text - # color depending on the brightness of the pixel. - black = np.array([0., 0., 0., 1.]) - white = np.array([1., 1., 1., 1.]) - text_color = np.tile(black, len(face_color)).reshape(face_color.shape) - text_color[self.brightness(face_color) < 0.5] = white - fmt = dict(ha="center", va="center", fontsize="xx-small") - for x, y, value, color in zip(collection.x, collection.y,\ - pha.flatten(), text_color): + # Draw the pixel values + fmt = dict(ha="center", va="center", fontsize="small") + for x, y, value in zip(collection.x, collection.y, pha.flatten()): if value > zero_sup_threshold: - plt.text(x, y, f"{value}", color=color, **fmt) + plt.text(x, y, f"{value}", color="black", **fmt) plt.gca().add_collection(collection) return collection @@ -261,18 +222,14 @@ def draw_positions(self, mc_event: MonteCarloEvent, digi_event: DigiEventBase, """Draw the Monte Carlo truth position and the reconstructed positions on top of the digi event. """ - marker = dict(marker="x", s=100) # Plot the Monte Carlo truth position. - plt.scatter(mc_event.absx, mc_event.absy, - **marker, color="green", label="Monte Carlo") + plt.scatter(mc_event.absx, mc_event.absy, marker=".", s=100, label="Monte Carlo") # Calculate the cluster from the digi event. cluster = ClusteringNN(readout, zero_sup_threshold, num_neighbors=6).run(digi_event) # Calculate and plot centroid position. centroid_position = cluster.centroid() - plt.scatter(*centroid_position, - **marker, color="yellow", - label="Centroid") + plt.scatter(*centroid_position, marker="x", s=100, label="Centroid") # Calculate and plot eta reconstructed position. eta_recon_args = (recon_defaults.eta_2pix_rad, recon_defaults.eta_3pix_rad0, recon_defaults.eta_3pix_rad1, recon_defaults.eta_3pix_theta0) @@ -280,10 +237,7 @@ def draw_positions(self, mc_event: MonteCarloEvent, digi_event: DigiEventBase, eta_position = cluster.eta(*eta_recon_args, pitch=readout.pitch) # If cluster size is not 2 or 3, eta returns the centroid position, so we only # plot it if it's different from the centroid. - if not np.array_equal(eta_position, centroid_position): - plt.scatter(*eta_position, - **marker, color="blue", - label=r"$\eta$") + plt.scatter(*eta_position, marker="+", s=100, label=r"$\eta$") except RuntimeError: pass plt.legend() From 361fab25ca59b960f950c436b53507b2beda3299 Mon Sep 17 00:00:00 2001 From: Augusto Cattafesta Date: Fri, 6 Feb 2026 09:25:46 +0100 Subject: [PATCH 8/9] Minor. --- src/hexsample/pipeline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hexsample/pipeline.py b/src/hexsample/pipeline.py index 9a580f48..2c08df58 100644 --- a/src/hexsample/pipeline.py +++ b/src/hexsample/pipeline.py @@ -62,7 +62,8 @@ def display(**kwargs) -> None: """Display events from a digi or recon file. """ input_file_path = kwargs["input_file"] - return tasks.display(input_file_path) + zero_sup_threshold = kwargs.get("zero_sup_threshold", tasks.DisplayDefaults.zero_sup_threshold) + return tasks.display(input_file_path, zero_sup_threshold) def quicklook(**kwargs) -> None: From 6d3c09bb8fbdafa6c5187a69e420493a39bc719e Mon Sep 17 00:00:00 2001 From: Augusto Cattafesta Date: Fri, 6 Feb 2026 11:31:51 +0100 Subject: [PATCH 9/9] Added option to select event id --- src/hexsample/cli.py | 12 +++++++++++- src/hexsample/pipeline.py | 3 ++- src/hexsample/tasks.py | 8 ++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/hexsample/cli.py b/src/hexsample/cli.py index 7554d2fa..947ec5b2 100644 --- a/src/hexsample/cli.py +++ b/src/hexsample/cli.py @@ -103,7 +103,7 @@ def __init__(self) -> None: formatter_class=self._FORMATTER_CLASS) self.add_input_file(display) self.add_logging_level(display) - self.add_zero_sup_threshold(display, default=tasks.DisplayDefaults.zero_sup_threshold) + self.add_display_options(display) display.set_defaults(runner=pipeline.display) # Run the quicklook? @@ -293,6 +293,16 @@ def add_recon_options(self, parser: argparse.ArgumentParser) -> None: group.add_argument("--model_path", type=str, help="path of the model to use, in case of custom model") + def add_display_options(self, parser: argparse.ArgumentParser) -> None: + """Add an option group for the event display. + """ + group = parser.add_argument_group("display", "Event display configuration") + CliArgumentParser.add_zero_sup_threshold(group, + default=tasks.DisplayDefaults.zero_sup_threshold) + group.add_argument("--event_id", type=int, + default=tasks.DisplayDefaults.event_id, + help="ID of the event to display") + def run(self) -> None: """Run the actual command tied to the specific options. """ diff --git a/src/hexsample/pipeline.py b/src/hexsample/pipeline.py index 2c08df58..c82448db 100644 --- a/src/hexsample/pipeline.py +++ b/src/hexsample/pipeline.py @@ -63,7 +63,8 @@ def display(**kwargs) -> None: """ input_file_path = kwargs["input_file"] zero_sup_threshold = kwargs.get("zero_sup_threshold", tasks.DisplayDefaults.zero_sup_threshold) - return tasks.display(input_file_path, zero_sup_threshold) + event_id = kwargs.get("event_id", tasks.DisplayDefaults.event_id) + return tasks.display(input_file_path, zero_sup_threshold, event_id) def quicklook(**kwargs) -> None: diff --git a/src/hexsample/tasks.py b/src/hexsample/tasks.py index 9c92bddd..578ef885 100644 --- a/src/hexsample/tasks.py +++ b/src/hexsample/tasks.py @@ -268,11 +268,13 @@ class DisplayDefaults: zero_sup_threshold: int = 30 num_neighbors: int = 6 + event_id: int = None def display( input_file_path: str, zero_sup_threshold: int = DisplayDefaults.zero_sup_threshold, + event_id: int = DisplayDefaults.event_id ) -> None: """Display events from a digi file. @@ -280,6 +282,10 @@ def display( --------- file_path : str The path to the digi file. + zero_sup_threshold : int + The zero-suppression threshold to use when displaying the digi event. + event_id : int + The ID of the event to display. If None, display all events. """ name, args = current_call() logger.info(f"Running {__name__}.{name} with arguments {args}...") @@ -308,6 +314,8 @@ def display( logger.info(f"Readout chip: {readout}") grid_display = HexagonalGridDisplay(readout) for i, event in enumerate(input_file): + if event_id is not None and i != event_id: + continue recon_defaults = ReconstructionDefaults mc_event = input_file.mc_event(i) grid_display.draw_digi_event(event, zero_sup_threshold=zero_sup_threshold)