From 7565a41507dcc5eb01ca73b5bc45cd2e782448cb Mon Sep 17 00:00:00 2001 From: Jason Watson Date: Fri, 20 Apr 2018 17:37:10 +0200 Subject: [PATCH 01/13] Created the bokeh/file_viewer tool Also includes - ctapipe.utils.rgbtohex - ctapipe.visualization.bokeh - ctapipe.plotting.bokeh_event_viewer --- ctapipe/plotting/bokeh_event_viewer.py | 489 ++++++++++++++++++ .../plotting/tests/test_bokeh_event_viewer.py | 165 ++++++ ctapipe/tools/bokeh/file_viewer/main.py | 396 ++++++++++++++ .../bokeh/file_viewer/templates/index.html | 24 + ctapipe/tools/bokeh/file_viewer/theme.yaml | 17 + ctapipe/tools/tests/test_tools.py | 8 + ctapipe/utils/rgbtohex.py | 73 +++ ctapipe/utils/rgbtohex_c.cc | 18 + ctapipe/utils/tests/test_rgbtohex.py | 20 + ctapipe/visualization/bokeh.py | 380 ++++++++++++++ ctapipe/visualization/tests/test_bokeh.py | 124 +++++ setup.py | 4 +- 12 files changed, 1717 insertions(+), 1 deletion(-) create mode 100644 ctapipe/plotting/bokeh_event_viewer.py create mode 100644 ctapipe/plotting/tests/test_bokeh_event_viewer.py create mode 100644 ctapipe/tools/bokeh/file_viewer/main.py create mode 100644 ctapipe/tools/bokeh/file_viewer/templates/index.html create mode 100644 ctapipe/tools/bokeh/file_viewer/theme.yaml create mode 100644 ctapipe/utils/rgbtohex.py create mode 100644 ctapipe/utils/rgbtohex_c.cc create mode 100644 ctapipe/utils/tests/test_rgbtohex.py create mode 100644 ctapipe/visualization/bokeh.py create mode 100644 ctapipe/visualization/tests/test_bokeh.py diff --git a/ctapipe/plotting/bokeh_event_viewer.py b/ctapipe/plotting/bokeh_event_viewer.py new file mode 100644 index 00000000000..23c3a4d8b05 --- /dev/null +++ b/ctapipe/plotting/bokeh_event_viewer.py @@ -0,0 +1,489 @@ +import numpy as np +from bokeh.layouts import layout, column +from bokeh.models import Select, Span +from ctapipe.core import Component +from ctapipe.visualization.bokeh import CameraDisplay, WaveformDisplay + + +class BokehEventViewerCamera(CameraDisplay): + def __init__(self, event_viewer, fig=None): + """ + A `ctapipe.visualization.bokeh.CameraDisplay` modified to utilise a + `ctapipe.core.container.DataContainer` directly. + + Parameters + ---------- + event_viewer : BokehEventViewer + The BokehEventViewer this object belongs to + fig : bokeh.plotting.figure + Figure to store the bokeh plot onto (optional) + """ + self._event = None + self._view = 'r0' + self._telid = None + self._channel = 0 + self._time = 0 + super().__init__(fig=fig) + + self._view_options = ['r0', 'r1', 'dl0', 'dl1', 'peakpos', 'cleaned'] + self.w_view = None + self._geom_tel = None + + self.event_viewer = event_viewer + + def _reset(self): + self.reset_pixels() + self.event_viewer.change_time(0) + + def _set_image(self): + e = self.event + v = self.view + t = self.telid + c = self.channel + time = self.time + if e: + tels = list(e.r0.tels_with_data) + if t is None: + t = tels[0] + if t not in tels: + raise KeyError("Telescope {} has no data".format(t)) + if v == 'r0': + samples = e.r0.tel[t].waveform + if samples is None: + self.image = None + else: + self.image = samples[c, :, time] + self.fig.title.text = 'R0 Slice (T = {})'.format(time) + elif v == 'r1': + samples = e.r1.tel[t].waveform + if samples is None: + self.image = None + else: + self.image = samples[c, :, time] + self.fig.title.text = 'R1 Slice (T = {})'.format(time) + elif v == 'dl0': + samples = e.dl0.tel[t].waveform + if samples is None: + self.image = None + else: + self.image = samples[c, :, time] + self.fig.title.text = 'DL0 Slice (T = {})'.format(time) + elif v == 'dl1': + self.image = e.dl1.tel[t].image[c, :] + self.fig.title.text = 'DL1 Image' + elif v == 'peakpos': + peakpos = e.dl1.tel[t].peakpos + if peakpos is None: + self.image = np.zeros(self.image.shape) + else: + self.image = peakpos[c, :] + self.fig.title.text = 'DL1 Peakpos Image' + elif v == 'cleaned': + samples = e.dl1.tel[t].cleaned + if samples is None: + self.image = None + else: + self.image = samples[c, :, time] + self.fig.title.text = 'Cleaned Slice (T = {})'.format(time) + else: + raise ValueError("No view configuration set up " + "for: {}".format(v)) + else: + self.event_viewer.log.warning("No event has been provided") + + def _update_geometry(self): + e = self.event + t = self.telid + if e: + # Check if geom actually needs to be changed + if not t == self._geom_tel: + self.geom = e.inst.subarray.tel[t].camera + self._geom_tel = t + else: + self.event_viewer.log.warning("No event has been provided") + + def refresh(self): + self._set_image() + + @property + def event(self): + return self._event + + @event.setter + def event(self, val): + self._event = val + self._update_geometry() + self._set_image() + + def change_event(self, event, telid): + if self.event: # Only reset when an event exists + self._reset() + self._telid = telid + self.event = event + + @property + def view(self): + return self._view + + @view.setter + def view(self, val): + if val not in self._view_options: + raise ValueError("View is not valid: {}".format(val)) + self._view = val + self._set_image() + + @property + def telid(self): + return self._telid + + @telid.setter + def telid(self, val): + if self.event: # Only reset when an event exists + self._reset() + self._telid = val + self._update_geometry() + self._set_image() + + @property + def channel(self): + return self._channel + + @channel.setter + def channel(self, val): + self._channel = val + self._set_image() + + @property + def time(self): + return self._time + + @time.setter + def time(self, val): + self._time = int(val) + self._set_image() + + def _on_pixel_click(self, pix_id): + super()._on_pixel_click(pix_id) + ai = self.active_index + self.event_viewer.waveforms[ai].pixel = pix_id + + def create_view_widget(self): + self.w_view = Select(title="View:", value="", options=[], width=5) + self.w_view.on_change('value', self.on_view_widget_change) + self.layout = column([self.w_view, self.layout]) + + def update_view_widget(self): + self.w_view.options = self._view_options + self.w_view.value = self.view + + def on_view_widget_change(self, _, __, ___): + if self.view != self.w_view.value: + self.view = self.w_view.value + + +class BokehEventViewerWaveform(WaveformDisplay): + def __init__(self, event_viewer, fig=None): + """ + A `ctapipe.visualization.bokeh.WaveformDisplay` modified to utilise a + `ctapipe.core.container.DataContainer` directly. + + Parameters + ---------- + event_viewer : BokehEventViewer + The BokehEventViewer this object belongs to + fig : bokeh.plotting.figure + Figure to store the bokeh plot onto (optional) + """ + self._event = None + self._view = 'r0' + self._telid = None + self._channel = 0 + self._pixel = 0 + super().__init__(fig=fig) + self._draw_integration_window() + + self._view_options = ['r0', 'r1', 'dl0', 'cleaned'] + self.w_view = None + + self.event_viewer = event_viewer + + def _reset(self): + for wav in self.event_viewer.waveforms: + wav.pixel = 0 + + def _set_waveform(self): + e = self.event + v = self.view + t = self.telid + c = self.channel + p = self.pixel + if e: + tels = list(e.r0.tels_with_data) + if t is None: + t = tels[0] + if t not in tels: + raise KeyError("Telescope {} has no data".format(t)) + if v == 'r0': + samples = e.r0.tel[t].waveform + if samples is None: + self.waveform = None + else: + self.waveform = samples[c, p] + self.fig.title.text = 'R0 Waveform (Pixel = {})'.format(p) + elif v == 'r1': + samples = e.r1.tel[t].waveform + if samples is None: + self.waveform = None + else: + self.waveform = samples[c, p] + self.fig.title.text = 'R1 Waveform (Pixel = {})'.format(p) + elif v == 'dl0': + samples = e.dl0.tel[t].waveform + if samples is None: + self.waveform = None + else: + self.waveform = samples[c, p] + self.fig.title.text = 'DL0 Waveform (Pixel = {})'.format(p) + elif v == 'cleaned': + samples = e.dl1.tel[t].cleaned + if samples is None: + self.waveform = None + else: + self.waveform = samples[c, p] + self.fig.title.text = 'Cleaned Waveform (Pixel = {})'.format(p) + else: + raise ValueError("No view configuration set up " + "for: {}".format(v)) + else: + self.event_viewer.log.warning("No event has been provided") + + def _draw_integration_window(self): + self.intwin1 = Span(location=0, dimension='height', + line_color='green', line_dash='dotted') + self.intwin2 = Span(location=0, dimension='height', + line_color='green', line_dash='dotted') + self.fig.add_layout(self.intwin1) + self.fig.add_layout(self.intwin2) + + def _set_integration_window(self): + e = self.event + t = self.telid + c = self.channel + p = self.pixel + if e: + if e.dl1.tel[t].extracted_samples is not None: + # Get Windows + windows = e.dl1.tel[t].extracted_samples[c, p] + length = np.sum(windows) + start = np.argmax(windows) + end = start + length - 1 + self.intwin1.location = start + self.intwin2.location = end + else: + self.event_viewer.log.warning("No event has been provided") + + def refresh(self): + self._set_waveform() + self._set_integration_window() + + @property + def event(self): + return self._event + + @event.setter + def event(self, val): + self._event = val + self._set_waveform() + self._set_integration_window() + + def change_event(self, event, telid): + if self.event: # Only reset when an event exists + self._reset() + self._telid = telid + self.event = event + + @property + def view(self): + return self._view + + @view.setter + def view(self, val): + if val not in self._view_options: + raise ValueError("View is not valid: {}".format(val)) + self._view = val + self._set_waveform() + self._set_integration_window() + + @property + def telid(self): + return self._telid + + @telid.setter + def telid(self, val): + if self.event: # Only reset when an event exists + self._reset() + self._telid = val + self._set_waveform() + self._set_integration_window() + + @property + def channel(self): + return self._channel + + @channel.setter + def channel(self, val): + self._channel = val + self._set_waveform() + self._set_integration_window() + + @property + def pixel(self): + return self._pixel + + @pixel.setter + def pixel(self, val): + self._pixel = val + self._set_waveform() + self._set_integration_window() + + def _on_waveform_click(self, time): + super()._on_waveform_click(time) + self.event_viewer.change_time(time) + + def create_view_widget(self): + self.w_view = Select(title="View:", value="", options=[], width=5) + self.w_view.on_change('value', self.on_view_widget_change) + self.layout = column([self.w_view, self.layout]) + + def update_view_widget(self): + self.w_view.options = self._view_options + self.w_view.value = self.view + + def on_view_widget_change(self, _, __, ___): + if self.view != self.w_view.value: + self.view = self.w_view.value + + +class BokehEventViewer(Component): + def __init__(self, config, tool, num_cameras=1, num_waveforms=2, **kwargs): + """ + A class to organise the interface between + `ctapipe.visualization.bokeh.CameraDisplay`, + `ctapipe.visualization.bokeh.WaveformDisplay` and + `ctapipe.core.container.DataContainer`. + + Parameters + ---------- + config : traitlets.loader.Config + Configuration specified by config file or cmdline arguments. + Used to set traitlet values. + Set to None if no configuration to pass. + tool : ctapipe.core.Tool + Tool executable that is calling this component. + Passes the correct logger to the component. + Set to None if no Tool to pass. + num_cameras : int + Number of camera figures to handle + num_waveforms : int + Number of waveform figures to handle + kwargs + """ + super().__init__(config=config, parent=tool, **kwargs) + + self._event = None + self._view = 'r0' + self._telid = None + self._channel = 0 + + self.num_cameras = num_cameras + self.num_waveforms = num_waveforms + + self.cameras = [] + self.camera_layouts = [] + self.waveforms = [] + self.waveform_layouts = [] + + self.layout = None + + def create(self): + for icam in range(self.num_cameras): + cam = BokehEventViewerCamera(self) + cam.enable_pixel_picker(self.num_waveforms) + cam.create_view_widget() + cam.update_view_widget() + cam.add_colorbar() + + self.cameras.append(cam) + self.camera_layouts.append(cam.layout) + + for iwav in range(self.num_waveforms): + wav = BokehEventViewerWaveform(self) + active_color = self.cameras[0].active_colors[iwav] + wav.fig.select(name='line')[0].glyph.line_color = active_color + wav.enable_time_picker() + wav.create_view_widget() + wav.update_view_widget() + + self.waveforms.append(wav) + self.waveform_layouts.append(wav.layout) + + self.layout = layout([ + [column(self.camera_layouts), column(self.waveform_layouts)], + ]) + + def enable_automatic_index_increment(self): + for cam in self.cameras: + cam.automatic_index_increment = True + + def change_time(self, time): + for wav in self.waveforms: + if wav.active_time != time: + wav.active_time = time + for camera in self.cameras: + camera.time = self.waveforms[0].active_time + + def sub_event_viewer_generator(self): + for camera in self.cameras: + yield camera + for waveform in self.waveforms: + yield waveform + + def refresh(self): + for sub in self.sub_event_viewer_generator(): + sub.refresh() + + @property + def event(self): + return self._event + + @event.setter + def event(self, val): + if self._event != val: + self._event = val + tels = list(val.r0.tels_with_data) + if self.telid not in tels: + self._telid = tels[0] + for sub in self.sub_event_viewer_generator(): + sub.change_event(val, self.telid) + + @property + def telid(self): + return self._telid + + @telid.setter + def telid(self, val): + if self._telid != val: + self._telid = val + for sub in self.sub_event_viewer_generator(): + sub.telid = val + + @property + def channel(self): + return self._channel + + @channel.setter + def channel(self, val): + if self._channel != val: + self._channel = val + for sub in self.sub_event_viewer_generator(): + sub.channel = val diff --git a/ctapipe/plotting/tests/test_bokeh_event_viewer.py b/ctapipe/plotting/tests/test_bokeh_event_viewer.py new file mode 100644 index 00000000000..e0b0164f254 --- /dev/null +++ b/ctapipe/plotting/tests/test_bokeh_event_viewer.py @@ -0,0 +1,165 @@ +from ctapipe.plotting.bokeh_event_viewer import BokehEventViewer +from ctapipe.calib.camera.calibrator import CameraCalibrator +import pytest + + +def test_bokeh_event_viewer_creation(): + viewer = BokehEventViewer(config=None, tool=None) + viewer.create() + + +def test_event_setting(test_event): + viewer = BokehEventViewer(config=None, tool=None) + viewer.create() + viewer.event = test_event + for cam in viewer.cameras: + assert cam.event == test_event + for wf in viewer.waveforms: + assert wf.event == test_event + + +def test_enable_automatic_index_increment(): + viewer = BokehEventViewer(config=None, tool=None) + viewer.create() + viewer.enable_automatic_index_increment() + for cam in viewer.cameras: + assert cam.automatic_index_increment + + +def test_change_time(test_event): + viewer = BokehEventViewer(config=None, tool=None) + viewer.create() + viewer.event = test_event + + t = 5 + viewer.change_time(t) + for cam in viewer.cameras: + assert cam.time == t + for wf in viewer.waveforms: + assert wf.active_time == t + + t = -11 + viewer.change_time(t) + for cam in viewer.cameras: + assert cam.time == 0 + for wf in viewer.waveforms: + assert wf.active_time == 0 + + tel = list(test_event.r0.tels_with_data)[0] + n_samples = test_event.r0.tel[tel].waveform.shape[-1] + t = 10000 + viewer.change_time(t) + for cam in viewer.cameras: + assert cam.time == n_samples - 1 + for wf in viewer.waveforms: + assert wf.active_time == n_samples - 1 + + +def test_on_waveform_click(test_event): + viewer = BokehEventViewer(config=None, tool=None) + viewer.create() + viewer.event = test_event + + t = 5 + viewer.waveforms[0]._on_waveform_click(t) + for cam in viewer.cameras: + assert cam.time == t + for wf in viewer.waveforms: + assert wf.active_time == t + + +def test_telid(test_event): + viewer = BokehEventViewer(config=None, tool=None) + viewer.create() + viewer.event = test_event + + tels = list(test_event.r0.tels_with_data) + + assert viewer.telid == tels[0] + for cam in viewer.cameras: + assert cam.telid == tels[0] + for wf in viewer.waveforms: + assert wf.telid == tels[0] + + viewer.telid = tels[1] + assert viewer.telid == tels[1] + for cam in viewer.cameras: + assert cam.telid == tels[1] + for wf in viewer.waveforms: + assert wf.telid == tels[1] + + +def test_telid_incorrect(test_event): + viewer = BokehEventViewer(config=None, tool=None) + viewer.create() + viewer.event = test_event + + with pytest.raises(KeyError): + viewer.telid = 148937242 + + +def test_on_pixel_click(test_event): + viewer = BokehEventViewer(config=None, tool=None) + viewer.create() + viewer.event = test_event + + p1 = 5 + viewer.cameras[0]._on_pixel_click(p1) + assert viewer.waveforms[viewer.cameras[0].active_index].pixel == p1 + + +def test_channel(test_event): + viewer = BokehEventViewer(config=None, tool=None) + viewer.create() + viewer.event = test_event + + assert viewer.channel == 0 + for cam in viewer.cameras: + assert cam.channel == 0 + for wf in viewer.waveforms: + assert wf.channel == 0 + + +def test_channel_incorrect(test_event): + viewer = BokehEventViewer(config=None, tool=None) + viewer.create() + viewer.event = test_event + + with pytest.raises(IndexError): + viewer.channel = 148937242 + + +def test_view_camera(test_event): + viewer = BokehEventViewer(config=None, tool=None) + viewer.create() + viewer.event = test_event + + c = CameraCalibrator() + c.calibrate(test_event) + + t = list(test_event.r0.tels_with_data)[0] + + cam = viewer.cameras[0] + cam.view = 'r1' + assert (cam.image == test_event.r1.tel[t].waveform[0, :, 0]).all() + + with pytest.raises(ValueError): + cam.view = 'q' + + +def test_view_wf(test_event): + viewer = BokehEventViewer(config=None, tool=None) + viewer.create() + viewer.event = test_event + + c = CameraCalibrator() + c.calibrate(test_event) + + t = list(test_event.r0.tels_with_data)[0] + + wf = viewer.waveforms[0] + wf.view = 'r1' + assert (wf.waveform == test_event.r1.tel[t].waveform[0, 0, :]).all() + + with pytest.raises(ValueError): + wf.view = 'q' diff --git a/ctapipe/tools/bokeh/file_viewer/main.py b/ctapipe/tools/bokeh/file_viewer/main.py new file mode 100644 index 00000000000..c57388c8f32 --- /dev/null +++ b/ctapipe/tools/bokeh/file_viewer/main.py @@ -0,0 +1,396 @@ +from bokeh.io import curdoc +from bokeh.layouts import widgetbox, layout +from bokeh.models import Select, TextInput, PreText, Button +from traitlets import Dict, List +from ctapipe.calib.camera.dl0 import CameraDL0Reducer +from ctapipe.calib.camera.dl1 import CameraDL1Calibrator +from ctapipe.calib.camera.r1 import CameraR1CalibratorFactory +from ctapipe.core import Tool +from ctapipe.image.charge_extractors import ChargeExtractorFactory +from ctapipe.image.waveform_cleaning import WaveformCleanerFactory +from ctapipe.io.eventsourcefactory import EventSourceFactory +from ctapipe.io.eventseeker import EventSeeker +from ctapipe.plotting.bokeh_event_viewer import BokehEventViewer +from ctapipe.utils import get_dataset_path + + +class BokehFileViewer(Tool): + name = "BokehFileViewer" + description = ("Interactively explore an event file using the bokeh " + "visualisation package") + + aliases = Dict(dict( + r='EventSourceFactory.product', + f='EventSourceFactory.input_url', + max_events='EventSourceFactory.max_events', + ped='CameraR1CalibratorFactory.pedestal_path', + tf='CameraR1CalibratorFactory.tf_path', + pe='CameraR1CalibratorFactory.pe_path', + ff='CameraR1CalibratorFactory.ff_path', + extractor='ChargeExtractorFactory.product', + extractor_t0='ChargeExtractorFactory.t0', + extractor_window_width='ChargeExtractorFactory.window_width', + extractor_window_shift='ChargeExtractorFactory.window_shift', + extractor_sig_amp_cut_HG='ChargeExtractorFactory.sig_amp_cut_HG', + extractor_sig_amp_cut_LG='ChargeExtractorFactory.sig_amp_cut_LG', + extractor_lwt='ChargeExtractorFactory.lwt', + clip_amplitude='CameraDL1Calibrator.clip_amplitude', + radius='CameraDL1Calibrator.radius', + cleaner='WaveformCleanerFactory.product', + )) + + classes = List([ + EventSourceFactory, + ChargeExtractorFactory, + CameraR1CalibratorFactory, + CameraDL1Calibrator, + WaveformCleanerFactory + ]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._event = None + self._event_index = None + self._event_id = None + self._telid = None + self._channel = None + + self.w_next_event = None + self.w_previous_event = None + self.w_event_index = None + self.w_event_id = None + self.w_goto_event_index = None + self.w_goto_event_id = None + self.w_telid = None + self.w_channel = None + self.w_dl1_dict = None + self.wb_extractor = None + self.layout = None + + self.reader = None + self.seeker = None + self.extractor = None + self.cleaner = None + self.r1 = None + self.dl0 = None + self.dl1 = None + self.viewer = None + + self._updating_dl1 = False + + def setup(self): + self.log_format = "%(levelname)s: %(message)s [%(name)s.%(funcName)s]" + kwargs = dict(config=self.config, tool=self) + + default_url = get_dataset_path("gamma_test.simtel.gz") + EventSourceFactory.input_url.default_value = default_url + self.reader = EventSourceFactory.produce(**kwargs) + self.seeker = EventSeeker(self.reader, **kwargs) + + self.extractor = ChargeExtractorFactory.produce(**kwargs) + self.cleaner = WaveformCleanerFactory.produce(**kwargs) + + self.r1 = CameraR1CalibratorFactory.produce( + eventsource=self.reader, + **kwargs + ) + self.dl0 = CameraDL0Reducer(**kwargs) + self.dl1 = CameraDL1Calibrator( + extractor=self.extractor, + cleaner=self.cleaner, + **kwargs + ) + + self.viewer = BokehEventViewer(**kwargs) + + # Setup widgets + self.viewer.create() + self.viewer.enable_automatic_index_increment() + self.create_previous_event_widget() + self.create_next_event_widget() + self.create_event_index_widget() + self.create_goto_event_index_widget() + self.create_event_id_widget() + self.create_goto_event_id_widget() + self.create_telid_widget() + self.create_channel_widget() + self.create_dl1_widgets() + self.update_dl1_widget_values() + + # Setup layout + self.layout = layout([ + [self.viewer.layout], + [ + self.w_previous_event, + self.w_next_event, + self.w_goto_event_index, + self.w_goto_event_id + ], + [self.w_event_index, self.w_event_id], + [self.w_telid, self.w_channel], + [self.wb_extractor] + ]) + + def start(self): + self.event_index = 0 + + def finish(self): + curdoc().add_root(self.layout) + curdoc().title = "Event Viewer" + + @property + def event_index(self): + return self._event_index + + @event_index.setter + def event_index(self, val): + try: + self.event = self.seeker[val] + except IndexError: + self.log.warning("Event Index {} does not exist".format(val)) + + @property + def event_id(self): + return self._event_id + + @event_id.setter + def event_id(self, val): + try: + self.event = self.seeker[str(val)] + except IndexError: + self.log.warning("Event ID {} does not exist".format(val)) + + @property + def telid(self): + return self._telid + + @telid.setter + def telid(self, val): + n_chan = self.event.inst.num_channels[val] + if self.channel is None or self.channel >= n_chan: + self.channel = 0 + + tels = list(self.event.r0.tels_with_data) + if val not in tels: + val = tels[0] + self._telid = val + self.viewer.telid = val + self.update_telid_widget() + + @property + def channel(self): + return self._channel + + @channel.setter + def channel(self, val): + self._channel = val + self.viewer.channel = val + self.update_channel_widget() + + @property + def event(self): + return self._event + + @event.setter + def event(self, val): + + # Calibrate + self.r1.calibrate(val) + self.dl0.reduce(val) + self.dl1.calibrate(val) + + self._event = val + + self.viewer.event = val + + self._event_index = val.count + self._event_id = val.r0.event_id + self.update_event_index_widget() + self.update_event_id_widget() + + self._telid = self.viewer.telid + self.update_telid_widget() + + self._channel = self.viewer.channel + self.update_channel_widget() + + def update_dl1_calibrator(self, extractor=None, cleaner=None): + """ + Recreate the dl1 calibrator with the specified extractor and cleaner + + Parameters + ---------- + extractor : ctapipe.image.charge_extractors.ChargeExtractor + cleaner : ctapipe.image.waveform_cleaning.WaveformCleaner + """ + if extractor is None: + extractor = self.dl1.extractor + if cleaner is None: + cleaner = self.dl1.cleaner + + self.extractor = extractor + self.cleaner = cleaner + + kwargs = dict(config=self.config, tool=self) + self.dl1 = CameraDL1Calibrator( + extractor=self.extractor, + cleaner=self.cleaner, + **kwargs + ) + self.dl1.calibrate(self.event) + self.viewer.refresh() + + def create_next_event_widget(self): + self.w_next_event = Button(label=">", button_type="default", width=50) + self.w_next_event.on_click(self.on_next_event_widget_click) + + def on_next_event_widget_click(self): + self.event_index += 1 + + def create_previous_event_widget(self): + self.w_previous_event = Button( + label="<", + button_type="default", + width=50 + ) + self.w_previous_event.on_click(self.on_previous_event_widget_click) + + def on_previous_event_widget_click(self): + self.event_index -= 1 + + def create_event_index_widget(self): + self.w_event_index = TextInput(title="Event Index:", value='') + + def update_event_index_widget(self): + if self.w_event_index: + self.w_event_index.value = str(self.event_index) + + def create_event_id_widget(self): + self.w_event_id = TextInput(title="Event ID:", value='') + + def update_event_id_widget(self): + if self.w_event_id: + self.w_event_id.value = str(self.event_id) + + def create_goto_event_index_widget(self): + self.w_goto_event_index = Button( + label="GOTO Index", + button_type="default", + width=100 + ) + self.w_goto_event_index.on_click(self.on_goto_event_index_widget_click) + + def on_goto_event_index_widget_click(self): + self.event_index = int(self.w_event_index.value) + + def create_goto_event_id_widget(self): + self.w_goto_event_id = Button( + label="GOTO ID", + button_type="default", + width=70 + ) + self.w_goto_event_id.on_click(self.on_goto_event_id_widget_click) + + def on_goto_event_id_widget_click(self): + self.event_id = int(self.w_event_id.value) + + def create_telid_widget(self): + self.w_telid = Select(title="Telescope:", value="", options=[]) + self.w_telid.on_change('value', self.on_telid_widget_change) + + def update_telid_widget(self): + if self.w_telid: + tels = [str(t) for t in self.event.r0.tels_with_data] + self.w_telid.options = tels + self.w_telid.value = str(self.telid) + + def on_telid_widget_change(self, _, __, ___): + if self.telid != int(self.w_telid.value): + self.telid = int(self.w_telid.value) + + def create_channel_widget(self): + self.w_channel = Select(title="Channel:", value="", options=[]) + self.w_channel.on_change('value', self.on_channel_widget_change) + + def update_channel_widget(self): + if self.channel: + n_chan = self.event.inst.num_channels[self.telid] + channels = [str(c) for c in range(n_chan)] + self.w_channel.options = channels + self.w_channel.value = str(self.channel) + + def on_channel_widget_change(self, _, __, ___): + if self.channel != int(self.w_channel.value): + self.channel = int(self.w_channel.value) + + def create_dl1_widgets(self): + self.w_dl1_dict = dict( + cleaner=Select(title="Cleaner:", value='', width=5, + options=WaveformCleanerFactory.subclass_names), + extractor=Select(title="Extractor:", value='', width=5, + options=ChargeExtractorFactory.subclass_names), + extractor_t0=TextInput(title="T0:", value=''), + extractor_window_width=TextInput(title="Window Width:", value=''), + extractor_window_shift=TextInput(title="Window Shift:", value=''), + extractor_sig_amp_cut_HG=TextInput(title="Significant Amplitude " + "Cut (HG):", value=''), + extractor_sig_amp_cut_LG=TextInput(title="Significant Amplitude " + "Cut (LG):", value=''), + extractor_lwt=TextInput(title="Local Pixel Weight:", value='')) + + for key, val in self.w_dl1_dict.items(): + val.on_change('value', self.on_dl1_widget_change) + + self.wb_extractor = widgetbox( + PreText(text="Charge Extractor Configuration"), + self.w_dl1_dict['cleaner'], + self.w_dl1_dict['extractor'], + self.w_dl1_dict['extractor_t0'], + self.w_dl1_dict['extractor_window_width'], + self.w_dl1_dict['extractor_window_shift'], + self.w_dl1_dict['extractor_sig_amp_cut_HG'], + self.w_dl1_dict['extractor_sig_amp_cut_LG'], + self.w_dl1_dict['extractor_lwt']) + + def update_dl1_widget_values(self): + if self.w_dl1_dict: + for key, val in self.w_dl1_dict.items(): + if 'extractor' in key: + if key == 'extractor': + val.value = self.extractor.__class__.__name__ + else: + key = key.replace("extractor_", "") + try: + val.value = str(getattr(self.extractor, key)) + except AttributeError: + val.value = '' + elif 'cleaner' in key: + if key == 'cleaner': + val.value = self.cleaner.__class__.__name__ + else: + key = key.replace("cleaner_", "") + try: + val.value = str(getattr(self.cleaner, key)) + except AttributeError: + val.value = '' + + def on_dl1_widget_change(self, _, __, ___): + if self.event: + if not self._updating_dl1: + self._updating_dl1 = True + cmdline = [] + for key, val in self.w_dl1_dict.items(): + if val.value: + cmdline.append('--{}'.format(key)) + cmdline.append(val.value) + self.parse_command_line(cmdline) + kwargs = dict(config=self.config, tool=self) + extractor = ChargeExtractorFactory.produce(**kwargs) + cleaner = WaveformCleanerFactory.produce(**kwargs) + self.update_dl1_calibrator(extractor, cleaner) + self.update_dl1_widget_values() + self._updating_dl1 = False + + +exe = BokehFileViewer() +exe.run() diff --git a/ctapipe/tools/bokeh/file_viewer/templates/index.html b/ctapipe/tools/bokeh/file_viewer/templates/index.html new file mode 100644 index 00000000000..8f495f50043 --- /dev/null +++ b/ctapipe/tools/bokeh/file_viewer/templates/index.html @@ -0,0 +1,24 @@ + + + + + + Bokeh Crossfilter Example + {{ bokeh_css }} + {{ bokeh_js }} + + + {{ plot_div|indent(8) }} + {{ plot_script|indent(8) }} + + + diff --git a/ctapipe/tools/bokeh/file_viewer/theme.yaml b/ctapipe/tools/bokeh/file_viewer/theme.yaml new file mode 100644 index 00000000000..3540cca0ead --- /dev/null +++ b/ctapipe/tools/bokeh/file_viewer/theme.yaml @@ -0,0 +1,17 @@ +attrs: + Figure: + background_fill_color: '#2F2F2F' + border_fill_color: '#2F2F2F' + outline_line_color: '#444444' + Axis: + axis_line_color: "white" + axis_label_text_color: "white" + major_label_text_color: "white" + major_tick_line_color: "white" + minor_tick_line_color: "white" + minor_tick_line_color: "white" + Grid: + grid_line_dash: [6, 4] + grid_line_alpha: .3 + Title: + text_color: "white" diff --git a/ctapipe/tools/tests/test_tools.py b/ctapipe/tools/tests/test_tools.py index c481a9bc81f..7ab619d9083 100644 --- a/ctapipe/tools/tests/test_tools.py +++ b/ctapipe/tools/tests/test_tools.py @@ -2,6 +2,7 @@ from ctapipe.tools.dump_triggers import DumpTriggersTool from ctapipe.tools.dump_instrument import DumpInstrumentTool from ctapipe.tools.info import info +from ctapipe.tools.bokeh.file_viewer.main import BokehFileViewer from ctapipe.utils import get_dataset_path @@ -41,3 +42,10 @@ def test_camdemo(): tool.cleanframes = 2 tool.display = False tool.run(argv=[]) + + +def test_bokeh_file_viewer(): + tool = BokehFileViewer() + tool.run() + + assert tool.reader.input_url == get_dataset_path("gamma_test.simtel.gz") diff --git a/ctapipe/utils/rgbtohex.py b/ctapipe/utils/rgbtohex.py new file mode 100644 index 00000000000..2c931f8a86f --- /dev/null +++ b/ctapipe/utils/rgbtohex.py @@ -0,0 +1,73 @@ +from matplotlib.cm import get_cmap +import numpy as np +import ctypes +from numpy.ctypeslib import ndpointer +import os + +viridis = get_cmap('viridis') +lib = np.ctypeslib.load_library("rgbtohex_c", os.path.dirname(__file__)) +rgbtohex = lib.rgbtohex +rgbtohex.restype = None +rgbtohex.argtypes = [ndpointer(ctypes.c_uint8, flags="C_CONTIGUOUS"), + ctypes.c_size_t, + ndpointer(ctypes.c_char, flags="C_CONTIGUOUS")] + + +def intensity_to_rgb(array, minval=None, maxval=None): + """ + Converts the values of an array to rgb representing a color for a z axis + + Parameters + ---------- + array : ndarray + 1D numpy array containing intensity values for a z axis + minval: int + minimum value of the image + maxval: int + maximum value of the image + + Returns + ------- + rgb : ndarray + rgb tuple representing the intensity as a color + + """ + if minval is None: + minval = array.min() + if maxval is None: + maxval = array.max() + if maxval == minval: + minval -= 1 + maxval += 1 + scaled = (array - minval) / (maxval - minval) + + rgb = (255 * viridis(scaled)).astype(np.uint8) + return rgb + + +def intensity_to_hex(array, minval=None, maxval=None): + """ + Converts the values of an array to hex representing a color for a z axis + + This is needed to efficiently change the values displayed by a + `ctapipe.visualization.bokeh.CameraDisplay`. + + Parameters + ---------- + array : ndarray + 1D numpy array containing intensity values for a z axis + minval: int + minimum value of the image + maxval: int + maximum value of the image + + Returns + ------- + hex_ : ndarray + hex strings representing the intensity as a color + + """ + array_size = array.size + hex_ = np.empty((array_size, 8), dtype='S1') + rgbtohex(intensity_to_rgb(array, minval, maxval), array_size, hex_) + return hex_.view('S8').astype('U8')[:, 0] diff --git a/ctapipe/utils/rgbtohex_c.cc b/ctapipe/utils/rgbtohex_c.cc new file mode 100644 index 00000000000..04ed9c18dee --- /dev/null +++ b/ctapipe/utils/rgbtohex_c.cc @@ -0,0 +1,18 @@ +/* +C extension to convert and rgb array into a array of chars containing the +hexidecimal string equivalent. +*/ + +#include +#include +#include + +extern "C" void rgbtohex(const uint8_t* rgb, size_t n_pix, char* hex) +{ + for (size_t i = 0; i < n_pix; ++i) { + uint8_t red = rgb[i*4 + 0]; + uint8_t green = rgb[i*4 + 1]; + uint8_t blue = rgb[i*4 + 2]; + snprintf(hex+i*8, 8, "#%02x%02x%02x", red, green, blue); + } +} \ No newline at end of file diff --git a/ctapipe/utils/tests/test_rgbtohex.py b/ctapipe/utils/tests/test_rgbtohex.py new file mode 100644 index 00000000000..a7e7e22cb8f --- /dev/null +++ b/ctapipe/utils/tests/test_rgbtohex.py @@ -0,0 +1,20 @@ +from ctapipe.utils.rgbtohex import intensity_to_rgb, intensity_to_hex +import numpy as np + + +def test_rgb(): + input_ = np.array([4]) + min_ = 0 + max_ = 10 + output = intensity_to_rgb(input_, min_, max_) + + assert (output == np.array([ 41, 120, 142, 255])).all() + + +def test_hex(): + input_ = np.array([4]) + min_ = 0 + max_ = 10 + output = intensity_to_hex(input_, min_, max_) + + assert (output == np.array(["#29788e"])).all() diff --git a/ctapipe/visualization/bokeh.py b/ctapipe/visualization/bokeh.py new file mode 100644 index 00000000000..b56a9763551 --- /dev/null +++ b/ctapipe/visualization/bokeh.py @@ -0,0 +1,380 @@ +import warnings +import numpy as np +from bokeh.plotting import figure +from bokeh.events import Tap +from bokeh.models import (ColumnDataSource, TapTool, palettes, Span, ColorBar, + LinearColorMapper) +from ctapipe.utils.rgbtohex import intensity_to_hex + +PLOTARGS = dict(tools="", toolbar_location=None, outline_line_color='#595959') + + +class CameraDisplay: + def __init__(self, geometry=None, image=None, fig=None): + """ + Camera display that utilises the bokeh visualisation library + + Parameters + ---------- + geometry : `~ctapipe.instrument.CameraGeometry` + Definition of the Camera/Image + image : ndarray + 1D array containing the image values for each pixel + fig : bokeh.plotting.figure + Figure to store the bokeh plot onto (optional) + """ + self._geom = None + self._image = None + self._colors = None + self._image_min = None + self._image_max = None + self._fig = None + + self._n_pixels = None + self._pix_sizes = np.ones(1) + self._pix_areas = np.ones(1) + self._pix_x = np.zeros(1) + self._pix_y = np.zeros(1) + + self.glyphs = None + self.cm = None + self.cb = None + + cdsource_d = dict(image=[], + x=[], y=[], + width=[], height=[], + outline_color=[], outline_alpha=[]) + self.cdsource = ColumnDataSource(data=cdsource_d) + + self._active_pixels = [] + self.active_index = 0 + self.active_colors = [] + self.automatic_index_increment = False + + self.geom = geometry + self.image = image + self.fig = fig + + self.layout = self.fig + + @property + def fig(self): + return self._fig + + @fig.setter + def fig(self, val): + if val is None: + val = figure(plot_width=550, plot_height=500, **PLOTARGS) + val.axis.visible = False + val.grid.grid_line_color = None + self._fig = val + + self._draw_camera() + + @property + def geom(self): + return self._geom + + @geom.setter + def geom(self, val): + self._geom = val + + if val is not None: + self._pix_areas = val.pix_area.value + self._pix_sizes = np.sqrt(self._pix_areas) + self._pix_x = val.pix_x.value + self._pix_y = val.pix_y.value + + self._n_pixels = self._pix_x.size + if self._n_pixels == len(self.cdsource.data['x']): + self.cdsource.data['x'] = self._pix_x + self.cdsource.data['y'] = self._pix_y + self.cdsource.data['width'] = self._pix_sizes + self.cdsource.data['height'] = self._pix_sizes + else: + image = np.empty(self._pix_x.shape) + alpha = [0] * self._n_pixels + color = ['black'] * self._n_pixels + cdsource_d = dict(image=image, + x=self._pix_x, y=self._pix_y, + width=self._pix_sizes, height=self._pix_sizes, + outline_color=color, outline_alpha=alpha + ) + self.cdsource.data = cdsource_d + + self.active_pixels = [0] * len(self.active_pixels) + + @property + def image(self): + return self._image + + @image.setter + def image(self, val): + if val is None: + val = np.zeros(self._n_pixels) + + image_min = val.min() + image_max = val.max() + if image_max == image_min: + image_min -= 1 + image_max += 1 + colors = intensity_to_hex(val, image_min, image_max) + + self._image = val + self._colors = colors + self.image_min = image_min + self.image_max = image_max + + if len(colors) == self._n_pixels: + with warnings.catch_warnings(): + warnings.simplefilter(action='ignore', category=FutureWarning) + self.cdsource.data['image'] = colors + else: + raise ValueError("Image has a different size {} than the current " + "CameraGeometry n_pixels {}" + .format(colors.size, self._n_pixels)) + + @property + def image_min(self): + return self._image_min + + @image_min.setter + def image_min(self, val): + self._image_min = val + if self.cb: + self.cm.low = np.asscalar(val) + + @property + def image_max(self): + return self._image_max + + @image_max.setter + def image_max(self, val): + self._image_max = val + if self.cb: + self.cm.high = np.asscalar(val) + + @property + def active_pixels(self): + return self._active_pixels + + @active_pixels.setter + def active_pixels(self, listval): + self._active_pixels = listval + + palette = palettes.Set1[9] + palette = [palette[0]] + palette[3:] + self.active_colors = [palette[i % (len(palette))] + for i in range(len(listval))] + self.highlight_pixels() + + def reset_pixels(self): + self.active_pixels = [0] * len(self.active_pixels) + + def _draw_camera(self): + # TODO: Support other pixel shapes OR switch to ellipse + # after https://github.com/bokeh/bokeh/issues/6985 + self.glyphs = self.fig.rect( + 'x', 'y', color='image', width='width', height='height', + line_color='outline_color', + line_alpha='outline_alpha', + line_width=2, + nonselection_fill_color='image', + nonselection_fill_alpha=1, + nonselection_line_color='outline_color', + nonselection_line_alpha='outline_alpha', + source=self.cdsource + ) + + def enable_pixel_picker(self, n_active): + """ + Enables the selection of a pixel by clicking on it + + Parameters + ---------- + n_active : int + Number of active pixels to keep record of + """ + self.active_pixels = [0] * n_active + self.fig.add_tools(TapTool()) + + def source_change_response(_, __, ___): + val = self.cdsource.selected['1d']['indices'] + if val: + pix = val[0] + ai = self.active_index + self.active_pixels[ai] = pix + + self.highlight_pixels() + self._on_pixel_click(pix) + + if self.automatic_index_increment: + self.active_index = (ai + 1) % len(self.active_pixels) + + self.cdsource.on_change('selected', source_change_response) + + def _on_pixel_click(self, pix_id): + print("Clicked pixel_id: {}".format(pix_id)) + print("Active Pixels: {}".format(self.active_pixels)) + + def highlight_pixels(self): + alpha = [0] * self._n_pixels + color = ['black'] * self._n_pixels + for i, pix in enumerate(self.active_pixels): + alpha[pix] = 1 + color[pix] = self.active_colors[i] + self.cdsource.data['outline_alpha'] = alpha + self.cdsource.data['outline_color'] = color + + def add_colorbar(self): + self.cm = LinearColorMapper(palette="Viridis256", low=0, high=100, + low_color='white', high_color='red') + self.cb = ColorBar(color_mapper=self.cm, + border_line_color=None, + background_fill_alpha=0, + major_label_text_color='green', + location=(0, 0)) + self.fig.add_layout(self.cb, 'right') + self.cm.low = np.asscalar(self.image_min) + self.cm.high = np.asscalar(self.image_max) + + +class FastCameraDisplay: + def __init__(self, x_pix, y_pix, pix_size): + """ + A fast and simple version of the bokeh camera plotter that does not + allow for geometry changes + + Parameters + ---------- + x_pix : ndarray + Pixel x positions + y_pix : ndarray + Pixel y positions + pix_size : ndarray + Pixel sizes + """ + self._image = None + n_pix = x_pix.size + + cdsource_d = dict(image=np.empty(n_pix, dtype=' max_t: + val = max_t + self.span.location = val + self._active_time = val + + def _draw_waveform(self): + self.fig.line(x="t", y="samples", source=self.cdsource, name='line') + + def enable_time_picker(self): + """ + Enables the selection of a time by clicking on the waveform + """ + self.span = Span(location=0, dimension='height', + line_color='red', line_dash='dashed') + self.fig.add_layout(self.span) + + taptool = TapTool() + self.fig.add_tools(taptool) + + def wf_tap_response(event): + time = event.x + if time is not None: + self.active_time = time + self._on_waveform_click(time) + + self.fig.on_event(Tap, wf_tap_response) + + def _on_waveform_click(self, time): + print("Clicked time: {}".format(time)) + print("Active time: {}".format(self.active_time)) diff --git a/ctapipe/visualization/tests/test_bokeh.py b/ctapipe/visualization/tests/test_bokeh.py new file mode 100644 index 00000000000..9661a87887b --- /dev/null +++ b/ctapipe/visualization/tests/test_bokeh.py @@ -0,0 +1,124 @@ +import numpy as np +import pytest +from ctapipe.visualization.bokeh import CameraDisplay, WaveformDisplay, \ + FastCameraDisplay, intensity_to_hex + + +def test_camera_display_create(): + c_display = CameraDisplay() + + +def test_camera_geom(test_event): + t = list(test_event.r0.tels_with_data)[0] + geom = test_event.inst.subarray.tel[t].camera + c_display = CameraDisplay(geom) + + assert (c_display.cdsource.data['x'] == geom.pix_x.value).all() + assert (c_display.cdsource.data['y'] == geom.pix_y.value).all() + + t = list(test_event.r0.tels_with_data)[1] + geom = test_event.inst.subarray.tel[t].camera + c_display.geom = geom + assert (c_display.cdsource.data['x'] == geom.pix_x.value).all() + assert (c_display.cdsource.data['y'] == geom.pix_y.value).all() + + +def test_camera_image(test_event): + t = list(test_event.r0.tels_with_data)[0] + geom = test_event.inst.subarray.tel[t].camera + n_pixels = geom.pix_x.value.size + image = np.ones(n_pixels) + colors = intensity_to_hex(image) + + with pytest.raises(ValueError): + c_display = CameraDisplay(None, image) + + c_display = CameraDisplay(geom, image) + assert (c_display.cdsource.data['image'] == colors).all() + assert c_display.image_min == 0 + assert c_display.image_max == 2 + + image[5] = 5 + colors = intensity_to_hex(image) + c_display.image = image + assert (c_display.cdsource.data['image'] == colors).all() + assert c_display.image_min == image.min() + assert c_display.image_max == image.max() + + +def test_camera_enable_pixel_picker(test_event): + t = list(test_event.r0.tels_with_data)[0] + geom = test_event.inst.subarray.tel[t].camera + n_pixels = geom.pix_x.value.size + image = np.ones(n_pixels) + c_display = CameraDisplay(geom, image) + + c_display.enable_pixel_picker(2) + assert len(c_display.active_pixels) == 2 + + c_display.enable_pixel_picker(3) + assert len(c_display.active_pixels) == 3 + + +def test_fast_camera_display_create(test_event): + t = list(test_event.r0.tels_with_data)[0] + geom = test_event.inst.subarray.tel[t].camera + + x = geom.pix_x.value + y = geom.pix_y.value + area = geom.pix_area.value + size = np.sqrt(area) + + c_display = FastCameraDisplay(x, y, size) + + +def test_fast_camera_image(test_event): + t = list(test_event.r0.tels_with_data)[0] + geom = test_event.inst.subarray.tel[t].camera + + x = geom.pix_x.value + y = geom.pix_y.value + area = geom.pix_area.value + size = np.sqrt(area) + + c_display = FastCameraDisplay(x, y, size) + + image = np.ones(x.size) + colors = intensity_to_hex(image) + c_display.image = colors + + assert (c_display.cdsource.data['image'] == colors).all() + + +def test_waveform_display_create(): + w_display = WaveformDisplay() + + +def test_waveform_values(): + wf = np.ones(30) + w_display = WaveformDisplay(wf) + + assert (w_display.cdsource.data['samples'] == wf).all() + assert (w_display.cdsource.data['t'] == np.arange(wf.size)).all() + + wf[5] = 5 + w_display.waveform=wf + + assert (w_display.cdsource.data['samples'] == wf).all() + assert (w_display.cdsource.data['t'] == np.arange(wf.size)).all() + + +def test_span(): + wf = np.ones(30) + w_display = WaveformDisplay(wf) + w_display.enable_time_picker() + w_display.active_time = 4 + assert w_display.span.location == 4 + + w_display.active_time = -3 + assert w_display.active_time == 0 + assert w_display.span.location == 0 + + w_display.active_time = wf.size + 10 + assert w_display.active_time == wf.size - 1 + assert w_display.span.location == wf.size -1 diff --git a/setup.py b/setup.py index e5afcb918fd..1af8cc22d42 100755 --- a/setup.py +++ b/setup.py @@ -44,6 +44,8 @@ # C Extensions neighboursum_module = Extension('ctapipe.utils.neighbour_sum_c', sources=['ctapipe/utils/neighbour_sum_c.cc']) +rgbtohex_module = Extension('ctapipe.utils.rgbtohex_c', + sources=['ctapipe/utils/rgbtohex_c.cc']) setup(name=PACKAGENAME, packages=find_packages(), @@ -86,5 +88,5 @@ zip_safe=False, use_2to3=False, entry_points=entry_points, - ext_modules=[neighboursum_module] + ext_modules=[neighboursum_module, rgbtohex_module] ) From 3a94748eb8396af4361f1013be42a19b5db413d5 Mon Sep 17 00:00:00 2001 From: Jason Watson Date: Fri, 20 Apr 2018 18:02:10 +0200 Subject: [PATCH 02/13] Fixed formatting --- ctapipe/plotting/bokeh_event_viewer.py | 2 +- ctapipe/tools/bokeh/file_viewer/main.py | 2 +- ctapipe/utils/tests/test_rgbtohex.py | 2 +- ctapipe/visualization/tests/test_bokeh.py | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ctapipe/plotting/bokeh_event_viewer.py b/ctapipe/plotting/bokeh_event_viewer.py index 23c3a4d8b05..555de162661 100644 --- a/ctapipe/plotting/bokeh_event_viewer.py +++ b/ctapipe/plotting/bokeh_event_viewer.py @@ -406,7 +406,7 @@ def __init__(self, config, tool, num_cameras=1, num_waveforms=2, **kwargs): self.layout = None def create(self): - for icam in range(self.num_cameras): + for _ in range(self.num_cameras): cam = BokehEventViewerCamera(self) cam.enable_pixel_picker(self.num_waveforms) cam.create_view_widget() diff --git a/ctapipe/tools/bokeh/file_viewer/main.py b/ctapipe/tools/bokeh/file_viewer/main.py index c57388c8f32..57c836f2275 100644 --- a/ctapipe/tools/bokeh/file_viewer/main.py +++ b/ctapipe/tools/bokeh/file_viewer/main.py @@ -338,7 +338,7 @@ def create_dl1_widgets(self): "Cut (LG):", value=''), extractor_lwt=TextInput(title="Local Pixel Weight:", value='')) - for key, val in self.w_dl1_dict.items(): + for val in self.w_dl1_dict.values(): val.on_change('value', self.on_dl1_widget_change) self.wb_extractor = widgetbox( diff --git a/ctapipe/utils/tests/test_rgbtohex.py b/ctapipe/utils/tests/test_rgbtohex.py index a7e7e22cb8f..626fc22c3bd 100644 --- a/ctapipe/utils/tests/test_rgbtohex.py +++ b/ctapipe/utils/tests/test_rgbtohex.py @@ -8,7 +8,7 @@ def test_rgb(): max_ = 10 output = intensity_to_rgb(input_, min_, max_) - assert (output == np.array([ 41, 120, 142, 255])).all() + assert (output == np.array([41, 120, 142, 255])).all() def test_hex(): diff --git a/ctapipe/visualization/tests/test_bokeh.py b/ctapipe/visualization/tests/test_bokeh.py index 9661a87887b..cb27b3fbe38 100644 --- a/ctapipe/visualization/tests/test_bokeh.py +++ b/ctapipe/visualization/tests/test_bokeh.py @@ -5,7 +5,7 @@ def test_camera_display_create(): - c_display = CameraDisplay() + CameraDisplay() def test_camera_geom(test_event): @@ -31,7 +31,7 @@ def test_camera_image(test_event): colors = intensity_to_hex(image) with pytest.raises(ValueError): - c_display = CameraDisplay(None, image) + CameraDisplay(None, image) c_display = CameraDisplay(geom, image) assert (c_display.cdsource.data['image'] == colors).all() @@ -69,7 +69,7 @@ def test_fast_camera_display_create(test_event): area = geom.pix_area.value size = np.sqrt(area) - c_display = FastCameraDisplay(x, y, size) + FastCameraDisplay(x, y, size) def test_fast_camera_image(test_event): @@ -91,7 +91,7 @@ def test_fast_camera_image(test_event): def test_waveform_display_create(): - w_display = WaveformDisplay() + WaveformDisplay() def test_waveform_values(): @@ -102,7 +102,7 @@ def test_waveform_values(): assert (w_display.cdsource.data['t'] == np.arange(wf.size)).all() wf[5] = 5 - w_display.waveform=wf + w_display.waveform = wf assert (w_display.cdsource.data['samples'] == wf).all() assert (w_display.cdsource.data['t'] == np.arange(wf.size)).all() @@ -121,4 +121,4 @@ def test_span(): w_display.active_time = wf.size + 10 assert w_display.active_time == wf.size - 1 - assert w_display.span.location == wf.size -1 + assert w_display.span.location == wf.size - 1 From d6f08e8f8e77af2b0dd24525772e91e154e9574f Mon Sep 17 00:00:00 2001 From: Jason Watson Date: Sat, 21 Apr 2018 00:11:55 +0100 Subject: [PATCH 03/13] Moved run to inside function (fixes tests) --- ctapipe/tools/bokeh/file_viewer/main.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ctapipe/tools/bokeh/file_viewer/main.py b/ctapipe/tools/bokeh/file_viewer/main.py index 57c836f2275..cee6572e767 100644 --- a/ctapipe/tools/bokeh/file_viewer/main.py +++ b/ctapipe/tools/bokeh/file_viewer/main.py @@ -392,5 +392,13 @@ def on_dl1_widget_change(self, _, __, ___): self._updating_dl1 = False -exe = BokehFileViewer() -exe.run() +def main(): + exe = BokehFileViewer() + exe.run() + + +if 'bk_script' in __name__: + main() + +if __name__ == '__main__': + main() From b3c673769bed3deac125a0931e6c28099037b2cd Mon Sep 17 00:00:00 2001 From: Jason Watson Date: Mon, 23 Apr 2018 09:53:57 +0100 Subject: [PATCH 04/13] Replaced the rgbtohex C extension with @dneise's Python solution --- ctapipe/utils/rgbtohex.py | 23 ++++++++++------------- ctapipe/utils/rgbtohex_c.cc | 18 ------------------ setup.py | 4 +--- 3 files changed, 11 insertions(+), 34 deletions(-) delete mode 100644 ctapipe/utils/rgbtohex_c.cc diff --git a/ctapipe/utils/rgbtohex.py b/ctapipe/utils/rgbtohex.py index 2c931f8a86f..fdbbceb87fa 100644 --- a/ctapipe/utils/rgbtohex.py +++ b/ctapipe/utils/rgbtohex.py @@ -1,16 +1,7 @@ from matplotlib.cm import get_cmap import numpy as np -import ctypes -from numpy.ctypeslib import ndpointer -import os - +import codecs viridis = get_cmap('viridis') -lib = np.ctypeslib.load_library("rgbtohex_c", os.path.dirname(__file__)) -rgbtohex = lib.rgbtohex -rgbtohex.restype = None -rgbtohex.argtypes = [ndpointer(ctypes.c_uint8, flags="C_CONTIGUOUS"), - ctypes.c_size_t, - ndpointer(ctypes.c_char, flags="C_CONTIGUOUS")] def intensity_to_rgb(array, minval=None, maxval=None): @@ -67,7 +58,13 @@ def intensity_to_hex(array, minval=None, maxval=None): hex strings representing the intensity as a color """ - array_size = array.size - hex_ = np.empty((array_size, 8), dtype='S1') - rgbtohex(intensity_to_rgb(array, minval, maxval), array_size, hex_) + hex_ = np.zeros((array.size, 8), dtype='B') + rgb = intensity_to_rgb(array, minval, maxval) + + hex_encoded = codecs.encode(rgb, 'hex') + bytes_ = np.frombuffer(hex_encoded, 'B') + bytes_2d = bytes_.reshape(-1, 8) + hex_[:, 0] = ord('#') + hex_[:, 1:7] = bytes_2d[:, 0:6] + return hex_.view('S8').astype('U8')[:, 0] diff --git a/ctapipe/utils/rgbtohex_c.cc b/ctapipe/utils/rgbtohex_c.cc deleted file mode 100644 index 04ed9c18dee..00000000000 --- a/ctapipe/utils/rgbtohex_c.cc +++ /dev/null @@ -1,18 +0,0 @@ -/* -C extension to convert and rgb array into a array of chars containing the -hexidecimal string equivalent. -*/ - -#include -#include -#include - -extern "C" void rgbtohex(const uint8_t* rgb, size_t n_pix, char* hex) -{ - for (size_t i = 0; i < n_pix; ++i) { - uint8_t red = rgb[i*4 + 0]; - uint8_t green = rgb[i*4 + 1]; - uint8_t blue = rgb[i*4 + 2]; - snprintf(hex+i*8, 8, "#%02x%02x%02x", red, green, blue); - } -} \ No newline at end of file diff --git a/setup.py b/setup.py index 1af8cc22d42..e5afcb918fd 100755 --- a/setup.py +++ b/setup.py @@ -44,8 +44,6 @@ # C Extensions neighboursum_module = Extension('ctapipe.utils.neighbour_sum_c', sources=['ctapipe/utils/neighbour_sum_c.cc']) -rgbtohex_module = Extension('ctapipe.utils.rgbtohex_c', - sources=['ctapipe/utils/rgbtohex_c.cc']) setup(name=PACKAGENAME, packages=find_packages(), @@ -88,5 +86,5 @@ zip_safe=False, use_2to3=False, entry_points=entry_points, - ext_modules=[neighboursum_module, rgbtohex_module] + ext_modules=[neighboursum_module] ) From 9709fdf3ee214df37cde593e77305db6911ffd07 Mon Sep 17 00:00:00 2001 From: Jason Watson Date: Mon, 23 Apr 2018 13:20:55 +0100 Subject: [PATCH 05/13] Corrected references to the old num_samples Field in inst --- ctapipe/calib/camera/dl1.py | 2 +- ctapipe/tools/bokeh/file_viewer/main.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ctapipe/calib/camera/dl1.py b/ctapipe/calib/camera/dl1.py index 4fc5e62213e..a083b33a648 100644 --- a/ctapipe/calib/camera/dl1.py +++ b/ctapipe/calib/camera/dl1.py @@ -179,7 +179,7 @@ def get_correction(self, event, telid): # Don't apply correction when window_shift or window_width # does not exist in extractor, or when container does not have # a reference pulse shape - return np.ones(event.inst.num_channels[telid]) + return np.ones(event.dl0.tel[telid].waveform.shape[0]) def calibrate(self, event): """ diff --git a/ctapipe/tools/bokeh/file_viewer/main.py b/ctapipe/tools/bokeh/file_viewer/main.py index cee6572e767..d4bc2c6ad85 100644 --- a/ctapipe/tools/bokeh/file_viewer/main.py +++ b/ctapipe/tools/bokeh/file_viewer/main.py @@ -166,7 +166,7 @@ def telid(self): @telid.setter def telid(self, val): - n_chan = self.event.inst.num_channels[val] + n_chan = self.event.r0.tel[self.telid].waveform.shape[0] if self.channel is None or self.channel >= n_chan: self.channel = 0 @@ -313,8 +313,8 @@ def create_channel_widget(self): self.w_channel.on_change('value', self.on_channel_widget_change) def update_channel_widget(self): - if self.channel: - n_chan = self.event.inst.num_channels[self.telid] + if self.w_channel: + n_chan = self.event.r0.tel[self.telid].waveform.shape[0] channels = [str(c) for c in range(n_chan)] self.w_channel.options = channels self.w_channel.value = str(self.channel) From dc2ee09a58e5ba1267848b0b1fc9df3021bfe313 Mon Sep 17 00:00:00 2001 From: Jason Watson Date: Mon, 23 Apr 2018 13:48:40 +0100 Subject: [PATCH 06/13] Further corrections to n_chan --- ctapipe/calib/camera/dl1.py | 7 ++++++- ctapipe/tools/bokeh/file_viewer/main.py | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/ctapipe/calib/camera/dl1.py b/ctapipe/calib/camera/dl1.py index a083b33a648..de7d53c5000 100644 --- a/ctapipe/calib/camera/dl1.py +++ b/ctapipe/calib/camera/dl1.py @@ -179,7 +179,12 @@ def get_correction(self, event, telid): # Don't apply correction when window_shift or window_width # does not exist in extractor, or when container does not have # a reference pulse shape - return np.ones(event.dl0.tel[telid].waveform.shape[0]) + shape = event.dl0.tel[telid].waveform.shape + if shape.size != 3: + raise KeyError("this function should return 1 when " + "channel is removed from dl0") + # TODO + return np.ones(shape[0]) def calibrate(self, event): """ diff --git a/ctapipe/tools/bokeh/file_viewer/main.py b/ctapipe/tools/bokeh/file_viewer/main.py index d4bc2c6ad85..c68b218345c 100644 --- a/ctapipe/tools/bokeh/file_viewer/main.py +++ b/ctapipe/tools/bokeh/file_viewer/main.py @@ -166,10 +166,7 @@ def telid(self): @telid.setter def telid(self, val): - n_chan = self.event.r0.tel[self.telid].waveform.shape[0] - if self.channel is None or self.channel >= n_chan: - self.channel = 0 - + self.channel = 0 tels = list(self.event.r0.tels_with_data) if val not in tels: val = tels[0] @@ -314,7 +311,10 @@ def create_channel_widget(self): def update_channel_widget(self): if self.w_channel: - n_chan = self.event.r0.tel[self.telid].waveform.shape[0] + try: + n_chan = self.event.r0.tel[self.telid].waveform.shape[0] + except AttributeError: + n_chan = 1 channels = [str(c) for c in range(n_chan)] self.w_channel.options = channels self.w_channel.value = str(self.channel) From ff6b0b16cf629d72da9d72f0d866831eb7cc78f7 Mon Sep 17 00:00:00 2001 From: Jason Watson Date: Mon, 23 Apr 2018 13:49:32 +0100 Subject: [PATCH 07/13] Improvements to setting view as suggested by @dneise --- ctapipe/plotting/bokeh_event_viewer.py | 140 +++++++++---------------- ctapipe/visualization/bokeh.py | 4 +- 2 files changed, 50 insertions(+), 94 deletions(-) diff --git a/ctapipe/plotting/bokeh_event_viewer.py b/ctapipe/plotting/bokeh_event_viewer.py index 555de162661..2832eab6c7d 100644 --- a/ctapipe/plotting/bokeh_event_viewer.py +++ b/ctapipe/plotting/bokeh_event_viewer.py @@ -25,7 +25,15 @@ def __init__(self, event_viewer, fig=None): self._time = 0 super().__init__(fig=fig) - self._view_options = ['r0', 'r1', 'dl0', 'dl1', 'peakpos', 'cleaned'] + self._view_options = { + 'r0': lambda e, t, c, time: e.r0.tel[t].waveform[c, :, time], + 'r1': lambda e, t, c, time: e.r1.tel[t].waveform[c, :, time], + 'dl0': lambda e, t, c, time: e.dl0.tel[t].waveform[c, :, time], + 'dl1': lambda e, t, c, time: e.dl1.tel[t].image[c, :], + 'peakpos': lambda e, t, c, time: e.dl1.tel[t].peakpos[c, :], + 'cleaned': lambda e, t, c, time: e.dl1.tel[t].cleaned[c, :, time], + } + self.w_view = None self._geom_tel = None @@ -41,55 +49,21 @@ def _set_image(self): t = self.telid c = self.channel time = self.time - if e: - tels = list(e.r0.tels_with_data) - if t is None: - t = tels[0] - if t not in tels: - raise KeyError("Telescope {} has no data".format(t)) - if v == 'r0': - samples = e.r0.tel[t].waveform - if samples is None: - self.image = None - else: - self.image = samples[c, :, time] - self.fig.title.text = 'R0 Slice (T = {})'.format(time) - elif v == 'r1': - samples = e.r1.tel[t].waveform - if samples is None: - self.image = None - else: - self.image = samples[c, :, time] - self.fig.title.text = 'R1 Slice (T = {})'.format(time) - elif v == 'dl0': - samples = e.dl0.tel[t].waveform - if samples is None: - self.image = None - else: - self.image = samples[c, :, time] - self.fig.title.text = 'DL0 Slice (T = {})'.format(time) - elif v == 'dl1': - self.image = e.dl1.tel[t].image[c, :] - self.fig.title.text = 'DL1 Image' - elif v == 'peakpos': - peakpos = e.dl1.tel[t].peakpos - if peakpos is None: - self.image = np.zeros(self.image.shape) - else: - self.image = peakpos[c, :] - self.fig.title.text = 'DL1 Peakpos Image' - elif v == 'cleaned': - samples = e.dl1.tel[t].cleaned - if samples is None: - self.image = None - else: - self.image = samples[c, :, time] - self.fig.title.text = 'Cleaned Slice (T = {})'.format(time) - else: - raise ValueError("No view configuration set up " - "for: {}".format(v)) - else: + if not e: self.event_viewer.log.warning("No event has been provided") + return + + tels = list(e.r0.tels_with_data) + if t is None: + t = tels[0] + if t not in tels: + raise KeyError("Telescope {} has no data".format(t)) + + try: + self.image = self._view_options[v](e, t, c, time) + self.fig.title.text = '{0} (T = {1})'.format(v, time) + except TypeError: + self.image = None def _update_geometry(self): e = self.event @@ -127,7 +101,7 @@ def view(self): @view.setter def view(self, val): - if val not in self._view_options: + if val not in list(self._view_options.keys()): raise ValueError("View is not valid: {}".format(val)) self._view = val self._set_image() @@ -173,7 +147,7 @@ def create_view_widget(self): self.layout = column([self.w_view, self.layout]) def update_view_widget(self): - self.w_view.options = self._view_options + self.w_view.options = list(self._view_options.keys()) self.w_view.value = self.view def on_view_widget_change(self, _, __, ___): @@ -202,7 +176,13 @@ def __init__(self, event_viewer, fig=None): super().__init__(fig=fig) self._draw_integration_window() - self._view_options = ['r0', 'r1', 'dl0', 'cleaned'] + self._view_options = { + 'r0': lambda e, t, c, p: e.r0.tel[t].waveform[c, p], + 'r1': lambda e, t, c, p: e.r1.tel[t].waveform[c, p], + 'dl0': lambda e, t, c, p: e.dl0.tel[t].waveform[c, p], + 'cleaned': lambda e, t, c, p: e.dl1.tel[t].cleaned[c, p], + } + self.w_view = None self.event_viewer = event_viewer @@ -217,45 +197,21 @@ def _set_waveform(self): t = self.telid c = self.channel p = self.pixel - if e: - tels = list(e.r0.tels_with_data) - if t is None: - t = tels[0] - if t not in tels: - raise KeyError("Telescope {} has no data".format(t)) - if v == 'r0': - samples = e.r0.tel[t].waveform - if samples is None: - self.waveform = None - else: - self.waveform = samples[c, p] - self.fig.title.text = 'R0 Waveform (Pixel = {})'.format(p) - elif v == 'r1': - samples = e.r1.tel[t].waveform - if samples is None: - self.waveform = None - else: - self.waveform = samples[c, p] - self.fig.title.text = 'R1 Waveform (Pixel = {})'.format(p) - elif v == 'dl0': - samples = e.dl0.tel[t].waveform - if samples is None: - self.waveform = None - else: - self.waveform = samples[c, p] - self.fig.title.text = 'DL0 Waveform (Pixel = {})'.format(p) - elif v == 'cleaned': - samples = e.dl1.tel[t].cleaned - if samples is None: - self.waveform = None - else: - self.waveform = samples[c, p] - self.fig.title.text = 'Cleaned Waveform (Pixel = {})'.format(p) - else: - raise ValueError("No view configuration set up " - "for: {}".format(v)) - else: + if not e: self.event_viewer.log.warning("No event has been provided") + return + + tels = list(e.r0.tels_with_data) + if t is None: + t = tels[0] + if t not in tels: + raise KeyError("Telescope {} has no data".format(t)) + + try: + self.waveform = self._view_options[v](e, t, c, p) + self.fig.title.text = '{0} (Pixel = {1})'.format(v, p) + except TypeError: + self.waveform = None def _draw_integration_window(self): self.intwin1 = Span(location=0, dimension='height', @@ -308,7 +264,7 @@ def view(self): @view.setter def view(self, val): - if val not in self._view_options: + if val not in list(self._view_options.keys()): raise ValueError("View is not valid: {}".format(val)) self._view = val self._set_waveform() @@ -356,7 +312,7 @@ def create_view_widget(self): self.layout = column([self.w_view, self.layout]) def update_view_widget(self): - self.w_view.options = self._view_options + self.w_view.options = list(self._view_options.keys()) self.w_view.value = self.view def on_view_widget_change(self, _, __, ___): diff --git a/ctapipe/visualization/bokeh.py b/ctapipe/visualization/bokeh.py index b56a9763551..eab001c981c 100644 --- a/ctapipe/visualization/bokeh.py +++ b/ctapipe/visualization/bokeh.py @@ -92,10 +92,10 @@ def geom(self, val): self.cdsource.data['width'] = self._pix_sizes self.cdsource.data['height'] = self._pix_sizes else: - image = np.empty(self._pix_x.shape) + self._image = np.empty(self._pix_x.shape) alpha = [0] * self._n_pixels color = ['black'] * self._n_pixels - cdsource_d = dict(image=image, + cdsource_d = dict(image=self.image, x=self._pix_x, y=self._pix_y, width=self._pix_sizes, height=self._pix_sizes, outline_color=color, outline_alpha=alpha From 31c39a225b1d6daed9d0aa07142d900aa2fabda6 Mon Sep 17 00:00:00 2001 From: Jason Watson Date: Mon, 23 Apr 2018 15:00:38 +0100 Subject: [PATCH 08/13] Improved method to execute file_viewer Used bokeh.server.server.Server to run file_viewer through `python` instead of `bokeh serve` Created entry_point ctapipe-event-viewer Rearranged directory structure of tools/bokeh --- ctapipe/tools/bokeh/__init__.py | 0 .../{file_viewer/main.py => file_viewer.py} | 48 ++++++++++++------- .../{file_viewer => }/templates/index.html | 0 .../tools/bokeh/{file_viewer => }/theme.yaml | 0 ctapipe/tools/tests/test_tools.py | 4 +- setup.py | 8 +++- 6 files changed, 39 insertions(+), 21 deletions(-) create mode 100644 ctapipe/tools/bokeh/__init__.py rename ctapipe/tools/bokeh/{file_viewer/main.py => file_viewer.py} (90%) rename ctapipe/tools/bokeh/{file_viewer => }/templates/index.html (100%) rename ctapipe/tools/bokeh/{file_viewer => }/theme.yaml (100%) diff --git a/ctapipe/tools/bokeh/__init__.py b/ctapipe/tools/bokeh/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ctapipe/tools/bokeh/file_viewer/main.py b/ctapipe/tools/bokeh/file_viewer.py similarity index 90% rename from ctapipe/tools/bokeh/file_viewer/main.py rename to ctapipe/tools/bokeh/file_viewer.py index c68b218345c..fc45b387a6b 100644 --- a/ctapipe/tools/bokeh/file_viewer/main.py +++ b/ctapipe/tools/bokeh/file_viewer.py @@ -1,7 +1,10 @@ -from bokeh.io import curdoc +import os from bokeh.layouts import widgetbox, layout from bokeh.models import Select, TextInput, PreText, Button -from traitlets import Dict, List +from bokeh.server.server import Server +from bokeh.document.document import jinja2 +from bokeh.themes import Theme +from traitlets import Dict, List, Int, Bool from ctapipe.calib.camera.dl0 import CameraDL0Reducer from ctapipe.calib.camera.dl1 import CameraDL1Calibrator from ctapipe.calib.camera.r1 import CameraR1CalibratorFactory @@ -19,7 +22,13 @@ class BokehFileViewer(Tool): description = ("Interactively explore an event file using the bokeh " "visualisation package") + port = Int(5006, help="Port to open bokeh server onto").tag(config=True) + disable_server = Bool(False, help="Do not start the bokeh server " + "(useful for testing)").tag(config=True) + aliases = Dict(dict( + port='BokehFileViewer.port', + disable_server='BokehFileViewer.disable_server', r='EventSourceFactory.product', f='EventSourceFactory.input_url', max_events='EventSourceFactory.max_events', @@ -27,16 +36,6 @@ class BokehFileViewer(Tool): tf='CameraR1CalibratorFactory.tf_path', pe='CameraR1CalibratorFactory.pe_path', ff='CameraR1CalibratorFactory.ff_path', - extractor='ChargeExtractorFactory.product', - extractor_t0='ChargeExtractorFactory.t0', - extractor_window_width='ChargeExtractorFactory.window_width', - extractor_window_shift='ChargeExtractorFactory.window_shift', - extractor_sig_amp_cut_HG='ChargeExtractorFactory.sig_amp_cut_HG', - extractor_sig_amp_cut_LG='ChargeExtractorFactory.sig_amp_cut_LG', - extractor_lwt='ChargeExtractorFactory.lwt', - clip_amplitude='CameraDL1Calibrator.clip_amplitude', - radius='CameraDL1Calibrator.radius', - cleaner='WaveformCleanerFactory.product', )) classes = List([ @@ -135,8 +134,26 @@ def start(self): self.event_index = 0 def finish(self): - curdoc().add_root(self.layout) - curdoc().title = "Event Viewer" + if not self.disable_server: + def modify_doc(doc): + doc.add_root(self.layout) + doc.title = self.name + + directory = os.path.abspath(os.path.dirname(__file__)) + theme_path = os.path.join(directory, "theme.yaml") + template_path = os.path.join(directory, "templates") + doc.theme = Theme(filename=theme_path) + env = jinja2.Environment( + loader=jinja2.FileSystemLoader(template_path) + ) + doc.template = env.get_template('index.html') + + self.log.info('Opening Bokeh application on ' + 'http://localhost:{}/'.format(self.port)) + server = Server({'/': modify_doc}, num_procs=1, port=self.port) + server.start() + server.io_loop.add_callback(server.show, "/") + server.io_loop.start() @property def event_index(self): @@ -397,8 +414,5 @@ def main(): exe.run() -if 'bk_script' in __name__: - main() - if __name__ == '__main__': main() diff --git a/ctapipe/tools/bokeh/file_viewer/templates/index.html b/ctapipe/tools/bokeh/templates/index.html similarity index 100% rename from ctapipe/tools/bokeh/file_viewer/templates/index.html rename to ctapipe/tools/bokeh/templates/index.html diff --git a/ctapipe/tools/bokeh/file_viewer/theme.yaml b/ctapipe/tools/bokeh/theme.yaml similarity index 100% rename from ctapipe/tools/bokeh/file_viewer/theme.yaml rename to ctapipe/tools/bokeh/theme.yaml diff --git a/ctapipe/tools/tests/test_tools.py b/ctapipe/tools/tests/test_tools.py index 7ab619d9083..1ab7dcdf6db 100644 --- a/ctapipe/tools/tests/test_tools.py +++ b/ctapipe/tools/tests/test_tools.py @@ -2,7 +2,7 @@ from ctapipe.tools.dump_triggers import DumpTriggersTool from ctapipe.tools.dump_instrument import DumpInstrumentTool from ctapipe.tools.info import info -from ctapipe.tools.bokeh.file_viewer.main import BokehFileViewer +from ctapipe.tools.bokeh.file_viewer import BokehFileViewer from ctapipe.utils import get_dataset_path @@ -45,7 +45,7 @@ def test_camdemo(): def test_bokeh_file_viewer(): - tool = BokehFileViewer() + tool = BokehFileViewer(disable_server=True) tool.run() assert tool.reader.input_url == get_dataset_path("gamma_test.simtel.gz") diff --git a/setup.py b/setup.py index e5afcb918fd..2637a914c5f 100755 --- a/setup.py +++ b/setup.py @@ -36,7 +36,8 @@ 'ctapipe-chargeres-plot = ctapipe.tools.plot_charge_resolution:main', 'ctapipe-chargeres-hist = ' 'ctapipe.tools.plot_charge_resolution_variation_hist:main', - 'ctapipe-dump-instrument=ctapipe.tools.dump_instrument:main' + 'ctapipe-dump-instrument=ctapipe.tools.dump_instrument:main', + 'ctapipe-event-viewer = ctapipe.tools.bokeh.file_viewer:main' ] package.version.update_release_version() @@ -86,5 +87,8 @@ zip_safe=False, use_2to3=False, entry_points=entry_points, - ext_modules=[neighboursum_module] + ext_modules=[neighboursum_module], + package_data={ + '': ['tools/bokeh/*.yaml', 'tools/bokeh/templates/*.html'], + } ) From 81883107c3e16a14230c8ddd1a2756dbb516b445 Mon Sep 17 00:00:00 2001 From: watsonjj Date: Fri, 9 Nov 2018 11:53:38 +0100 Subject: [PATCH 09/13] Fixed selection of extractor --- ctapipe/tools/bokeh/file_viewer.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ctapipe/tools/bokeh/file_viewer.py b/ctapipe/tools/bokeh/file_viewer.py index fc45b387a6b..03781770ecb 100644 --- a/ctapipe/tools/bokeh/file_viewer.py +++ b/ctapipe/tools/bokeh/file_viewer.py @@ -36,6 +36,14 @@ class BokehFileViewer(Tool): tf='CameraR1CalibratorFactory.tf_path', pe='CameraR1CalibratorFactory.pe_path', ff='CameraR1CalibratorFactory.ff_path', + extractor='ChargeExtractorFactory.product', + extractor_t0='ChargeExtractorFactory.t0', + extractor_window_width='ChargeExtractorFactory.window_width', + extractor_window_shift='ChargeExtractorFactory.window_shift', + extractor_sig_amp_cut_HG='ChargeExtractorFactory.sig_amp_cut_HG', + extractor_sig_amp_cut_LG='ChargeExtractorFactory.sig_amp_cut_LG', + extractor_lwt='ChargeExtractorFactory.lwt', + cleaner='WaveformCleanerFactory.product', )) classes = List([ From 0b389dbddb6b3cbdd8df4eda80dc657b21b0329a Mon Sep 17 00:00:00 2001 From: watsonjj Date: Fri, 9 Nov 2018 11:56:52 +0100 Subject: [PATCH 10/13] Fixed selection of pixels to work with bokeh > 1.0 Added bokeh>1.1 as a dependency to ensure compatibility --- ctapipe/visualization/bokeh.py | 5 ++--- setup.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ctapipe/visualization/bokeh.py b/ctapipe/visualization/bokeh.py index eab001c981c..326a4cef628 100644 --- a/ctapipe/visualization/bokeh.py +++ b/ctapipe/visualization/bokeh.py @@ -198,8 +198,7 @@ def enable_pixel_picker(self, n_active): self.active_pixels = [0] * n_active self.fig.add_tools(TapTool()) - def source_change_response(_, __, ___): - val = self.cdsource.selected['1d']['indices'] + def source_change_response(_, __, val): if val: pix = val[0] ai = self.active_index @@ -211,7 +210,7 @@ def source_change_response(_, __, ___): if self.automatic_index_increment: self.active_index = (ai + 1) % len(self.active_pixels) - self.cdsource.on_change('selected', source_change_response) + self.cdsource.selected.on_change('indices', source_change_response) def _on_pixel_click(self, pix_id): print("Clicked pixel_id: {}".format(pix_id)) diff --git a/setup.py b/setup.py index 2637a914c5f..350a96cba8c 100755 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ 'matplotlib>=2.0', 'numba', 'pandas', + 'bokeh>=1.1', ], tests_require=['pytest', 'ctapipe-extra>=0.2.11'], author=AUTHOR, From 40c53daf90f3a12c4a5b42b932614bb4d21af9b1 Mon Sep 17 00:00:00 2001 From: watsonjj Date: Fri, 9 Nov 2018 11:57:59 +0100 Subject: [PATCH 11/13] Changed pixel shape from rectangular to circular to better display all cameras --- ctapipe/visualization/bokeh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctapipe/visualization/bokeh.py b/ctapipe/visualization/bokeh.py index 326a4cef628..79a320422dd 100644 --- a/ctapipe/visualization/bokeh.py +++ b/ctapipe/visualization/bokeh.py @@ -174,7 +174,7 @@ def reset_pixels(self): def _draw_camera(self): # TODO: Support other pixel shapes OR switch to ellipse # after https://github.com/bokeh/bokeh/issues/6985 - self.glyphs = self.fig.rect( + self.glyphs = self.fig.ellipse( 'x', 'y', color='image', width='width', height='height', line_color='outline_color', line_alpha='outline_alpha', From 18da2b91991ebb10e1d0952b5fa2120957f6129a Mon Sep 17 00:00:00 2001 From: watsonjj Date: Fri, 9 Nov 2018 12:09:43 +0100 Subject: [PATCH 12/13] Corrected tests to use example_event --- .../plotting/tests/test_bokeh_event_viewer.py | 62 +++++++++---------- ctapipe/visualization/tests/test_bokeh.py | 34 +++++----- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/ctapipe/plotting/tests/test_bokeh_event_viewer.py b/ctapipe/plotting/tests/test_bokeh_event_viewer.py index e0b0164f254..3b5f06a5b86 100644 --- a/ctapipe/plotting/tests/test_bokeh_event_viewer.py +++ b/ctapipe/plotting/tests/test_bokeh_event_viewer.py @@ -8,14 +8,14 @@ def test_bokeh_event_viewer_creation(): viewer.create() -def test_event_setting(test_event): +def test_event_setting(example_event): viewer = BokehEventViewer(config=None, tool=None) viewer.create() - viewer.event = test_event + viewer.event = example_event for cam in viewer.cameras: - assert cam.event == test_event + assert cam.event == example_event for wf in viewer.waveforms: - assert wf.event == test_event + assert wf.event == example_event def test_enable_automatic_index_increment(): @@ -26,10 +26,10 @@ def test_enable_automatic_index_increment(): assert cam.automatic_index_increment -def test_change_time(test_event): +def test_change_time(example_event): viewer = BokehEventViewer(config=None, tool=None) viewer.create() - viewer.event = test_event + viewer.event = example_event t = 5 viewer.change_time(t) @@ -45,8 +45,8 @@ def test_change_time(test_event): for wf in viewer.waveforms: assert wf.active_time == 0 - tel = list(test_event.r0.tels_with_data)[0] - n_samples = test_event.r0.tel[tel].waveform.shape[-1] + tel = list(example_event.r0.tels_with_data)[0] + n_samples = example_event.r0.tel[tel].waveform.shape[-1] t = 10000 viewer.change_time(t) for cam in viewer.cameras: @@ -55,10 +55,10 @@ def test_change_time(test_event): assert wf.active_time == n_samples - 1 -def test_on_waveform_click(test_event): +def test_on_waveform_click(example_event): viewer = BokehEventViewer(config=None, tool=None) viewer.create() - viewer.event = test_event + viewer.event = example_event t = 5 viewer.waveforms[0]._on_waveform_click(t) @@ -68,12 +68,12 @@ def test_on_waveform_click(test_event): assert wf.active_time == t -def test_telid(test_event): +def test_telid(example_event): viewer = BokehEventViewer(config=None, tool=None) viewer.create() - viewer.event = test_event + viewer.event = example_event - tels = list(test_event.r0.tels_with_data) + tels = list(example_event.r0.tels_with_data) assert viewer.telid == tels[0] for cam in viewer.cameras: @@ -89,29 +89,29 @@ def test_telid(test_event): assert wf.telid == tels[1] -def test_telid_incorrect(test_event): +def test_telid_incorrect(example_event): viewer = BokehEventViewer(config=None, tool=None) viewer.create() - viewer.event = test_event + viewer.event = example_event with pytest.raises(KeyError): viewer.telid = 148937242 -def test_on_pixel_click(test_event): +def test_on_pixel_click(example_event): viewer = BokehEventViewer(config=None, tool=None) viewer.create() - viewer.event = test_event + viewer.event = example_event p1 = 5 viewer.cameras[0]._on_pixel_click(p1) assert viewer.waveforms[viewer.cameras[0].active_index].pixel == p1 -def test_channel(test_event): +def test_channel(example_event): viewer = BokehEventViewer(config=None, tool=None) viewer.create() - viewer.event = test_event + viewer.event = example_event assert viewer.channel == 0 for cam in viewer.cameras: @@ -120,46 +120,46 @@ def test_channel(test_event): assert wf.channel == 0 -def test_channel_incorrect(test_event): +def test_channel_incorrect(example_event): viewer = BokehEventViewer(config=None, tool=None) viewer.create() - viewer.event = test_event + viewer.event = example_event with pytest.raises(IndexError): viewer.channel = 148937242 -def test_view_camera(test_event): +def test_view_camera(example_event): viewer = BokehEventViewer(config=None, tool=None) viewer.create() - viewer.event = test_event + viewer.event = example_event c = CameraCalibrator() - c.calibrate(test_event) + c.calibrate(example_event) - t = list(test_event.r0.tels_with_data)[0] + t = list(example_event.r0.tels_with_data)[0] cam = viewer.cameras[0] cam.view = 'r1' - assert (cam.image == test_event.r1.tel[t].waveform[0, :, 0]).all() + assert (cam.image == example_event.r1.tel[t].waveform[0, :, 0]).all() with pytest.raises(ValueError): cam.view = 'q' -def test_view_wf(test_event): +def test_view_wf(example_event): viewer = BokehEventViewer(config=None, tool=None) viewer.create() - viewer.event = test_event + viewer.event = example_event c = CameraCalibrator() - c.calibrate(test_event) + c.calibrate(example_event) - t = list(test_event.r0.tels_with_data)[0] + t = list(example_event.r0.tels_with_data)[0] wf = viewer.waveforms[0] wf.view = 'r1' - assert (wf.waveform == test_event.r1.tel[t].waveform[0, 0, :]).all() + assert (wf.waveform == example_event.r1.tel[t].waveform[0, 0, :]).all() with pytest.raises(ValueError): wf.view = 'q' diff --git a/ctapipe/visualization/tests/test_bokeh.py b/ctapipe/visualization/tests/test_bokeh.py index cb27b3fbe38..a6d0f77e6f1 100644 --- a/ctapipe/visualization/tests/test_bokeh.py +++ b/ctapipe/visualization/tests/test_bokeh.py @@ -8,24 +8,24 @@ def test_camera_display_create(): CameraDisplay() -def test_camera_geom(test_event): - t = list(test_event.r0.tels_with_data)[0] - geom = test_event.inst.subarray.tel[t].camera +def test_camera_geom(example_event): + t = list(example_event.r0.tels_with_data)[0] + geom = example_event.inst.subarray.tel[t].camera c_display = CameraDisplay(geom) assert (c_display.cdsource.data['x'] == geom.pix_x.value).all() assert (c_display.cdsource.data['y'] == geom.pix_y.value).all() - t = list(test_event.r0.tels_with_data)[1] - geom = test_event.inst.subarray.tel[t].camera + t = list(example_event.r0.tels_with_data)[1] + geom = example_event.inst.subarray.tel[t].camera c_display.geom = geom assert (c_display.cdsource.data['x'] == geom.pix_x.value).all() assert (c_display.cdsource.data['y'] == geom.pix_y.value).all() -def test_camera_image(test_event): - t = list(test_event.r0.tels_with_data)[0] - geom = test_event.inst.subarray.tel[t].camera +def test_camera_image(example_event): + t = list(example_event.r0.tels_with_data)[0] + geom = example_event.inst.subarray.tel[t].camera n_pixels = geom.pix_x.value.size image = np.ones(n_pixels) colors = intensity_to_hex(image) @@ -46,9 +46,9 @@ def test_camera_image(test_event): assert c_display.image_max == image.max() -def test_camera_enable_pixel_picker(test_event): - t = list(test_event.r0.tels_with_data)[0] - geom = test_event.inst.subarray.tel[t].camera +def test_camera_enable_pixel_picker(example_event): + t = list(example_event.r0.tels_with_data)[0] + geom = example_event.inst.subarray.tel[t].camera n_pixels = geom.pix_x.value.size image = np.ones(n_pixels) c_display = CameraDisplay(geom, image) @@ -60,9 +60,9 @@ def test_camera_enable_pixel_picker(test_event): assert len(c_display.active_pixels) == 3 -def test_fast_camera_display_create(test_event): - t = list(test_event.r0.tels_with_data)[0] - geom = test_event.inst.subarray.tel[t].camera +def test_fast_camera_display_create(example_event): + t = list(example_event.r0.tels_with_data)[0] + geom = example_event.inst.subarray.tel[t].camera x = geom.pix_x.value y = geom.pix_y.value @@ -72,9 +72,9 @@ def test_fast_camera_display_create(test_event): FastCameraDisplay(x, y, size) -def test_fast_camera_image(test_event): - t = list(test_event.r0.tels_with_data)[0] - geom = test_event.inst.subarray.tel[t].camera +def test_fast_camera_image(example_event): + t = list(example_event.r0.tels_with_data)[0] + geom = example_event.inst.subarray.tel[t].camera x = geom.pix_x.value y = geom.pix_y.value From a3e0d21c796c8b681daae6e752a36b6ac8a8788d Mon Sep 17 00:00:00 2001 From: watsonjj Date: Fri, 9 Nov 2018 13:13:59 +0100 Subject: [PATCH 13/13] Corrected bokeh version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8a93a838f93..e7f9c8c6c4f 100755 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ 'matplotlib>=2.0', 'numba', 'pandas', - 'bokeh>=1.1', + 'bokeh>=1.0.1', ], tests_require=['pytest', 'ctapipe-extra>=0.2.11'], author=AUTHOR,