gammasimtools 0.19.0__py3-none-any.whl → 0.21.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.19.0.dist-info → gammasimtools-0.21.0.dist-info}/METADATA +1 -3
- {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/RECORD +54 -51
- {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/entry_points.txt +3 -3
- simtools/_version.py +2 -2
- simtools/applications/calculate_incident_angles.py +182 -0
- simtools/applications/db_add_simulation_model_from_repository_to_db.py +17 -14
- simtools/applications/db_add_value_from_json_to_db.py +6 -9
- simtools/applications/db_generate_compound_indexes.py +7 -3
- simtools/applications/db_get_file_from_db.py +11 -23
- simtools/applications/derive_psf_parameters.py +58 -39
- simtools/applications/derive_trigger_rates.py +91 -0
- simtools/applications/generate_corsika_histograms.py +7 -184
- simtools/applications/maintain_simulation_model_add_production.py +105 -0
- simtools/applications/plot_simtel_events.py +5 -189
- simtools/applications/print_version.py +8 -7
- simtools/applications/validate_file_using_schema.py +7 -4
- simtools/configuration/commandline_parser.py +17 -11
- simtools/corsika/corsika_histograms.py +81 -0
- simtools/data_model/validate_data.py +8 -3
- simtools/db/db_handler.py +122 -31
- simtools/db/db_model_upload.py +51 -30
- simtools/dependencies.py +10 -5
- simtools/layout/array_layout_utils.py +37 -5
- simtools/model/array_model.py +18 -1
- simtools/model/model_repository.py +118 -63
- simtools/model/site_model.py +25 -0
- simtools/production_configuration/derive_corsika_limits.py +9 -34
- simtools/ray_tracing/incident_angles.py +706 -0
- simtools/ray_tracing/psf_parameter_optimisation.py +999 -565
- simtools/schemas/model_parameter_and_data_schema.metaschema.yml +2 -2
- simtools/schemas/model_parameters/nsb_reference_spectrum.schema.yml +1 -1
- simtools/schemas/model_parameters/nsb_spectrum.schema.yml +22 -29
- simtools/schemas/model_parameters/stars.schema.yml +1 -1
- simtools/schemas/production_tables.schema.yml +5 -0
- simtools/simtel/simtel_config_writer.py +18 -20
- simtools/simtel/simtel_io_event_histograms.py +253 -516
- simtools/simtel/simtel_io_event_reader.py +51 -2
- simtools/simtel/simtel_io_event_writer.py +31 -11
- simtools/simtel/simtel_io_metadata.py +1 -1
- simtools/simtel/simtel_table_reader.py +3 -3
- simtools/simulator.py +1 -4
- simtools/telescope_trigger_rates.py +119 -0
- simtools/testing/log_inspector.py +13 -11
- simtools/utils/geometry.py +20 -0
- simtools/version.py +89 -0
- simtools/{corsika/corsika_histograms_visualize.py → visualization/plot_corsika_histograms.py} +109 -0
- simtools/visualization/plot_incident_angles.py +431 -0
- simtools/visualization/plot_psf.py +673 -0
- simtools/visualization/plot_simtel_event_histograms.py +376 -0
- simtools/visualization/{simtel_event_plots.py → plot_simtel_events.py} +284 -87
- simtools/visualization/visualize.py +1 -3
- simtools/applications/calculate_trigger_rate.py +0 -187
- simtools/applications/generate_sim_telarray_histograms.py +0 -196
- simtools/applications/maintain_simulation_model_add_production_table.py +0 -71
- simtools/simtel/simtel_io_histogram.py +0 -623
- simtools/simtel/simtel_io_histograms.py +0 -556
- {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/WHEEL +0 -0
- {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/licenses/LICENSE +0 -0
- {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/top_level.txt +0 -0
|
@@ -9,7 +9,7 @@ from astropy.coordinates import angular_separation
|
|
|
9
9
|
|
|
10
10
|
from simtools.corsika.primary_particle import PrimaryParticle
|
|
11
11
|
from simtools.io import table_handler
|
|
12
|
-
from simtools.utils.geometry import transform_ground_to_shower_coordinates
|
|
12
|
+
from simtools.utils.geometry import solid_angle, transform_ground_to_shower_coordinates
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@dataclass
|
|
@@ -28,6 +28,7 @@ class ShowerEventData:
|
|
|
28
28
|
x_core_shower: list[np.float64] = field(default_factory=list)
|
|
29
29
|
y_core_shower: list[np.float64] = field(default_factory=list)
|
|
30
30
|
core_distance_shower: list[np.float64] = field(default_factory=list)
|
|
31
|
+
angular_distance: list[float] = field(default_factory=list)
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
@dataclass
|
|
@@ -52,6 +53,7 @@ class SimtelIOEventDataReader:
|
|
|
52
53
|
self.telescope_list = telescope_list
|
|
53
54
|
|
|
54
55
|
self.data_sets = self.read_table_list(event_data_file)
|
|
56
|
+
self.reduced_file_info = None
|
|
55
57
|
|
|
56
58
|
def read_table_list(self, event_data_file):
|
|
57
59
|
"""
|
|
@@ -78,7 +80,14 @@ class SimtelIOEventDataReader:
|
|
|
78
80
|
)
|
|
79
81
|
|
|
80
82
|
data_sets = []
|
|
81
|
-
|
|
83
|
+
try:
|
|
84
|
+
sorted_indices = sorted(
|
|
85
|
+
range(len(dataset_dict["SHOWERS"])),
|
|
86
|
+
key=lambda i: int(dataset_dict["SHOWERS"][i].split("_")[-1]),
|
|
87
|
+
)
|
|
88
|
+
except (ValueError, AttributeError):
|
|
89
|
+
sorted_indices = [0] # Handle the case where the key is only "SHOWERS"
|
|
90
|
+
for i in sorted_indices:
|
|
82
91
|
data_sets.append(
|
|
83
92
|
{
|
|
84
93
|
"SHOWERS": dataset_dict["SHOWERS"][i],
|
|
@@ -122,6 +131,16 @@ class SimtelIOEventDataReader:
|
|
|
122
131
|
shower_data.core_distance_shower = np.hypot(
|
|
123
132
|
shower_data.x_core_shower, shower_data.y_core_shower
|
|
124
133
|
)
|
|
134
|
+
shower_data.angular_distance = (
|
|
135
|
+
angular_separation(
|
|
136
|
+
shower_data.shower_azimuth * u.deg,
|
|
137
|
+
shower_data.shower_altitude * u.deg,
|
|
138
|
+
self.reduced_file_info["azimuth"],
|
|
139
|
+
(90.0 * u.deg - self.reduced_file_info["zenith"]),
|
|
140
|
+
)
|
|
141
|
+
.to(u.deg)
|
|
142
|
+
.value
|
|
143
|
+
)
|
|
125
144
|
|
|
126
145
|
return shower_data
|
|
127
146
|
|
|
@@ -238,6 +257,9 @@ class SimtelIOEventDataReader:
|
|
|
238
257
|
event_data_file,
|
|
239
258
|
table_names=[get_name(k) for k in ("SHOWERS", "TRIGGERS", "FILE_INFO")],
|
|
240
259
|
)
|
|
260
|
+
self.reduced_file_info = self.get_reduced_simulation_file_info(
|
|
261
|
+
tables[get_name("FILE_INFO")]
|
|
262
|
+
)
|
|
241
263
|
|
|
242
264
|
shower_data = self._table_to_shower_data(tables[get_name("SHOWERS")])
|
|
243
265
|
triggered_data = self._table_to_triggered_data(tables[get_name("TRIGGERS")])
|
|
@@ -355,4 +377,31 @@ class SimtelIOEventDataReader:
|
|
|
355
377
|
value = value * simulation_file_info[key].unit
|
|
356
378
|
reduced_info[key] = value
|
|
357
379
|
|
|
380
|
+
reduced_info["solid_angle"] = solid_angle(
|
|
381
|
+
angle_min=reduced_info.get("viewcone_min", 0.0 * u.rad),
|
|
382
|
+
angle_max=reduced_info.get("viewcone_max", 0.0 * u.rad),
|
|
383
|
+
)
|
|
384
|
+
reduced_info["scatter_area"] = self.scatter_area(
|
|
385
|
+
core_scatter_min=reduced_info.get("core_scatter_min", 0.0 * u.m),
|
|
386
|
+
core_scatter_max=reduced_info.get("core_scatter_max", 0.0 * u.m),
|
|
387
|
+
)
|
|
388
|
+
|
|
358
389
|
return reduced_info
|
|
390
|
+
|
|
391
|
+
def scatter_area(self, core_scatter_min, core_scatter_max):
|
|
392
|
+
"""
|
|
393
|
+
Calculate the scatter area of the core.
|
|
394
|
+
|
|
395
|
+
Parameters
|
|
396
|
+
----------
|
|
397
|
+
core_scatter_min : astropy.units.Quantity
|
|
398
|
+
Minimum core scatter radius.
|
|
399
|
+
core_scatter_max : astropy.units.Quantity
|
|
400
|
+
Maximum core scatter radius.
|
|
401
|
+
|
|
402
|
+
Returns
|
|
403
|
+
-------
|
|
404
|
+
astropy.units.Quantity
|
|
405
|
+
Scatter area.
|
|
406
|
+
"""
|
|
407
|
+
return np.pi * (core_scatter_max**2 - core_scatter_min**2)
|
|
@@ -20,6 +20,7 @@ from simtools.corsika.primary_particle import PrimaryParticle
|
|
|
20
20
|
from simtools.simtel.simtel_io_file_info import get_corsika_run_header
|
|
21
21
|
from simtools.simtel.simtel_io_metadata import (
|
|
22
22
|
get_sim_telarray_telescope_id_to_telescope_name_mapping,
|
|
23
|
+
read_sim_telarray_metadata,
|
|
23
24
|
)
|
|
24
25
|
from simtools.utils.geometry import calculate_circular_mean
|
|
25
26
|
from simtools.utils.names import get_common_identifier_from_array_element_name
|
|
@@ -175,7 +176,7 @@ class SimtelIOEventDataWriter:
|
|
|
175
176
|
"core_scatter_max": run_info["core_range"][1],
|
|
176
177
|
"zenith": 90.0 - np.degrees(run_info["direction"][1]),
|
|
177
178
|
"azimuth": np.degrees(run_info["direction"][0]),
|
|
178
|
-
"nsb_level": self.
|
|
179
|
+
"nsb_level": self.get_nsb_level_from_sim_telarray_metadata(file),
|
|
179
180
|
}
|
|
180
181
|
)
|
|
181
182
|
|
|
@@ -300,14 +301,33 @@ class SimtelIOEventDataWriter:
|
|
|
300
301
|
self.telescope_id_to_name.get(tel_id, f"Unknown_{tel_id}") for tel_id in telescope_ids
|
|
301
302
|
]
|
|
302
303
|
|
|
303
|
-
def
|
|
304
|
+
def get_nsb_level_from_sim_telarray_metadata(self, file):
|
|
304
305
|
"""
|
|
305
|
-
Return
|
|
306
|
+
Return NSB level from sim_telarray metadata.
|
|
306
307
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
308
|
+
Falls back to preliminary NSB level if not found.
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
file : Path
|
|
313
|
+
Path to the sim_telarray file.
|
|
314
|
+
|
|
315
|
+
Returns
|
|
316
|
+
-------
|
|
317
|
+
float
|
|
318
|
+
NSB level.
|
|
319
|
+
"""
|
|
320
|
+
metadata, _ = read_sim_telarray_metadata(file)
|
|
321
|
+
nsb_integrated_flux = metadata.get("nsb_integrated_flux")
|
|
322
|
+
return nsb_integrated_flux or self._get_nsb_level_from_file_name(str(file))
|
|
323
|
+
|
|
324
|
+
def _get_nsb_level_from_file_name(self, file):
|
|
325
|
+
"""
|
|
326
|
+
Return NSB level from file name.
|
|
327
|
+
|
|
328
|
+
Hardwired values are used for "dark", "half", and "full" NSB levels.
|
|
329
|
+
Allows to read legacy sim_telarray files without 'nsb_integrated_flux'
|
|
330
|
+
metadata field.
|
|
311
331
|
|
|
312
332
|
Parameters
|
|
313
333
|
----------
|
|
@@ -319,15 +339,15 @@ class SimtelIOEventDataWriter:
|
|
|
319
339
|
float
|
|
320
340
|
NSB level extracted from file name.
|
|
321
341
|
"""
|
|
322
|
-
nsb_levels = {"dark":
|
|
342
|
+
nsb_levels = {"dark": 0.24, "half": 0.835, "full": 1.2}
|
|
323
343
|
|
|
324
344
|
for key, value in nsb_levels.items():
|
|
325
345
|
try:
|
|
326
346
|
if key in file.lower():
|
|
327
|
-
self._logger.warning(f"NSB level set to hardwired value of {value}")
|
|
347
|
+
self._logger.warning(f"NSB level set to hardwired value of {value} for {file}")
|
|
328
348
|
return value
|
|
329
349
|
except AttributeError as exc:
|
|
330
350
|
raise AttributeError("Invalid file name.") from exc
|
|
331
351
|
|
|
332
|
-
self._logger.warning("No NSB level found in file
|
|
333
|
-
return
|
|
352
|
+
self._logger.warning(f"No NSB level found in {file}, defaulting to None")
|
|
353
|
+
return None
|
|
@@ -78,7 +78,7 @@ def _decode_dictionary(meta, encoding="utf8"):
|
|
|
78
78
|
return {k.decode(encoding, errors="ignore"): v.decode(encoding) for k, v in meta.items()}
|
|
79
79
|
except UnicodeDecodeError as e:
|
|
80
80
|
_logger.warning(
|
|
81
|
-
f"
|
|
81
|
+
f"Unable to decode metadata with encoding {encoding}: {e}. "
|
|
82
82
|
"Falling back to 'utf-8' with errors='ignore'."
|
|
83
83
|
)
|
|
84
84
|
return {safe_decode(k, encoding): safe_decode(v, encoding) for k, v in meta.items()}
|
|
@@ -229,8 +229,8 @@ def _data_columns_pulse_shape(n_columns):
|
|
|
229
229
|
return _columns, "Pulse shape"
|
|
230
230
|
|
|
231
231
|
|
|
232
|
-
def
|
|
233
|
-
"""Column description for
|
|
232
|
+
def _data_columns_nsb_spectrum():
|
|
233
|
+
"""Column description for parameters describing the nsb spectrum."""
|
|
234
234
|
return (
|
|
235
235
|
[
|
|
236
236
|
{"name": "wavelength", "description": "Wavelength", "unit": "nm"},
|
|
@@ -240,7 +240,7 @@ def _data_columns_nsb_reference_spectrum():
|
|
|
240
240
|
"unit": "1.e9 / (nm s m^2 sr)",
|
|
241
241
|
},
|
|
242
242
|
],
|
|
243
|
-
"NSB
|
|
243
|
+
"NSB spectrum",
|
|
244
244
|
)
|
|
245
245
|
|
|
246
246
|
|
simtools/simulator.py
CHANGED
|
@@ -19,6 +19,7 @@ from simtools.runners.corsika_simtel_runner import CorsikaSimtelRunner
|
|
|
19
19
|
from simtools.simtel.simtel_io_event_writer import SimtelIOEventDataWriter
|
|
20
20
|
from simtools.simtel.simulator_array import SimulatorArray
|
|
21
21
|
from simtools.testing.sim_telarray_metadata import assert_sim_telarray_metadata
|
|
22
|
+
from simtools.version import semver_to_int
|
|
22
23
|
|
|
23
24
|
__all__ = [
|
|
24
25
|
"InvalidRunsToSimulateError",
|
|
@@ -163,10 +164,6 @@ class Simulator:
|
|
|
163
164
|
if seed:
|
|
164
165
|
return int(seed.split(",")[0].strip())
|
|
165
166
|
|
|
166
|
-
def semver_to_int(version: str):
|
|
167
|
-
major, minor, patch = map(int, version.split("."))
|
|
168
|
-
return major * 10000 + minor * 100 + patch
|
|
169
|
-
|
|
170
167
|
seed = semver_to_int(model_version) * 10000000
|
|
171
168
|
seed = seed + 1000000 if self.args_dict.get("site") != "North" else seed + 2000000
|
|
172
169
|
seed = seed + (int)(self.args_dict["zenith_angle"].value) * 1000
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Trigger rate calculation for telescopes and arrays of telescopes."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from astropy import units as u
|
|
7
|
+
from ctao_cr_spectra.definitions import IRFDOC_PROTON_SPECTRUM
|
|
8
|
+
|
|
9
|
+
from simtools.io import ascii_handler, io_handler
|
|
10
|
+
from simtools.layout.array_layout_utils import get_array_elements_from_db_for_layouts
|
|
11
|
+
from simtools.simtel.simtel_io_event_histograms import SimtelIOEventHistograms
|
|
12
|
+
from simtools.visualization import plot_simtel_event_histograms
|
|
13
|
+
|
|
14
|
+
_logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def telescope_trigger_rates(args_dict, db_config):
|
|
18
|
+
"""
|
|
19
|
+
Calculate trigger rates for single telescopes or arrays of telescopes.
|
|
20
|
+
|
|
21
|
+
Main function to read event data, fill histograms, and derive trigger rates.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
if args_dict.get("array_layout_name"):
|
|
26
|
+
telescope_configs = get_array_elements_from_db_for_layouts(
|
|
27
|
+
args_dict["array_layout_name"],
|
|
28
|
+
args_dict.get("site"),
|
|
29
|
+
args_dict.get("model_version"),
|
|
30
|
+
db_config,
|
|
31
|
+
)
|
|
32
|
+
else:
|
|
33
|
+
telescope_configs = ascii_handler.collect_data_from_file(args_dict["telescope_ids"])[
|
|
34
|
+
"telescope_configs"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
for array_name, telescope_ids in telescope_configs.items():
|
|
38
|
+
_logger.info(
|
|
39
|
+
f"Processing file: {args_dict['event_data_file']} with telescope config: {array_name}"
|
|
40
|
+
)
|
|
41
|
+
histograms = SimtelIOEventHistograms(
|
|
42
|
+
args_dict["event_data_file"], array_name=array_name, telescope_list=telescope_ids
|
|
43
|
+
)
|
|
44
|
+
histograms.fill()
|
|
45
|
+
|
|
46
|
+
_calculate_trigger_rates(histograms, array_name)
|
|
47
|
+
|
|
48
|
+
if args_dict["plot_histograms"]:
|
|
49
|
+
plot_simtel_event_histograms.plot(
|
|
50
|
+
histograms.histograms,
|
|
51
|
+
output_path=io_handler.IOHandler().get_output_directory(),
|
|
52
|
+
array_name=array_name,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _calculate_trigger_rates(histograms, array_name):
|
|
57
|
+
"""
|
|
58
|
+
Calculate trigger rates from the filled histograms.
|
|
59
|
+
|
|
60
|
+
Missing
|
|
61
|
+
|
|
62
|
+
- custom definition of energy spectra
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
efficiency = histograms.histograms.get("energy_eff", {}).get("histogram")
|
|
66
|
+
energy_axis = histograms.histograms.get("energy_eff", {}).get("bin_edges")
|
|
67
|
+
|
|
68
|
+
cr_spectrum = get_cosmic_ray_spectrum()
|
|
69
|
+
_logger.info(f"Cosmic ray spectrum: {cr_spectrum}")
|
|
70
|
+
e_min = energy_axis[:-1] * u.TeV
|
|
71
|
+
e_max = energy_axis[1:] * u.TeV
|
|
72
|
+
cr_rates = (
|
|
73
|
+
np.array(
|
|
74
|
+
[
|
|
75
|
+
cr_spectrum.integrate_energy(e1, e2).decompose(bases=[u.s, u.cm, u.sr]).value
|
|
76
|
+
for e1, e2 in zip(e_min, e_max)
|
|
77
|
+
]
|
|
78
|
+
)
|
|
79
|
+
* histograms.file_info["scatter_area"].to("cm2").value
|
|
80
|
+
* histograms.file_info["solid_angle"].to("sr").value
|
|
81
|
+
* u.Hz
|
|
82
|
+
)
|
|
83
|
+
trigger_rates = efficiency * cr_rates
|
|
84
|
+
trigger_rate = np.sum(trigger_rates, axis=0)
|
|
85
|
+
|
|
86
|
+
_logger.info(f"Scatter area from MC: {histograms.file_info['scatter_area'].to('m2')}")
|
|
87
|
+
_logger.info(f"Solid angle from MC: {histograms.file_info['solid_angle']}")
|
|
88
|
+
_logger.info(f"Trigger rate for {array_name} array: {trigger_rate.to('Hz')}")
|
|
89
|
+
|
|
90
|
+
histograms.histograms["cr_rates_mc"] = histograms.get_histogram_definition(
|
|
91
|
+
histogram=cr_rates.value,
|
|
92
|
+
bin_edges=energy_axis,
|
|
93
|
+
title="Cosmic Ray Rates (MC)",
|
|
94
|
+
axis_titles=["Energy (TeV)", "Cosmic Ray Rate (Hz)"],
|
|
95
|
+
plot_scales={"x": "log", "y": "log"},
|
|
96
|
+
)
|
|
97
|
+
histograms.histograms["trigger_rates"] = histograms.get_histogram_definition(
|
|
98
|
+
histogram=trigger_rates.value,
|
|
99
|
+
bin_edges=energy_axis,
|
|
100
|
+
title="Trigger Rates (MC)",
|
|
101
|
+
axis_titles=["Energy (TeV)", "Trigger Rate (Hz)"],
|
|
102
|
+
plot_scales={"x": "log", "y": "log"},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return cr_rates, trigger_rates, trigger_rate
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_cosmic_ray_spectrum():
|
|
109
|
+
"""
|
|
110
|
+
Return the cosmic ray spectrum.
|
|
111
|
+
|
|
112
|
+
To be extended in future to read a larger variety of spectra.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
astropy.units.Quantity
|
|
117
|
+
Cosmic ray spectrum.
|
|
118
|
+
"""
|
|
119
|
+
return IRFDOC_PROTON_SPECTRUM
|
|
@@ -15,6 +15,8 @@ ERROR_PATTERNS = [
|
|
|
15
15
|
re.compile(r"segmentation fault", re.IGNORECASE),
|
|
16
16
|
]
|
|
17
17
|
|
|
18
|
+
IGNORE_PATTERNS = [re.compile(r"Falling back to 'utf-8' with errors='ignore'", re.IGNORECASE)]
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
def inspect(log_text):
|
|
20
22
|
"""
|
|
@@ -34,17 +36,17 @@ def inspect(log_text):
|
|
|
34
36
|
True if no errors or warnings are found, False otherwise.
|
|
35
37
|
"""
|
|
36
38
|
log_text = log_text if isinstance(log_text, list) else [log_text]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
break
|
|
39
|
+
|
|
40
|
+
issues = [
|
|
41
|
+
(lineno, line)
|
|
42
|
+
for txt in log_text
|
|
43
|
+
for lineno, line in enumerate(txt.splitlines(), 1)
|
|
44
|
+
if "INFO::" not in line
|
|
45
|
+
and any(p.search(line) for p in ERROR_PATTERNS)
|
|
46
|
+
and not any(p.search(line) for p in IGNORE_PATTERNS)
|
|
47
|
+
]
|
|
47
48
|
|
|
48
49
|
for lineno, line in issues:
|
|
49
50
|
_logger.error(f"Error or warning found in log at line {lineno}: {line.strip()}")
|
|
50
|
-
|
|
51
|
+
|
|
52
|
+
return not issues
|
simtools/utils/geometry.py
CHANGED
|
@@ -182,6 +182,26 @@ def calculate_circular_mean(angles):
|
|
|
182
182
|
return np.arctan2(sin_sum, cos_sum)
|
|
183
183
|
|
|
184
184
|
|
|
185
|
+
@u.quantity_input(angle_max=u.rad, angle_min=u.rad)
|
|
186
|
+
def solid_angle(angle_max, angle_min=0 * u.rad):
|
|
187
|
+
"""
|
|
188
|
+
Calculate the solid angle subtended by a given range of angles.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
angle_max: astropy.units.Quantity
|
|
193
|
+
The maximum angle for which to calculate the solid angle.
|
|
194
|
+
angle_min: astropy.units.Quantity
|
|
195
|
+
The minimum angle for which to calculate the solid angle (default is 0 rad).
|
|
196
|
+
|
|
197
|
+
Returns
|
|
198
|
+
-------
|
|
199
|
+
astropy.units.Quantity
|
|
200
|
+
The solid angle subtended by the given range of angles (in steradians).
|
|
201
|
+
"""
|
|
202
|
+
return 2 * np.pi * (np.cos(angle_min.to("rad")) - np.cos(angle_max.to("rad"))) * u.sr
|
|
203
|
+
|
|
204
|
+
|
|
185
205
|
def transform_ground_to_shower_coordinates(x_ground, y_ground, z_ground, azimuth, altitude):
|
|
186
206
|
"""
|
|
187
207
|
Transform ground to shower coordinates.
|
simtools/version.py
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
# which is adapted from https://github.com/astropy/astropy/blob/master/astropy/version.py
|
|
5
5
|
# see https://github.com/astropy/astropy/pull/10774 for a discussion on why this needed.
|
|
6
6
|
|
|
7
|
+
from packaging.version import InvalidVersion, Version
|
|
8
|
+
|
|
7
9
|
try:
|
|
8
10
|
try:
|
|
9
11
|
from ._dev_version import version
|
|
@@ -17,3 +19,90 @@ except Exception: # pylint: disable=broad-except
|
|
|
17
19
|
version = "0.0.0" # pylint: disable=invalid-name
|
|
18
20
|
|
|
19
21
|
__version__ = version
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resolve_version_to_latest_patch(partial_version, available_versions):
|
|
25
|
+
"""
|
|
26
|
+
Resolve a partial version (major.minor) to the latest patch version.
|
|
27
|
+
|
|
28
|
+
Given a partial version string (e.g., "6.0") and a list of available versions,
|
|
29
|
+
finds the latest patch version that matches the major.minor pattern.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
partial_version : str
|
|
34
|
+
Partial version string in format "major.minor" (e.g., "6.0", "5.2")
|
|
35
|
+
available_versions : list of str
|
|
36
|
+
List of available semantic versions (e.g., ["5.0.0", "5.0.1", "6.0.0", "6.0.2"])
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
str
|
|
41
|
+
Latest patch version matching the partial version pattern
|
|
42
|
+
|
|
43
|
+
Raises
|
|
44
|
+
------
|
|
45
|
+
ValueError
|
|
46
|
+
If partial_version is not in major.minor format
|
|
47
|
+
ValueError
|
|
48
|
+
If no matching versions are found
|
|
49
|
+
|
|
50
|
+
Examples
|
|
51
|
+
--------
|
|
52
|
+
>>> versions = ["5.0.0", "5.0.1", "6.0.0", "6.0.2", "6.1.0"]
|
|
53
|
+
>>> resolve_version_to_latest_patch("6.0", versions)
|
|
54
|
+
'6.0.2'
|
|
55
|
+
>>> resolve_version_to_latest_patch("5.0", versions)
|
|
56
|
+
'5.0.1'
|
|
57
|
+
>>> resolve_version_to_latest_patch("5.0.1", versions)
|
|
58
|
+
'5.0.1'
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
pv = Version(partial_version)
|
|
62
|
+
except InvalidVersion as exc:
|
|
63
|
+
raise ValueError(f"Invalid version string: {partial_version}") from exc
|
|
64
|
+
|
|
65
|
+
if pv.release and len(pv.release) >= 3:
|
|
66
|
+
return str(pv)
|
|
67
|
+
|
|
68
|
+
if len(pv.release) != 2:
|
|
69
|
+
raise ValueError(f"Partial version must be major.minor, got: {partial_version}")
|
|
70
|
+
|
|
71
|
+
major, minor = pv.release
|
|
72
|
+
|
|
73
|
+
candidates = [
|
|
74
|
+
v for v in available_versions if Version(v).major == major and Version(v).minor == minor
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
if not candidates:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"No versions found matching '{partial_version}.x' "
|
|
80
|
+
f"in available versions: {sorted(available_versions)}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return str(max(map(Version, candidates)))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def semver_to_int(version_string):
|
|
87
|
+
"""
|
|
88
|
+
Convert a semantic version string to an integer.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
version_string : str
|
|
93
|
+
Semantic version string (e.g., "6.0.2")
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
int
|
|
98
|
+
Integer representation of the version (e.g., 60002 for "6.0.2")
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
v = Version(version_string)
|
|
103
|
+
except InvalidVersion as exc:
|
|
104
|
+
raise ValueError(f"Invalid version: {version_string}") from exc
|
|
105
|
+
|
|
106
|
+
release = v.release + (0,) * (3 - len(v.release))
|
|
107
|
+
major, minor, patch = release[:3]
|
|
108
|
+
return major * 10000 + minor * 100 + patch
|
simtools/{corsika/corsika_histograms_visualize.py → visualization/plot_corsika_histograms.py}
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Visualize Cherenkov photon distributions from CORSIKA."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import re
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
import matplotlib.pyplot as plt
|
|
@@ -569,3 +570,111 @@ def save_figs_to_pdf(figs, pdf_file_name):
|
|
|
569
570
|
plt.tight_layout()
|
|
570
571
|
pdf_pages.savefig(fig)
|
|
571
572
|
pdf_pages.close()
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def build_all_photon_figures(histograms_instance, test: bool = False):
|
|
576
|
+
"""Return list of all photon histogram figures for the given instance.
|
|
577
|
+
|
|
578
|
+
When test is True, only generate the first two figure groups to reduce runtime.
|
|
579
|
+
"""
|
|
580
|
+
plot_function_names = sorted(
|
|
581
|
+
[
|
|
582
|
+
name
|
|
583
|
+
for name, obj in globals().items()
|
|
584
|
+
if name.startswith("plot_")
|
|
585
|
+
and "event_header_distribution" not in name
|
|
586
|
+
and callable(obj)
|
|
587
|
+
]
|
|
588
|
+
)
|
|
589
|
+
if test:
|
|
590
|
+
plot_function_names = plot_function_names[:2]
|
|
591
|
+
|
|
592
|
+
figure_list = []
|
|
593
|
+
module_obj = globals()
|
|
594
|
+
for fn_name in plot_function_names:
|
|
595
|
+
plot_fn = module_obj[fn_name]
|
|
596
|
+
figs = plot_fn(histograms_instance)
|
|
597
|
+
for fig in figs:
|
|
598
|
+
figure_list.append(fig)
|
|
599
|
+
return np.array(figure_list).flatten()
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def export_all_photon_figures_pdf(histograms_instance, test: bool = False):
|
|
603
|
+
"""Build and save all photon histogram figures into a single PDF.
|
|
604
|
+
|
|
605
|
+
The PDF name is derived from the HDF5 file name core and written under output_path.
|
|
606
|
+
"""
|
|
607
|
+
figs = build_all_photon_figures(histograms_instance, test=test)
|
|
608
|
+
core_name = re.sub(r"\.hdf5$", "", Path(histograms_instance.hdf5_file_name).name)
|
|
609
|
+
output_file_name = Path(histograms_instance.output_path).joinpath(f"{core_name}.pdf")
|
|
610
|
+
save_figs_to_pdf(figs, output_file_name)
|
|
611
|
+
return output_file_name
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def derive_event_1d_histograms(
|
|
615
|
+
histograms_instance,
|
|
616
|
+
event_1d_header_keys,
|
|
617
|
+
pdf: bool,
|
|
618
|
+
hdf5: bool,
|
|
619
|
+
overwrite: bool = False,
|
|
620
|
+
):
|
|
621
|
+
"""Create 1D event header histograms; optionally save to PDF and/or HDF5."""
|
|
622
|
+
figure_list = []
|
|
623
|
+
for key in event_1d_header_keys:
|
|
624
|
+
if pdf:
|
|
625
|
+
fig = plot_1d_event_header_distribution(histograms_instance, key)
|
|
626
|
+
figure_list.append(fig)
|
|
627
|
+
if hdf5:
|
|
628
|
+
histograms_instance.export_event_header_1d_histogram(
|
|
629
|
+
key, bins=50, hist_range=None, overwrite=overwrite
|
|
630
|
+
)
|
|
631
|
+
if pdf:
|
|
632
|
+
figs_array = np.array(figure_list).flatten()
|
|
633
|
+
pdf_name = Path(histograms_instance.output_path).joinpath(
|
|
634
|
+
f"{Path(histograms_instance.hdf5_file_name).name}_event_1d_histograms.pdf"
|
|
635
|
+
)
|
|
636
|
+
save_figs_to_pdf(figs_array, pdf_name)
|
|
637
|
+
return pdf_name
|
|
638
|
+
return None
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def derive_event_2d_histograms(
|
|
642
|
+
histograms_instance,
|
|
643
|
+
event_2d_header_keys,
|
|
644
|
+
pdf: bool,
|
|
645
|
+
hdf5: bool,
|
|
646
|
+
overwrite: bool = False,
|
|
647
|
+
):
|
|
648
|
+
"""Create 2D event header histograms in pairs; optionally save PDF and/or HDF5.
|
|
649
|
+
|
|
650
|
+
If an odd number of keys is provided, the last one is ignored (with a warning).
|
|
651
|
+
"""
|
|
652
|
+
if len(event_2d_header_keys) % 2 == 1:
|
|
653
|
+
_logger.warning(
|
|
654
|
+
"An odd number of keys was passed to generate 2D histograms.\n"
|
|
655
|
+
"The last key is being ignored."
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
figure_list = []
|
|
659
|
+
for i, _ in enumerate(event_2d_header_keys[::2]):
|
|
660
|
+
if pdf:
|
|
661
|
+
fig = plot_2d_event_header_distribution(
|
|
662
|
+
histograms_instance, event_2d_header_keys[i], event_2d_header_keys[i + 1]
|
|
663
|
+
)
|
|
664
|
+
figure_list.append(fig)
|
|
665
|
+
if hdf5:
|
|
666
|
+
histograms_instance.export_event_header_2d_histogram(
|
|
667
|
+
event_2d_header_keys[i],
|
|
668
|
+
event_2d_header_keys[i + 1],
|
|
669
|
+
bins=50,
|
|
670
|
+
hist_range=None,
|
|
671
|
+
overwrite=overwrite,
|
|
672
|
+
)
|
|
673
|
+
if pdf:
|
|
674
|
+
figs_array = np.array(figure_list).flatten()
|
|
675
|
+
pdf_name = Path(histograms_instance.output_path).joinpath(
|
|
676
|
+
f"{Path(histograms_instance.hdf5_file_name).name}_event_2d_histograms.pdf"
|
|
677
|
+
)
|
|
678
|
+
save_figs_to_pdf(figs_array, pdf_name)
|
|
679
|
+
return pdf_name
|
|
680
|
+
return None
|