Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EventViewer as part of ctapipe-process tool #2556

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
25 changes: 24 additions & 1 deletion src/ctapipe/tools/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from ..calib import CameraCalibrator, GainSelector
from ..core import QualityQuery, Tool
from ..core.traits import Bool, classes_with_traits, flag
from ..core.traits import Bool, ComponentName, classes_with_traits, flag
from ..image import ImageCleaner, ImageModifier, ImageProcessor
from ..image.extractor import ImageExtractor
from ..image.muon import MuonProcessor
Expand All @@ -24,6 +24,7 @@
from ..io.datawriter import DATA_MODEL_VERSION
from ..reco import Reconstructor, ShowerProcessor
from ..utils import EventTypeFilter
from ..visualization import EventViewer

COMPATIBLE_DATALEVELS = [
DataLevel.R1,
Expand Down Expand Up @@ -76,6 +77,13 @@ class ProcessorTool(Tool):
default_value=False,
).tag(config=True)

event_viewer_name = ComponentName(
EventViewer,
default_value="QtEventViewer",
).tag(config=True)

open_viewer = Bool(False, help="Open EventViewer").tag(config=True)

aliases = {
("i", "input"): "EventSource.input_url",
("o", "output"): "DataWriter.output_path",
Expand Down Expand Up @@ -137,6 +145,12 @@ class ProcessorTool(Tool):
"store DL1/Event/Telescope muon parameters in output",
"don't store DL1/Event/Telescope muon parameters in output",
),
**flag(
"viewer",
"ProcessorTool.open_viewer",
"Open EventViewer",
"Do not open EventViewer",
),
"camera-frame": (
{"ImageProcessor": {"use_telescope_frame": False}},
"Use camera frame for image parameters instead of telescope frame",
Expand All @@ -162,6 +176,7 @@ class ProcessorTool(Tool):
+ classes_with_traits(ImageModifier)
+ classes_with_traits(EventTypeFilter)
+ classes_with_traits(Reconstructor)
+ classes_with_traits(EventViewer)
)

def setup(self):
Expand Down Expand Up @@ -207,6 +222,11 @@ def setup(self):
"shower distributions read from the input Simulation file are invalid)."
)

if self.open_viewer:
self.event_viewer = EventViewer.from_name(self.event_viewer_name, subarray)
else:
self.event_viewer = None

@property
def should_compute_dl2(self):
"""returns true if we should compute DL2 info"""
Expand Down Expand Up @@ -323,6 +343,9 @@ def start(self):
if self.should_compute_dl2:
self.process_shower(event)

if self.event_viewer is not None:
self.event_viewer(event)

self.write(event)

def finish(self):
Expand Down
19 changes: 10 additions & 9 deletions src/ctapipe/visualization/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Visualization: Methods for displaying data
"""
from .mpl_array import ArrayDisplay
from .mpl_camera import CameraDisplay
from .qt_eventviewer import QtEventViewer
from .viewer import EventViewer

try:
from .mpl_array import ArrayDisplay
from .mpl_camera import CameraDisplay
except ImportError:
pass


__all__ = ["CameraDisplay", "ArrayDisplay"]
__all__ = [
"CameraDisplay",
"ArrayDisplay",
"EventViewer",
"QtEventViewer",
]
274 changes: 274 additions & 0 deletions src/ctapipe/visualization/_qt_viewer_impl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
from queue import Empty

import astropy.units as u
import numpy as np
from PySide6 import QtGui
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtWidgets import (
QApplication,
QComboBox,
QHBoxLayout,
QLabel,
QMainWindow,
QPushButton,
QStackedLayout,
QTabWidget,
QVBoxLayout,
QWidget,
)

# import matplotlib after qt so it can detect which bindings are in use
from matplotlib.backends import backend_qtagg # isort: skip
from matplotlib.figure import Figure # isort: skip

from ..containers import ArrayEventContainer
from ..coordinates import EastingNorthingFrame, GroundFrame
from .mpl_array import ArrayDisplay
from .mpl_camera import CameraDisplay


class CameraDisplayWidget(QWidget):
def __init__(self, geometry, **kwargs):
super().__init__(**kwargs)

self.geometry = geometry

self.fig = Figure(layout="constrained")
self.canvas = backend_qtagg.FigureCanvasQTAgg(self.fig)

self.ax = self.fig.add_subplot(1, 1, 1)
self.display = CameraDisplay(geometry, ax=self.ax)
self.display.add_colorbar()

layout = QVBoxLayout()
layout.addWidget(self.canvas)
self.setLayout(layout)


class TelescopeDataWidget(QWidget):
def __init__(self, subarray, **kwargs):
super().__init__(**kwargs)
self.subarray = subarray
self.current_event = None

layout = QVBoxLayout()

top = QHBoxLayout()
label = QLabel(text="tel_id: ")
label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignCenter)
top.addWidget(label)
self.tel_selector = QComboBox(self)
self.tel_selector.currentTextChanged.connect(self.update_tel_image)
top.addWidget(self.tel_selector)
layout.addLayout(top)

self.camera_displays = []
self.widget_index = {}
self.camera_display_stack = QStackedLayout()

for i, tel in enumerate(self.subarray.telescope_types):
widget = CameraDisplayWidget(tel.camera.geometry)
self.camera_displays.append(widget)
self.camera_display_stack.addWidget(widget)

for tel_id in subarray.get_tel_ids_for_type(tel):
self.widget_index[tel_id] = i

layout.addLayout(self.camera_display_stack)
self.setLayout(layout)

def update_tel_image(self, tel_id):
# tel_selector.clear also calls this, but with an empty tel_id
if tel_id == "":
return

tel_id = int(tel_id)
index = self.widget_index[tel_id]
widget = self.camera_displays[index]

self.camera_display_stack.setCurrentIndex(index)
widget.display.image = self.current_event.dl1.tel[tel_id].image
widget.display.axes.figure.canvas.draw()

def update_event(self, event):
self.current_event = event

if event.dl1 is not None:
tels_with_image = [
str(tel_id)
for tel_id, dl1 in event.dl1.tel.items()
if dl1.image is not None
]
self.tel_selector.clear()
self.tel_selector.addItems(tels_with_image)
self.tel_selector.setCurrentIndex(0)


class SubarrayDataWidget(QWidget):
def __init__(self, subarray, **kwargs):
super().__init__(**kwargs)
self.subarray = subarray

self.fig = Figure(layout="constrained")
self.canvas = backend_qtagg.FigureCanvasQTAgg(self.fig)

self.ax = self.fig.add_subplot(1, 1, 1)
self.display = ArrayDisplay(
subarray,
axes=self.ax,
frame=EastingNorthingFrame(),
)
self.display.add_labels()
self.display.telescopes.set_linewidth(0)

layout = QVBoxLayout()
layout.addWidget(self.canvas)
self.setLayout(layout)
self.tel_types = self.display.telescopes.get_array().astype(float)

(self.true_impact,) = self.ax.plot(
[],
[],
marker="x",
linestyle="",
ms=15,
color="k",
label="true impact",
)
self.display.legend_elements.append(self.true_impact)
self.ax.legend(handles=self.display.legend_elements)
self.reco_impacts = {}

def update_event(self, event):
trigger_pattern = self.tel_types.copy()
mask = self.subarray.tel_ids_to_mask(event.trigger.tels_with_trigger)
trigger_pattern[~mask] = np.nan
self.display.values = trigger_pattern

if (sim := event.simulation) is not None and (shower := sim.shower) is not None:
impact = GroundFrame(shower.core_x, shower.core_y, 0 * u.m)
impact = impact.transform_to(self.display.frame)
x = impact.easting.to_value(u.m)
y = impact.northing.to_value(u.m)
self.true_impact.set_data(x, y)

for key, reco in event.dl2.stereo.geometry.items():
if key not in self.reco_impacts:
(marker,) = self.ax.plot(
[],
[],
marker="x",
linestyle="",
ms=15,
label=key,
)
self.reco_impacts[key] = marker
self.display.legend_elements.append(marker)
self.ax.legend(handles=self.display.legend_elements)

impact = GroundFrame(reco.core_x, reco.core_y, 0 * u.m)
impact = impact.transform_to(self.display.frame)
x = impact.easting.to_value(u.m)
y = impact.northing.to_value(u.m)
self.reco_impacts[key].set_data(x, y)

self.canvas.draw()


class ViewerMainWindow(QMainWindow):
new_event_signal = Signal(ArrayEventContainer)

def __init__(self, subarray, queue, **kwargs):
super().__init__(**kwargs)
self.subarray = subarray
self.queue = queue
self.current_event = None
self.setWindowTitle("ctapipe event display")

layout = QVBoxLayout()

top = QHBoxLayout()
self.label = QLabel(self)
self.label.setAlignment(
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignCenter
)
top.addWidget(self.label)
layout.addLayout(top)

tabs = QTabWidget()
self.subarray_data = SubarrayDataWidget(subarray)
tabs.addTab(self.subarray_data, "Subarray Data")
self.tel_data = TelescopeDataWidget(subarray)
tabs.addTab(self.tel_data, "Telescope Data")
layout.addWidget(tabs)

self.next_button = QPushButton("Next Event", parent=self)
self.next_button.pressed.connect(self.next)
layout.addWidget(self.next_button)

widget = QWidget(self)
widget.setLayout(layout)
self.setCentralWidget(widget)

self.event_thread = EventLoop(self)
self.event_thread.start()

self.new_event_signal.connect(self.update_event)

# set window size slightly smaller than available desktop space
size = QtGui.QGuiApplication.primaryScreen().availableGeometry().size()
self.resize(0.9 * size)

def update_event(self, event):
if event is None:
return

self.current_event = event

label = f"obs_id: {event.index.obs_id}" f", event_id: {event.index.event_id}"
if event.simulation is not None and event.simulation.shower is not None:
label += f", E={event.simulation.shower.energy:.3f}"

self.label.setText(label)
self.subarray_data.update_event(event)
self.tel_data.update_event(event)

def next(self):
if self.current_event is not None:
self.queue.task_done()

def closeEvent(self, event):
self.event_thread.stop_signal.emit()
self.event_thread.wait()
self.next()
super().closeEvent(event)


class EventLoop(QThread):
stop_signal = Signal()

def __init__(self, display):
super().__init__()
self.display = display
self.closed = False
self.stop_signal.connect(self.close)

def close(self):
self.closed = True

def run(self):
while not self.closed:
try:
event = self.display.queue.get(timeout=0.1)
self.display.new_event_signal.emit(event)
except Empty:
continue
except ValueError:
break


def viewer_main(subarray, queue):
app = QApplication()
window = ViewerMainWindow(subarray, queue)
window.show()
app.exec_()
Loading
Loading