diff --git a/ctapipe/analysis/camera/charge_resolution.py b/ctapipe/analysis/camera/charge_resolution.py index 832c161f641..f36b4a87d49 100644 --- a/ctapipe/analysis/camera/charge_resolution.py +++ b/ctapipe/analysis/camera/charge_resolution.py @@ -5,37 +5,38 @@ class ChargeResolutionCalculator: - def __init__(self, mc_true=True): - """ - Calculates the charge resolution with an efficient, low-memory, - interative approach, allowing the contribution of data/events - without reading the entire dataset into memory. - - Utilises Pandas DataFrames, and makes no assumptions on the order of - the data, and does not require the true charge to be integer (as may - be the case for lab measurements where an average illumination - is used). - - A list is filled with a dataframe for each contribution, and only - amalgamated into a single dataframe (reducing memory) once the memory - of the list becomes large (or at the end of the filling), - reducing the time required to produce the output. - - Parameters - ---------- - mc_true : bool - Indicate if the "true charge" values are from the sim_telarray - files, and therefore without poisson error. The poisson error will - therefore be included in the charge resolution calculation. + """ + Calculates the charge resolution with an efficient, low-memory, + interative approach, allowing the contribution of data/events + without reading the entire dataset into memory. + + Utilises Pandas DataFrames, and makes no assumptions on the order of + the data, and does not require the true charge to be integer (as may + be the case for lab measurements where an average illumination + is used). + + A list is filled with a dataframe for each contribution, and only + amalgamated into a single dataframe (reducing memory) once the memory + of the list becomes large (or at the end of the filling), + reducing the time required to produce the output. + + Parameters + ---------- + mc_true : bool + Indicate if the "true charge" values are from the sim_telarray + files, and therefore without poisson error. The poisson error will + therefore be included in the charge resolution calculation. + + Attributes + ---------- + self._mc_true : bool + self._df_list : list + self._df : pd.DataFrame + self._n_bytes : int + Monitors the number of bytes being held in memory + """ - Attributes - ---------- - self._mc_true : bool - self._df_list : list - self._df : pd.DataFrame - self._n_bytes : int - Monitors the number of bytes being held in memory - """ + def __init__(self, mc_true=True): self._mc_true = mc_true self._df_list = [] self._df = pd.DataFrame() diff --git a/ctapipe/core/__init__.py b/ctapipe/core/__init__.py index 67a3460ccc8..829aaf6205e 100644 --- a/ctapipe/core/__init__.py +++ b/ctapipe/core/__init__.py @@ -1,4 +1,7 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +Core functionality of ctapipe +""" from .component import Component, non_abstract_children from .container import Container, Field, Map diff --git a/ctapipe/core/component.py b/ctapipe/core/component.py index 795aea1c587..20df0090820 100644 --- a/ctapipe/core/component.py +++ b/ctapipe/core/component.py @@ -1,9 +1,11 @@ """ Class to handle configuration for algorithms """ from abc import ABCMeta -from logging import getLogger from inspect import isabstract -from traitlets.config import Configurable +from logging import getLogger + from traitlets import TraitError +from traitlets.config import Configurable + from ctapipe.core.plugins import detect_and_import_io_plugins @@ -31,10 +33,10 @@ def non_abstract_children(base): class AbstractConfigurableMeta(type(Configurable), ABCMeta): - ''' + """ Metaclass to be able to make Component abstract see: http://stackoverflow.com/a/7314847/3838691 - ''' + """ pass @@ -108,7 +110,8 @@ def __init__(self, config=None, parent=None, **kwargs): if not self.has_trait(key): raise TraitError(f"Traitlet does not exist: {key}") - # set up logging + # set up logging (for some reason the logger registered by LoggingConfig + # doesn't use a child logger of the parent by default) if self.parent: self.log = self.parent.log.getChild(self.__class__.__name__) else: @@ -130,11 +133,11 @@ def from_name(cls, name, config=None, parent=None): Used to set traitlet values. This argument is typically only specified when using this method from within a Tool. - tool : ctapipe.core.Tool + parent : ctapipe.core.Tool Tool executable that is calling this component. - Passes the correct logger to the component. + Passes the correct logger and configuration to the component. This argument is typically only specified when using this method - from within a Tool. + from within a Tool (config need not be passed if parent is used). Returns ------- @@ -149,3 +152,33 @@ def from_name(cls, name, config=None, parent=None): requested_subclass = subclasses[name] return requested_subclass(config=config, parent=parent) + + def get_current_config(self): + """ return the current configuration as a dict (e.g. the values + of all traits, even if they were not set during configuration) + """ + return { + self.__class__.__name__: { + k: v.get(self) for k, v in self.traits(config=True).items() + } + } + + def _repr_html_(self): + """ nice HTML rep, with blue for non-default values""" + traits = self.traits() + name = self.__class__.__name__ + lines = [ + f"{name}", + f"
{self.__class__.__doc__ or 'Undocumented!'}
", + "{key} | ") + if val != traits[key].default_value: + lines.append(f"{val} | ") + else: + lines.append(f"{val} | ") + lines.append(f'{thehelp} |
---|
{self.__class__.__doc__ or self.description}
", + "{key} | ") + if val != default: + lines.append(f"{val} | ") + else: + lines.append(f"{val} | ") + lines.append(f'{thehelp} |
---|
Components:") + lines.append(", ".join([x.__name__ for x in self.classes])) + lines.append("
") + + return "\n".join(lines) + + +def export_tool_config_to_commented_yaml(tool_instance: Tool, classes=None): + """ + Turn the config of a single Component into a commented YAML string. + + This is a hacked version of + traitlets.config.Configurable._class_config_section() changed to + output a YAML file with defaults *and* current values filled in. + + Parameters + ---------- + tool_instance: Tool + a constructed Tool instance + classes: list, optional + The list of other classes in the config file. + Used to reduce redundant information. + """ + + tool = tool_instance.__class__ + config = tool_instance.get_current_config()[tool_instance.__class__.__name__] + + def commented(text, indent_level=2, width=70): + """return a commented, wrapped block.""" + return textwrap.fill( + text, + width=width, + initial_indent=" " * indent_level + "# ", + subsequent_indent=" " * indent_level + "# ", + ) + + # section header + breaker = '#' + '-' * 78 + parent_classes = ', '.join( + p.__name__ for p in tool.__bases__ + if issubclass(p, Configurable) + ) + + section_header = f"# {tool.__name__}({parent_classes}) configuration" + + lines = [breaker, section_header] + # get the description trait + desc = tool.class_traits().get('description') + if desc: + desc = desc.default_value + if not desc: + # no description from trait, use __doc__ + desc = getattr(tool, '__doc__', '') + if desc: + lines.append(commented(desc, indent_level=0)) + lines.append(breaker) + lines.append(f'{tool.__name__}:') + + for name, trait in sorted(tool.class_traits(config=True).items()): + default_repr = trait.default_value_repr() + current_repr = config.get(name, "") + if isinstance(current_repr, str): + current_repr = f'"{current_repr}"' + + if classes: + defining_class = tool._defining_class(trait, classes) + else: + defining_class = tool + if defining_class is tool: + # cls owns the trait, show full help + if trait.help: + lines.append(commented(trait.help)) + if 'Enum' in type(trait).__name__: + # include Enum choices + lines.append(commented(f'Choices: {trait.info()}')) + lines.append(commented(f'Default: {default_repr}')) + else: + # Trait appears multiple times and isn't defined here. + # Truncate help to first line + "See also Original.trait" + if trait.help: + lines.append(commented(trait.help.split('\n', 1)[0])) + lines.append( + f' # See also: {defining_class.__name__}.{name}') + lines.append(f' {name}: {current_repr}') + lines.append('') + return '\n'.join(lines) diff --git a/ctapipe/core/traits.py b/ctapipe/core/traits.py index 854a12f634e..2eb422ef486 100644 --- a/ctapipe/core/traits.py +++ b/ctapipe/core/traits.py @@ -1,17 +1,50 @@ -from traitlets import (Int, Integer, Float, Unicode, Enum, Long, List, - Bool, CRegExp, Dict, TraitError, observe, - CaselessStrEnum, TraitType) -from traitlets.config import boolean_flag as flag import os -__all__ = ['Path', 'Int', 'Integer', 'Float', 'Unicode', 'Enum', 'Long', 'List', - 'Bool', 'CRegExp', 'Dict', 'flag', 'TraitError', 'observe', - 'CaselessStrEnum'] +from traitlets import ( + Bool, + CaselessStrEnum, + CRegExp, + Dict, + Enum, + Float, + Int, + Integer, + List, + Long, + TraitError, + TraitType, + Unicode, + observe, +) +from traitlets.config import boolean_flag as flag + +from .component import non_abstract_children + +__all__ = [ + "Path", + "Int", + "Integer", + "Float", + "Unicode", + "Enum", + "Long", + "List", + "Bool", + "CRegExp", + "Dict", + "flag", + "TraitError", + "observe", + "CaselessStrEnum", + "enum_trait", + "classes_with_traits", + "has_traits", +] class Path(TraitType): def __init__(self, exists=None, directory_ok=True, file_ok=True): - ''' + """ A path Trait for input/output files. Parameters @@ -23,7 +56,7 @@ def __init__(self, exists=None, directory_ok=True, file_ok=True): If False, path must not be a directory file_ok: boolean If False, path must not be a file - ''' + """ super().__init__() self.exists = exists self.directory_ok = directory_ok @@ -35,20 +68,59 @@ def validate(self, obj, value): value = os.path.abspath(value) if self.exists is not None: if os.path.exists(value) != self.exists: - raise TraitError('Path "{}" {} exist'.format( - value, - 'does not' if self.exists else 'must' - )) - if os.path.exists(value): - if os.path.isdir(value) and not self.directory_ok: raise TraitError( - f'Path "{value}" must not be a directory' + 'Path "{}" {} exist'.format( + value, "does not" if self.exists else "must" + ) ) + if os.path.exists(value): + if os.path.isdir(value) and not self.directory_ok: + raise TraitError(f'Path "{value}" must not be a directory') if os.path.isfile(value) and not self.file_ok: - raise TraitError( - f'Path "{value}" must not be a file' - ) + raise TraitError(f'Path "{value}" must not be a file') return value return self.error(obj, value) + + +def enum_trait(base_class, default, help_str=None): + """create a configurable CaselessStrEnum traitlet from baseclass + + the enumeration should contain all names of non_abstract_children() + of said baseclass and the default choice should be given by + `base_class._default` name. + + default must be specified and must be the name of one child-class + """ + if help_str is None: + help_str = "{} to use.".format(base_class.__name__) + + choices = [cls.__name__ for cls in non_abstract_children(base_class)] + if default not in choices: + raise ValueError( + "{default} is not in choices: {choices}".format( + default=default, choices=choices + ) + ) + + return CaselessStrEnum(choices, default, allow_none=True, help=help_str).tag( + config=True + ) + + +def classes_with_traits(base_class): + """ Returns a list of the base class plus its non-abstract children + if they have traits """ + all_classes = [base_class] + non_abstract_children(base_class) + return [cls for cls in all_classes if has_traits(cls)] + + +def has_traits(cls, ignore=("config", "parent")): + """True if cls has any traits apart from the usual ones + + all our components have at least 'config' and 'parent' as traitlets + this is inherited from `traitlets.config.Configurable` so we ignore them + here. + """ + return bool(set(cls.class_trait_names()) - set(ignore)) diff --git a/ctapipe/io/eventsource.py b/ctapipe/io/eventsource.py index a058d82d8ef..552e94d4531 100644 --- a/ctapipe/io/eventsource.py +++ b/ctapipe/io/eventsource.py @@ -3,10 +3,12 @@ """ from abc import abstractmethod from os.path import exists + from traitlets import Unicode, Int, Set, TraitError +from traitlets.config.loader import LazyConfigValue + from ctapipe.core import Component, non_abstract_children from ctapipe.core import Provenance -from traitlets.config.loader import LazyConfigValue from ctapipe.core.plugins import detect_and_import_io_plugins __all__ = [ diff --git a/ctapipe/io/simteleventsource.py b/ctapipe/io/simteleventsource.py index 4416bda606b..c85c3803a31 100644 --- a/ctapipe/io/simteleventsource.py +++ b/ctapipe/io/simteleventsource.py @@ -73,7 +73,7 @@ def __init__(self, config=None, parent=None, **kwargs): # so we explicitly pass None in that case self.file_ = SimTelFile( self.input_url, - allowed_telescopes=self.allowed_tels if self.allowed_tels else None, + allowed_telescopes=set(self.allowed_tels) if self.allowed_tels else None, skip_calibration=self.skip_calibration_events, zcat=not self.back_seekable, ) diff --git a/ctapipe/tools/bokeh/file_viewer.py b/ctapipe/tools/bokeh/file_viewer.py index 6b63e68813f..2b92ac4b242 100644 --- a/ctapipe/tools/bokeh/file_viewer.py +++ b/ctapipe/tools/bokeh/file_viewer.py @@ -1,18 +1,19 @@ import os -from bokeh.layouts import widgetbox, layout -from bokeh.models import Select, TextInput, PreText, Button -from bokeh.server.server import Server + from bokeh.document.document import jinja2 +from bokeh.layouts import layout, widgetbox +from bokeh.models import Button, PreText, Select, TextInput +from bokeh.server.server import Server from bokeh.themes import Theme -from traitlets import Dict, List, Int, Bool +from traitlets import Bool, Dict, Int, List + from ctapipe.calib import CameraCalibrator -from ctapipe.core import Tool +from ctapipe.core import Tool, traits from ctapipe.image.extractor import ImageExtractor from ctapipe.io import EventSource from ctapipe.io.eventseeker import EventSeeker from ctapipe.plotting.bokeh_event_viewer import BokehEventViewer from ctapipe.utils import get_dataset_path -import ctapipe.utils.tools as tool_utils class BokehFileViewer(Tool): @@ -27,7 +28,7 @@ class BokehFileViewer(Tool): default_url = get_dataset_path("gamma_test_large.simtel.gz") EventSource.input_url.default_value = default_url - extractor_product = tool_utils.enum_trait( + extractor_product = traits.enum_trait( ImageExtractor, default='NeighborPeakWindowSum' ) @@ -43,7 +44,7 @@ class BokehFileViewer(Tool): classes = List( [ EventSource, - ] + tool_utils.classes_with_traits(ImageExtractor) + ] + traits.classes_with_traits(ImageExtractor) ) def __init__(self, **kwargs): diff --git a/ctapipe/tools/display_dl1.py b/ctapipe/tools/display_dl1.py index 7dd22d9e5a0..d9b1c9eb430 100644 --- a/ctapipe/tools/display_dl1.py +++ b/ctapipe/tools/display_dl1.py @@ -1,31 +1,32 @@ """ Calibrate dl0 data to dl1, and plot the photoelectron images. """ -from matplotlib import pyplot as plt, colors +from matplotlib import colors +from matplotlib import pyplot as plt from matplotlib.backends.backend_pdf import PdfPages -from traitlets import Dict, List, Int, Bool, Unicode +from traitlets import Bool, Dict, Int, List, Unicode from ctapipe.calib import CameraCalibrator -from ctapipe.visualization import CameraDisplay -from ctapipe.core import Tool, Component -from ctapipe.utils import get_dataset_path +from ctapipe.core import Component, Tool +from ctapipe.core import traits from ctapipe.image.extractor import ImageExtractor from ctapipe.io import EventSource -import ctapipe.utils.tools as tool_utils +from ctapipe.utils import get_dataset_path +from ctapipe.visualization import CameraDisplay class ImagePlotter(Component): + """ Plotter for camera images """ + display = Bool( - True, - help='Display the photoelectron images on-screen as they ' - 'are produced.' + True, help="Display the photoelectron images on-screen as they are produced." ).tag(config=True) output_path = Unicode( None, allow_none=True, - help='Output path for the pdf containing all the ' - 'images. Set to None for no saved ' - 'output.' + help="Output path for the pdf containing all the " + "images. Set to None for no saved " + "output.", ).tag(config=True) def __init__(self, config=None, parent=None, **kwargs): @@ -85,17 +86,20 @@ def plot(self, event, telid): tmaxmin = event.dl0.tel[telid].waveform.shape[2] t_chargemax = pulse_time[image.argmax()] cmap_time = colors.LinearSegmentedColormap.from_list( - 'cmap_t', - [(0 / tmaxmin, 'darkgreen'), - (0.6 * t_chargemax / tmaxmin, 'green'), - (t_chargemax / tmaxmin, 'yellow'), - (1.4 * t_chargemax / tmaxmin, 'blue'), (1, 'darkblue')] + "cmap_t", + [ + (0 / tmaxmin, "darkgreen"), + (0.6 * t_chargemax / tmaxmin, "green"), + (t_chargemax / tmaxmin, "yellow"), + (1.4 * t_chargemax / tmaxmin, "blue"), + (1, "darkblue"), + ], ) self.c_pulse_time.pixels.set_cmap(cmap_time) if not self.cb_intensity: self.c_intensity.add_colorbar( - ax=self.ax_intensity, label='Intensity (p.e.)' + ax=self.ax_intensity, label="Intensity (p.e.)" ) self.cb_intensity = self.c_intensity.colorbar else: @@ -103,7 +107,7 @@ def plot(self, event, telid): self.c_intensity.update(True) if not self.cb_pulse_time: self.c_pulse_time.add_colorbar( - ax=self.ax_pulse_time, label='Pulse Time (ns)' + ax=self.ax_pulse_time, label="Pulse Time (ns)" ) self.cb_pulse_time = self.c_pulse_time.colorbar else: @@ -115,8 +119,9 @@ def plot(self, event, telid): self.c_pulse_time.image = pulse_time self.fig.suptitle( - "Event_index={} Event_id={} Telescope={}" - .format(event.count, event.r0.event_id, telid) + "Event_index={} Event_id={} Telescope={}".format( + event.count, event.r0.event_id, telid + ) ) if self.display: @@ -137,41 +142,32 @@ class DisplayDL1Calib(Tool): telescope = Int( None, allow_none=True, - help='Telescope to view. Set to None to display all ' - 'telescopes.' + help="Telescope to view. Set to None to display all telescopes.", ).tag(config=True) - extractor_product = tool_utils.enum_trait( - ImageExtractor, - default='NeighborPeakWindowSum' + extractor_product = traits.enum_trait( + ImageExtractor, default="NeighborPeakWindowSum" ) aliases = Dict( dict( - max_events='EventSource.max_events', - extractor='DisplayDL1Calib.extractor_product', - T='DisplayDL1Calib.telescope', - O='ImagePlotter.output_path' + input="EventSource.input_url", + max_events="EventSource.max_events", + extractor="DisplayDL1Calib.extractor_product", + T="DisplayDL1Calib.telescope", + O="ImagePlotter.output_path", ) ) flags = Dict( dict( D=( - { - 'ImagePlotter': { - 'display': True - } - }, - "Display the photoelectron images on-screen as they " - "are produced." + {"ImagePlotter": {"display": True}}, + "Display the photo-electron images on-screen as they are produced.", ) ) ) classes = List( - [ - EventSource, - ImagePlotter - ] + tool_utils.classes_with_traits(ImageExtractor) + [EventSource, ImagePlotter] + traits.classes_with_traits(ImageExtractor) ) def __init__(self, **kwargs): @@ -181,14 +177,14 @@ def __init__(self, **kwargs): self.plotter = None def setup(self): - self.eventsource = EventSource.from_url( - get_dataset_path("gamma_test_large.simtel.gz"), - parent=self, + self.eventsource = self.add_component( + EventSource.from_url( + get_dataset_path("gamma_test_large.simtel.gz"), parent=self + ) ) - self.calibrator = CameraCalibrator(parent=self) - - self.plotter = ImagePlotter(parent=self) + self.calibrator = self.add_component(CameraCalibrator(parent=self)) + self.plotter = self.add_component(ImagePlotter(parent=self)) def start(self): for event in self.eventsource: diff --git a/ctapipe/tools/display_events_single_tel.py b/ctapipe/tools/display_events_single_tel.py index 88327546c4c..7fde80cb196 100755 --- a/ctapipe/tools/display_events_single_tel.py +++ b/ctapipe/tools/display_events_single_tel.py @@ -73,10 +73,14 @@ def __init__(self, **kwargs): def setup(self): print('TOLLES INFILE', self.infile) - self.event_source = EventSource.from_url(self.infile, parent=self) + self.event_source = self.add_component( + EventSource.from_url(self.infile, parent=self) + ) self.event_source.allowed_tels = {self.tel, } - self.calibrator = CameraCalibrator(parent=self) + self.calibrator = self.add_component( + CameraCalibrator(parent=self) + ) self.log.info(f'SELECTING EVENTS FROM TELESCOPE {self.tel}') diff --git a/ctapipe/tools/display_integrator.py b/ctapipe/tools/display_integrator.py index 6843e4fedda..74114d48cfd 100644 --- a/ctapipe/tools/display_integrator.py +++ b/ctapipe/tools/display_integrator.py @@ -7,12 +7,12 @@ from matplotlib import pyplot as plt from traitlets import Dict, List, Int, Bool, Enum -import ctapipe.utils.tools as tool_utils +from ctapipe.core import traits from ctapipe.calib import CameraCalibrator from ctapipe.core import Tool from ctapipe.image.extractor import ImageExtractor -from ctapipe.io.eventseeker import EventSeeker from ctapipe.io import EventSource +from ctapipe.io.eventseeker import EventSeeker from ctapipe.visualization import CameraDisplay @@ -38,7 +38,7 @@ def plot(event, telid, chan, extractor_name): ax_max_nei = {} ax_min_nei = {} fig_waveforms = plt.figure(figsize=(18, 9)) - fig_waveforms.subplots_adjust(hspace=.5) + fig_waveforms.subplots_adjust(hspace=0.5) fig_camera = plt.figure(figsize=(15, 12)) ax_max_pix = fig_waveforms.add_subplot(4, 2, 1) @@ -60,8 +60,8 @@ def plot(event, telid, chan, extractor_name): ax_max_pix.set_xlabel("Time (ns)") ax_max_pix.set_ylabel("DL0 Samples (ADC)") ax_max_pix.set_title( - f'(Max) Pixel: {max_pix}, True: {t_pe[max_pix]}, ' - f'Measured = {dl1[max_pix]:.3f}' + f"(Max) Pixel: {max_pix}, True: {t_pe[max_pix]}, " + f"Measured = {dl1[max_pix]:.3f}" ) max_ylim = ax_max_pix.get_ylim() for i, ax in ax_max_nei.items(): @@ -71,8 +71,9 @@ def plot(event, telid, chan, extractor_name): ax.set_xlabel("Time (ns)") ax.set_ylabel("DL0 Samples (ADC)") ax.set_title( - "(Max Nei) Pixel: {}, True: {}, Measured = {:.3f}" - .format(pix, t_pe[pix], dl1[pix]) + "(Max Nei) Pixel: {}, True: {}, Measured = {:.3f}".format( + pix, t_pe[pix], dl1[pix] + ) ) ax.set_ylim(max_ylim) @@ -81,8 +82,8 @@ def plot(event, telid, chan, extractor_name): ax_min_pix.set_xlabel("Time (ns)") ax_min_pix.set_ylabel("DL0 Samples (ADC)") ax_min_pix.set_title( - f'(Min) Pixel: {min_pix}, True: {t_pe[min_pix]}, ' - f'Measured = {dl1[min_pix]:.3f}' + f"(Min) Pixel: {min_pix}, True: {t_pe[min_pix]}, " + f"Measured = {dl1[min_pix]:.3f}" ) ax_min_pix.set_ylim(max_ylim) for i, ax in ax_min_nei.items(): @@ -92,8 +93,8 @@ def plot(event, telid, chan, extractor_name): ax.set_xlabel("Time (ns)") ax.set_ylabel("DL0 Samples (ADC)") ax.set_title( - f'(Min Nei) Pixel: {pix}, True: {t_pe[pix]}, ' - f'Measured = {dl1[pix]:.3f}' + f"(Min Nei) Pixel: {pix}, True: {t_pe[pix]}, " + f"Measured = {dl1[pix]:.3f}" ) ax.set_ylim(max_ylim) @@ -109,22 +110,22 @@ def plot(event, telid, chan, extractor_name): ax_img_nei.annotate( f"Pixel: {max_pix}", xy=(geom.pix_x.value[max_pix], geom.pix_y.value[max_pix]), - xycoords='data', + xycoords="data", xytext=(0.05, 0.98), - textcoords='axes fraction', - arrowprops=dict(facecolor='red', width=2, alpha=0.4), - horizontalalignment='left', - verticalalignment='top' + textcoords="axes fraction", + arrowprops=dict(facecolor="red", width=2, alpha=0.4), + horizontalalignment="left", + verticalalignment="top", ) ax_img_nei.annotate( f"Pixel: {min_pix}", xy=(geom.pix_x.value[min_pix], geom.pix_y.value[min_pix]), - xycoords='data', + xycoords="data", xytext=(0.05, 0.94), - textcoords='axes fraction', - arrowprops=dict(facecolor='orange', width=2, alpha=0.4), - horizontalalignment='left', - verticalalignment='top' + textcoords="axes fraction", + arrowprops=dict(facecolor="orange", width=2, alpha=0.4), + horizontalalignment="left", + verticalalignment="top", ) camera = CameraDisplay(geom, ax=ax_img_max) camera.image = dl0[:, max_time] @@ -133,22 +134,22 @@ def plot(event, telid, chan, extractor_name): ax_img_max.annotate( f"Pixel: {max_pix}", xy=(geom.pix_x.value[max_pix], geom.pix_y.value[max_pix]), - xycoords='data', + xycoords="data", xytext=(0.05, 0.98), - textcoords='axes fraction', - arrowprops=dict(facecolor='red', width=2, alpha=0.4), - horizontalalignment='left', - verticalalignment='top' + textcoords="axes fraction", + arrowprops=dict(facecolor="red", width=2, alpha=0.4), + horizontalalignment="left", + verticalalignment="top", ) ax_img_max.annotate( f"Pixel: {min_pix}", xy=(geom.pix_x.value[min_pix], geom.pix_y.value[min_pix]), - xycoords='data', + xycoords="data", xytext=(0.05, 0.94), - textcoords='axes fraction', - arrowprops=dict(facecolor='orange', width=2, alpha=0.4), - horizontalalignment='left', - verticalalignment='top' + textcoords="axes fraction", + arrowprops=dict(facecolor="orange", width=2, alpha=0.4), + horizontalalignment="left", + verticalalignment="top", ) camera = CameraDisplay(geom, ax=ax_img_true) @@ -158,22 +159,22 @@ def plot(event, telid, chan, extractor_name): ax_img_true.annotate( f"Pixel: {max_pix}", xy=(geom.pix_x.value[max_pix], geom.pix_y.value[max_pix]), - xycoords='data', + xycoords="data", xytext=(0.05, 0.98), - textcoords='axes fraction', - arrowprops=dict(facecolor='red', width=2, alpha=0.4), - horizontalalignment='left', - verticalalignment='top' + textcoords="axes fraction", + arrowprops=dict(facecolor="red", width=2, alpha=0.4), + horizontalalignment="left", + verticalalignment="top", ) ax_img_true.annotate( f"Pixel: {min_pix}", xy=(geom.pix_x.value[min_pix], geom.pix_y.value[min_pix]), - xycoords='data', + xycoords="data", xytext=(0.05, 0.94), - textcoords='axes fraction', - arrowprops=dict(facecolor='orange', width=2, alpha=0.4), - horizontalalignment='left', - verticalalignment='top' + textcoords="axes fraction", + arrowprops=dict(facecolor="orange", width=2, alpha=0.4), + horizontalalignment="left", + verticalalignment="top", ) camera = CameraDisplay(geom, ax=ax_img_cal) @@ -183,22 +184,22 @@ def plot(event, telid, chan, extractor_name): ax_img_cal.annotate( f"Pixel: {max_pix}", xy=(geom.pix_x.value[max_pix], geom.pix_y.value[max_pix]), - xycoords='data', + xycoords="data", xytext=(0.05, 0.98), - textcoords='axes fraction', - arrowprops=dict(facecolor='red', width=2, alpha=0.4), - horizontalalignment='left', - verticalalignment='top' + textcoords="axes fraction", + arrowprops=dict(facecolor="red", width=2, alpha=0.4), + horizontalalignment="left", + verticalalignment="top", ) ax_img_cal.annotate( f"Pixel: {min_pix}", xy=(geom.pix_x.value[min_pix], geom.pix_y.value[min_pix]), - xycoords='data', + xycoords="data", xytext=(0.05, 0.94), - textcoords='axes fraction', - arrowprops=dict(facecolor='orange', width=2, alpha=0.4), - horizontalalignment='left', - verticalalignment='top' + textcoords="axes fraction", + arrowprops=dict(facecolor="orange", width=2, alpha=0.4), + horizontalalignment="left", + verticalalignment="top", ) fig_waveforms.suptitle(f"Integrator = {extractor_name}") @@ -211,51 +212,42 @@ class DisplayIntegrator(Tool): name = "ctapipe-display-integration" description = __doc__ - event_index = Int(0, help='Event index to view.').tag(config=True) + event_index = Int(0, help="Event index to view.").tag(config=True) use_event_id = Bool( False, - help='event_index will obtain an event using event_id instead of ' - 'index.' + help="event_index will obtain an event using event_id instead of index.", ).tag(config=True) telescope = Int( None, allow_none=True, - help='Telescope to view. Set to None to display the first' - 'telescope with data.' + help="Telescope to view. Set to None to display the first" + "telescope with data.", ).tag(config=True) - channel = Enum([0, 1], 0, help='Channel to view').tag(config=True) + channel = Enum([0, 1], 0, help="Channel to view").tag(config=True) - extractor_product = tool_utils.enum_trait( - ImageExtractor, - default='NeighborPeakWindowSum' + extractor_product = traits.enum_trait( + ImageExtractor, default="NeighborPeakWindowSum" ) aliases = Dict( dict( - f='EventSource.input_url', - max_events='EventSource.max_events', - extractor='DisplayIntegrator.extractor_product', - E='DisplayIntegrator.event_index', - T='DisplayIntegrator.telescope', - C='DisplayIntegrator.channel', + f="EventSource.input_url", + max_events="EventSource.max_events", + extractor="DisplayIntegrator.extractor_product", + E="DisplayIntegrator.event_index", + T="DisplayIntegrator.telescope", + C="DisplayIntegrator.channel", ) ) flags = Dict( dict( id=( - { - 'DisplayDL1Calib': { - 'use_event_index': True - } - }, 'event_index will obtain an event using ' - 'event_id instead of index.') + {"DisplayDL1Calib": {"use_event_index": True}}, + "event_index will obtain an event using event_id instead of index.", + ) ) ) - classes = List( - [ - EventSource, - ] + tool_utils.classes_with_traits(ImageExtractor) - ) + classes = List([EventSource] + traits.classes_with_traits(ImageExtractor)) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -268,15 +260,13 @@ def __init__(self, **kwargs): def setup(self): self.log_format = "%(levelname)s: %(message)s [%(name)s.%(funcName)s]" - event_source = EventSource.from_config(parent=self) - self.eventseeker = EventSeeker(event_source, parent=self) - self.extractor = ImageExtractor.from_name( - self.extractor_product, - parent=self, + event_source = self.add_component(EventSource.from_config(parent=self)) + self.eventseeker = self.add_component(EventSeeker(event_source, parent=self)) + self.extractor = self.add_component( + ImageExtractor.from_name(self.extractor_product, parent=self) ) - self.calibrator = CameraCalibrator( - parent=self, - image_extractor=self.extractor, + self.calibrate = self.add_component( + CameraCalibrator(parent=self, image_extractor=self.extractor) ) def start(self): @@ -286,7 +276,7 @@ def start(self): event = self.eventseeker[event_num] # Calibrate - self.calibrator(event) + self.calibrate(event) # Select telescope tels = list(event.r0.tels_with_data) diff --git a/ctapipe/tools/extract_charge_resolution.py b/ctapipe/tools/extract_charge_resolution.py index 08a92e5cff4..17b94026137 100644 --- a/ctapipe/tools/extract_charge_resolution.py +++ b/ctapipe/tools/extract_charge_resolution.py @@ -4,54 +4,51 @@ """ import os + import numpy as np import pandas as pd from tqdm import tqdm +from traitlets import Dict, Int, List, Unicode -from traitlets import Dict, List, Int, Unicode - -import ctapipe.utils.tools as tool_utils - -from ctapipe.analysis.camera.charge_resolution import \ - ChargeResolutionCalculator +from ctapipe.analysis.camera.charge_resolution import ChargeResolutionCalculator from ctapipe.calib import CameraCalibrator -from ctapipe.core import Tool, Provenance +from ctapipe.core import Provenance, Tool, traits from ctapipe.image.extractor import ImageExtractor - from ctapipe.io.simteleventsource import SimTelEventSource class ChargeResolutionGenerator(Tool): name = "ChargeResolutionGenerator" - description = ("Calculate the Charge Resolution from a sim_telarray " - "simulation and store within a HDF5 file.") + description = ( + "Calculate the Charge Resolution from a sim_telarray " + "simulation and store within a HDF5 file." + ) - telescopes = List(Int, None, allow_none=True, - help='Telescopes to include from the event file. ' - 'Default = All telescopes').tag(config=True) + telescopes = List( + Int, + None, + allow_none=True, + help="Telescopes to include from the event file. Default = All telescopes", + ).tag(config=True) output_path = Unicode( - 'charge_resolution.h5', - help='Path to store the output HDF5 file' + "charge_resolution.h5", help="Path to store the output HDF5 file" ).tag(config=True) - extractor_product = tool_utils.enum_trait( - ImageExtractor, - default='NeighborPeakWindowSum' + extractor_product = traits.enum_trait( + ImageExtractor, default="NeighborPeakWindowSum" ) - aliases = Dict(dict( - f='SimTelEventSource.input_url', - max_events='SimTelEventSource.max_events', - T='SimTelEventSource.allowed_tels', - extractor='ChargeResolutionGenerator.extractor_product', - O='ChargeResolutionGenerator.output_path', - )) - - classes = List( - [ - SimTelEventSource, - ] + tool_utils.classes_with_traits(ImageExtractor) + aliases = Dict( + dict( + f="SimTelEventSource.input_url", + max_events="SimTelEventSource.max_events", + T="SimTelEventSource.allowed_tels", + extractor="ChargeResolutionGenerator.extractor_product", + O="ChargeResolutionGenerator.output_path", + ) ) + classes = List([SimTelEventSource] + traits.classes_with_traits(ImageExtractor)) + def __init__(self, **kwargs): super().__init__(**kwargs) self.eventsource = None @@ -61,16 +58,14 @@ def __init__(self, **kwargs): def setup(self): self.log_format = "%(levelname)s: %(message)s [%(name)s.%(funcName)s]" - self.eventsource = SimTelEventSource(parent=self) + self.eventsource = self.add_component(SimTelEventSource(parent=self)) - extractor = ImageExtractor.from_name( - self.extractor_product, - parent=self + extractor = self.add_component( + ImageExtractor.from_name(self.extractor_product, parent=self) ) - self.calibrator = CameraCalibrator( - parent=self, - image_extractor=extractor, + self.calibrator = self.add_component( + CameraCalibrator(parent=self, image_extractor=extractor) ) self.calculator = ChargeResolutionCalculator() @@ -86,9 +81,7 @@ def start(self): if np.all(pe == 0): raise KeyError except KeyError: - self.log.exception( - 'Source does not contain true charge!' - ) + self.log.exception("Source does not contain true charge!") raise for mc, dl1 in zip(event.mc.tel.values(), event.dl1.tel.values()): @@ -105,12 +98,11 @@ def finish(self): self.log.info(f"Creating directory: {output_directory}") os.makedirs(output_directory) - with pd.HDFStore(self.output_path, 'w') as store: - store['charge_resolution_pixel'] = df_p - store['charge_resolution_camera'] = df_c + with pd.HDFStore(self.output_path, "w") as store: + store["charge_resolution_pixel"] = df_p + store["charge_resolution_camera"] = df_c - self.log.info("Created charge resolution file: {}" - .format(self.output_path)) + self.log.info("Created charge resolution file: {}".format(self.output_path)) Provenance().add_output_file(self.output_path) @@ -119,5 +111,5 @@ def main(): exe.run() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ctapipe/tools/muon_reconstruction.py b/ctapipe/tools/muon_reconstruction.py index a6fb68f09b4..131b3484316 100644 --- a/ctapipe/tools/muon_reconstruction.py +++ b/ctapipe/tools/muon_reconstruction.py @@ -17,7 +17,7 @@ from ctapipe.core import traits as t from ctapipe.image.muon.muon_diagnostic_plots import plot_muon_event from ctapipe.image.muon.muon_reco_functions import analyze_muon_event -from ctapipe.io import EventSource, event_source +from ctapipe.io import EventSource from ctapipe.io import HDF5TableWriter warnings.filterwarnings("ignore") # Supresses iminuit warnings @@ -40,11 +40,10 @@ class MuonDisplayerTool(Tool): name = 'ctapipe-reconstruct-muons' description = t.Unicode(__doc__) - events = t.Unicode("", - help="input event data file").tag(config=True) - - outfile = t.Unicode("muons.hdf5", help='HDF5 output file name').tag( - config=True) + outfile = t.Unicode( + "muons.hdf5", + help='HDF5 output file name' + ).tag(config=True) display = t.Bool( help='display the camera events', default=False @@ -55,7 +54,7 @@ class MuonDisplayerTool(Tool): ]) aliases = t.Dict({ - 'input': 'MuonDisplayerTool.events', + 'input': 'EventSource.input_url', 'outfile': 'MuonDisplayerTool.outfile', 'display': 'MuonDisplayerTool.display', 'max_events': 'EventSource.max_events', @@ -63,12 +62,14 @@ class MuonDisplayerTool(Tool): }) def setup(self): - if self.events == '': + self.source: EventSource = self.add_component( + EventSource.from_config(parent=self) + ) + if self.source.input_url == '': raise ToolConfigurationError("please specify --input