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
@@ -3,9 +3,9 @@
3
3
  import logging
4
4
  from pathlib import Path
5
5
 
6
+ from simtools import settings
6
7
  from simtools.io import ascii_handler
7
8
  from simtools.runners.simtel_runner import SimtelRunner
8
- from simtools.utils import general
9
9
 
10
10
 
11
11
  class SimulatorCameraEfficiency(SimtelRunner):
@@ -20,12 +20,12 @@ class SimulatorCameraEfficiency(SimtelRunner):
20
20
  Instance of SiteModel class.
21
21
  label: str
22
22
  Instance label. Important for output file naming.
23
- simtel_path: str or Path
24
- Location of sim_telarray installation.
25
23
  file_simtel: str or Path
26
24
  Location of the sim_telarray testeff tool output file.
27
25
  zenith_angle: float
28
26
  Zenith angle given in the config to CameraEfficiency.
27
+ x_max: float
28
+ Maximum depth of shower development in g/cm2.
29
29
  nsb_spectrum: str or Path
30
30
  Path to the nsb spectrum file.
31
31
  skip_correction_to_nsb_spectrum: bool
@@ -37,18 +37,18 @@ class SimulatorCameraEfficiency(SimtelRunner):
37
37
  telescope_model,
38
38
  site_model,
39
39
  label=None,
40
- simtel_path=None,
41
40
  file_simtel=None,
42
41
  file_log=None,
43
42
  zenith_angle=None,
43
+ x_max=None,
44
44
  nsb_spectrum=None,
45
45
  skip_correction_to_nsb_spectrum=False,
46
46
  ):
47
- """Initialize SimtelRunner."""
47
+ """Camera efficiency simulator initialization."""
48
48
  self._logger = logging.getLogger(__name__)
49
49
  self._logger.debug("Init SimulatorCameraEfficiency")
50
50
 
51
- super().__init__(label=label, simtel_path=simtel_path)
51
+ super().__init__(label=label)
52
52
 
53
53
  self._telescope_model = telescope_model
54
54
  self._site_model = site_model
@@ -57,6 +57,7 @@ class SimulatorCameraEfficiency(SimtelRunner):
57
57
  self._file_simtel = file_simtel
58
58
  self._file_log = file_log
59
59
  self.zenith_angle = zenith_angle
60
+ self.x_max = x_max
60
61
  self.nsb_spectrum = nsb_spectrum
61
62
  self.skip_correction_to_nsb_spectrum = skip_correction_to_nsb_spectrum
62
63
 
@@ -76,7 +77,7 @@ class SimulatorCameraEfficiency(SimtelRunner):
76
77
  / Path(self._site_model.get_parameter_value("nsb_reference_spectrum")).name
77
78
  )
78
79
 
79
- def _make_run_command(self, run_number=None, input_file=None): # pylint: disable=unused-argument
80
+ def make_run_command(self, run_number=None, input_file=None): # pylint: disable=unused-argument
80
81
  """Prepare the command used to run testeff."""
81
82
  self._logger.debug("Preparing the command to run testeff")
82
83
 
@@ -109,52 +110,73 @@ class SimulatorCameraEfficiency(SimtelRunner):
109
110
  "mirror_reflectivity", "secondary_mirror_incidence_angle"
110
111
  )
111
112
 
112
- command = str(self._simtel_path.joinpath("sim_telarray/testeff"))
113
+ cmd = [str(settings.config.sim_telarray_path / "bin/testeff")]
114
+
113
115
  if self.skip_correction_to_nsb_spectrum:
114
- command += " -nc" # Do not apply correction to original altitude where B&E was derived
115
- command += " -I" # Clear the fall-back configuration directories
116
- command += f" -I{self._telescope_model.config_file_directory}"
117
- if self.nsb_spectrum is not None:
118
- command += f" -fnsb {self.nsb_spectrum}"
119
- command += " -nm -nsb-extra"
120
- command += f" -alt {self._site_model.get_parameter_value('corsika_observation_level')}"
121
- command += f" -fatm {self._site_model.get_parameter_value('atmospheric_transmission')}"
122
- command += f" -flen {focal_length}"
123
- command += f" -fcur {curvature_radius:.3f}"
124
- command += f" {pixel_shape_cmd} {pixel_diameter}"
125
- if mirror_class == 0:
126
- command += f" -fmir {self._telescope_model.get_parameter_value('mirror_list')}"
127
- if mirror_class == 2:
128
- command += f" -fmir {self._telescope_model.get_parameter_value('fake_mirror_list')}"
129
- command += f" -fref {mirror_reflectivity}"
130
- if mirror_class == 2:
131
- command += " -m2"
132
- command += f" -fref2 {mirror_reflectivity_secondary}"
133
- command += " -teltrans "
134
- command += f"{self._telescope_model.get_parameter_value('telescope_transmission')[0]}"
135
- command += f" -camtrans {camera_transmission}"
136
- command += f" -fflt {camera_filter_file}"
137
- command += (
138
- f" -fang {self._telescope_model.camera.get_lightguide_efficiency_angle_file_name()}"
116
+ cmd.append("-nc")
117
+
118
+ cmd.extend(
119
+ [
120
+ "-I",
121
+ f"-I{self._telescope_model.config_file_directory}",
122
+ ]
139
123
  )
140
- command += (
141
- f" -fwl {self._telescope_model.camera.get_lightguide_efficiency_wavelength_file_name()}"
124
+
125
+ if self.nsb_spectrum is not None:
126
+ cmd.extend(["-fnsb", str(self.nsb_spectrum)])
127
+
128
+ cmd.extend(
129
+ [
130
+ "-nm",
131
+ "-nsb-extra",
132
+ "-alt",
133
+ str(self._site_model.get_parameter_value("corsika_observation_level")),
134
+ "-fatm",
135
+ str(self._site_model.get_parameter_value("atmospheric_transmission")),
136
+ "-flen",
137
+ str(focal_length),
138
+ "-fcur",
139
+ f"{curvature_radius:.3f}",
140
+ pixel_shape_cmd,
141
+ str(pixel_diameter),
142
+ ]
142
143
  )
143
- command += f" -fqe {self._telescope_model.get_parameter_value('quantum_efficiency')}"
144
- command += " 200 1000" # lmin and lmax
145
- command += " 300" # Xmax
146
- command += f" {self._site_model.get_parameter_value('atmospheric_profile')}"
147
- command += f" {self.zenith_angle}"
148
144
 
149
- # Remove the default sim_telarray configuration directories
150
- command = general.clear_default_sim_telarray_cfg_directories(command)
145
+ if mirror_class == 0:
146
+ cmd.extend(["-fmir", self._telescope_model.get_parameter_value("mirror_list")])
147
+ elif mirror_class == 2:
148
+ cmd.extend(["-fmir", self._telescope_model.get_parameter_value("fake_mirror_list")])
151
149
 
152
- return (
153
- f"cd {self._simtel_path.joinpath('sim_telarray')} && {command}",
154
- self._file_simtel,
155
- self._file_log,
150
+ cmd.extend(["-fref", mirror_reflectivity])
151
+
152
+ if mirror_class == 2:
153
+ cmd.append("-m2")
154
+ cmd.extend(["-fref2", mirror_reflectivity_secondary])
155
+
156
+ cmd.extend(
157
+ [
158
+ "-teltrans",
159
+ str(self._telescope_model.get_parameter_value("telescope_transmission")[0]),
160
+ "-camtrans",
161
+ str(camera_transmission),
162
+ "-fflt",
163
+ camera_filter_file,
164
+ "-fang",
165
+ self._telescope_model.camera.get_lightguide_efficiency_angle_file_name(),
166
+ "-fwl",
167
+ self._telescope_model.camera.get_lightguide_efficiency_wavelength_file_name(),
168
+ "-fqe",
169
+ str(self._telescope_model.get_parameter_value("quantum_efficiency")),
170
+ "200", # lmin
171
+ "1000", # lmax
172
+ f"{self.x_max:.1f}" if self.x_max is not None else "300",
173
+ str(self._site_model.get_parameter_value("atmospheric_profile")),
174
+ str(self.zenith_angle),
175
+ ]
156
176
  )
157
177
 
178
+ return cmd, self._file_simtel, self._file_log
179
+
158
180
  def _check_run_result(self, run_number=None): # pylint: disable=unused-argument
159
181
  """Check run results.
160
182
 
@@ -2,18 +2,18 @@
2
2
 
3
3
  import logging
4
4
  import shutil
5
- import stat
6
- import subprocess
7
5
  from pathlib import Path
8
6
 
9
7
  import astropy.units as u
10
8
  import numpy as np
11
9
 
10
+ from simtools import settings
12
11
  from simtools.io import io_handler
12
+ from simtools.job_execution import job_manager
13
13
  from simtools.model.model_utils import initialize_simulation_models
14
- from simtools.runners.simtel_runner import SimtelRunner
14
+ from simtools.runners import runner_services
15
+ from simtools.runners.simtel_runner import SimtelRunner, sim_telarray_env_as_string
15
16
  from simtools.simtel.simtel_config_writer import SimtelConfigWriter
16
- from simtools.utils.general import clear_default_sim_telarray_cfg_directories
17
17
  from simtools.utils.geometry import fiducial_radius_from_shape
18
18
 
19
19
 
@@ -31,21 +31,19 @@ class SimulatorLightEmission(SimtelRunner):
31
31
  Label for the simulation
32
32
  """
33
33
 
34
- def __init__(self, light_emission_config, db_config=None, label=None):
34
+ def __init__(self, light_emission_config, label=None):
35
35
  """Initialize SimulatorLightEmission."""
36
36
  self._logger = logging.getLogger(__name__)
37
37
  self.io_handler = io_handler.IOHandler()
38
38
 
39
- super().__init__(
40
- simtel_path=light_emission_config.get("simtel_path"), label=label, corsika_config=None
39
+ super().__init__(label=label, config=light_emission_config)
40
+ self.job_files = runner_services.RunnerServices(
41
+ light_emission_config, run_type="sub", label=label
41
42
  )
42
43
 
43
- self.output_directory = self.io_handler.get_output_directory()
44
-
45
44
  self.telescope_model, self.site_model, self.calibration_model = (
46
45
  initialize_simulation_models(
47
46
  label=label,
48
- db_config=db_config,
49
47
  site=light_emission_config.get("site"),
50
48
  telescope_name=light_emission_config.get("telescope"),
51
49
  calibration_device_name=light_emission_config.get("light_source"),
@@ -68,7 +66,7 @@ class SimulatorLightEmission(SimtelRunner):
68
66
  config["flasher_photons"] = (
69
67
  self.calibration_model.get_parameter_value("flasher_photons")
70
68
  if not config.get("test", False)
71
- else 1e8
69
+ else 1e5
72
70
  )
73
71
 
74
72
  if config.get("light_source_position") is not None:
@@ -79,73 +77,47 @@ class SimulatorLightEmission(SimtelRunner):
79
77
  return config
80
78
 
81
79
  def simulate(self):
82
- """
83
- Simulate light emission.
84
-
85
- Returns
86
- -------
87
- Path
88
- The output simtel file path.
89
- """
90
- run_script = self.prepare_script()
91
- log_path = Path(self.output_directory) / "logfile.log"
92
- with open(log_path, "w", encoding="utf-8") as fh:
93
- subprocess.run(
94
- run_script,
95
- shell=False,
96
- check=False,
97
- text=True,
98
- stdout=fh,
99
- stderr=fh,
100
- )
101
- out = Path(self._get_simulation_output_filename())
102
- if not out.exists():
103
- self._logger.warning(f"Expected sim_telarray output not found: {out}")
104
- return out
80
+ """Simulate light emission."""
81
+ run_script = self.prepare_run()
82
+ job_manager.submit(
83
+ run_script,
84
+ out_file=self.job_files.get_file_name("sub_out"),
85
+ err_file=self.job_files.get_file_name("sub_err"),
86
+ )
105
87
 
106
- def prepare_script(self):
88
+ def prepare_run(self):
107
89
  """
108
- Build and return bash run script containing the light-emission command.
90
+ Prepare the bash run script containing the light-emission command.
109
91
 
110
92
  Returns
111
93
  -------
112
94
  Path
113
95
  Full path of the run script.
114
96
  """
115
- script_dir = self.output_directory.joinpath("scripts")
116
- script_dir.mkdir(parents=True, exist_ok=True)
117
-
118
- app_name = self._get_light_emission_application_name()
119
- script_file = script_dir / f"{app_name}-light_emission.sh"
120
- self._logger.debug(f"Run bash script - {script_file}")
121
-
122
- target_out = Path(self._get_simulation_output_filename())
123
- if target_out.exists():
97
+ script_file = self.job_files.get_file_name(file_type="sub_script")
98
+ output_file = self.runner_service.get_file_name(file_type="sim_telarray_output")
99
+ if output_file.exists():
124
100
  raise FileExistsError(
125
- f"sim_telarray output file exists, cancelling simulation: {target_out}"
101
+ f"sim_telarray output file exists, cancelling simulation: {output_file}"
126
102
  )
103
+ lines = self.make_run_command()
104
+ script_file.write_text("".join(lines), encoding="utf-8")
105
+ return script_file
127
106
 
128
- lines = [
107
+ def make_run_command(self, run_number=None, input_file=None): # pylint: disable=unused-argument
108
+ """Light emission and sim_telarray run command."""
109
+ iact_output = self.runner_service.get_file_name(file_type="iact_output")
110
+ return [
129
111
  "#!/usr/bin/env bash\n",
130
- f"{self._make_light_emission_script()}\n\n",
112
+ f"{self._make_light_emission_command(iact_output)}\n\n",
131
113
  (
132
- f"[ -s '{self.output_directory}/{app_name}.iact.gz' ] || "
114
+ f"[ -s '{iact_output}' ] || "
133
115
  f"{{ echo 'LightEmission did not produce IACT file' >&2; exit 1; }}\n\n"
134
116
  ),
135
117
  f"{self._make_simtel_script()}\n\n",
136
- f"rm -f '{self.output_directory}/{app_name}.iact.gz'\n\n",
118
+ f"rm -f '{iact_output}'\n\n",
137
119
  ]
138
120
 
139
- script_file.write_text("".join(lines), encoding="utf-8")
140
- script_file.chmod(script_file.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP)
141
- return script_file
142
-
143
- def _get_prefix(self):
144
- prefix = self.light_emission_config.get("output_prefix", "")
145
- if prefix is not None:
146
- return f"{prefix}_"
147
- return ""
148
-
149
121
  def _get_light_emission_application_name(self):
150
122
  """
151
123
  Return the LightEmission application and mode from type.
@@ -244,7 +216,9 @@ class SimulatorLightEmission(SimtelRunner):
244
216
  radius = self.telescope_model.get_parameter_value_with_unit("telescope_sphere_radius")
245
217
  radius = radius.to(u.cm).value # Convert radius to cm
246
218
 
247
- telescope_position_file = self.output_directory.joinpath("telescope_position.dat")
219
+ telescope_position_file = (
220
+ self.io_handler.get_output_directory("light_emission") / "telescope_position.dat"
221
+ )
248
222
  telescope_position_file.write_text(f"{x_tel} {y_tel} {z_tel} {radius}\n", encoding="utf-8")
249
223
  return telescope_position_file
250
224
 
@@ -272,13 +246,18 @@ class SimulatorLightEmission(SimtelRunner):
272
246
  self._logger.warning(f"Failed to create atmosphere alias {dst.name}: {copy_err}")
273
247
  return model_id
274
248
 
275
- def _make_light_emission_script(self):
249
+ def _make_light_emission_command(self, iact_output):
276
250
  """
277
- Create the light emission script to run the light emission package.
251
+ Create the light emission command to run the light emission package.
278
252
 
279
253
  Require the specified pre-compiled light emission package application
280
254
  in the sim_telarray/LightEmission/ path.
281
255
 
256
+ Parameters
257
+ ----------
258
+ iact_output: str or Path
259
+ The output iact file path.
260
+
282
261
  Returns
283
262
  -------
284
263
  str
@@ -287,24 +266,24 @@ class SimulatorLightEmission(SimtelRunner):
287
266
  config_directory = self.io_handler.get_model_configuration_directory(
288
267
  model_version=self.site_model.model_version
289
268
  )
290
- app_name = self._get_light_emission_application_name()
291
- corsika_observation_level = self.site_model.get_parameter_value_with_unit(
292
- "corsika_observation_level"
293
- )
269
+ obs_level = self.site_model.get_parameter_value_with_unit("corsika_observation_level")
270
+
271
+ app = self._get_light_emission_application_name()
272
+ cmd = [
273
+ str(settings.config.sim_telarray_path / "LightEmission" / app),
274
+ *self._get_site_command(app, config_directory, obs_level),
275
+ *self._get_light_source_command(),
276
+ ]
294
277
 
295
- parts = [str(self._simtel_path / "sim_telarray/LightEmission") + f"/{app_name}"]
296
- parts.extend(self._get_site_command(app_name, config_directory, corsika_observation_level))
297
- parts.extend(self._get_light_source_command())
298
278
  if self.light_emission_config["light_source_type"] == "illuminator":
299
- parts += [
279
+ cmd += [
300
280
  "-A",
301
- (
302
- f"{config_directory}/"
303
- f"{self.telescope_model.get_parameter_value('atmospheric_profile')}"
304
- ),
281
+ f"{config_directory}/"
282
+ f"{self.telescope_model.get_parameter_value('atmospheric_profile')}",
305
283
  ]
306
- parts += [f"-o {self.output_directory}/{app_name}.iact.gz", "\n"]
307
- return " ".join(parts)
284
+
285
+ cmd += ["-o", str(iact_output)]
286
+ return " ".join(cmd)
308
287
 
309
288
  def _get_site_command(self, app_name, config_directory, corsika_observation_level):
310
289
  """Return site command with altitude, atmosphere and telescope_position handling."""
@@ -312,7 +291,7 @@ class SimulatorLightEmission(SimtelRunner):
312
291
  atmo_id = self._prepare_flasher_atmosphere_files(config_directory)
313
292
  return [
314
293
  "-I.",
315
- f"-I{self._simtel_path / 'sim_telarray/cfg'}",
294
+ f"-I{settings.config.sim_telarray_path / 'cfg'}",
316
295
  f"-I{config_directory}",
317
296
  f"--altitude {corsika_observation_level.to(u.m).value}",
318
297
  f"--atmosphere {atmo_id}",
@@ -349,34 +328,32 @@ class SimulatorLightEmission(SimtelRunner):
349
328
  dist_cm = self.calculate_distance_focal_plane_calibration_device().to(u.cm).value
350
329
  angular_distribution = self._get_angular_distribution_string_for_sim_telarray()
351
330
 
352
- # Build pulse table for ff-1m using model width/exp parameters; else use token.
331
+ # Build pulse table for ff-1m using unified list parameter [shape, width, exp]
332
+ pulse_shape_value = self.calibration_model.get_parameter_value("flasher_pulse_shape")
333
+ shape_name = pulse_shape_value[0]
334
+ width_ns = pulse_shape_value[1]
335
+ exp_ns = pulse_shape_value[2]
353
336
  pulse_arg = self._get_pulse_shape_string_for_sim_telarray()
354
- pulse_shape = self.calibration_model.get_parameter_value("flasher_pulse_shape")
355
- width_q = self.calibration_model.get_parameter_value_with_unit("flasher_pulse_width")
356
- exp_q = self.calibration_model.get_parameter_value_with_unit("flasher_pulse_exp_decay")
357
- if (
358
- isinstance(exp_q, u.Quantity)
359
- and isinstance(width_q, u.Quantity)
360
- and pulse_shape == "Gauss-Exponential"
361
- ):
362
- try:
363
- base_dir = self.io_handler.get_output_directory("pulse_shapes")
364
-
365
- def _sanitize_name(value):
366
- return "".join(
367
- ch if (ch.isalnum() or ch in ("-", "_")) else "_" for ch in str(value)
368
- )
369
337
 
338
+ if shape_name == "Gauss-Exponential":
339
+ if width_ns <= 0 or exp_ns <= 0:
340
+ raise ValueError(
341
+ "Gauss-Exponential pulse shape requires positive width"
342
+ " and exponential decay values"
343
+ )
344
+ try:
370
345
  tel = self.light_emission_config.get("telescope") or "telescope"
371
346
  cal = self.light_emission_config.get("light_source") or "calibration"
372
- fname = f"flasher_pulse_shape_{_sanitize_name(tel)}_{_sanitize_name(cal)}.dat"
373
- table_path = base_dir / fname
347
+ fname = (
348
+ f"flasher_pulse_shape_{self._sanitize_name(tel)}_{self._sanitize_name(cal)}.dat"
349
+ )
350
+ table_path = self.io_handler.get_output_directory("light_emission") / fname
374
351
  fadc_bins = self.telescope_model.get_parameter_value("fadc_sum_bins")
375
352
 
376
- SimtelConfigWriter.write_lightpulse_table_gauss_expconv(
353
+ SimtelConfigWriter.write_light_pulse_table_gauss_exp_conv(
377
354
  file_path=table_path,
378
- width_ns=width_q.to(u.ns).value,
379
- exp_decay_ns=exp_q.to(u.ns).value,
355
+ width_ns=width_ns,
356
+ exp_decay_ns=exp_ns,
380
357
  fadc_sum_bins=fadc_bins,
381
358
  time_margin_ns=5.0,
382
359
  )
@@ -396,6 +373,9 @@ class SimulatorLightEmission(SimtelRunner):
396
373
  f"--angular-distribution {angular_distribution}",
397
374
  ]
398
375
 
376
+ def _sanitize_name(self, value):
377
+ return "".join(ch if (ch.isalnum() or ch in ("-", "_")) else "_" for ch in str(value))
378
+
399
379
  def _add_illuminator_command_options(self):
400
380
  """Get illuminator-specific command options for light emission script."""
401
381
  pos = self.light_emission_config.get("light_source_position")
@@ -434,56 +414,52 @@ class SimulatorLightEmission(SimtelRunner):
434
414
  The command to run sim_telarray
435
415
  """
436
416
  theta, phi = self._get_telescope_pointing()
437
-
438
- simtel_bin = self._simtel_path.joinpath("sim_telarray/bin/sim_telarray/")
417
+ simtel_bin = str(settings.config.sim_telarray_exe)
439
418
 
440
419
  parts = [
441
- f"{simtel_bin}",
420
+ simtel_bin,
442
421
  f"-I{self.telescope_model.config_file_directory}",
443
422
  f"-I{simtel_bin}",
444
423
  f"-c {self.telescope_model.config_file_path}",
445
424
  "-DNUM_TELESCOPES=1",
446
- super().get_config_option(
425
+ ]
426
+
427
+ options = [
428
+ (
447
429
  "altitude",
448
430
  self.site_model.get_parameter_value_with_unit("corsika_observation_level")
449
431
  .to(u.m)
450
432
  .value,
451
433
  ),
452
- super().get_config_option(
434
+ (
453
435
  "atmospheric_transmission",
454
436
  self.site_model.get_parameter_value("atmospheric_transmission"),
455
437
  ),
456
- super().get_config_option("TRIGGER_TELESCOPES", "1"),
457
- super().get_config_option("TELTRIG_MIN_SIGSUM", "2"),
458
- super().get_config_option("PULSE_ANALYSIS", "-30"),
459
- super().get_config_option("MAXIMUM_TELESCOPES", 1),
460
- super().get_config_option("telescope_theta", f"{theta}"),
461
- super().get_config_option("telescope_phi", f"{phi}"),
438
+ ("TRIGGER_TELESCOPES", "1"),
439
+ ("TELTRIG_MIN_SIGSUM", "2"),
440
+ ("PULSE_ANALYSIS", "-30"),
441
+ ("MAXIMUM_TELESCOPES", 1),
442
+ ("telescope_theta", f"{theta}"),
443
+ ("telescope_phi", f"{phi}"),
462
444
  ]
463
445
 
464
446
  if self.light_emission_config["light_source_type"] == "flat_fielding":
465
- parts.append(super().get_config_option("Bypass_Optics", "1"))
466
-
467
- app_name = self._get_light_emission_application_name()
468
- pref = self._get_prefix()
469
- parts += [
470
- super().get_config_option("power_law", "2.68"),
471
- super().get_config_option("input_file", f"{self.output_directory}/{app_name}.iact.gz"),
472
- super().get_config_option(
473
- "output_file", f"{self.output_directory}/{pref}{app_name}.simtel.zst"
474
- ),
475
- super().get_config_option(
476
- "histogram_file", f"{self.output_directory}/{pref}{app_name}.ctsim.hdata\n"
477
- ),
447
+ options.append(("Bypass_Optics", "1"))
448
+
449
+ input_file = self.runner_service.get_file_name(file_type="iact_output")
450
+ output_file = self.runner_service.get_file_name(file_type="sim_telarray_output")
451
+ histo_file = self.runner_service.get_file_name(file_type="sim_telarray_histogram")
452
+
453
+ options += [
454
+ ("power_law", "2.68"),
455
+ ("input_file", f"{input_file}"),
456
+ ("output_file", f"{output_file}"),
457
+ ("histogram_file", f"{histo_file}"),
478
458
  ]
479
459
 
480
- return clear_default_sim_telarray_cfg_directories(" ".join(parts))
460
+ parts += [f"-C {key}={value}" for key, value in options]
481
461
 
482
- def _get_simulation_output_filename(self):
483
- """Get the filename of the simulation output."""
484
- app_name = self._get_light_emission_application_name()
485
- pref = self._get_prefix()
486
- return f"{self.output_directory}/{pref}{app_name}.simtel.zst"
462
+ return sim_telarray_env_as_string() + " ".join(parts)
487
463
 
488
464
  def calculate_distance_focal_plane_calibration_device(self):
489
465
  """
@@ -503,6 +479,20 @@ class SimulatorLightEmission(SimtelRunner):
503
479
  )
504
480
  return focal_length - flasher_z
505
481
 
482
+ def _generate_lambertian_angular_distribution_table(self):
483
+ """Generate Lambertian angular distribution table via config writer and return path.
484
+
485
+ Uses a pure cosine profile normalized to 1 at 0 deg and spans 0..90 deg by default.
486
+ """
487
+ tel = self._sanitize_name(self.light_emission_config.get("telescope") or "telescope")
488
+ cal = self._sanitize_name(self.light_emission_config.get("light_source") or "calibration")
489
+ fname = f"flasher_angular_distribution_{tel}_{cal}.dat"
490
+ return SimtelConfigWriter.write_angular_distribution_table_lambertian(
491
+ file_path=self.io_handler.get_output_directory("light_emission") / fname,
492
+ max_angle_deg=90.0,
493
+ n_samples=100,
494
+ )
495
+
506
496
  def _get_angular_distribution_string_for_sim_telarray(self):
507
497
  """
508
498
  Get the angular distribution string for sim_telarray.
@@ -514,6 +504,19 @@ class SimulatorLightEmission(SimtelRunner):
514
504
  """
515
505
  opt = self.calibration_model.get_parameter_value("flasher_angular_distribution")
516
506
  option_string = str(opt).lower() if opt is not None else ""
507
+ if option_string == "lambertian":
508
+ try:
509
+ return self._generate_lambertian_angular_distribution_table()
510
+ except (OSError, ValueError) as err:
511
+ self._logger.warning(
512
+ f"Failed to write Lambertian angular distribution table: {err};"
513
+ f" using token instead."
514
+ )
515
+ return option_string
516
+
517
+ if option_string == "isotropic":
518
+ return option_string
519
+
517
520
  width = self.calibration_model.get_parameter_value_with_unit(
518
521
  "flasher_angular_distribution_width"
519
522
  )
@@ -529,6 +532,35 @@ class SimulatorLightEmission(SimtelRunner):
529
532
  The pulse shape string.
530
533
  """
531
534
  opt = self.calibration_model.get_parameter_value("flasher_pulse_shape")
532
- option_string = str(opt).lower() if opt is not None else ""
533
- width = self.calibration_model.get_parameter_value_with_unit("flasher_pulse_width")
534
- return f"{option_string}:{width.to(u.ns).value}" if width is not None else option_string
535
+ shape = opt[0].lower()
536
+ # Map internal shapes to sim_telarray expected tokens
537
+ # 'tophat' corresponds to a simple (flat) pulse in sim_telarray.
538
+ shape_token_map = {
539
+ "tophat": "simple",
540
+ }
541
+ shape_out = shape_token_map.get(shape, shape)
542
+ width = opt[1]
543
+ expv = opt[2]
544
+ if shape_out == "gauss-exponential" and width is not None and expv is not None:
545
+ return f"{shape_out}:{float(width)}:{float(expv)}"
546
+ if shape_out in ("gauss", "simple") and width is not None:
547
+ return f"{shape_out}:{float(width)}"
548
+ if shape_out == "exponential" and expv is not None:
549
+ return f"{shape_out}:{float(expv)}"
550
+ return shape_out
551
+
552
+ def verify_simulations(self):
553
+ """
554
+ Verify that the simulations were successful.
555
+
556
+ Returns
557
+ -------
558
+ bool
559
+ True if simulations were successful, False otherwise.
560
+ """
561
+ out = Path(self.runner_service.get_file_name(file_type="sim_telarray_output"))
562
+ if not out.exists():
563
+ self._logger.error(f"Expected sim_telarray output not found: {out}")
564
+ return False
565
+ self._logger.info(f"sim_telarray output found: {out}")
566
+ return True