Source code for production_configuration.derive_corsika_limits

"""Calculate CORSIKA thresholds for energy, radial distance, and viewcone."""

import logging

import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np

from simtools.simtel.simtel_io_event_reader import SimtelIOEventDataReader


[docs] class LimitCalculator: """ Compute thresholds for CORSIKA configuration for energy, radial distance, and viewcone. Event data is read from the reduced MC event data file. Parameters ---------- event_data_file : str Path to the HDF5 file containing the event data. telescope_list : list, optional List of telescope IDs to filter the events (default is None). """ def __init__(self, event_data_file, telescope_list=None): """Initialize the LimitCalculator with the given event data file.""" self._logger = logging.getLogger(__name__) self.event_data_file = event_data_file self.telescope_list = telescope_list self.reader = SimtelIOEventDataReader(event_data_file, telescope_list=telescope_list) self.event_data = self.reader.triggered_shower_data self.triggered_data = self.reader.triggered_data def _compute_limits(self, hist, bin_edges, loss_fraction, limit_type="lower"): """ Compute the limits based on the loss fraction. Parameters ---------- hist : np.ndarray 1D histogram array. bin_edges : np.ndarray Array of bin edges. loss_fraction : float Fraction of events to be lost. limit_type : str, optional Type of limit ('lower' or 'upper'). Default is 'lower'. Returns ------- float Bin edge value corresponding to the threshold. """ cumulative_sum = np.cumsum(hist) if limit_type == "upper" else np.cumsum(hist[::-1]) total_events = np.sum(hist) threshold = (1 - loss_fraction) * total_events bin_index = np.searchsorted(cumulative_sum, threshold) return bin_edges[bin_index] if limit_type == "upper" else bin_edges[-bin_index] @property def energy_bins(self): """Return bins for the energy histogram.""" return np.logspace( np.log10(self.event_data.simulated_energy.min()), np.log10(self.event_data.simulated_energy.max()), 1000, )
[docs] def compute_lower_energy_limit(self, loss_fraction): """ Compute the lower energy limit in TeV based on the event loss fraction. Parameters ---------- loss_fraction : float Fraction of events to be lost. Returns ------- astropy.units.Quantity Lower energy limit. """ hist, _ = np.histogram(self.event_data.simulated_energy, bins=self.energy_bins) return ( self._compute_limits(hist, self.energy_bins, loss_fraction, limit_type="lower") * u.TeV )
@property def core_distance_bins(self): """Return bins for the core distance histogram.""" return np.linspace( self.event_data.core_distance_shower.min(), self.event_data.core_distance_shower.max(), 1000, )
[docs] def compute_upper_radial_distance(self, loss_fraction): """ Compute the upper radial distance based on the event loss fraction. Parameters ---------- loss_fraction : float Fraction of events to be lost. Returns ------- astropy.units.Quantity Upper radial distance in m. """ hist, _ = np.histogram(self.event_data.core_distance_shower, bins=self.core_distance_bins) return ( self._compute_limits(hist, self.core_distance_bins, loss_fraction, limit_type="upper") * u.m )
@property def view_cone_bins(self): """Return bins for the viewcone histogram.""" return np.linspace( self.triggered_data.angular_distance.min(), self.triggered_data.angular_distance.max(), 1000, )
[docs] def compute_viewcone(self, loss_fraction): """ Compute the viewcone based on the event loss fraction. The shower IDs of triggered events are used to create a mask for the azimuth and altitude of the triggered events. A mapping is created between the triggered events and the simulated events using the shower IDs. Parameters ---------- loss_fraction : float Fraction of events to be lost. Returns ------- astropy.units.Quantity Viewcone radius in degrees. """ hist, _ = np.histogram(self.triggered_data.angular_distance, bins=self.view_cone_bins) return ( self._compute_limits(hist, self.view_cone_bins, loss_fraction, limit_type="upper") * u.deg )
[docs] def plot_data(self, lower_energy_limit, upper_radial_distance, viewcone, output_path=None): """ Plot the core distances and energies of triggered events. Parameters ---------- lower_energy_limit: astropy.units.Quantity Lower energy limit to display on plots. upper_radial_distance: astropy.units.Quantity Upper radial distance limit to display on plots. viewcone: astropy.units.Quantity Viewcone radius to display on plots. output_path: Path or str, optional Directory to save plots. If None, plots will be displayed. """ event_counts = "Event Count" plots = { "core_vs_energy": { "x_data": self.event_data.core_distance_shower, "y_data": self.event_data.simulated_energy, "bins": [self.core_distance_bins, self.energy_bins], "plot_type": "histogram2d", "plot_params": {"norm": "log", "cmap": "viridis"}, "labels": { "x": "Core Distance [m]", "y": "Energy [TeV]", "title": "Triggered events: core distance vs energy", }, "scales": {"y": "log"}, "colorbar_label": event_counts, "filename": "core_vs_energy_distribution.png", }, "energy_distribution": { "x_data": self.event_data.simulated_energy, "bins": np.logspace(-3, 0.0, 100), "plot_type": "histogram", "plot_params": {"histtype": "step", "color": "k", "lw": 2}, "labels": { "x": "Energy [TeV]", "y": event_counts, "title": "Triggered events: energy distribution", }, "scales": {"x": "log", "y": "log"}, "lines": {"x": lower_energy_limit.value}, "filename": "energy_distribution.png", }, "core_distance": { "x_data": self.event_data.core_distance_shower, "bins": self.core_distance_bins, "plot_type": "histogram", "plot_params": {"histtype": "step", "color": "k", "lw": 2}, "labels": { "x": "Core Distance [m]", "y": event_counts, "title": "Triggered events: core distance distribution", }, "lines": {"x": upper_radial_distance.value}, "filename": "core_distance_distribution.png", }, "core_xy": { "x_data": self.event_data.x_core_shower, "y_data": self.event_data.y_core_shower, "bins": 100, "plot_type": "histogram2d", "plot_params": {"norm": "log", "cmap": "viridis"}, "labels": { "x": "Core X [m]", "y": "Core Y [m]", "title": "Triggered events: core x vs core y", }, "colorbar_label": event_counts, "lines": {"x": upper_radial_distance.value, "y": upper_radial_distance.value}, "filename": "core_xy_distribution.png", }, "view-cone": { "x_data": self.triggered_data.angular_distance, "bins": self.view_cone_bins, "plot_type": "histogram", "plot_params": {"histtype": "step", "color": "k", "lw": 2}, "labels": { "x": "Distance to pointing direction [deg]", "y": event_counts, "title": "Triggered events: viewcone distribution", }, "lines": {"x": viewcone.value}, "filename": "viewcone_distribution.png", }, } for _, plot_args in plots.items(): filename = plot_args.pop("filename") output_file = output_path / filename if output_path else None self._create_plot(**plot_args, output_file=output_file)
def _create_plot( self, x_data, y_data=None, bins=None, plot_type="histogram", plot_params=None, labels=None, scales=None, colorbar_label=None, output_file=None, lines=None, ): """ Create and save a plot with the given parameters. Parameters ---------- x_data : array-like Data for the x-axis or primary data for histograms. y_data : array-like, optional Data for the y-axis in scatter or 2D histograms. bins : int, array-like, or list, optional Bins specification for histograms. plot_type : str, optional Type of plot: 'histogram', 'histogram2d', or 'scatter'. plot_params : dict, optional Additional parameters to pass to the plotting function. labels : dict, optional Dictionary containing 'x', 'y', and 'title' labels. scales : dict, optional Dictionary containing 'x' and 'y' scale types ('log' or 'linear'). colorbar_label : str, optional Label for the colorbar in 2D histograms. output_file : Path, optional File path to save the plot. If not provided, the plot will be displayed. lines : dict, optional Dictionary containing 'x' and 'y' values for reference lines. Returns ------- matplotlib.figure.Figure The created figure object. """ fig = plt.figure(figsize=(8, 6)) plot_params = plot_params or {} labels = labels or {} scales = scales or {} lines = lines or {} if plot_type == "histogram": plt.hist(x_data, bins=bins, **plot_params) elif plot_type == "histogram2d": plt.hist2d(x_data, y_data, bins=bins, **plot_params) if colorbar_label: plt.colorbar(label=colorbar_label) elif plot_type == "scatter": plt.scatter(x_data, y_data, **plot_params) if "x" in lines: plt.axvline(lines["x"], color="r", linestyle="--") if "y" in lines: plt.axhline(lines["y"], color="r", linestyle="--") plt.xlabel(labels.get("x", "")) plt.ylabel(labels.get("y", "")) plt.title(labels.get("title", "")) if "x" in scales: plt.xscale(scales["x"]) if "y" in scales: plt.yscale(scales["y"]) if output_file: self._logger.info(f"Saving plot to {output_file}") plt.savefig(output_file, dpi=300, bbox_inches="tight") plt.close() else: plt.tight_layout() plt.show() return fig