gammasimtools 0.26.0__py3-none-any.whl → 0.27.1__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 (70) hide show
  1. {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.1.dist-info}/METADATA +5 -1
  2. {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.1.dist-info}/RECORD +70 -66
  3. {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.1.dist-info}/WHEEL +1 -1
  4. {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.1.dist-info}/entry_points.txt +1 -1
  5. simtools/_version.py +2 -2
  6. simtools/applications/convert_geo_coordinates_of_array_elements.py +2 -1
  7. simtools/applications/db_get_array_layouts_from_db.py +1 -1
  8. simtools/applications/{calculate_incident_angles.py → derive_incident_angle.py} +16 -16
  9. simtools/applications/derive_mirror_rnda.py +111 -177
  10. simtools/applications/generate_corsika_histograms.py +38 -1
  11. simtools/applications/generate_regular_arrays.py +73 -36
  12. simtools/applications/simulate_flasher.py +3 -13
  13. simtools/applications/simulate_illuminator.py +2 -10
  14. simtools/applications/simulate_pedestals.py +1 -1
  15. simtools/applications/simulate_prod.py +8 -7
  16. simtools/applications/submit_data_from_external.py +2 -1
  17. simtools/applications/validate_camera_efficiency.py +28 -27
  18. simtools/applications/validate_cumulative_psf.py +1 -3
  19. simtools/applications/validate_optics.py +2 -1
  20. simtools/atmosphere.py +83 -0
  21. simtools/camera/camera_efficiency.py +171 -48
  22. simtools/camera/single_photon_electron_spectrum.py +6 -6
  23. simtools/configuration/commandline_parser.py +47 -9
  24. simtools/constants.py +5 -0
  25. simtools/corsika/corsika_config.py +88 -185
  26. simtools/corsika/corsika_histograms.py +246 -69
  27. simtools/data_model/model_data_writer.py +46 -49
  28. simtools/data_model/schema.py +2 -0
  29. simtools/db/db_handler.py +4 -2
  30. simtools/db/mongo_db.py +2 -2
  31. simtools/io/ascii_handler.py +52 -4
  32. simtools/io/io_handler.py +23 -12
  33. simtools/job_execution/job_manager.py +154 -79
  34. simtools/job_execution/process_pool.py +137 -0
  35. simtools/layout/array_layout.py +0 -1
  36. simtools/layout/array_layout_utils.py +143 -21
  37. simtools/model/array_model.py +22 -50
  38. simtools/model/calibration_model.py +4 -4
  39. simtools/model/model_parameter.py +123 -73
  40. simtools/model/model_utils.py +40 -1
  41. simtools/model/site_model.py +4 -4
  42. simtools/model/telescope_model.py +4 -5
  43. simtools/ray_tracing/incident_angles.py +87 -6
  44. simtools/ray_tracing/mirror_panel_psf.py +337 -217
  45. simtools/ray_tracing/psf_analysis.py +57 -42
  46. simtools/ray_tracing/psf_parameter_optimisation.py +3 -2
  47. simtools/ray_tracing/ray_tracing.py +37 -10
  48. simtools/runners/corsika_runner.py +52 -191
  49. simtools/runners/corsika_simtel_runner.py +74 -100
  50. simtools/runners/runner_services.py +214 -213
  51. simtools/runners/simtel_runner.py +27 -155
  52. simtools/runners/simtools_runner.py +9 -69
  53. simtools/schemas/application_workflow.metaschema.yml +8 -0
  54. simtools/settings.py +19 -0
  55. simtools/simtel/simtel_config_writer.py +0 -55
  56. simtools/simtel/simtel_seeds.py +184 -0
  57. simtools/simtel/simulator_array.py +115 -103
  58. simtools/simtel/simulator_camera_efficiency.py +66 -42
  59. simtools/simtel/simulator_light_emission.py +110 -123
  60. simtools/simtel/simulator_ray_tracing.py +78 -63
  61. simtools/simulator.py +135 -346
  62. simtools/testing/sim_telarray_metadata.py +13 -11
  63. simtools/testing/validate_output.py +87 -19
  64. simtools/utils/general.py +6 -17
  65. simtools/utils/random.py +36 -0
  66. simtools/visualization/plot_corsika_histograms.py +2 -0
  67. simtools/visualization/plot_incident_angles.py +48 -1
  68. simtools/visualization/plot_psf.py +160 -18
  69. {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.1.dist-info}/licenses/LICENSE +0 -0
  70. {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.1.dist-info}/top_level.txt +0 -0
@@ -57,7 +57,8 @@ r"""
57
57
  """
58
58
 
59
59
  from simtools.application_control import startup_application
60
- from simtools.configuration import configurator
60
+ from simtools.configuration import commandline_parser, configurator
61
+ from simtools.constants import CORSIKA_MAX_SEED
61
62
  from simtools.simulator import Simulator
62
63
 
63
64
 
@@ -97,11 +98,11 @@ def _parse():
97
98
  default=False,
98
99
  )
99
100
  config.parser.add_argument(
100
- "--corsika_test_seeds",
101
- help="Use predefined random seeds for CORSIKA for testing purposes.",
102
- action="store_true",
103
- required=False,
104
- default=False,
101
+ "--corsika_seeds",
102
+ help="Use fixed random seeds for CORSIKA for testing purposes.",
103
+ nargs=4,
104
+ type=commandline_parser.CommandLineParser.bounded_int(1, CORSIKA_MAX_SEED),
105
+ metavar=("S1", "S2", "S3", "S4"),
105
106
  )
106
107
  config.parser.add_argument(
107
108
  "--sequential",
@@ -127,7 +128,7 @@ def main():
127
128
  """Run simulations for productions."""
128
129
  app_context = startup_application(_parse, setup_io_handler=False)
129
130
 
130
- simulator = Simulator(label=app_context.args.get("label"), args_dict=app_context.args)
131
+ simulator = Simulator(label=app_context.args.get("label"))
131
132
 
132
133
  simulator.simulate()
133
134
  simulator.validate_metadata()
@@ -95,7 +95,8 @@ def main():
95
95
  )
96
96
 
97
97
  writer.ModelDataWriter.dump(
98
- args_dict=app_context.args,
98
+ output_file=app_context.args["output_file"],
99
+ output_file_format=app_context.args.get("output_file_format"),
99
100
  metadata=_metadata,
100
101
  product_data=data_validator.validate_and_transform(),
101
102
  )
@@ -44,12 +44,11 @@ r"""
44
44
  The output is saved in simtools-output/validate_camera_efficiency.
45
45
  """
46
46
 
47
- from pathlib import Path
48
-
49
- import simtools.data_model.model_data_writer as writer
50
47
  from simtools.application_control import get_application_label, startup_application
51
48
  from simtools.camera.camera_efficiency import CameraEfficiency
52
49
  from simtools.configuration import configurator
50
+ from simtools.io.ascii_handler import write_data_to_file
51
+ from simtools.utils import names
53
52
 
54
53
 
55
54
  def _parse():
@@ -106,31 +105,33 @@ def main():
106
105
  """Calculate the camera efficiency and NSB pixel rates."""
107
106
  app_context = startup_application(_parse)
108
107
 
109
- ce = CameraEfficiency(
110
- label=app_context.args.get("label"),
111
- config_data=app_context.args,
112
- )
113
- ce.simulate()
114
- ce.analyze(force=True)
115
- ce.plot_efficiency(efficiency_type="Cherenkov", save_fig=True)
116
- ce.plot_efficiency(efficiency_type="NSB", save_fig=True)
117
-
118
- writer.ModelDataWriter.dump_model_parameter(
119
- parameter_name="nsb_pixel_rate",
120
- value=ce.get_nsb_pixel_rate(
121
- reference_conditions=app_context.args.get(
122
- "write_reference_nsb_rate_as_parameter", False
123
- )
124
- ),
125
- instrument=app_context.args["telescope"],
126
- parameter_version=app_context.args.get("parameter_version", "0.0.0"),
127
- output_file=Path(
128
- f"nsb_pixel_rate-{app_context.args.get('parameter_version', '0.0.0')}.json"
129
- ),
130
- output_path=app_context.io_handler.get_output_directory()
131
- / app_context.args["telescope"]
132
- / "nsb_pixel_rate",
108
+ results = {}
109
+ for efficiency_type in ["Shower", "NSB", "Muon"]:
110
+ ce = CameraEfficiency(
111
+ label=app_context.args.get("label"),
112
+ config_data=app_context.args,
113
+ efficiency_type=efficiency_type,
114
+ )
115
+ ce.simulate()
116
+ ce.analyze(force=True)
117
+ results |= ce.results_summary()
118
+ ce.plot_efficiency(save_fig=True)
119
+
120
+ if ce.efficiency_type == "nsb":
121
+ ce.dump_nsb_pixel_rate()
122
+ if ce.efficiency_type == "muon":
123
+ ce.calc_partial_efficiency()
124
+
125
+ results_file = app_context.io_handler.get_output_directory() / names.generate_file_name(
126
+ file_type="camera_efficiency_summary",
127
+ suffix=".yml",
128
+ site=app_context.args["site"],
129
+ telescope_model_name=app_context.args["telescope"],
130
+ zenith_angle=app_context.args["zenith_angle"].value,
131
+ azimuth_angle=app_context.args["azimuth_angle"].value,
133
132
  )
133
+ app_context.logger.info(f"Writing results summary to {results_file}")
134
+ write_data_to_file(results, results_file, unique_lines=True)
134
135
 
135
136
 
136
137
  if __name__ == "__main__":
@@ -135,12 +135,10 @@ def main():
135
135
  model_version=app_context.args["model_version"],
136
136
  )
137
137
 
138
- if app_context.args.get("overwrite_model_parameters"):
139
- tel_model.overwrite_parameters_from_file(app_context.args["overwrite_model_parameters"])
140
-
141
138
  ray = RayTracing(
142
139
  telescope_model=tel_model,
143
140
  site_model=site_model,
141
+ label=app_context.args.get("label"),
144
142
  zenith_angle=app_context.args["zenith"] * u.deg,
145
143
  source_distance=app_context.args["src_distance"] * u.km,
146
144
  off_axis_angle=[0.0] * u.deg,
@@ -134,6 +134,7 @@ def main():
134
134
  ray = RayTracing(
135
135
  telescope_model=tel_model,
136
136
  site_model=site_model,
137
+ label=app_context.args.get("label") or Path(__file__).stem,
137
138
  zenith_angle=app_context.args["zenith"] * u.deg,
138
139
  source_distance=app_context.args["src_distance"] * u.km,
139
140
  off_axis_angle=np.linspace(
@@ -147,7 +148,7 @@ def main():
147
148
  ray.analyze(force=True)
148
149
 
149
150
  # Plotting
150
- for key in ["d80_deg", "d80_cm", "eff_area", "eff_flen"]:
151
+ for key in ["psf_deg", "psf_cm", "eff_area", "eff_flen"]:
151
152
  plt.figure(figsize=(8, 6), tight_layout=True)
152
153
 
153
154
  ray.plot(key, marker="o", linestyle=":", color="k")
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
@@ -26,9 +30,11 @@ class CameraEfficiency:
26
30
  Instance label, optional.
27
31
  config_data: dict.
28
32
  Dict containing the configurable parameters.
33
+ efficiency_type: str
34
+ The type of efficiency to simulate (e.g., 'Shower', 'Muon', or 'NSB').
29
35
  """
30
36
 
31
- def __init__(self, config_data, label):
37
+ def __init__(self, label, config_data, efficiency_type):
32
38
  """Initialize the CameraEfficiency class."""
33
39
  self._logger = logging.getLogger(__name__)
34
40
 
@@ -45,8 +51,9 @@ class CameraEfficiency:
45
51
 
46
52
  self._results = None
47
53
  self._has_results = False
54
+ self.efficiency_type = efficiency_type.lower()
48
55
 
49
- self.config = self._configuration_from_args_dict(config_data)
56
+ self.config = self._configuration(config_data)
50
57
  self._file = self._load_files()
51
58
 
52
59
  self.nsb_pixel_pe_per_ns = None
@@ -56,9 +63,9 @@ class CameraEfficiency:
56
63
  """Return string representation of the CameraEfficiency instance."""
57
64
  return f"CameraEfficiency(label={self.label})\n"
58
65
 
59
- def _configuration_from_args_dict(self, config_data):
66
+ def _configuration(self, config_data):
60
67
  """
61
- Extract configuration data from command line arguments.
68
+ Extract configuration data from command line and class parameters.
62
69
 
63
70
  Parameters
64
71
  ----------
@@ -74,6 +81,7 @@ class CameraEfficiency:
74
81
  "zenith_angle": config_data["zenith_angle"].to("deg").value,
75
82
  "azimuth_angle": config_data["azimuth_angle"].to("deg").value,
76
83
  "nsb_spectrum": config_data.get("nsb_spectrum", None),
84
+ "efficiency_type": self.efficiency_type,
77
85
  }
78
86
 
79
87
  def _load_files(self):
@@ -84,15 +92,13 @@ class CameraEfficiency:
84
92
  [".ecsv", ".dat", ".log"],
85
93
  ):
86
94
  file_name = names.generate_file_name(
87
- file_type=(
88
- "camera_efficiency_table" if label == "results" else "camera_efficiency"
89
- ),
95
+ file_type="camera_efficiency",
90
96
  suffix=suffix,
91
97
  site=self.telescope_model.site,
92
98
  telescope_model_name=self.telescope_model.name,
93
99
  zenith_angle=self.config["zenith_angle"],
94
100
  azimuth_angle=self.config["azimuth_angle"],
95
- label=self.label,
101
+ label=self.efficiency_type,
96
102
  )
97
103
 
98
104
  _file[label] = self.io_handler.get_output_directory().joinpath(file_name)
@@ -111,6 +117,7 @@ class CameraEfficiency:
111
117
  file_simtel=self._file["sim_telarray"],
112
118
  file_log=self._file["log"],
113
119
  label=self.label,
120
+ x_max=self._get_x_max_for_efficiency_type(),
114
121
  nsb_spectrum=self.config["nsb_spectrum"],
115
122
  skip_correction_to_nsb_spectrum=self.config.get(
116
123
  "skip_correction_to_nsb_spectrum", False
@@ -245,34 +252,78 @@ class CameraEfficiency:
245
252
 
246
253
  def results_summary(self):
247
254
  """
248
- Print a summary of the results.
255
+ Fill a dictionary with summary of the results.
249
256
 
250
257
  Include a header for the zenith/azimuth settings and the NSB spectrum file which was used.
251
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.
252
264
  """
253
- nsb_spectrum_text = (
254
- f"NSB spectrum file: {self.config['nsb_spectrum']}"
255
- if self.config["nsb_spectrum"]
256
- else "default sim_telarray spectrum."
257
- )
258
- return (
259
- f"Results summary for {self.telescope_model.name} at "
260
- f"zenith={self.config['zenith_angle']:.1f} deg, "
261
- f"azimuth={self.config['azimuth_angle']:.1f} deg\n"
262
- f"Using the {nsb_spectrum_text}\n"
263
- f"\nSpectrum weighted reflectivity: {self.calc_reflectivity():.4f}\n"
264
- "Camera nominal efficiency with gaps (B-TEL-1170): "
265
- f"{self.calc_camera_efficiency():.4f}\n"
266
- "Telescope total efficiency"
267
- f" with gaps (was A-PERF-2020): {self.calc_tel_efficiency():.4f}\n"
268
- "Telescope total Cherenkov light efficiency / sqrt(total NSB efficiency) "
269
- "(A-PERF-2025/B-TEL-0090): "
270
- f"{self.calc_tot_efficiency(self.calc_tel_efficiency()):.4f}\n"
271
- "Expected NSB pixel rate for the provided NSB spectrum: "
272
- f"{self.nsb_pixel_pe_per_ns:.4f} [p.e./ns]\n"
273
- "Expected NSB pixel rate for the reference NSB: "
274
- f"{self.nsb_rate_ref_conditions:.4f} [p.e./ns]\n"
275
- )
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
276
327
 
277
328
  def export_results(self):
278
329
  """Export results to a ecsv file."""
@@ -283,12 +334,9 @@ class CameraEfficiency:
283
334
  astropy.io.ascii.write(
284
335
  self._results, self._file["results"], format="basic", overwrite=True
285
336
  )
286
- _results_summary_file = (
287
- str(self._file["results"]).replace(".ecsv", ".txt").replace("_table_", "_summary_")
288
- )
337
+ _results_summary_file = str(self._file["results"]).replace(".ecsv", "_summary.yml")
289
338
  self._logger.info(f"Exporting summary results to {_results_summary_file}")
290
- with open(_results_summary_file, "w", encoding="utf-8") as file:
291
- file.write(self.results_summary())
339
+ ascii_handler.write_data_to_file(self.results_summary(), Path(_results_summary_file))
292
340
 
293
341
  def _read_results(self):
294
342
  """Read existing results file and store it in _results."""
@@ -363,6 +411,39 @@ class CameraEfficiency:
363
411
 
364
412
  return tel_efficiency / np.sqrt(tel_efficiency_nsb)
365
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
+
366
447
  def calc_reflectivity(self):
367
448
  """
368
449
  Calculate the Cherenkov spectrum weighted reflectivity in the range 300-550 nm.
@@ -428,14 +509,12 @@ class CameraEfficiency:
428
509
  )
429
510
  return self.nsb_pixel_pe_per_ns * u.GHz, self.nsb_rate_ref_conditions * u.GHz
430
511
 
431
- def plot_efficiency(self, efficiency_type, save_fig=False):
512
+ def plot_efficiency(self, save_fig=False):
432
513
  """
433
514
  Plot efficiency vs wavelength.
434
515
 
435
516
  Parameters
436
517
  ----------
437
- efficiency_type: str
438
- The type of efficiency to plot (Cherenkov 'C' or NSB 'N')
439
518
  save_fig: bool
440
519
  If True, the figure will be saved to a file.
441
520
 
@@ -444,9 +523,9 @@ class CameraEfficiency:
444
523
  fig
445
524
  The figure instance of pyplot
446
525
  """
447
- self._logger.info(f"Plotting {efficiency_type} efficiency vs wavelength")
526
+ self._logger.info(f"Plotting {self.efficiency_type} efficiency vs wavelength")
448
527
 
449
- _col_type = "C" if efficiency_type == "Cherenkov" else "N"
528
+ _col_type = "C" if self.efficiency_type in ("shower", "muon") else "N"
450
529
 
451
530
  column_titles = {
452
531
  "wl": "Wavelength [nm]",
@@ -464,21 +543,21 @@ class CameraEfficiency:
464
543
  for column_now, column_title in column_titles.items():
465
544
  table_to_plot.rename_column(column_now, column_title)
466
545
 
467
- y_title = f"{efficiency_type} light efficiency"
468
- if efficiency_type == "NSB":
546
+ y_title = f"{self.efficiency_type} light efficiency"
547
+ if self.efficiency_type == "nsb":
469
548
  y_title = r"Diff. ph. rate [$10^{9} \times $ph/(nm s m$^2$ sr)]"
470
549
  plot = visualize.plot_table(
471
550
  table_to_plot,
472
551
  y_title=y_title,
473
- title=f"{self.telescope_model.name} response to {efficiency_type} light",
552
+ title=f"{self.telescope_model.name} response to {self.efficiency_type} light",
474
553
  no_markers=True,
475
554
  )
476
- if efficiency_type == "NSB":
555
+ if self.efficiency_type == "nsb":
477
556
  plot.gca().set_yscale("log")
478
557
  ylim = plot.gca().get_ylim()
479
558
  plot.gca().set_ylim(1e-3, ylim[1])
480
559
  if save_fig:
481
- self._save_plot(plot, efficiency_type.lower())
560
+ self._save_plot(plot, self.efficiency_type)
482
561
  return plot
483
562
 
484
563
  def _save_plot(self, fig, plot_title):
@@ -496,3 +575,47 @@ class CameraEfficiency:
496
575
  self.label + "_" + self.telescope_model.name + "_" + plot_title
497
576
  )
498
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
@@ -17,6 +16,7 @@ from simtools.constants import MODEL_PARAMETER_SCHEMA_URL, SCHEMA_PATH
17
16
  from simtools.data_model import validate_data
18
17
  from simtools.data_model.metadata_collector import MetadataCollector
19
18
  from simtools.io import io_handler
19
+ from simtools.job_execution import job_manager
20
20
 
21
21
 
22
22
  class SinglePhotonElectronSpectrum:
@@ -97,7 +97,8 @@ class SinglePhotonElectronSpectrum:
97
97
  )
98
98
 
99
99
  writer.ModelDataWriter.dump(
100
- args_dict=self.args_dict,
100
+ output_file=self.args_dict["output_file"],
101
+ output_file_format=self.args_dict.get("output_file_format"),
101
102
  metadata=self.metadata,
102
103
  product_data=table,
103
104
  validate_schema_file=None,
@@ -126,7 +127,7 @@ class SinglePhotonElectronSpectrum:
126
127
 
127
128
  Raises
128
129
  ------
129
- subprocess.CalledProcessError
130
+ job_manager.JobExecutionError
130
131
  If the command execution fails.
131
132
  """
132
133
  tmp_input_file = self._get_input_data(
@@ -153,10 +154,9 @@ class SinglePhotonElectronSpectrum:
153
154
 
154
155
  self._logger.info(f"Running norm_spe command: {' '.join(command)}")
155
156
  try:
156
- result = subprocess.run(command, capture_output=True, text=True, check=True)
157
- except subprocess.CalledProcessError as exc:
157
+ result = job_manager.submit(command)
158
+ except job_manager.JobExecutionError as exc:
158
159
  self._logger.error(f"Error running norm_spe: {exc}")
159
- self._logger.error(f"stderr: {exc.stderr}")
160
160
  raise exc
161
161
  finally:
162
162
  for tmp_file in [tmp_input_file, tmp_ap_file]: