gammasimtools 0.10.0__py3-none-any.whl → 0.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gammasimtools-0.10.0.dist-info → gammasimtools-0.12.0.dist-info}/METADATA +3 -1
- {gammasimtools-0.10.0.dist-info → gammasimtools-0.12.0.dist-info}/RECORD +84 -77
- {gammasimtools-0.10.0.dist-info → gammasimtools-0.12.0.dist-info}/WHEEL +1 -1
- {gammasimtools-0.10.0.dist-info → gammasimtools-0.12.0.dist-info}/entry_points.txt +4 -0
- simtools/_version.py +9 -4
- simtools/applications/convert_all_model_parameters_from_simtel.py +0 -1
- simtools/applications/convert_model_parameter_from_simtel.py +0 -1
- simtools/applications/db_add_file_to_db.py +0 -1
- simtools/applications/db_get_parameter_from_db.py +7 -28
- simtools/applications/derive_mirror_rnda.py +1 -2
- simtools/applications/derive_psf_parameters.py +1 -0
- simtools/applications/docs_produce_array_element_report.py +71 -0
- simtools/applications/docs_produce_model_parameter_reports.py +63 -0
- simtools/applications/generate_corsika_histograms.py +2 -2
- simtools/applications/generate_regular_arrays.py +4 -2
- simtools/applications/production_derive_limits.py +95 -0
- simtools/applications/production_generate_simulation_config.py +15 -29
- simtools/applications/production_scale_events.py +2 -7
- simtools/applications/run_application.py +165 -0
- simtools/applications/simulate_light_emission.py +0 -4
- simtools/applications/submit_model_parameter_from_external.py +11 -6
- simtools/applications/validate_file_using_schema.py +3 -3
- simtools/configuration/commandline_parser.py +30 -1
- simtools/configuration/configurator.py +8 -10
- simtools/corsika/corsika_config.py +11 -10
- simtools/corsika/corsika_histograms.py +4 -6
- simtools/corsika/corsika_histograms_visualize.py +2 -4
- simtools/data_model/metadata_collector.py +18 -9
- simtools/data_model/model_data_writer.py +67 -15
- simtools/data_model/schema.py +10 -3
- simtools/data_model/validate_data.py +70 -24
- simtools/db/db_handler.py +46 -14
- simtools/dependencies.py +112 -0
- simtools/layout/array_layout.py +5 -4
- simtools/model/model_parameter.py +35 -2
- simtools/production_configuration/calculate_statistical_errors_grid_point.py +5 -6
- simtools/production_configuration/event_scaler.py +3 -19
- simtools/production_configuration/generate_simulation_config.py +4 -12
- simtools/production_configuration/interpolation_handler.py +2 -5
- simtools/production_configuration/limits_calculation.py +202 -0
- simtools/reporting/docs_read_parameters.py +310 -0
- simtools/runners/corsika_simtel_runner.py +1 -3
- simtools/schemas/{integration_tests_config.metaschema.yml → application_workflow.metaschema.yml} +51 -27
- simtools/schemas/array_elements.yml +8 -0
- simtools/schemas/model_parameter.metaschema.yml +96 -0
- simtools/schemas/model_parameter_and_data_schema.metaschema.yml +2 -1
- simtools/schemas/model_parameters/correct_nsb_spectrum_to_telescope_altitude.schema.yml +1 -1
- simtools/schemas/model_parameters/corsika_cherenkov_photon_bunch_size.schema.yml +2 -0
- simtools/schemas/model_parameters/corsika_cherenkov_photon_wavelength_range.schema.yml +2 -0
- simtools/schemas/model_parameters/corsika_first_interaction_height.schema.yml +2 -0
- simtools/schemas/model_parameters/corsika_iact_io_buffer.schema.yml +2 -0
- simtools/schemas/model_parameters/corsika_iact_max_bunches.schema.yml +2 -0
- simtools/schemas/model_parameters/corsika_iact_split_auto.schema.yml +2 -0
- simtools/schemas/model_parameters/corsika_longitudinal_shower_development.schema.yml +2 -0
- simtools/schemas/model_parameters/corsika_particle_kinetic_energy_cutoff.schema.yml +2 -0
- simtools/schemas/model_parameters/corsika_starting_grammage.schema.yml +2 -0
- simtools/schemas/model_parameters/iobuf_maximum.schema.yml +1 -1
- simtools/schemas/model_parameters/iobuf_output_maximum.schema.yml +1 -1
- simtools/schemas/model_parameters/lightguide_efficiency_vs_incidence_angle.schema.yml +1 -1
- simtools/schemas/model_parameters/lightguide_efficiency_vs_wavelength.schema.yml +1 -1
- simtools/schemas/model_parameters/min_photoelectrons.schema.yml +1 -1
- simtools/schemas/model_parameters/min_photons.schema.yml +1 -1
- simtools/schemas/model_parameters/random_generator.schema.yml +1 -1
- simtools/schemas/model_parameters/sampled_output.schema.yml +1 -1
- simtools/schemas/model_parameters/save_pe_with_amplitude.schema.yml +1 -1
- simtools/schemas/model_parameters/store_photoelectrons.schema.yml +1 -1
- simtools/schemas/model_parameters/tailcut_scale.schema.yml +1 -1
- simtools/schemas/production_tables.schema.yml +1 -1
- simtools/simtel/simtel_config_reader.py +1 -2
- simtools/simtel/simtel_config_writer.py +1 -2
- simtools/simtel/simtel_io_histogram.py +0 -1
- simtools/simtel/simtel_io_histograms.py +2 -4
- simtools/simtel/simulator_camera_efficiency.py +1 -3
- simtools/simtel/simulator_light_emission.py +2 -5
- simtools/simtel/simulator_ray_tracing.py +1 -3
- simtools/testing/configuration.py +2 -1
- simtools/testing/validate_output.py +23 -13
- simtools/utils/general.py +12 -2
- simtools/utils/names.py +290 -152
- simtools/utils/value_conversion.py +20 -14
- simtools/version.py +2 -2
- simtools/visualization/legend_handlers.py +2 -0
- {gammasimtools-0.10.0.dist-info → gammasimtools-0.12.0.dist-info}/LICENSE +0 -0
- {gammasimtools-0.10.0.dist-info → gammasimtools-0.12.0.dist-info}/top_level.txt +0 -0
simtools/dependencies.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simtools dependencies version management.
|
|
3
|
+
|
|
4
|
+
This modules provides two main functionalities:
|
|
5
|
+
|
|
6
|
+
- retrieve the versions of simtools dependencies (e.g., databases, sim_telarray, CORSIKA)
|
|
7
|
+
- provide space for future implementations of version management
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import simtools.utils.general as gen
|
|
18
|
+
from simtools.db.db_handler import DatabaseHandler
|
|
19
|
+
|
|
20
|
+
_logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_version_string(db_config=None):
|
|
24
|
+
"""Print the versions of the dependencies."""
|
|
25
|
+
return (
|
|
26
|
+
f"Database version: {get_database_version(db_config)}\n"
|
|
27
|
+
f"sim_telarray version: {get_sim_telarray_version()}\n"
|
|
28
|
+
f"CORSIKA version: {get_corsika_version()}\n"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_database_version(db_config):
|
|
33
|
+
"""
|
|
34
|
+
Get the version of the simulation model data base used.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
db_config : dict
|
|
39
|
+
Dictionary containing the database configuration.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
str
|
|
44
|
+
Version of the simulation model data base used.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
if db_config is None:
|
|
48
|
+
return None
|
|
49
|
+
db = DatabaseHandler(db_config)
|
|
50
|
+
return db.mongo_db_config.get("db_simulation_model")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_sim_telarray_version():
|
|
54
|
+
"""
|
|
55
|
+
Get the version of the sim_telarray package using 'sim_telarray --version'.
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
str
|
|
60
|
+
Version of the sim_telarray package.
|
|
61
|
+
"""
|
|
62
|
+
sim_telarray_path = os.getenv("SIMTOOLS_SIMTEL_PATH")
|
|
63
|
+
if sim_telarray_path is None:
|
|
64
|
+
_logger.warning("Environment variable SIMTOOLS_SIMTEL_PATH is not set.")
|
|
65
|
+
return None
|
|
66
|
+
sim_telarray_path = Path(sim_telarray_path) / "sim_telarray" / "bin" / "sim_telarray"
|
|
67
|
+
|
|
68
|
+
# expect stdout with e.g. a line 'Release: 2024.271.0 from 2024-09-27'
|
|
69
|
+
result = subprocess.run(
|
|
70
|
+
[sim_telarray_path, "--version"],
|
|
71
|
+
capture_output=True,
|
|
72
|
+
text=True,
|
|
73
|
+
check=False,
|
|
74
|
+
)
|
|
75
|
+
match = re.search(r"^Release:\s+(.+)", result.stdout, re.MULTILINE)
|
|
76
|
+
|
|
77
|
+
if match:
|
|
78
|
+
return match.group(1).split()[0]
|
|
79
|
+
raise ValueError(f"sim_telarray release not found in {result.stdout}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_corsika_version():
|
|
83
|
+
"""
|
|
84
|
+
Get the version of the corsika package.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
str
|
|
89
|
+
Version of the corsika package.
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
build_opts = get_build_options()
|
|
93
|
+
except (FileNotFoundError, TypeError):
|
|
94
|
+
_logger.warning("CORSIKA version not implemented yet.")
|
|
95
|
+
return None
|
|
96
|
+
return build_opts.get("corsika_version")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_build_options():
|
|
100
|
+
"""
|
|
101
|
+
Return CORSIKA / sim_telarray build options.
|
|
102
|
+
|
|
103
|
+
Expects a build_opts.yml file in the sim_telarray directory.
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
return gen.collect_data_from_file(
|
|
107
|
+
Path(os.getenv("SIMTOOLS_SIMTEL_PATH")) / "build_opts.yml"
|
|
108
|
+
)
|
|
109
|
+
except FileNotFoundError as exc:
|
|
110
|
+
raise FileNotFoundError("No build_opts.yml file found.") from exc
|
|
111
|
+
except TypeError as exc:
|
|
112
|
+
raise TypeError("SIMTOOLS_SIMTEL_PATH not defined.") from exc
|
simtools/layout/array_layout.py
CHANGED
|
@@ -445,9 +445,11 @@ class ArrayLayout:
|
|
|
445
445
|
)
|
|
446
446
|
try:
|
|
447
447
|
tel_model = self._get_telescope_model(telescope_name)
|
|
448
|
-
except ValueError:
|
|
448
|
+
except ValueError: # telescope not found in the database revert to design model
|
|
449
449
|
tel_model = self._get_telescope_model(
|
|
450
|
-
names.
|
|
450
|
+
names.array_element_design_types(
|
|
451
|
+
names.get_array_element_type_from_name(telescope_name)
|
|
452
|
+
)[0]
|
|
451
453
|
)
|
|
452
454
|
|
|
453
455
|
for para in ("telescope_axis_height", "telescope_sphere_radius"):
|
|
@@ -716,8 +718,7 @@ class ArrayLayout:
|
|
|
716
718
|
set(_telescope_list_from_name + _telescope_list_from_type)
|
|
717
719
|
)
|
|
718
720
|
self._logger.info(
|
|
719
|
-
f"Selected {len(self._telescope_list)} telescopes"
|
|
720
|
-
f" (from originally {_n_telescopes})"
|
|
721
|
+
f"Selected {len(self._telescope_list)} telescopes (from originally {_n_telescopes})"
|
|
721
722
|
)
|
|
722
723
|
except TypeError:
|
|
723
724
|
self._logger.info("No asset list provided, keeping all telescopes")
|
|
@@ -76,6 +76,9 @@ class ModelParameter:
|
|
|
76
76
|
if array_element_name is not None
|
|
77
77
|
else None
|
|
78
78
|
)
|
|
79
|
+
self.design_model = self.db.get_design_model(
|
|
80
|
+
self.model_version, self.name, collection="telescopes"
|
|
81
|
+
)
|
|
79
82
|
self._config_file_directory = None
|
|
80
83
|
self._config_file_path = None
|
|
81
84
|
self._load_parameters_from_db()
|
|
@@ -239,6 +242,22 @@ class ModelParameter:
|
|
|
239
242
|
self._logger.debug(f"Parameter {par_name} does not have a file associated with it.")
|
|
240
243
|
return False
|
|
241
244
|
|
|
245
|
+
def get_parameter_version(self, par_name):
|
|
246
|
+
"""
|
|
247
|
+
Get version for a given parameter used in the model.
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
par_name: str
|
|
252
|
+
Name of the parameter.
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
str
|
|
257
|
+
parameter version used in the model (eg. '1.0.0')
|
|
258
|
+
"""
|
|
259
|
+
return self._get_parameter_dict(par_name)["parameter_version"]
|
|
260
|
+
|
|
242
261
|
def print_parameters(self):
|
|
243
262
|
"""Print parameters and their values for debugging purposes."""
|
|
244
263
|
for par in self._parameters:
|
|
@@ -281,6 +300,21 @@ class ModelParameter:
|
|
|
281
300
|
"""
|
|
282
301
|
return self._simulation_config_parameters.get(simulation_software)
|
|
283
302
|
|
|
303
|
+
def has_parameter(self, par_name):
|
|
304
|
+
"""Check if a parameter exists in the model.
|
|
305
|
+
|
|
306
|
+
Parameters
|
|
307
|
+
----------
|
|
308
|
+
par_name : str
|
|
309
|
+
Name of the parameter.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
bool
|
|
314
|
+
True if parameter exists in the model.
|
|
315
|
+
"""
|
|
316
|
+
return par_name in self._parameters
|
|
317
|
+
|
|
284
318
|
def _load_simulation_software_parameter(self):
|
|
285
319
|
"""Read simulation software parameters from DB."""
|
|
286
320
|
for simulation_software in self._simulation_config_parameters:
|
|
@@ -409,8 +443,7 @@ class ModelParameter:
|
|
|
409
443
|
)
|
|
410
444
|
|
|
411
445
|
self._logger.debug(
|
|
412
|
-
f"Changing parameter {par_name} "
|
|
413
|
-
f"from {self.get_parameter_value(par_name)} to {value}"
|
|
446
|
+
f"Changing parameter {par_name} from {self.get_parameter_value(par_name)} to {value}"
|
|
414
447
|
)
|
|
415
448
|
self._parameters[par_name]["value"] = value
|
|
416
449
|
|
|
@@ -81,8 +81,7 @@ class StatisticalErrorEvaluator:
|
|
|
81
81
|
unique_zeniths = 90 * u.deg - np.unique(events_data["PNT_ALT"]) * u.deg
|
|
82
82
|
if len(unique_azimuths) > 1 or len(unique_zeniths) > 1:
|
|
83
83
|
msg = (
|
|
84
|
-
f"Multiple values found for azimuth ({unique_azimuths}) "
|
|
85
|
-
f"zenith ({unique_zeniths})."
|
|
84
|
+
f"Multiple values found for azimuth ({unique_azimuths}) zenith ({unique_zeniths})."
|
|
86
85
|
)
|
|
87
86
|
self._logger.error(msg)
|
|
88
87
|
raise ValueError(msg)
|
|
@@ -326,15 +325,15 @@ class StatisticalErrorEvaluator:
|
|
|
326
325
|
def calculate_metrics(self):
|
|
327
326
|
"""Calculate all defined metrics as specified in self.metrics and store results."""
|
|
328
327
|
if "uncertainty_effective_area" in self.metrics:
|
|
329
|
-
|
|
330
328
|
self.uncertainty_effective_area = self.calculate_uncertainty_effective_area()
|
|
331
329
|
if self.uncertainty_effective_area:
|
|
332
330
|
energy_range = self.metrics.get("uncertainty_effective_area", {}).get(
|
|
333
331
|
"energy_range"
|
|
334
332
|
)
|
|
335
|
-
min_energy, max_energy =
|
|
336
|
-
energy_range["unit"]
|
|
337
|
-
|
|
333
|
+
min_energy, max_energy = (
|
|
334
|
+
energy_range["value"][0] * u.Unit(energy_range["unit"]),
|
|
335
|
+
energy_range["value"][1] * u.Unit(energy_range["unit"]),
|
|
336
|
+
)
|
|
338
337
|
|
|
339
338
|
valid_errors = [
|
|
340
339
|
error
|
|
@@ -19,21 +19,18 @@ class EventScaler:
|
|
|
19
19
|
Supports scaling both the entire dataset and specific grid points like energy values.
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
def __init__(self, evaluator,
|
|
22
|
+
def __init__(self, evaluator, metrics: dict):
|
|
23
23
|
"""
|
|
24
|
-
Initialize the EventScaler with the evaluator
|
|
24
|
+
Initialize the EventScaler with the evaluator and metrics.
|
|
25
25
|
|
|
26
26
|
Parameters
|
|
27
27
|
----------
|
|
28
28
|
evaluator : StatisticalErrorEvaluator
|
|
29
29
|
The evaluator responsible for calculating metrics and handling event data.
|
|
30
|
-
science_case : str
|
|
31
|
-
The science case used to adjust the uncertainty factor.
|
|
32
30
|
metrics : dict
|
|
33
31
|
Dictionary containing metrics, including target error for effective area.
|
|
34
32
|
"""
|
|
35
33
|
self.evaluator = evaluator
|
|
36
|
-
self.science_case = science_case
|
|
37
34
|
self.metrics = metrics
|
|
38
35
|
|
|
39
36
|
def scale_events(self, return_sum: bool = True) -> u.Quantity:
|
|
@@ -77,20 +74,7 @@ class EventScaler:
|
|
|
77
74
|
"value"
|
|
78
75
|
]
|
|
79
76
|
|
|
80
|
-
return (
|
|
81
|
-
current_max_error / target_max_error
|
|
82
|
-
) ** 2 * self._apply_science_case_scaling_factor()
|
|
83
|
-
|
|
84
|
-
def _apply_science_case_scaling_factor(self) -> float:
|
|
85
|
-
"""
|
|
86
|
-
Apply the uncertainty factor based on the science case.
|
|
87
|
-
|
|
88
|
-
Returns
|
|
89
|
-
-------
|
|
90
|
-
float
|
|
91
|
-
The final scaling factor after applying uncertainty.
|
|
92
|
-
"""
|
|
93
|
-
return 1 if self.science_case == "science case 1" else 1.0
|
|
77
|
+
return (current_max_error / target_max_error) ** 2
|
|
94
78
|
|
|
95
79
|
def _number_of_simulated_events(self) -> u.Quantity:
|
|
96
80
|
"""
|
|
@@ -16,10 +16,6 @@ class SimulationConfig:
|
|
|
16
16
|
----------
|
|
17
17
|
grid_point : dict
|
|
18
18
|
Dictionary representing a grid point with azimuth, elevation, and night sky background.
|
|
19
|
-
ctao_data_level : str
|
|
20
|
-
The data level (e.g., 'A', 'B', 'C') for the simulation configuration.
|
|
21
|
-
science_case : str
|
|
22
|
-
The science case for the simulation configuration.
|
|
23
19
|
file_path : str
|
|
24
20
|
Path to the DL2 MC event file for statistical uncertainty evaluation.
|
|
25
21
|
file_type : str
|
|
@@ -31,26 +27,22 @@ class SimulationConfig:
|
|
|
31
27
|
def __init__(
|
|
32
28
|
self,
|
|
33
29
|
grid_point: dict[str, float],
|
|
34
|
-
ctao_data_level: str,
|
|
35
|
-
science_case: str,
|
|
36
30
|
file_path: str,
|
|
37
31
|
file_type: str,
|
|
38
32
|
metrics: dict[str, float] | None = None,
|
|
39
33
|
):
|
|
40
34
|
"""Initialize the simulation configuration for a grid point."""
|
|
41
35
|
self.grid_point = grid_point
|
|
42
|
-
self.ctao_data_level = ctao_data_level
|
|
43
|
-
self.science_case = science_case
|
|
44
36
|
self.file_path = file_path
|
|
45
37
|
self.file_type = file_type
|
|
46
38
|
self.metrics = metrics or {}
|
|
47
39
|
self.evaluator = StatisticalErrorEvaluator(file_path, file_type, metrics)
|
|
48
|
-
self.event_scaler = EventScaler(self.evaluator,
|
|
40
|
+
self.event_scaler = EventScaler(self.evaluator, self.metrics)
|
|
49
41
|
self.simulation_params = {}
|
|
50
42
|
|
|
51
43
|
def configure_simulation(self) -> dict[str, float]:
|
|
52
44
|
"""
|
|
53
|
-
Configure the simulation parameters for the grid point
|
|
45
|
+
Configure the simulation parameters for the grid point.
|
|
54
46
|
|
|
55
47
|
Returns
|
|
56
48
|
-------
|
|
@@ -80,7 +72,7 @@ class SimulationConfig:
|
|
|
80
72
|
|
|
81
73
|
def _calculate_core_scatter_area(self) -> float:
|
|
82
74
|
"""
|
|
83
|
-
Calculate the core scatter area based on the grid point
|
|
75
|
+
Calculate the core scatter area based on the grid point.
|
|
84
76
|
|
|
85
77
|
Returns
|
|
86
78
|
-------
|
|
@@ -93,7 +85,7 @@ class SimulationConfig:
|
|
|
93
85
|
|
|
94
86
|
def _calculate_viewcone(self) -> float:
|
|
95
87
|
"""
|
|
96
|
-
Calculate the viewcone based on the grid point conditions
|
|
88
|
+
Calculate the viewcone based on the grid point conditions.
|
|
97
89
|
|
|
98
90
|
Returns
|
|
99
91
|
-------
|
|
@@ -12,13 +12,10 @@ __all__ = ["InterpolationHandler"]
|
|
|
12
12
|
class InterpolationHandler:
|
|
13
13
|
"""Handle interpolation between multiple StatisticalErrorEvaluator instances."""
|
|
14
14
|
|
|
15
|
-
def __init__(self, evaluators,
|
|
15
|
+
def __init__(self, evaluators, metrics: dict):
|
|
16
16
|
self.evaluators = evaluators
|
|
17
|
-
self.science_case = science_case
|
|
18
17
|
self.metrics = metrics
|
|
19
|
-
self.event_scalers = [
|
|
20
|
-
EventScaler(e, self.science_case, self.metrics) for e in self.evaluators
|
|
21
|
-
]
|
|
18
|
+
self.event_scalers = [EventScaler(e, self.metrics) for e in self.evaluators]
|
|
22
19
|
|
|
23
20
|
self.azimuths = [e.grid_point[1].to(u.deg).value for e in self.evaluators]
|
|
24
21
|
self.zeniths = [e.grid_point[2].to(u.deg).value for e in self.evaluators]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Calculate the thresholds for energy, radial distance, and viewcone."""
|
|
2
|
+
|
|
3
|
+
import astropy.units as u
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LimitCalculator:
|
|
8
|
+
"""
|
|
9
|
+
Compute thresholds/limits for energy, radial distance, and viewcone.
|
|
10
|
+
|
|
11
|
+
Histograms are generated with simtools-generate-simtel-array-histograms with --hdf5 flag.
|
|
12
|
+
|
|
13
|
+
Event data is read from the generated HDF5 file from the following tables:
|
|
14
|
+
- angle_to_observing_position__triggered_showers_ for the viewcone limit.
|
|
15
|
+
- event_weight__ra3d__log10_e__ for the energy and radial distance limit.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
event_data_file : list of astropy.table.Table
|
|
21
|
+
The list of tables containing the event data.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, event_data_file_tables):
|
|
25
|
+
"""
|
|
26
|
+
Initialize the LimitCalculator with the given event data file.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
event_data_file : list of astropy.table.Table
|
|
31
|
+
The list of tables containing the event data.
|
|
32
|
+
"""
|
|
33
|
+
self.angle_to_observing_position__triggered_showers_ = None
|
|
34
|
+
self.event_weight__ra3d__log10_e__ = None
|
|
35
|
+
|
|
36
|
+
for table in event_data_file_tables:
|
|
37
|
+
if (
|
|
38
|
+
"Title" in table.meta
|
|
39
|
+
and table.meta["Title"] == "angle_to_observing_position__triggered_showers_"
|
|
40
|
+
):
|
|
41
|
+
self.angle_to_observing_position__triggered_showers_ = table
|
|
42
|
+
elif "Title" in table.meta and table.meta["Title"] == "event_weight__ra3d__log10_e__":
|
|
43
|
+
self.event_weight__ra3d__log10_e__ = table
|
|
44
|
+
|
|
45
|
+
def _compute_limits(
|
|
46
|
+
self, event_weight_array, bin_edges, loss_fraction, axis=0, limit_type="lower"
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Compute the limits based on the loss fraction.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
event_weight_array : np.ndarray
|
|
54
|
+
Array of event weights.
|
|
55
|
+
bin_edges : np.ndarray
|
|
56
|
+
Array of bin edges.
|
|
57
|
+
loss_fraction : float
|
|
58
|
+
Fraction of events to be lost.
|
|
59
|
+
axis : int, optional
|
|
60
|
+
Axis along which to sum the event weights. Default is 0.
|
|
61
|
+
limit_type : str, optional
|
|
62
|
+
Type of limit ('lower' or 'upper'). Default is 'lower'.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
int
|
|
67
|
+
Bin index where the threshold is reached.
|
|
68
|
+
float
|
|
69
|
+
Bin edge value corresponding to the threshold.
|
|
70
|
+
"""
|
|
71
|
+
projection = np.sum(event_weight_array, axis=axis)
|
|
72
|
+
bin_edge_value = None
|
|
73
|
+
cumulative_sum = None
|
|
74
|
+
if limit_type == "upper":
|
|
75
|
+
cumulative_sum = np.cumsum(projection)
|
|
76
|
+
|
|
77
|
+
elif limit_type == "lower":
|
|
78
|
+
cumulative_sum = np.cumsum(projection[::-1])
|
|
79
|
+
|
|
80
|
+
total_events = np.sum(projection)
|
|
81
|
+
threshold = (1 - loss_fraction) * total_events
|
|
82
|
+
bin_index = np.searchsorted(cumulative_sum, threshold)
|
|
83
|
+
if limit_type == "upper":
|
|
84
|
+
bin_edge_value = bin_edges[bin_index]
|
|
85
|
+
elif limit_type == "lower":
|
|
86
|
+
bin_edge_value = bin_edges[-bin_index]
|
|
87
|
+
return bin_index, bin_edge_value
|
|
88
|
+
|
|
89
|
+
def get_bin_edges_and_units(self, table, axis="x"):
|
|
90
|
+
"""
|
|
91
|
+
Extract bin edges and units from the table metadata.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
table : astropy.table.Table
|
|
96
|
+
Table containing the event data.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
tuple
|
|
101
|
+
Tuple containing the bin edges and their units.
|
|
102
|
+
"""
|
|
103
|
+
bin_edges = table.meta[f"{axis}_bin_edges"]
|
|
104
|
+
try:
|
|
105
|
+
bin_edges_unit = table.meta[f"{axis}_bin_edges_unit"]
|
|
106
|
+
except KeyError:
|
|
107
|
+
bin_edges_unit = ""
|
|
108
|
+
return bin_edges, bin_edges_unit
|
|
109
|
+
|
|
110
|
+
def compute_lower_energy_limit(self, loss_fraction):
|
|
111
|
+
"""
|
|
112
|
+
Compute the lower energy limit in TeV based on the event loss fraction.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
loss_fraction : float
|
|
117
|
+
Fraction of events to be lost.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
astropy.units.Quantity
|
|
122
|
+
Lower energy limit.
|
|
123
|
+
"""
|
|
124
|
+
event_weight_array = np.column_stack(
|
|
125
|
+
[
|
|
126
|
+
self.event_weight__ra3d__log10_e__[name]
|
|
127
|
+
for name in self.event_weight__ra3d__log10_e__.dtype.names
|
|
128
|
+
]
|
|
129
|
+
)
|
|
130
|
+
bin_edges, bin_edges_unit = self.get_bin_edges_and_units(
|
|
131
|
+
self.event_weight__ra3d__log10_e__, axis="y"
|
|
132
|
+
)
|
|
133
|
+
if bin_edges_unit == "":
|
|
134
|
+
bin_edges_unit = "TeV"
|
|
135
|
+
_, lower_bin_edge_value = self._compute_limits(
|
|
136
|
+
event_weight_array, bin_edges, loss_fraction, axis=0, limit_type="lower"
|
|
137
|
+
)
|
|
138
|
+
return (10**lower_bin_edge_value) * u.Unit(bin_edges_unit)
|
|
139
|
+
|
|
140
|
+
def compute_upper_radial_distance(self, loss_fraction):
|
|
141
|
+
"""
|
|
142
|
+
Compute the upper radial distance based on the event loss fraction.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
loss_fraction : float
|
|
147
|
+
Fraction of events to be lost.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
astropy.units.Quantity
|
|
152
|
+
Upper radial distance in m.
|
|
153
|
+
"""
|
|
154
|
+
event_weight_array = np.column_stack(
|
|
155
|
+
[
|
|
156
|
+
self.event_weight__ra3d__log10_e__[name]
|
|
157
|
+
for name in self.event_weight__ra3d__log10_e__.dtype.names
|
|
158
|
+
]
|
|
159
|
+
)
|
|
160
|
+
bin_edges, bin_edges_unit = self.get_bin_edges_and_units(
|
|
161
|
+
self.event_weight__ra3d__log10_e__, axis="x"
|
|
162
|
+
)
|
|
163
|
+
if bin_edges_unit == "":
|
|
164
|
+
bin_edges_unit = "m"
|
|
165
|
+
_, upper_bin_edge_value = self._compute_limits(
|
|
166
|
+
event_weight_array, bin_edges, loss_fraction, axis=1, limit_type="upper"
|
|
167
|
+
)
|
|
168
|
+
return upper_bin_edge_value * u.Unit(bin_edges_unit)
|
|
169
|
+
|
|
170
|
+
def compute_viewcone(self, loss_fraction):
|
|
171
|
+
"""
|
|
172
|
+
Compute the viewcone based on the event loss fraction.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
loss_fraction : float
|
|
177
|
+
Fraction of events to be lost.
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
astropy.units.Quantity
|
|
182
|
+
Viewcone radius in degrees.
|
|
183
|
+
"""
|
|
184
|
+
angle_to_observing_position__triggered_showers = np.column_stack(
|
|
185
|
+
[
|
|
186
|
+
self.angle_to_observing_position__triggered_showers_[name]
|
|
187
|
+
for name in self.angle_to_observing_position__triggered_showers_.dtype.names
|
|
188
|
+
]
|
|
189
|
+
)
|
|
190
|
+
bin_edges, bin_edges_unit = self.get_bin_edges_and_units(
|
|
191
|
+
self.angle_to_observing_position__triggered_showers_, axis="x"
|
|
192
|
+
)
|
|
193
|
+
if bin_edges_unit == "":
|
|
194
|
+
bin_edges_unit = "deg"
|
|
195
|
+
_, upper_bin_edge_value = self._compute_limits(
|
|
196
|
+
angle_to_observing_position__triggered_showers,
|
|
197
|
+
bin_edges,
|
|
198
|
+
loss_fraction,
|
|
199
|
+
axis=0,
|
|
200
|
+
limit_type="upper",
|
|
201
|
+
)
|
|
202
|
+
return upper_bin_edge_value * u.Unit(bin_edges_unit)
|