From 8a02577257440c67798a5a6b7a9d6c6f11bb4629 Mon Sep 17 00:00:00 2001 From: Despina Mousadi <56841063+dmousadi@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:46:47 +0100 Subject: [PATCH] Added final test (final version of user script before I leave) (#158) * camera test scripts for TRR * Converted test_scripts submodule to a regular directory (ignore previous 2 commits) * Delete src/nectarchain/user_scripts/dmousadi/test_scripts/requirements.txt * added trigger timing * removed unknown property * ridmi * Remove empty file * lint * Update test_tools_components.py --------- Co-authored-by: MOUSADI Despoina --- .../dmousadi/test_scripts/README.md | 6 - .../user_scripts/dmousadi/test_scripts/gui.py | 144 +++++++++---- .../tests/test_tools_components.py | 200 +++++++++++++++--- .../test_scripts/tests/trigger_timing_test.py | 184 ++++++++++++++++ 4 files changed, 458 insertions(+), 76 deletions(-) create mode 100644 src/nectarchain/user_scripts/dmousadi/test_scripts/tests/trigger_timing_test.py diff --git a/src/nectarchain/user_scripts/dmousadi/test_scripts/README.md b/src/nectarchain/user_scripts/dmousadi/test_scripts/README.md index beec9ddb..e07efff0 100644 --- a/src/nectarchain/user_scripts/dmousadi/test_scripts/README.md +++ b/src/nectarchain/user_scripts/dmousadi/test_scripts/README.md @@ -19,12 +19,6 @@ Automated tests from the Test Readiness Review document for the CTA NectarCAM ba ## Installation -Instructions on how to install and set up the project: -``` -git clone https://drf-gitlab.cea.fr/dmousadi/camera-test-scripts -cd camera-test-scripts -pip install -r requirements.txt -``` If you want to automatically download your data, one of the requirements is also DIRAC, for which you need to have a grid certificate. It is not necessary for this repo, if you have your NectarCAM runs (fits files) locally stored. You can find more information about DIRAC [here](https://gitlab.cta-observatory.org/cta-computing/dpps/workload/CTADIRAC). If you are installing these packages for the first time and getting 'error building wheel', you might need to (re)install some of these: swig, ca-certificates, openssl, boost, protobuff, cmake. Once you have set up your environment, if you're not already a nectarchain user you need to set the NECTARCAMDATA environment variable to the directory where you have the NectarCAM runs: diff --git a/src/nectarchain/user_scripts/dmousadi/test_scripts/gui.py b/src/nectarchain/user_scripts/dmousadi/test_scripts/gui.py index b01bc5c0..f665e6e8 100644 --- a/src/nectarchain/user_scripts/dmousadi/test_scripts/gui.py +++ b/src/nectarchain/user_scripts/dmousadi/test_scripts/gui.py @@ -11,7 +11,6 @@ The class uses the PyQt5 library for the GUI implementation and the Matplotlib library for plotting the test results. """ - import argparse import os import pickle @@ -38,6 +37,7 @@ QTextEdit, QVBoxLayout, QWidget, + QWidgetItem, ) # Ensure the tests directory is in sys.path @@ -52,9 +52,19 @@ import pedestal_test import pix_couple_tim_uncertainty_test import pix_tim_uncertainty_test +import trigger_timing_test class TestRunner(QWidget): + test_modules = { + "Linearity Test": linearity_test, + "Deadtime Test": deadtime_test, + "Pedestal Test": pedestal_test, + "Pixel Time Uncertainty Test": pix_tim_uncertainty_test, + "Time Uncertainty Between Couples of Pixels": pix_couple_tim_uncertainty_test, + "Trigger Timing Test": trigger_timing_test, + } + def __init__(self): super().__init__() self.params = {} @@ -89,25 +99,13 @@ def init_ui(self): color: #ffffff; /* Light text */ border: 1px solid #888888; /* Light border */ padding: 5px; /* Add padding */ - fixed-height: 30px; /* Set a fixed height */ min-width: 200px; /* Fixed width */ } - QPushButton { - background-color: #4caf50; /* Green button */ - color: white; /* White text */ - border: none; /* No border */ - padding: 10px; /* Add padding */ - border-radius: 5px; /* Rounded corners */ - } - QPushButton:hover { - background-color: #45a049; /* Darker green on hover */ - } QTextEdit { background-color: #1e1e1e; /* Dark output box */ color: #ffffff; /* Light text */ border: 1px solid #888888; /* Light border */ padding: 5px; /* Add padding */ - fixed-height: 150px; /* Set a fixed height */ min-width: 800px; /* Set a minimum width to match the canvas */ } QTextEdit:focus { @@ -144,6 +142,7 @@ def init_ui(self): controls_layout.addWidget(self.label) self.test_selector = QComboBox(self) + self.test_selector.addItem("Select Test") self.test_selector.addItems( [ "Linearity Test", @@ -151,6 +150,7 @@ def init_ui(self): "Pedestal Test", "Pixel Time Uncertainty Test", "Time Uncertainty Between Couples of Pixels", + "Trigger Timing Test", ] ) self.test_selector.setFixedWidth(400) # Fixed width for the dropdown @@ -165,6 +165,8 @@ def init_ui(self): # Button to run the test self.run_button = QPushButton("Run Test", self) + # Disable the run button initially + self.run_button.setEnabled(False) self.run_button.clicked.connect(self.run_test) controls_layout.addWidget(self.run_button) @@ -240,11 +242,14 @@ def get_parameters_from_module(self, module): # Fetch parameters from the module if hasattr(module, "get_args"): parser = module.get_args() - return { - arg.dest: arg.default - for arg in parser._actions - if isinstance(arg, argparse._StoreAction) - } + params = {} + for arg in parser._actions: + if isinstance(arg, argparse._StoreAction): + params[arg.dest] = { + "default": arg.default, + "help": arg.help, # Store the help text + } + return params else: raise RuntimeError("No get_args function found in module.") @@ -258,32 +263,83 @@ def debug_layout(self): def update_parameters(self): # Clear existing parameter fields for i in reversed(range(self.param_layout.count())): - widget = self.param_layout.itemAt(i).widget() - if widget: - widget.deleteLater() + item = self.param_layout.itemAt(i) + + if isinstance( + item, QHBoxLayout + ): # Check if the item is a QHBoxLayout (contains label and help button) + for j in reversed(range(item.count())): + widget = item.itemAt(j).widget() + if widget: + widget.deleteLater() + elif isinstance(item, QWidgetItem): # For direct widgets like QLineEdit + widget = item.widget() + if widget: + widget.deleteLater() + + # Remove the item itself from the layout + self.param_layout.removeItem(item) # Get the selected test and corresponding module selected_test = self.test_selector.currentText() - test_modules = { - "Linearity Test": linearity_test, - "Deadtime Test": deadtime_test, - "Pedestal Test": pedestal_test, - "Pixel Time Uncertainty Test": pix_tim_uncertainty_test, - "Time Uncertainty Between Couples of Pixels": pix_couple_tim_uncertainty_test, - } - - module = test_modules.get(selected_test) + + # If the placeholder is selected, do nothing + if selected_test == "Select Test": + self.run_button.setEnabled(False) + return + + module = self.test_modules.get(selected_test) if module: try: self.params = self.get_parameters_from_module(module) - for param, default in self.params.items(): - if param == "temp_output": + + for param, param_info in self.params.items(): + if param == "temp_output": # Skip temp_output continue + + # Create a horizontal layout for the label and help button + param_layout = QHBoxLayout() + + # Create label label = QLabel(f"{param}:", self) - self.param_layout.addWidget(label) + param_layout.addWidget(label) + + # Create tiny grey circle help button with a white question mark + help_button = QPushButton("?", self) + help_button.setFixedSize(16, 16) # Smaller button size + help_button.setStyleSheet( + """ + QPushButton { + background-color: grey; + color: white; + border-radius: 8px; /* Circular button */ + font-weight: bold; + font-size: 10px; /* Smaller font size */ + } + QPushButton:hover { + background-color: darkgrey; /* Change color on hover */ + } + """ + ) + help_button.setToolTip(param_info["help"]) + + # # Use lambda to capture the current param's help text + # help_button.clicked.connect(lambda _, p=param_info["help"]: self.show_help(p)) + + # Add the help button to the layout (next to the label) + param_layout.addWidget(help_button) + param_layout.addStretch() # Add stretch to push the help button to the right + + # Add the horizontal layout (label + help button) to the main layout + self.param_layout.addLayout(param_layout) + + # Create the input field for the parameter entry = QLineEdit(self) entry.setText( - str(default).replace("[", "").replace("]", "").replace(",", "") + str(param_info["default"]) + .replace("[", "") + .replace("]", "") + .replace(",", "") ) entry.setObjectName(param) entry.setFixedWidth(400) # Set fixed width for QLineEdit @@ -294,24 +350,24 @@ def update_parameters(self): QTimer.singleShot( 0, self.param_widgets.update ) # Ensures the layout is updated + + self.run_button.setEnabled(True) except Exception as e: QMessageBox.critical(self, "Error", f"Failed to fetch parameters: {e}") + else: QMessageBox.critical(self, "Error", "No test selected or test not found") + def show_help(self, help_text): + QMessageBox.information(self, "Parameter Help", help_text) + def run_test(self): # Clean up old plot files to avoid loading leftover files self.cleanup_tempdir() selected_test = self.test_selector.currentText() - test_modules = { - "Linearity Test": linearity_test, - "Deadtime Test": deadtime_test, - "Pedestal Test": pedestal_test, - "Pixel Time Uncertainty Test": pix_tim_uncertainty_test, - "Time Uncertainty Between Couples of Pixels": pix_couple_tim_uncertainty_test, - } - module = test_modules.get(selected_test) + + module = self.test_modules.get(selected_test) if module: params = [] @@ -320,9 +376,9 @@ def run_test(self): # Generate temporary output path self.temp_output = tempfile.gettempdir() - print(f"Temporary output dir: {self.temp_output}") # Debug print + # print(f"Temporary output dir: {self.temp_output}") # Debug print - for param, default in self.params.items(): + for param, _ in self.params.items(): widget_list = self.param_widgets.findChildren(QLineEdit, param) if widget_list: widget = widget_list[0] diff --git a/src/nectarchain/user_scripts/dmousadi/test_scripts/tests/test_tools_components.py b/src/nectarchain/user_scripts/dmousadi/test_scripts/tests/test_tools_components.py index 966a15a4..e22f575e 100644 --- a/src/nectarchain/user_scripts/dmousadi/test_scripts/tests/test_tools_components.py +++ b/src/nectarchain/user_scripts/dmousadi/test_scripts/tests/test_tools_components.py @@ -47,7 +47,6 @@ def _init_output_path(self): class ChargeContainer(NectarCAMContainer): - """ This class contains fields that store various properties and data related to NectarCAM events, including: @@ -87,7 +86,6 @@ class ChargeContainer(NectarCAMContainer): class ChargeComp(NectarCAMComponent): - """ This class `ChargeComp` is a NectarCAMComponent that processes NectarCAM event data. It extracts the charge information from the waveforms of each event, handling cases of saturated or noisy events. The class has the following configurable parameters: @@ -594,7 +592,6 @@ def finish(self): class TimingResolutionTestTool(EventsLoopNectarCAMCalibrationTool): - """ This class, `TimingResolutionTestTool`, is a subclass of `EventsLoopNectarCAMCalibrationTool` and is used to perform timing resolution tests on NectarCAM data. It reads the output data from the `ToMContainer` dataset and processes the charge, timing, and event information to calculate the timing resolution and mean charge in photoelectrons. @@ -611,17 +608,8 @@ class TimingResolutionTestTool(EventsLoopNectarCAMCalibrationTool): help="List of Component names to be apply, the order will be respected", ).tag(config=True) - def _init_output_path(self): - if self.max_events is None: - filename = f"{self.name}_run{self.run_number}.h5" - else: - filename = f"{self.name}_run{self.run_number}_maxevents{self.max_events}.h5" - self.output_path = pathlib.Path( - f"{os.environ.get('NECTARCAMDATA','/tmp')}/tests/{filename}" - ) - def finish(self, bootstrap=False, *args, **kwargs): - super().finish(return_output_component=True, *args, **kwargs) + super().finish(return_output_component=False, *args, **kwargs) # tom_mu_all= output[0].tom_mu # tom_sigma_all= output[0].tom_sigma @@ -868,7 +856,6 @@ def finish(self, *args, **kwargs): class PedestalContainer(NectarCAMContainer): - """ Attributes of the PedestalContainer class that store various data related to the pedestal of a NectarCAM event. @@ -932,7 +919,6 @@ class PedestalContainer(NectarCAMContainer): class PedestalComp(NectarCAMComponent): - """ The `PedestalComp` class is a NectarCAMComponent that is responsible for processing the pedestal and RMS of the high and low gain waveforms for each event. @@ -1096,6 +1082,12 @@ class UCTSContainer(NectarCAMContainer): ucts_timestamp = Field( type=np.ndarray, dtype=np.uint64, ndim=1, description="events ucts timestamp" ) + mean_event_charge = Field( + type=np.ndarray, + dtype=np.uint32, + ndim=1, + description="average pixel charge for event", + ) event_type = Field( type=np.ndarray, dtype=np.uint8, ndim=1, description="trigger event type" ) @@ -1109,7 +1101,6 @@ class UCTSContainer(NectarCAMContainer): class UCTSComp(NectarCAMComponent): - """ The `__init__` method initializes the `UCTSComp` class, which is a NectarCAMComponent. It sets up several member variables to store UCTS related data, such as timestamps, event types, event IDs, busy counters, and event counters. @@ -1118,7 +1109,19 @@ class UCTSComp(NectarCAMComponent): The `finish` method creates and returns a `UCTSContainer` object, which is a container for the UCTS-related data that was collected during the event loop. """ - def __init__(self, subarray, config=None, parent=None, *args, **kwargs): + window_shift = Integer( + default_value=6, + help="the time in ns before the peak to extract charge", + ).tag(config=True) + + window_width = Integer( + default_value=16, + help="the duration of the extraction window in ns", + ).tag(config=True) + + def __init__( + self, subarray, config=None, parent=None, excl_muons=None, *args, **kwargs + ): super().__init__( subarray=subarray, config=config, parent=parent, *args, **kwargs ) @@ -1129,14 +1132,71 @@ def __init__(self, subarray, config=None, parent=None, *args, **kwargs): self.__event_id = [] self.__ucts_busy_counter = [] self.__ucts_event_counter = [] + self.excl_muons = None + self.__mean_event_charge = [] ##This method need to be defined ! def __call__(self, event: NectarCAMDataContainer, *args, **kwargs): - self.__event_id.append(np.uint32(event.index.event_id)) - self.__event_type.append(event.trigger.event_type.value) - self.__ucts_timestamp.append(event.nectarcam.tel[0].evt.ucts_timestamp) - self.__ucts_busy_counter.append(event.nectarcam.tel[0].evt.ucts_busy_counter) - self.__ucts_event_counter.append(event.nectarcam.tel[0].evt.ucts_event_counter) + take_event = True + + # exclude muon events for the trigger timing test + if self.excl_muons: + wfs = [] + wfs.append(event.r0.tel[0].waveform[constants.HIGH_GAIN][self.pixels_id]) + # print(self.pixels_id) + wf = np.array(wfs[0]) + # print(wf.shape) + index_peak = np.argmax(wf, axis=1) # tom per event/pixel + # print(wf[100]) + # print(index_peak[100]) + index_peak[index_peak < 20] = 20 + index_peak[index_peak > 40] = 40 + signal_start = index_peak - self.window_shift + # signal_stop = index_peak + self.window_width - self.window_shift + # print(index_peak) + # print(signal_start) + # print(signal_stop) + chg = np.zeros(len(self.pixels_id)) + + ped = np.array( + [ + np.mean(wf[pix, 0 : signal_start[pix]]) + for pix in range(len(self.pixels_id)) + ] + ) + + for pix in range(len(self.pixels_id)): + # print("iterating through pixels") + # print("pix", pix) + + y = ( + wf[pix] - ped[pix] + ) # np.maximum(wf[pix] - ped[pix],np.zeros(len(wf[pix]))) + charge_sum = y[ + signal_start[pix] : signal_start[pix] + self.window_width + ].sum() + # print(charge_sum) + chg[pix] = charge_sum + + # is it a good event? + if np.max(chg) > 10 * np.mean(chg): + # print("is not good evt") + take_event = False + mean_charge = np.mean(chg) / 58.0 + + if take_event: + self.__event_id.append(np.uint32(event.index.event_id)) + self.__event_type.append(event.trigger.event_type.value) + self.__ucts_timestamp.append(event.nectarcam.tel[0].evt.ucts_timestamp) + self.__ucts_busy_counter.append( + event.nectarcam.tel[0].evt.ucts_busy_counter + ) + self.__ucts_event_counter.append( + event.nectarcam.tel[0].evt.ucts_event_counter + ) + + if self.excl_muons: + self.__mean_event_charge.append(mean_charge) ##This method need to be defined ! def finish(self): @@ -1147,20 +1207,22 @@ def finish(self): ucts_timestamp=UCTSContainer.fields["ucts_timestamp"].dtype.type( self.__ucts_timestamp ), + mean_event_charge=UCTSContainer.fields["mean_event_charge"].dtype.type( + self.__mean_event_charge + ), + event_type=UCTSContainer.fields["event_type"].dtype.type(self.__event_type), + event_id=UCTSContainer.fields["event_id"].dtype.type(self.__event_id), ucts_busy_counter=UCTSContainer.fields["ucts_busy_counter"].dtype.type( self.__ucts_busy_counter ), ucts_event_counter=UCTSContainer.fields["ucts_event_counter"].dtype.type( self.__ucts_event_counter ), - event_type=UCTSContainer.fields["event_type"].dtype.type(self.__event_type), - event_id=UCTSContainer.fields["event_id"].dtype.type(self.__event_id), ) return output class DeadtimeTestTool(EventsLoopNectarCAMCalibrationTool): - """ The `DeadtimeTestTool` class is an `EventsLoopNectarCAMCalibrationTool` that is used to test the deadtime of NectarCAM. @@ -1188,7 +1250,6 @@ def finish(self, *args, **kwargs): group = output_file[thing] dataset = group["UCTSContainer"] data = dataset[:] - # print("data",data) for tup in data: try: ucts_timestamps.extend(tup[3]) @@ -1226,3 +1287,90 @@ def finish(self, *args, **kwargs): time_tot, deadtime_pc, ) + + +class TriggerTimingTestTool(EventsLoopNectarCAMCalibrationTool): + """ + The `TriggerTimingTestTool` class is an `EventsLoopNectarCAMCalibrationTool` that is used to test the trigger timing of NectarCAM. + + The `finish` method is responsible for reading the data from the HDF5 file, extracting the relevant information (UCTS timestamps), and calculating the RMS value of the difference between consecutive triggers. The method returns the UCTS timestamps, the time differences between consecutive triggers for events concerning more than 10 pixels (non-muon related events). + """ + + name = "TriggerTimingTestTool" + + componentsList = ComponentNameList( + NectarCAMComponent, + default_value=["UCTSComp"], + help="List of Component names to be apply, the order will be respected", + ).tag(config=True) + + def setup(self): + super().setup() + for component in self.components: + if isinstance(component, UCTSComp): + component.excl_muons = True + + def finish(self, *args, **kwargs): + super().finish(return_output_component=False, *args, **kwargs) + # print(self.output_path) + output_file = h5py.File(self.output_path) + + ucts_timestamps = [] + charge_per_event = [] + + for thing in output_file: + group = output_file[thing] + dataset = group["UCTSContainer"] + data = dataset[:] + # print("data",data) + for tup in data: + try: + ucts_timestamps.extend(tup[3]) + charge_per_event.extend(tup[4]) + + except: + break + # print(output_file.keys()) + # tom_mu_all= output[0].tom_mu + # tom_sigma_all= output[0].tom_sigma + # ucts_timestamps= np.array(output_file["ucts_timestamp"]) + ucts_timestamps = np.array(ucts_timestamps).flatten() + + # dt in nanoseconds + delta_t = [ + ucts_timestamps[i] - ucts_timestamps[i - 1] + for i in range(1, len(ucts_timestamps)) + ] + # event_counter = np.array(output_file['ucts_event_counter']) + # busy_counter=np.array(output_file['ucts_busy_counter']) + output_file.close() + + # make hist to get rms value + hist_values, bin_edges = np.histogram(delta_t, bins=50) + # Compute bin centers + bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2 + weighted_mean = np.average(bin_centers, weights=hist_values) + # print("Weighted Mean:", weighted_mean) + + # Compute weighted variance + weighted_variance = np.average( + (bin_centers - weighted_mean) ** 2, weights=hist_values + ) + # print("Weighted Variance:", weighted_variance) + + # Compute RMS value (Standard deviation) + rms = np.sqrt(weighted_variance) + # print("RMS:", rms[pix]) + + # Compute the total number of data points (sum of histogram values, i.e. N) + N = np.sum(hist_values) + # print("Total number of events (N):", N) + + # Error on the standard deviation + err = rms / np.sqrt(2 * N) + # print("Error on RMS:", err[pix]) + + # charge per run + charge_per_run = np.mean(charge_per_event) + + return ucts_timestamps, delta_t, rms, err, charge_per_run diff --git a/src/nectarchain/user_scripts/dmousadi/test_scripts/tests/trigger_timing_test.py b/src/nectarchain/user_scripts/dmousadi/test_scripts/tests/trigger_timing_test.py new file mode 100644 index 00000000..819402ff --- /dev/null +++ b/src/nectarchain/user_scripts/dmousadi/test_scripts/tests/trigger_timing_test.py @@ -0,0 +1,184 @@ +# don't forget to set environment variable NECTARCAMDATA + +import argparse +import os +import pathlib +import pickle +import sys + +import matplotlib.pyplot as plt +import numpy as np +from test_tools_components import TriggerTimingTestTool +from utils import pe2photons + + +def get_args(): + """ + Parses command-line arguments for the deadtime test script. + + Returns: + argparse.ArgumentParser: The parsed command-line arguments. + """ + parser = argparse.ArgumentParser( + description="Trigger Timing Test B-TEL-1410. \n" + + "According to the nectarchain component interface, you have to set a NECTARCAMDATA environment variable in the folder where you have the data from your runs or where you want them to be downloaded.\n" + + "You have to give a list of runs (run numbers with spaces inbetween) and an output directory to save the final plot.\n" + + "If the data is not in NECTARCAMDATA, the files will be downloaded through DIRAC.\n For the purposes of testing this script, default data is from the runs used for this test in the TRR document.\n" + + "You can optionally specify the number of events to be processed (default 1000).\n" + ) + parser.add_argument( + "-r", + "--runlist", + type=int, + nargs="+", + help="List of runs (numbers separated by space)", + required=False, + default=[i for i in range(3259, 3263)], + ) + parser.add_argument( + "-e", + "--evts", + type=int, + help="Number of events to process from each run. Default is 1000", + required=False, + default=1000, + ) + parser.add_argument( + "-o", + "--output", + type=str, + help="Output directory. If none, plot will be saved in current directory", + required=False, + default="./", + ) + parser.add_argument( + "--temp_output", help="Temporary output directory for GUI", default=None + ) + + return parser + + +def main(): + """ + Runs the deadtime test script, which performs deadtime tests B-TEL-1260 and B-TEL-1270. + + The script takes command-line arguments to specify the list of runs, corresponding sources, number of events to process, and output directory. It then processes the data for each run, performs an exponential fit to the deadtime distribution, and generates two plots: + + 1. A plot of deadtime percentage vs. collected trigger rate, with the CTA requirement indicated. + 2. A plot of the rate from the fit vs. the collected trigger rate, with the relative difference shown in the bottom panel. + + The script also saves the generated plots to the specified output directory, and optionally saves them to a temporary output directory for use in a GUI. + """ + + parser = get_args() + args = parser.parse_args() + + runlist = args.runlist + + nevents = args.evts + + output_dir = os.path.abspath(args.output) + temp_output = os.path.abspath(args.temp_output) if args.temp_output else None + + print(f"Output directory: {output_dir}") # Debug print + print(f"Temporary output file: {temp_output}") # Debug print + + sys.argv = sys.argv[:1] + + # ucts_timestamps = np.zeros((len(runlist),nevents)) + # delta_t = np.zeros((len(runlist),nevents-1)) + # event_counter = np.zeros((len(runlist),nevents)) + # busy_counter = np.zeros((len(runlist),nevents)) + # collected_triger_rate = np.zeros(len(runlist)) + # time_tot = np.zeros(len(runlist)) + # deadtime_us=np.zeros((len(runlist),nevents-1)) + # deadtime_pc = np.zeros(len(runlist)) + + rms = [] + err = [] + charge = [] + + nevents = 1000 + + for i, run in enumerate(runlist): + print("PROCESSING RUN {}".format(run)) + tool = TriggerTimingTestTool( + progress_bar=True, + run_number=run, + max_events=nevents, + events_per_slice=10000, + log_level=20, + peak_height=10, + window_width=16, + overwrite=True, + ) + tool.initialize() + tool.setup() + tool.start() + output = tool.finish() + # ucts_timestamps.append(output[0]) + # delta_t.append(output[1]) + rms.append(output[2]) + err.append(output[3]) + charge.append(output[4]) + + rms = np.array(rms) + err = np.array(err) + charge = np.array(charge) + + fig, ax = plt.subplots() + + # Plot the error bars + ax.errorbar(charge, rms, yerr=err, fmt="o", color="blue") + ax.set_xscale("log") + ax.set_yscale("log") + + ax.set_xlabel("Illumination [p.e.]") + ax.set_ylabel("Trigger time resolution [ns]") + ax.set_xlim([10, 500]) + ax.set_ylim([1e-1, 10]) + + # CTA requirement line + cta_requirement_y = 5 # Y-value for the CTA requirement + ax.axhline(y=cta_requirement_y, color="purple", linestyle="--") + + # Add the small vertical arrows starting from the CTA requirement line and pointing downwards + arrow_positions = [20, 80, 200] # X-positions for the arrows + for x_pos in arrow_positions: + ax.annotate( + "", + xy=(x_pos, cta_requirement_y - 2), + xytext=(x_pos, cta_requirement_y), + arrowprops=dict(arrowstyle="->", color="purple", lw=1.5), + ) # Arrow pointing downwards + + # Add the CTA requirement label exactly above the dashed line, centered between arrows + ax.text( + 140, + cta_requirement_y + 0.5, + "CTA requirement", + color="purple", + ha="center", + fontsize=10, + ) + + # Create a second x-axis at the top with illumination in photons (independent scale) + ax2 = ax.twiny() # Create a new twin x-axis + ax2.set_xscale("log") + + # Set the label for the top x-axis + ax2.set_xlabel("Illumination [photons]") + + ax2.set_xlim( + pe2photons(ax.get_xlim()[0]), pe2photons(ax.get_xlim()[1]) + ) # Match limits + + plt.savefig(os.path.join(output_dir, "trigger_timing.png")) + + if temp_output: + with open(os.path.join(args.temp_output, "plot1.pkl"), "wb") as f: + pickle.dump(fig, f) + + +if __name__ == "__main__": + main()