gammasimtools 0.25.0__py3-none-any.whl → 0.27.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 (138) hide show
  1. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/METADATA +6 -1
  2. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/RECORD +135 -130
  3. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/WHEEL +1 -1
  4. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/entry_points.txt +3 -2
  5. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/licenses/LICENSE +1 -1
  6. simtools/_version.py +2 -2
  7. simtools/application_control.py +35 -7
  8. simtools/applications/convert_geo_coordinates_of_array_elements.py +3 -3
  9. simtools/applications/db_add_file_to_db.py +1 -1
  10. simtools/applications/db_add_simulation_model_from_repository_to_db.py +1 -1
  11. simtools/applications/db_add_value_from_json_to_db.py +1 -1
  12. simtools/applications/db_generate_compound_indexes.py +1 -1
  13. simtools/applications/db_get_array_layouts_from_db.py +3 -7
  14. simtools/applications/db_get_file_from_db.py +1 -1
  15. simtools/applications/db_get_parameter_from_db.py +1 -1
  16. simtools/applications/db_inspect_databases.py +1 -1
  17. simtools/applications/db_upload_model_repository.py +1 -1
  18. simtools/applications/derive_ctao_array_layouts.py +1 -2
  19. simtools/applications/{calculate_incident_angles.py → derive_incident_angle.py} +16 -18
  20. simtools/applications/derive_mirror_rnda.py +112 -180
  21. simtools/applications/derive_psf_parameters.py +0 -1
  22. simtools/applications/derive_pulse_shape_parameters.py +0 -1
  23. simtools/applications/derive_trigger_rates.py +1 -1
  24. simtools/applications/docs_produce_array_element_report.py +2 -8
  25. simtools/applications/docs_produce_calibration_reports.py +1 -3
  26. simtools/applications/docs_produce_model_parameter_reports.py +0 -2
  27. simtools/applications/docs_produce_simulation_configuration_report.py +1 -3
  28. simtools/applications/generate_array_config.py +0 -1
  29. simtools/applications/generate_corsika_histograms.py +79 -229
  30. simtools/applications/generate_regular_arrays.py +76 -69
  31. simtools/applications/generate_simtel_event_data.py +2 -2
  32. simtools/applications/maintain_simulation_model_add_production.py +2 -2
  33. simtools/applications/maintain_simulation_model_write_array_element_positions.py +87 -0
  34. simtools/applications/plot_array_layout.py +5 -111
  35. simtools/applications/plot_simulated_event_distributions.py +57 -0
  36. simtools/applications/plot_tabular_data.py +0 -1
  37. simtools/applications/plot_tabular_data_for_model_parameter.py +1 -6
  38. simtools/applications/production_derive_corsika_limits.py +1 -1
  39. simtools/applications/production_generate_grid.py +0 -1
  40. simtools/applications/run_application.py +1 -1
  41. simtools/applications/simulate_flasher.py +3 -15
  42. simtools/applications/simulate_illuminator.py +2 -11
  43. simtools/applications/simulate_pedestals.py +1 -5
  44. simtools/applications/simulate_prod.py +8 -11
  45. simtools/applications/simulate_prod_htcondor_generator.py +1 -1
  46. simtools/applications/submit_array_layouts.py +2 -4
  47. simtools/applications/submit_data_from_external.py +2 -1
  48. simtools/applications/submit_model_parameter_from_external.py +1 -3
  49. simtools/applications/validate_camera_efficiency.py +28 -28
  50. simtools/applications/validate_camera_fov.py +0 -1
  51. simtools/applications/validate_cumulative_psf.py +1 -5
  52. simtools/applications/validate_optics.py +2 -14
  53. simtools/atmosphere.py +83 -0
  54. simtools/camera/camera_efficiency.py +171 -53
  55. simtools/camera/single_photon_electron_spectrum.py +8 -7
  56. simtools/configuration/commandline_parser.py +82 -11
  57. simtools/configuration/configurator.py +6 -11
  58. simtools/constants.py +5 -0
  59. simtools/corsika/corsika_config.py +100 -202
  60. simtools/corsika/corsika_histograms.py +561 -1708
  61. simtools/corsika/primary_particle.py +1 -1
  62. simtools/data_model/metadata_collector.py +5 -2
  63. simtools/data_model/metadata_model.py +0 -4
  64. simtools/data_model/model_data_writer.py +59 -64
  65. simtools/data_model/schema.py +2 -0
  66. simtools/data_model/validate_data.py +1 -3
  67. simtools/db/db_handler.py +23 -10
  68. simtools/db/mongo_db.py +2 -2
  69. simtools/dependencies.py +81 -38
  70. simtools/io/ascii_handler.py +55 -5
  71. simtools/io/io_handler.py +23 -12
  72. simtools/io/table_handler.py +1 -1
  73. simtools/job_execution/job_manager.py +154 -79
  74. simtools/job_execution/process_pool.py +137 -0
  75. simtools/layout/array_layout.py +4 -13
  76. simtools/layout/array_layout_utils.py +348 -57
  77. simtools/model/array_model.py +23 -63
  78. simtools/model/calibration_model.py +4 -8
  79. simtools/model/legacy_model_parameter.py +134 -0
  80. simtools/model/model_parameter.py +147 -86
  81. simtools/model/model_utils.py +40 -6
  82. simtools/model/site_model.py +4 -8
  83. simtools/model/telescope_model.py +10 -16
  84. simtools/production_configuration/derive_corsika_limits.py +6 -11
  85. simtools/production_configuration/interpolation_handler.py +16 -16
  86. simtools/ray_tracing/incident_angles.py +92 -17
  87. simtools/ray_tracing/mirror_panel_psf.py +338 -222
  88. simtools/ray_tracing/psf_analysis.py +62 -48
  89. simtools/ray_tracing/psf_parameter_optimisation.py +3 -3
  90. simtools/ray_tracing/ray_tracing.py +43 -25
  91. simtools/reporting/docs_auto_report_generator.py +8 -13
  92. simtools/reporting/docs_read_parameters.py +2 -8
  93. simtools/runners/corsika_runner.py +52 -195
  94. simtools/runners/corsika_simtel_runner.py +77 -108
  95. simtools/runners/runner_services.py +214 -213
  96. simtools/runners/simtel_runner.py +27 -160
  97. simtools/runners/simtools_runner.py +11 -73
  98. simtools/schemas/application_workflow.metaschema.yml +8 -0
  99. simtools/settings.py +173 -0
  100. simtools/{io/eventio_handler.py → sim_events/file_info.py} +3 -3
  101. simtools/{simtel/simtel_io_event_histograms.py → sim_events/histograms.py} +25 -15
  102. simtools/{simtel/simtel_io_event_reader.py → sim_events/reader.py} +20 -17
  103. simtools/{simtel/simtel_io_event_writer.py → sim_events/writer.py} +84 -25
  104. simtools/simtel/pulse_shapes.py +7 -2
  105. simtools/simtel/simtel_config_writer.py +79 -91
  106. simtools/simtel/simtel_seeds.py +184 -0
  107. simtools/simtel/simtel_table_reader.py +6 -4
  108. simtools/simtel/simulator_array.py +114 -109
  109. simtools/simtel/simulator_camera_efficiency.py +68 -46
  110. simtools/simtel/simulator_light_emission.py +164 -132
  111. simtools/simtel/simulator_ray_tracing.py +80 -71
  112. simtools/simulator.py +137 -355
  113. simtools/telescope_trigger_rates.py +3 -4
  114. simtools/testing/assertions.py +84 -33
  115. simtools/testing/configuration.py +1 -2
  116. simtools/testing/helpers.py +2 -3
  117. simtools/testing/log_inspector.py +1 -0
  118. simtools/testing/sim_telarray_metadata.py +14 -12
  119. simtools/testing/validate_output.py +121 -42
  120. simtools/utils/general.py +43 -17
  121. simtools/utils/geometry.py +0 -77
  122. simtools/utils/names.py +5 -5
  123. simtools/utils/random.py +36 -0
  124. simtools/visualization/legend_handlers.py +7 -6
  125. simtools/visualization/plot_array_layout.py +91 -16
  126. simtools/visualization/plot_corsika_histograms.py +145 -605
  127. simtools/visualization/plot_incident_angles.py +48 -1
  128. simtools/visualization/plot_mirrors.py +1 -4
  129. simtools/visualization/plot_pixels.py +2 -4
  130. simtools/visualization/plot_psf.py +160 -19
  131. simtools/visualization/plot_simtel_event_histograms.py +4 -4
  132. simtools/visualization/plot_simtel_events.py +6 -11
  133. simtools/visualization/plot_tables.py +8 -19
  134. simtools/visualization/visualize.py +22 -2
  135. simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +0 -160
  136. simtools/applications/print_version.py +0 -53
  137. simtools/io/hdf5_handler.py +0 -139
  138. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/top_level.txt +0 -0
simtools/atmosphere.py ADDED
@@ -0,0 +1,83 @@
1
+ """Profiles of atmospheric parameters as a function of altitude."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ import astropy.units as u
7
+ import numpy as np
8
+
9
+
10
+ class AtmosphereProfile:
11
+ """
12
+ Profiles of atmospheric parameters as a function of altitude.
13
+
14
+ Parameters
15
+ ----------
16
+ filename: str or Path
17
+ Path to the atmosphere profile file (CORSIKA table format)
18
+ """
19
+
20
+ def __init__(self, filename):
21
+ """AtmosphereProfile initialization."""
22
+ self._logger = logging.getLogger(__name__)
23
+ self.columns = {}
24
+ self.read_atmosphere_profile(filename)
25
+
26
+ def read_atmosphere_profile(self, filename):
27
+ """
28
+ Read atmosphere profile from file.
29
+
30
+ Parameters
31
+ ----------
32
+ filename: str or Path
33
+ Path to the atmosphere profile file (CORSIKA table format)
34
+
35
+ """
36
+ filename = Path(filename)
37
+ data = []
38
+ self._logger.debug(f"Reading atmosphere profile from {filename}")
39
+ with filename.open(encoding="utf-8") as f:
40
+ for line in f:
41
+ if not line.strip() or line.lstrip().startswith("#"):
42
+ continue
43
+ data.append([float(x) for x in line.split()])
44
+
45
+ self.data = np.array(data)
46
+ self.columns = {
47
+ "alt": 0,
48
+ "rho": 1,
49
+ "thick": 2,
50
+ "n_minus_1": 3,
51
+ "T": 4,
52
+ "p": 5,
53
+ "pw_over_p": 6,
54
+ }
55
+
56
+ def interpolate(self, altitude, column="thick"):
57
+ """
58
+ Interpolate the atmosphere profile at a given altitude.
59
+
60
+ Parameters
61
+ ----------
62
+ altitude: astropy.units.Quantity
63
+ Altitude
64
+ column: str
65
+ Column name to interpolate.
66
+
67
+ Returns
68
+ -------
69
+ float
70
+ Interpolated value.
71
+ """
72
+ if column not in self.columns:
73
+ raise KeyError(f"Unknown column: {column}")
74
+
75
+ altitude = altitude.to(u.km).value
76
+
77
+ x = self.data[:, 0]
78
+ y = self.data[:, self.columns[column]]
79
+
80
+ if altitude < x.min() or altitude > x.max():
81
+ raise ValueError("Altitude out of bounds")
82
+
83
+ return np.interp(altitude, x, y)
@@ -3,13 +3,17 @@
3
3
  import logging
4
4
  import re
5
5
  from collections import defaultdict
6
+ from pathlib import Path
6
7
 
7
8
  import astropy.io.ascii
8
9
  import astropy.units as u
9
10
  import numpy as np
10
11
  from astropy.table import Table
11
12
 
12
- from simtools.io import io_handler
13
+ import simtools.data_model.model_data_writer as writer
14
+ from simtools import settings
15
+ from simtools.atmosphere import AtmosphereProfile
16
+ from simtools.io import ascii_handler, io_handler
13
17
  from simtools.model.model_utils import initialize_simulation_models
14
18
  from simtools.simtel.simulator_camera_efficiency import SimulatorCameraEfficiency
15
19
  from simtools.utils import names
@@ -22,25 +26,23 @@ class CameraEfficiency:
22
26
 
23
27
  Parameters
24
28
  ----------
25
- db_config: dict
26
- Configuration for the database.
27
29
  label: str
28
30
  Instance label, optional.
29
31
  config_data: dict.
30
32
  Dict containing the configurable parameters.
33
+ efficiency_type: str
34
+ The type of efficiency to simulate (e.g., 'Shower', 'Muon', or 'NSB').
31
35
  """
32
36
 
33
- def __init__(self, config_data, label, db_config):
37
+ def __init__(self, label, config_data, efficiency_type):
34
38
  """Initialize the CameraEfficiency class."""
35
39
  self._logger = logging.getLogger(__name__)
36
40
 
37
- self._simtel_path = config_data.get("simtel_path")
38
41
  self.label = label
39
42
 
40
43
  self.io_handler = io_handler.IOHandler()
41
44
  self.telescope_model, self.site_model, _ = initialize_simulation_models(
42
45
  label=self.label,
43
- db_config=db_config,
44
46
  model_version=config_data["model_version"],
45
47
  site=config_data["site"],
46
48
  telescope_name=config_data["telescope"],
@@ -49,8 +51,9 @@ class CameraEfficiency:
49
51
 
50
52
  self._results = None
51
53
  self._has_results = False
54
+ self.efficiency_type = efficiency_type.lower()
52
55
 
53
- self.config = self._configuration_from_args_dict(config_data)
56
+ self.config = self._configuration(config_data)
54
57
  self._file = self._load_files()
55
58
 
56
59
  self.nsb_pixel_pe_per_ns = None
@@ -60,9 +63,9 @@ class CameraEfficiency:
60
63
  """Return string representation of the CameraEfficiency instance."""
61
64
  return f"CameraEfficiency(label={self.label})\n"
62
65
 
63
- def _configuration_from_args_dict(self, config_data):
66
+ def _configuration(self, config_data):
64
67
  """
65
- Extract configuration data from command line arguments.
68
+ Extract configuration data from command line and class parameters.
66
69
 
67
70
  Parameters
68
71
  ----------
@@ -78,6 +81,7 @@ class CameraEfficiency:
78
81
  "zenith_angle": config_data["zenith_angle"].to("deg").value,
79
82
  "azimuth_angle": config_data["azimuth_angle"].to("deg").value,
80
83
  "nsb_spectrum": config_data.get("nsb_spectrum", None),
84
+ "efficiency_type": self.efficiency_type,
81
85
  }
82
86
 
83
87
  def _load_files(self):
@@ -88,15 +92,13 @@ class CameraEfficiency:
88
92
  [".ecsv", ".dat", ".log"],
89
93
  ):
90
94
  file_name = names.generate_file_name(
91
- file_type=(
92
- "camera_efficiency_table" if label == "results" else "camera_efficiency"
93
- ),
95
+ file_type="camera_efficiency",
94
96
  suffix=suffix,
95
97
  site=self.telescope_model.site,
96
98
  telescope_model_name=self.telescope_model.name,
97
99
  zenith_angle=self.config["zenith_angle"],
98
100
  azimuth_angle=self.config["azimuth_angle"],
99
- label=self.label,
101
+ label=self.efficiency_type,
100
102
  )
101
103
 
102
104
  _file[label] = self.io_handler.get_output_directory().joinpath(file_name)
@@ -109,13 +111,13 @@ class CameraEfficiency:
109
111
  self.export_model_files()
110
112
 
111
113
  simtel = SimulatorCameraEfficiency(
112
- simtel_path=self._simtel_path,
113
114
  telescope_model=self.telescope_model,
114
115
  site_model=self.site_model,
115
116
  zenith_angle=self.config["zenith_angle"],
116
117
  file_simtel=self._file["sim_telarray"],
117
118
  file_log=self._file["log"],
118
119
  label=self.label,
120
+ x_max=self._get_x_max_for_efficiency_type(),
119
121
  nsb_spectrum=self.config["nsb_spectrum"],
120
122
  skip_correction_to_nsb_spectrum=self.config.get(
121
123
  "skip_correction_to_nsb_spectrum", False
@@ -250,34 +252,78 @@ class CameraEfficiency:
250
252
 
251
253
  def results_summary(self):
252
254
  """
253
- Print a summary of the results.
255
+ Fill a dictionary with summary of the results.
254
256
 
255
257
  Include a header for the zenith/azimuth settings and the NSB spectrum file which was used.
256
258
  The summary includes the various CTAO requirements and the final expected NSB pixel rate.
259
+
260
+ Returns
261
+ -------
262
+ dict
263
+ Summary of the results.
257
264
  """
258
- nsb_spectrum_text = (
259
- f"NSB spectrum file: {self.config['nsb_spectrum']}"
260
- if self.config["nsb_spectrum"]
261
- else "default sim_telarray spectrum."
262
- )
263
- return (
264
- f"Results summary for {self.telescope_model.name} at "
265
- f"zenith={self.config['zenith_angle']:.1f} deg, "
266
- f"azimuth={self.config['azimuth_angle']:.1f} deg\n"
267
- f"Using the {nsb_spectrum_text}\n"
268
- f"\nSpectrum weighted reflectivity: {self.calc_reflectivity():.4f}\n"
269
- "Camera nominal efficiency with gaps (B-TEL-1170): "
270
- f"{self.calc_camera_efficiency():.4f}\n"
271
- "Telescope total efficiency"
272
- f" with gaps (was A-PERF-2020): {self.calc_tel_efficiency():.4f}\n"
273
- "Telescope total Cherenkov light efficiency / sqrt(total NSB efficiency) "
274
- "(A-PERF-2025/B-TEL-0090): "
275
- f"{self.calc_tot_efficiency(self.calc_tel_efficiency()):.4f}\n"
276
- "Expected NSB pixel rate for the provided NSB spectrum: "
277
- f"{self.nsb_pixel_pe_per_ns:.4f} [p.e./ns]\n"
278
- "Expected NSB pixel rate for the reference NSB: "
279
- f"{self.nsb_rate_ref_conditions:.4f} [p.e./ns]\n"
280
- )
265
+ meta = {
266
+ "meta": {
267
+ "tel": self.telescope_model.name,
268
+ "model_version": self.telescope_model.model_version,
269
+ "zen": self.config["zenith_angle"],
270
+ "az": self.config["azimuth_angle"],
271
+ "nsb": (
272
+ self.config["nsb_spectrum"]
273
+ if self.config["nsb_spectrum"]
274
+ else "default sim_telarray spectrum"
275
+ ),
276
+ }
277
+ }
278
+
279
+ metrics = {}
280
+ if self.efficiency_type == "shower":
281
+ metrics |= {
282
+ "reflectivity": {
283
+ "value": self.calc_reflectivity(),
284
+ "description": "Spectrum weighted reflectivity",
285
+ },
286
+ "cam_eff": {
287
+ "value": self.calc_camera_efficiency(),
288
+ "description": "Camera nominal efficiency with gaps (B-TEL-1170)",
289
+ },
290
+ "tel_eff": {
291
+ "value": self.calc_tel_efficiency(),
292
+ "description": "Telescope total efficiency with gaps (was A-PERF-2020)",
293
+ },
294
+ "tot_sens": {
295
+ "value": self.calc_tot_efficiency(self.calc_tel_efficiency()),
296
+ "description": (
297
+ "Telescope total Cherenkov light efficiency / sqrt(total NSB efficiency) "
298
+ "(A-PERF-2025/B-TEL-0090)"
299
+ ),
300
+ },
301
+ }
302
+
303
+ elif self.efficiency_type == "nsb":
304
+ metrics |= {
305
+ "nsb_rate": {
306
+ "value": self.nsb_pixel_pe_per_ns,
307
+ "description": "Expected NSB pixel rate for the provided NSB spectrum",
308
+ },
309
+ "nsb_ref": {
310
+ "value": self.nsb_rate_ref_conditions,
311
+ "description": "Expected NSB pixel rate for the reference NSB",
312
+ },
313
+ }
314
+
315
+ elif self.efficiency_type == "muon":
316
+ metrics |= {
317
+ "muon_frac": {
318
+ "value": self.calc_partial_efficiency(lambda_min=200.0, lambda_max=290.0),
319
+ "description": (
320
+ "Fraction of light (from muons) in the wavelength range 200-290 nm "
321
+ "(B-TEL-0095)"
322
+ ),
323
+ },
324
+ }
325
+
326
+ return meta | metrics
281
327
 
282
328
  def export_results(self):
283
329
  """Export results to a ecsv file."""
@@ -288,12 +334,9 @@ class CameraEfficiency:
288
334
  astropy.io.ascii.write(
289
335
  self._results, self._file["results"], format="basic", overwrite=True
290
336
  )
291
- _results_summary_file = (
292
- str(self._file["results"]).replace(".ecsv", ".txt").replace("_table_", "_summary_")
293
- )
337
+ _results_summary_file = str(self._file["results"]).replace(".ecsv", "_summary.yml")
294
338
  self._logger.info(f"Exporting summary results to {_results_summary_file}")
295
- with open(_results_summary_file, "w", encoding="utf-8") as file:
296
- file.write(self.results_summary())
339
+ ascii_handler.write_data_to_file(self.results_summary(), Path(_results_summary_file))
297
340
 
298
341
  def _read_results(self):
299
342
  """Read existing results file and store it in _results."""
@@ -368,6 +411,39 @@ class CameraEfficiency:
368
411
 
369
412
  return tel_efficiency / np.sqrt(tel_efficiency_nsb)
370
413
 
414
+ def calc_partial_efficiency(self, lambda_min=200.0, lambda_max=290.0):
415
+ """
416
+ Compare efficiency in a given wavelength range with total efficiency.
417
+
418
+ Parameters
419
+ ----------
420
+ lambda_min: float
421
+ Minimum wavelength in nm.
422
+ lambda_max: float
423
+ Maximum wavelength in nm.
424
+
425
+ Returns
426
+ -------
427
+ Float
428
+ Fraction of light in the given wavelength range compared to total efficiency.
429
+
430
+ """
431
+ # Sum(C4) from lamba_min to lambda_max nm:
432
+ c4_reduced_wl = self._results["C4"][
433
+ [lambda_min < wl_now < lambda_max for wl_now in self._results["wl"]]
434
+ ]
435
+ c4_sum = np.sum(c4_reduced_wl)
436
+ # Sum(C4) from 200 - 999 nm:
437
+ c4_sum_total = np.sum(self._results["C4"])
438
+ # (no need to apply masts or fill factors as in calc_tel_efficiency, they cancel out)
439
+
440
+ self._logger.info(
441
+ f"Fraction of light in the wavelength range {lambda_min}-{lambda_max} nm: "
442
+ f"{c4_sum / c4_sum_total:.4f}"
443
+ )
444
+
445
+ return c4_sum / c4_sum_total
446
+
371
447
  def calc_reflectivity(self):
372
448
  """
373
449
  Calculate the Cherenkov spectrum weighted reflectivity in the range 300-550 nm.
@@ -433,14 +509,12 @@ class CameraEfficiency:
433
509
  )
434
510
  return self.nsb_pixel_pe_per_ns * u.GHz, self.nsb_rate_ref_conditions * u.GHz
435
511
 
436
- def plot_efficiency(self, efficiency_type, save_fig=False):
512
+ def plot_efficiency(self, save_fig=False):
437
513
  """
438
514
  Plot efficiency vs wavelength.
439
515
 
440
516
  Parameters
441
517
  ----------
442
- efficiency_type: str
443
- The type of efficiency to plot (Cherenkov 'C' or NSB 'N')
444
518
  save_fig: bool
445
519
  If True, the figure will be saved to a file.
446
520
 
@@ -449,9 +523,9 @@ class CameraEfficiency:
449
523
  fig
450
524
  The figure instance of pyplot
451
525
  """
452
- self._logger.info(f"Plotting {efficiency_type} efficiency vs wavelength")
526
+ self._logger.info(f"Plotting {self.efficiency_type} efficiency vs wavelength")
453
527
 
454
- _col_type = "C" if efficiency_type == "Cherenkov" else "N"
528
+ _col_type = "C" if self.efficiency_type in ("shower", "muon") else "N"
455
529
 
456
530
  column_titles = {
457
531
  "wl": "Wavelength [nm]",
@@ -469,21 +543,21 @@ class CameraEfficiency:
469
543
  for column_now, column_title in column_titles.items():
470
544
  table_to_plot.rename_column(column_now, column_title)
471
545
 
472
- y_title = f"{efficiency_type} light efficiency"
473
- if efficiency_type == "NSB":
546
+ y_title = f"{self.efficiency_type} light efficiency"
547
+ if self.efficiency_type == "nsb":
474
548
  y_title = r"Diff. ph. rate [$10^{9} \times $ph/(nm s m$^2$ sr)]"
475
549
  plot = visualize.plot_table(
476
550
  table_to_plot,
477
551
  y_title=y_title,
478
- title=f"{self.telescope_model.name} response to {efficiency_type} light",
552
+ title=f"{self.telescope_model.name} response to {self.efficiency_type} light",
479
553
  no_markers=True,
480
554
  )
481
- if efficiency_type == "NSB":
555
+ if self.efficiency_type == "nsb":
482
556
  plot.gca().set_yscale("log")
483
557
  ylim = plot.gca().get_ylim()
484
558
  plot.gca().set_ylim(1e-3, ylim[1])
485
559
  if save_fig:
486
- self._save_plot(plot, efficiency_type.lower())
560
+ self._save_plot(plot, self.efficiency_type)
487
561
  return plot
488
562
 
489
563
  def _save_plot(self, fig, plot_title):
@@ -501,3 +575,47 @@ class CameraEfficiency:
501
575
  self.label + "_" + self.telescope_model.name + "_" + plot_title
502
576
  )
503
577
  visualize.save_figure(fig, plot_file, log_title=f"{plot_title} efficiency")
578
+
579
+ def dump_nsb_pixel_rate(self):
580
+ """Write NSB pixel rate parameter file."""
581
+ cfg = settings.config.args
582
+
583
+ writer.ModelDataWriter.dump_model_parameter(
584
+ parameter_name="nsb_pixel_rate",
585
+ value=self.get_nsb_pixel_rate(
586
+ reference_conditions=settings.config.args.get(
587
+ "write_reference_nsb_rate_as_parameter", False
588
+ )
589
+ ),
590
+ instrument=cfg.get("telescope"),
591
+ parameter_version=cfg.get("parameter_version") or "0.0.0",
592
+ output_file=Path(f"nsb_pixel_rate-{cfg.get('parameter_version', '0.0.0')}.json"),
593
+ output_path=self.output_dir / cfg.get("telescope") / "nsb_pixel_rate",
594
+ )
595
+
596
+ def _get_x_max_for_efficiency_type(self):
597
+ """
598
+ Get X max value in g/cm2 depending on the efficiency type.
599
+
600
+ Returns
601
+ -------
602
+ float
603
+ max value in g/cm2
604
+ """
605
+ # typical value for shower X-max around 10 km (not relevant for NSB type)
606
+ x_max = 300.0
607
+ obs_level = self.site_model.get_parameter_value_with_unit("corsika_observation_level")
608
+ if self.efficiency_type == "muon":
609
+ atmo = AtmosphereProfile(
610
+ self.site_model.config_file_directory
611
+ / self.site_model.get_parameter_value("atmospheric_profile")
612
+ )
613
+ alt = obs_level.to(u.km) + 0.1 * u.km
614
+ x_max = atmo.interpolate(altitude=alt, column="thick")
615
+
616
+ self._logger.info(
617
+ f"Using X-max for {self.efficiency_type} efficiency: {x_max:.2f} g/cm2"
618
+ f" (at observation level: {obs_level:.2f})"
619
+ )
620
+
621
+ return x_max
@@ -2,7 +2,6 @@
2
2
 
3
3
  import logging
4
4
  import re
5
- import subprocess
6
5
  import tempfile
7
6
  from io import BytesIO
8
7
  from pathlib import Path
@@ -12,10 +11,12 @@ from astropy.table import Table
12
11
  from scipy.optimize import curve_fit
13
12
 
14
13
  import simtools.data_model.model_data_writer as writer
14
+ from simtools import settings
15
15
  from simtools.constants import MODEL_PARAMETER_SCHEMA_URL, SCHEMA_PATH
16
16
  from simtools.data_model import validate_data
17
17
  from simtools.data_model.metadata_collector import MetadataCollector
18
18
  from simtools.io import io_handler
19
+ from simtools.job_execution import job_manager
19
20
 
20
21
 
21
22
  class SinglePhotonElectronSpectrum:
@@ -96,7 +97,8 @@ class SinglePhotonElectronSpectrum:
96
97
  )
97
98
 
98
99
  writer.ModelDataWriter.dump(
99
- args_dict=self.args_dict,
100
+ output_file=self.args_dict["output_file"],
101
+ output_file_format=self.args_dict.get("output_file_format"),
100
102
  metadata=self.metadata,
101
103
  product_data=table,
102
104
  validate_schema_file=None,
@@ -125,7 +127,7 @@ class SinglePhotonElectronSpectrum:
125
127
 
126
128
  Raises
127
129
  ------
128
- subprocess.CalledProcessError
130
+ job_manager.JobExecutionError
129
131
  If the command execution fails.
130
132
  """
131
133
  tmp_input_file = self._get_input_data(
@@ -140,7 +142,7 @@ class SinglePhotonElectronSpectrum:
140
142
  )
141
143
 
142
144
  command = [
143
- f"{self.args_dict['simtel_path']}/sim_telarray/bin/norm_spe",
145
+ f"{settings.config.sim_telarray_path}/bin/norm_spe",
144
146
  "-r",
145
147
  f"{self.args_dict['step_size']},{self.args_dict['max_amplitude']}",
146
148
  ]
@@ -152,10 +154,9 @@ class SinglePhotonElectronSpectrum:
152
154
 
153
155
  self._logger.info(f"Running norm_spe command: {' '.join(command)}")
154
156
  try:
155
- result = subprocess.run(command, capture_output=True, text=True, check=True)
156
- except subprocess.CalledProcessError as exc:
157
+ result = job_manager.submit(command)
158
+ except job_manager.JobExecutionError as exc:
157
159
  self._logger.error(f"Error running norm_spe: {exc}")
158
- self._logger.error(f"stderr: {exc.stderr}")
159
160
  raise exc
160
161
  finally:
161
162
  for tmp_file in [tmp_input_file, tmp_ap_file]:
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  import astropy.units as u
9
9
 
10
10
  import simtools.version
11
+ from simtools import constants
11
12
  from simtools.utils import names
12
13
 
13
14
 
@@ -106,7 +107,7 @@ class CommandLineParser(argparse.ArgumentParser):
106
107
  required=False,
107
108
  )
108
109
  _job_group.add_argument(
109
- "--simtel_path",
110
+ "--sim_telarray_path",
110
111
  help="path pointing to sim_telarray installation",
111
112
  type=Path,
112
113
  required=False,
@@ -160,6 +161,18 @@ class CommandLineParser(argparse.ArgumentParser):
160
161
  _job_group.add_argument(
161
162
  "--version", action="version", version=f"%(prog)s {simtools.version.__version__}"
162
163
  )
164
+ _job_group.add_argument(
165
+ "--build_info",
166
+ action=BuildInfoAction,
167
+ build_info=f"%(prog)s {simtools.version.__version__}",
168
+ help="show build information and exit",
169
+ )
170
+ _job_group.add_argument(
171
+ "--export_build_info",
172
+ help="export build information to file",
173
+ required=False,
174
+ type=str,
175
+ )
163
176
 
164
177
  def initialize_user_arguments(self):
165
178
  """Initialize user arguments."""
@@ -269,7 +282,7 @@ class CommandLineParser(argparse.ArgumentParser):
269
282
  type=self.telescope,
270
283
  )
271
284
  if "layout" in model_options or "layout_file" in model_options:
272
- _job_group = self._add_model_option_layout(
285
+ self._add_model_option_layout(
273
286
  job_group=_job_group,
274
287
  model_options=model_options,
275
288
  # layout info is always required for layout related tasks with the exception
@@ -277,6 +290,13 @@ class CommandLineParser(argparse.ArgumentParser):
277
290
  required="--list_available_layouts" not in self._option_string_actions,
278
291
  )
279
292
 
293
+ _job_group.add_argument(
294
+ "--ignore_missing_design_model",
295
+ help="Ignore missing design model definition of DB",
296
+ action="store_true",
297
+ required=False,
298
+ )
299
+
280
300
  def initialize_simulation_configuration_arguments(self, simulation_configuration):
281
301
  """
282
302
  Initialize default arguments for simulation configuration and simulation software.
@@ -438,21 +458,35 @@ class CommandLineParser(argparse.ArgumentParser):
438
458
  def _get_dictionary_with_sim_telarray_configuration():
439
459
  """Return dictionary with sim_telarray configuration parameters."""
440
460
  return {
441
- "sim_telarray_instrument_seeds": {
442
- "help": (
443
- "Random seed used for sim_telarray instrument setup. "
444
- "If '--sim_telarray_random_instrument_instances' is not set: "
445
- "use as sim_telarray seed ('random_seed' parameter). Otherwise: "
446
- "use as base seed to generate the random instrument instance seeds."
447
- ),
448
- "type": str,
461
+ "sim_telarray_instrument_seed": {
462
+ "help": "Random seed used for sim_telarray instrument setup.",
463
+ "type": CommandLineParser.bounded_int(1, constants.SIMTEL_MAX_SEED),
449
464
  "required": False,
450
465
  },
451
466
  "sim_telarray_random_instrument_instances": {
452
467
  "help": "Number of random instrument instances initialized in sim_telarray.",
453
- "type": int,
468
+ "type": CommandLineParser.bounded_int(1, 1024),
469
+ "required": False,
470
+ "default": 1,
471
+ },
472
+ "sim_telarray_seed": {
473
+ "help": (
474
+ "Random seed used for sim_telarray simulation. "
475
+ "Single value: seed for event simulation. "
476
+ "Two values: [instrument_seed, simulation_seed] (use for testing only)."
477
+ ),
478
+ "type": CommandLineParser.bounded_int(1, constants.SIMTEL_MAX_SEED),
479
+ "nargs": "+",
454
480
  "required": False,
455
481
  },
482
+ # hidden argument to specify the sim_telarray seeds file name
483
+ # (defined it here for convenience)
484
+ "sim_telarray_seed_file": {
485
+ "help": argparse.SUPPRESS,
486
+ "type": str,
487
+ "required": False,
488
+ "default": "sim_telarray_instrument_seeds.txt",
489
+ },
456
490
  }
457
491
 
458
492
  def _initialize_simulation_configuration(
@@ -825,3 +859,40 @@ class CommandLineParser(argparse.ArgumentParser):
825
859
  raise ValueError("Input string does not contain an integer and a astropy quantity.")
826
860
 
827
861
  return (int(match.group(1)), u.Quantity(float(match.group(2)), match.group(3)))
862
+
863
+ @staticmethod
864
+ def bounded_int(min_value, max_value):
865
+ """Argument parser type to check that an integer is within a given interval."""
866
+
867
+ def bounded_int_type(value):
868
+ try:
869
+ int_value = int(value)
870
+ except ValueError as exc:
871
+ raise ValueError(f"expected an integer in [{min_value},{max_value}]") from exc
872
+
873
+ if min_value <= int_value <= max_value:
874
+ return int_value
875
+ raise ValueError(f"{int_value} not in [{min_value},{max_value}]")
876
+
877
+ return bounded_int_type
878
+
879
+
880
+ class BuildInfoAction(argparse.Action):
881
+ """Custom argparse action to display build information."""
882
+
883
+ def __init__(self, option_strings, dest=argparse.SUPPRESS, default=argparse.SUPPRESS, **kwargs):
884
+ """Initialize BuildInfoAction."""
885
+ self.build_info = kwargs.pop("build_info", "Build information")
886
+ kwargs.pop("nargs", None)
887
+ super().__init__(option_strings, dest=dest, default=default, nargs=0, **kwargs)
888
+
889
+ def __call__(self, parser, namespace, values, option_string=None):
890
+ """Display build information and exit."""
891
+ # for efficiency reason, allow import here
892
+ from simtools import dependencies # pylint: disable=C0415
893
+
894
+ build_options = dependencies.get_build_options()
895
+ print(f"{self.build_info}")
896
+ for key, value in build_options.items():
897
+ print(f"{key}: {value}")
898
+ parser.exit()