gammasimtools 0.19.0__py3-none-any.whl → 0.20.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.
Files changed (47) hide show
  1. {gammasimtools-0.19.0.dist-info → gammasimtools-0.20.0.dist-info}/METADATA +1 -3
  2. {gammasimtools-0.19.0.dist-info → gammasimtools-0.20.0.dist-info}/RECORD +43 -41
  3. {gammasimtools-0.19.0.dist-info → gammasimtools-0.20.0.dist-info}/entry_points.txt +2 -2
  4. simtools/_version.py +2 -2
  5. simtools/applications/calculate_incident_angles.py +182 -0
  6. simtools/applications/db_add_simulation_model_from_repository_to_db.py +17 -14
  7. simtools/applications/db_add_value_from_json_to_db.py +6 -9
  8. simtools/applications/db_generate_compound_indexes.py +7 -3
  9. simtools/applications/db_get_file_from_db.py +11 -23
  10. simtools/applications/derive_trigger_rates.py +91 -0
  11. simtools/applications/plot_simtel_events.py +73 -31
  12. simtools/applications/validate_file_using_schema.py +7 -4
  13. simtools/configuration/commandline_parser.py +17 -11
  14. simtools/data_model/validate_data.py +8 -3
  15. simtools/db/db_handler.py +83 -26
  16. simtools/db/db_model_upload.py +11 -16
  17. simtools/dependencies.py +10 -5
  18. simtools/layout/array_layout_utils.py +37 -5
  19. simtools/model/array_model.py +18 -1
  20. simtools/model/site_model.py +25 -0
  21. simtools/production_configuration/derive_corsika_limits.py +9 -34
  22. simtools/ray_tracing/incident_angles.py +706 -0
  23. simtools/schemas/model_parameter_and_data_schema.metaschema.yml +2 -2
  24. simtools/schemas/model_parameters/nsb_reference_spectrum.schema.yml +1 -1
  25. simtools/schemas/model_parameters/nsb_spectrum.schema.yml +22 -29
  26. simtools/schemas/model_parameters/stars.schema.yml +1 -1
  27. simtools/schemas/production_tables.schema.yml +5 -0
  28. simtools/simtel/simtel_config_writer.py +17 -19
  29. simtools/simtel/simtel_io_event_histograms.py +253 -516
  30. simtools/simtel/simtel_io_event_reader.py +51 -2
  31. simtools/simtel/simtel_io_event_writer.py +31 -11
  32. simtools/simtel/simtel_io_metadata.py +1 -1
  33. simtools/simtel/simtel_table_reader.py +3 -3
  34. simtools/telescope_trigger_rates.py +119 -0
  35. simtools/testing/log_inspector.py +13 -11
  36. simtools/utils/geometry.py +20 -0
  37. simtools/visualization/plot_incident_angles.py +431 -0
  38. simtools/visualization/plot_simtel_event_histograms.py +376 -0
  39. simtools/visualization/visualize.py +1 -3
  40. simtools/applications/calculate_trigger_rate.py +0 -187
  41. simtools/applications/generate_sim_telarray_histograms.py +0 -196
  42. simtools/simtel/simtel_io_histogram.py +0 -623
  43. simtools/simtel/simtel_io_histograms.py +0 -556
  44. {gammasimtools-0.19.0.dist-info → gammasimtools-0.20.0.dist-info}/WHEEL +0 -0
  45. {gammasimtools-0.19.0.dist-info → gammasimtools-0.20.0.dist-info}/licenses/LICENSE +0 -0
  46. {gammasimtools-0.19.0.dist-info → gammasimtools-0.20.0.dist-info}/top_level.txt +0 -0
  47. /simtools/visualization/{simtel_event_plots.py → plot_simtel_events.py} +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
- for i in range(len(dataset_dict["SHOWERS"])):
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._get_preliminary_nsb_level(str(file)),
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 _get_preliminary_nsb_level(self, file):
304
+ def get_nsb_level_from_sim_telarray_metadata(self, file):
304
305
  """
305
- Return preliminary NSB level from file name.
306
+ Return NSB level from sim_telarray metadata.
306
307
 
307
- Hardwired values are used for "dark", "half", and "full" NSB levels
308
- (actual values are made up for this example). Will be replaced with
309
- reading of sim_telarray metadata entry for NSB level (to be implemented,
310
- see issue #1572).
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": 1.0, "half": 2.0, "full": 5.0}
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 name, defaulting to 1.0")
333
- return 1.0
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"Failed to decode metadata with encoding {encoding}: {e}. "
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 _data_columns_nsb_reference_spectrum():
233
- """Column description for parameter nsb_reference_spectrum."""
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 reference spectrum",
243
+ "NSB spectrum",
244
244
  )
245
245
 
246
246
 
@@ -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
- issues = []
38
- for txt in log_text:
39
- for lineno, line in enumerate(txt.splitlines(), 1):
40
- # Skip lines containing "INFO::"
41
- if "INFO::" in line:
42
- continue
43
- for pattern in ERROR_PATTERNS:
44
- if pattern.search(line):
45
- issues.append((lineno, line))
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
- return len(issues) == 0
51
+
52
+ return not issues
@@ -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.