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
@@ -2,8 +2,6 @@
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
@@ -11,10 +9,11 @@ import numpy as np
11
9
 
12
10
  from simtools import settings
13
11
  from simtools.io import io_handler
12
+ from simtools.job_execution import job_manager
14
13
  from simtools.model.model_utils import initialize_simulation_models
15
- 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
16
16
  from simtools.simtel.simtel_config_writer import SimtelConfigWriter
17
- from simtools.utils.general import clear_default_sim_telarray_cfg_directories
18
17
  from simtools.utils.geometry import fiducial_radius_from_shape
19
18
 
20
19
 
@@ -37,9 +36,10 @@ class SimulatorLightEmission(SimtelRunner):
37
36
  self._logger = logging.getLogger(__name__)
38
37
  self.io_handler = io_handler.IOHandler()
39
38
 
40
- super().__init__(label=label, corsika_config=None)
41
-
42
- self.output_directory = self.io_handler.get_output_directory()
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
42
+ )
43
43
 
44
44
  self.telescope_model, self.site_model, self.calibration_model = (
45
45
  initialize_simulation_models(
@@ -77,73 +77,47 @@ class SimulatorLightEmission(SimtelRunner):
77
77
  return config
78
78
 
79
79
  def simulate(self):
80
- """
81
- Simulate light emission.
82
-
83
- Returns
84
- -------
85
- Path
86
- The output simtel file path.
87
- """
88
- run_script = self.prepare_script()
89
- log_path = Path(self.output_directory) / "logfile.log"
90
- with open(log_path, "w", encoding="utf-8") as fh:
91
- subprocess.run(
92
- run_script,
93
- shell=False,
94
- check=False,
95
- text=True,
96
- stdout=fh,
97
- stderr=fh,
98
- )
99
- out = Path(self._get_simulation_output_filename())
100
- if not out.exists():
101
- self._logger.warning(f"Expected sim_telarray output not found: {out}")
102
- 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
+ )
103
87
 
104
- def prepare_script(self):
88
+ def prepare_run(self):
105
89
  """
106
- Build and return bash run script containing the light-emission command.
90
+ Prepare the bash run script containing the light-emission command.
107
91
 
108
92
  Returns
109
93
  -------
110
94
  Path
111
95
  Full path of the run script.
112
96
  """
113
- script_dir = self.output_directory.joinpath("scripts")
114
- script_dir.mkdir(parents=True, exist_ok=True)
115
-
116
- app_name = self._get_light_emission_application_name()
117
- script_file = script_dir / f"{app_name}-light_emission.sh"
118
- self._logger.debug(f"Run bash script - {script_file}")
119
-
120
- target_out = Path(self._get_simulation_output_filename())
121
- 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():
122
100
  raise FileExistsError(
123
- f"sim_telarray output file exists, cancelling simulation: {target_out}"
101
+ f"sim_telarray output file exists, cancelling simulation: {output_file}"
124
102
  )
103
+ lines = self.make_run_command()
104
+ script_file.write_text("".join(lines), encoding="utf-8")
105
+ return script_file
125
106
 
126
- 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 [
127
111
  "#!/usr/bin/env bash\n",
128
- f"{self._make_light_emission_script()}\n\n",
112
+ f"{self._make_light_emission_command(iact_output)}\n\n",
129
113
  (
130
- f"[ -s '{self.output_directory}/{app_name}.iact.gz' ] || "
114
+ f"[ -s '{iact_output}' ] || "
131
115
  f"{{ echo 'LightEmission did not produce IACT file' >&2; exit 1; }}\n\n"
132
116
  ),
133
117
  f"{self._make_simtel_script()}\n\n",
134
- f"rm -f '{self.output_directory}/{app_name}.iact.gz'\n\n",
118
+ f"rm -f '{iact_output}'\n\n",
135
119
  ]
136
120
 
137
- script_file.write_text("".join(lines), encoding="utf-8")
138
- script_file.chmod(script_file.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP)
139
- return script_file
140
-
141
- def _get_prefix(self):
142
- prefix = self.light_emission_config.get("output_prefix", "")
143
- if prefix is not None:
144
- return f"{prefix}_"
145
- return ""
146
-
147
121
  def _get_light_emission_application_name(self):
148
122
  """
149
123
  Return the LightEmission application and mode from type.
@@ -242,7 +216,9 @@ class SimulatorLightEmission(SimtelRunner):
242
216
  radius = self.telescope_model.get_parameter_value_with_unit("telescope_sphere_radius")
243
217
  radius = radius.to(u.cm).value # Convert radius to cm
244
218
 
245
- 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
+ )
246
222
  telescope_position_file.write_text(f"{x_tel} {y_tel} {z_tel} {radius}\n", encoding="utf-8")
247
223
  return telescope_position_file
248
224
 
@@ -270,13 +246,18 @@ class SimulatorLightEmission(SimtelRunner):
270
246
  self._logger.warning(f"Failed to create atmosphere alias {dst.name}: {copy_err}")
271
247
  return model_id
272
248
 
273
- def _make_light_emission_script(self):
249
+ def _make_light_emission_command(self, iact_output):
274
250
  """
275
- Create the light emission script to run the light emission package.
251
+ Create the light emission command to run the light emission package.
276
252
 
277
253
  Require the specified pre-compiled light emission package application
278
254
  in the sim_telarray/LightEmission/ path.
279
255
 
256
+ Parameters
257
+ ----------
258
+ iact_output: str or Path
259
+ The output iact file path.
260
+
280
261
  Returns
281
262
  -------
282
263
  str
@@ -285,24 +266,24 @@ class SimulatorLightEmission(SimtelRunner):
285
266
  config_directory = self.io_handler.get_model_configuration_directory(
286
267
  model_version=self.site_model.model_version
287
268
  )
288
- app_name = self._get_light_emission_application_name()
289
- corsika_observation_level = self.site_model.get_parameter_value_with_unit(
290
- "corsika_observation_level"
291
- )
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
+ ]
292
277
 
293
- parts = [str(settings.config.sim_telarray_path / "LightEmission") + f"/{app_name}"]
294
- parts.extend(self._get_site_command(app_name, config_directory, corsika_observation_level))
295
- parts.extend(self._get_light_source_command())
296
278
  if self.light_emission_config["light_source_type"] == "illuminator":
297
- parts += [
279
+ cmd += [
298
280
  "-A",
299
- (
300
- f"{config_directory}/"
301
- f"{self.telescope_model.get_parameter_value('atmospheric_profile')}"
302
- ),
281
+ f"{config_directory}/"
282
+ f"{self.telescope_model.get_parameter_value('atmospheric_profile')}",
303
283
  ]
304
- parts += [f"-o {self.output_directory}/{app_name}.iact.gz", "\n"]
305
- return " ".join(parts)
284
+
285
+ cmd += ["-o", str(iact_output)]
286
+ return " ".join(cmd)
306
287
 
307
288
  def _get_site_command(self, app_name, config_directory, corsika_observation_level):
308
289
  """Return site command with altitude, atmosphere and telescope_position handling."""
@@ -361,17 +342,12 @@ class SimulatorLightEmission(SimtelRunner):
361
342
  " and exponential decay values"
362
343
  )
363
344
  try:
364
- base_dir = self.io_handler.get_output_directory("pulse_shapes")
365
-
366
- def _sanitize_name(value):
367
- return "".join(
368
- ch if (ch.isalnum() or ch in ("-", "_")) else "_" for ch in str(value)
369
- )
370
-
371
345
  tel = self.light_emission_config.get("telescope") or "telescope"
372
346
  cal = self.light_emission_config.get("light_source") or "calibration"
373
- fname = f"flasher_pulse_shape_{_sanitize_name(tel)}_{_sanitize_name(cal)}.dat"
374
- 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
375
351
  fadc_bins = self.telescope_model.get_parameter_value("fadc_sum_bins")
376
352
 
377
353
  SimtelConfigWriter.write_light_pulse_table_gauss_exp_conv(
@@ -397,6 +373,9 @@ class SimulatorLightEmission(SimtelRunner):
397
373
  f"--angular-distribution {angular_distribution}",
398
374
  ]
399
375
 
376
+ def _sanitize_name(self, value):
377
+ return "".join(ch if (ch.isalnum() or ch in ("-", "_")) else "_" for ch in str(value))
378
+
400
379
  def _add_illuminator_command_options(self):
401
380
  """Get illuminator-specific command options for light emission script."""
402
381
  pos = self.light_emission_config.get("light_source_position")
@@ -435,56 +414,52 @@ class SimulatorLightEmission(SimtelRunner):
435
414
  The command to run sim_telarray
436
415
  """
437
416
  theta, phi = self._get_telescope_pointing()
438
-
439
417
  simtel_bin = str(settings.config.sim_telarray_exe)
440
418
 
441
419
  parts = [
442
- f"{simtel_bin}",
420
+ simtel_bin,
443
421
  f"-I{self.telescope_model.config_file_directory}",
444
422
  f"-I{simtel_bin}",
445
423
  f"-c {self.telescope_model.config_file_path}",
446
424
  "-DNUM_TELESCOPES=1",
447
- super().get_config_option(
425
+ ]
426
+
427
+ options = [
428
+ (
448
429
  "altitude",
449
430
  self.site_model.get_parameter_value_with_unit("corsika_observation_level")
450
431
  .to(u.m)
451
432
  .value,
452
433
  ),
453
- super().get_config_option(
434
+ (
454
435
  "atmospheric_transmission",
455
436
  self.site_model.get_parameter_value("atmospheric_transmission"),
456
437
  ),
457
- super().get_config_option("TRIGGER_TELESCOPES", "1"),
458
- super().get_config_option("TELTRIG_MIN_SIGSUM", "2"),
459
- super().get_config_option("PULSE_ANALYSIS", "-30"),
460
- super().get_config_option("MAXIMUM_TELESCOPES", 1),
461
- super().get_config_option("telescope_theta", f"{theta}"),
462
- 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}"),
463
444
  ]
464
445
 
465
446
  if self.light_emission_config["light_source_type"] == "flat_fielding":
466
- parts.append(super().get_config_option("Bypass_Optics", "1"))
467
-
468
- app_name = self._get_light_emission_application_name()
469
- pref = self._get_prefix()
470
- parts += [
471
- super().get_config_option("power_law", "2.68"),
472
- super().get_config_option("input_file", f"{self.output_directory}/{app_name}.iact.gz"),
473
- super().get_config_option(
474
- "output_file", f"{self.output_directory}/{pref}{app_name}.simtel.zst"
475
- ),
476
- super().get_config_option(
477
- "histogram_file", f"{self.output_directory}/{pref}{app_name}.ctsim.hdata\n"
478
- ),
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}"),
479
458
  ]
480
459
 
481
- return clear_default_sim_telarray_cfg_directories(" ".join(parts))
460
+ parts += [f"-C {key}={value}" for key, value in options]
482
461
 
483
- def _get_simulation_output_filename(self):
484
- """Get the filename of the simulation output."""
485
- app_name = self._get_light_emission_application_name()
486
- pref = self._get_prefix()
487
- return f"{self.output_directory}/{pref}{app_name}.simtel.zst"
462
+ return sim_telarray_env_as_string() + " ".join(parts)
488
463
 
489
464
  def calculate_distance_focal_plane_calibration_device(self):
490
465
  """
@@ -509,21 +484,14 @@ class SimulatorLightEmission(SimtelRunner):
509
484
 
510
485
  Uses a pure cosine profile normalized to 1 at 0 deg and spans 0..90 deg by default.
511
486
  """
512
- base_dir = self.io_handler.get_output_directory("angular_distributions")
513
-
514
- def _sanitize_name(value):
515
- return "".join(ch if (ch.isalnum() or ch in ("-", "_")) else "_" for ch in str(value))
516
-
517
- tel = self.light_emission_config.get("telescope") or "telescope"
518
- cal = self.light_emission_config.get("light_source") or "calibration"
519
- fname = f"flasher_angular_distribution_{_sanitize_name(tel)}_{_sanitize_name(cal)}.dat"
520
- table_path = base_dir / fname
521
- SimtelConfigWriter.write_angular_distribution_table_lambertian(
522
- file_path=table_path,
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,
523
492
  max_angle_deg=90.0,
524
493
  n_samples=100,
525
494
  )
526
- return str(table_path)
527
495
 
528
496
  def _get_angular_distribution_string_for_sim_telarray(self):
529
497
  """
@@ -546,6 +514,9 @@ class SimulatorLightEmission(SimtelRunner):
546
514
  )
547
515
  return option_string
548
516
 
517
+ if option_string == "isotropic":
518
+ return option_string
519
+
549
520
  width = self.calibration_model.get_parameter_value_with_unit(
550
521
  "flasher_angular_distribution_width"
551
522
  )
@@ -577,3 +548,19 @@ class SimulatorLightEmission(SimtelRunner):
577
548
  if shape_out == "exponential" and expv is not None:
578
549
  return f"{shape_out}:{float(expv)}"
579
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
@@ -9,7 +9,6 @@ from simtools import settings
9
9
  from simtools.io import io_handler
10
10
  from simtools.runners.simtel_runner import SimtelRunner
11
11
  from simtools.utils import names
12
- from simtools.utils.general import clear_default_sim_telarray_cfg_directories
13
12
 
14
13
  # pylint: disable=no-member
15
14
  # The line above is needed because there are members which are created
@@ -67,6 +66,8 @@ class SimulatorRayTracing(SimtelRunner):
67
66
  self._rep_number = 0
68
67
  self.runs_per_set = 1 if self.config.single_mirror_mode else 20
69
68
  self.photons_per_run = 100000 if not test else 5000
69
+ self._single_pixel_camera_file = None
70
+ self._funnel_file = None
70
71
 
71
72
  self._load_required_files(force_simulate)
72
73
 
@@ -106,12 +107,13 @@ class SimulatorRayTracing(SimtelRunner):
106
107
  self.__dict__["_" + base_name + "_file"] = file
107
108
 
108
109
  if not file.exists() or force_simulate:
110
+ config_file_path = self.telescope_model.get_config_file_path(label=self.label)
109
111
  # Adding header to photon list file.
110
112
  with self._photons_file.open("w", encoding="utf-8") as file:
111
113
  file.write(f"#{50 * '='}\n")
112
114
  file.write("# List of photons for RayTracing simulations\n")
113
115
  file.write(f"#{50 * '='}\n")
114
- file.write(f"# config_file = {self.telescope_model.config_file_path}\n")
116
+ file.write(f"# config_file = {config_file_path}\n")
115
117
  file.write(f"# zenith_angle [deg] = {self.config.zenith_angle}\n")
116
118
  file.write(f"# off_axis_angle [deg] = {self.config.off_axis_angle}\n")
117
119
  file.write(f"# source_distance [km] = {self.config.source_distance}\n")
@@ -130,15 +132,20 @@ class SimulatorRayTracing(SimtelRunner):
130
132
 
131
133
  if self.config.single_mirror_mode:
132
134
  self._logger.debug("For single mirror mode, need to prepare the single pixel camera.")
133
- self._write_out_single_pixel_camera_file()
135
+ self._write_out_single_pixel_camera_files()
134
136
 
135
- def _make_run_command(self, run_number=None, input_file=None): # pylint: disable=unused-argument
137
+ def make_run_command(self, run_number=None, input_file=None): # pylint: disable=unused-argument
136
138
  """
137
139
  Generate sim_telarray run command. Export sim_telarray configuration file(s).
138
140
 
139
141
  The run_number and input_file parameters are not relevant for the ray tracing simulation.
140
142
  """
141
- self.telescope_model.write_sim_telarray_config_file(additional_models=self.site_model)
143
+ self.telescope_model.write_sim_telarray_config_file(
144
+ additional_models=self.site_model,
145
+ label=self.label,
146
+ )
147
+
148
+ config_file_path = self.telescope_model.get_config_file_path(label=self.label)
142
149
 
143
150
  if self.config.single_mirror_mode:
144
151
  # Note: no mirror length defined for dual-mirror telescopes
@@ -146,54 +153,56 @@ class SimulatorRayTracing(SimtelRunner):
146
153
  self.telescope_model.get_parameter_value("mirror_focal_length")
147
154
  )
148
155
 
149
- # RayTracing
150
- command = str(settings.config.sim_telarray_exe)
151
- command += f" -c {self.telescope_model.config_file_path}"
152
- command += f" -I{self.telescope_model.config_file_directory}"
153
- command += super().get_config_option("random_state", "none")
154
- command += super().get_config_option("IMAGING_LIST", str(self._photons_file))
155
- command += super().get_config_option("stars", str(self._stars_file))
156
- command += super().get_config_option(
157
- "altitude", self.site_model.get_parameter_value("corsika_observation_level")
158
- )
159
- command += super().get_config_option(
160
- "telescope_theta",
161
- self.config.zenith_angle + self.config.off_axis_angle,
162
- )
163
- command += super().get_config_option("star_photons", str(self.photons_per_run))
164
- command += super().get_config_option("telescope_phi", "0")
165
- command += super().get_config_option("camera_transmission", "1.0")
166
- command += super().get_config_option("nightsky_background", "all:0.")
167
- command += super().get_config_option("trigger_current_limit", "1e10")
168
- command += super().get_config_option("telescope_random_angle", "0")
169
- command += super().get_config_option("telescope_random_error", "0")
170
- command += super().get_config_option("convergent_depth", "0")
171
- command += super().get_config_option("maximum_telescopes", "1")
172
- command += super().get_config_option("show", "all")
173
- command += super().get_config_option("camera_filter", "none")
156
+ options = {
157
+ "random_state": "none",
158
+ "IMAGING_LIST": str(self._photons_file),
159
+ "stars": str(self._stars_file),
160
+ "altitude": self.site_model.get_parameter_value("corsika_observation_level"),
161
+ "telescope_theta": self.config.zenith_angle + self.config.off_axis_angle,
162
+ "star_photons": str(self.photons_per_run),
163
+ "telescope_phi": "0",
164
+ "camera_transmission": "1.0",
165
+ "nightsky_background": "all:0.",
166
+ "trigger_current_limit": "1e10",
167
+ "telescope_random_angle": "0",
168
+ "telescope_random_error": "0",
169
+ "convergent_depth": "0",
170
+ "maximum_telescopes": "1",
171
+ "show": "all",
172
+ "camera_filter": "none",
173
+ }
174
+
174
175
  if self.config.single_mirror_mode:
175
- command += super().get_config_option("focus_offset", "all:0.")
176
- command += super().get_config_option("camera_config_file", "single_pixel_camera.dat")
177
- command += super().get_config_option("camera_pixels", "1")
178
- command += super().get_config_option("trigger_pixels", "1")
179
- command += super().get_config_option("camera_body_diameter", "0")
180
- command += super().get_config_option(
181
- "mirror_list",
182
- self.telescope_model.get_single_mirror_list_file(
183
- self.config.mirror_numbers, self.config.use_random_focal_length
184
- ),
185
- )
186
- command += super().get_config_option(
187
- "focal_length", self.config.source_distance * u.km.to(u.cm)
176
+ options.update(
177
+ {
178
+ "focus_offset": "all:0.",
179
+ "camera_config_file": str(self._single_pixel_camera_file),
180
+ "camera_pixels": "1",
181
+ "trigger_pixels": "1",
182
+ "camera_body_diameter": "0",
183
+ "mirror_list": self.telescope_model.get_single_mirror_list_file(
184
+ self.config.mirror_numbers, self.config.use_random_focal_length
185
+ ),
186
+ "focal_length": self.config.source_distance * u.km.to(u.cm),
187
+ "dish_shape_length": _mirror_focal_length,
188
+ "mirror_focal_length": _mirror_focal_length,
189
+ "parabolic_dish": "0",
190
+ "mirror_align_random_distance": "0.",
191
+ "mirror_align_random_vertical": "0.,28.,0.,0.",
192
+ }
188
193
  )
189
- command += super().get_config_option("dish_shape_length", _mirror_focal_length)
190
- command += super().get_config_option("mirror_focal_length", _mirror_focal_length)
191
- command += super().get_config_option("parabolic_dish", "0")
192
- command += super().get_config_option("mirror_align_random_distance", "0.")
193
- command += super().get_config_option("mirror_align_random_vertical", "0.,28.,0.,0.")
194
- command += " " + str(settings.config.corsika_dummy_file)
195
194
 
196
- return clear_default_sim_telarray_cfg_directories(command), self._log_file, self._log_file
195
+ cmd = [
196
+ str(settings.config.sim_telarray_exe),
197
+ "-c",
198
+ str(config_file_path),
199
+ f"-I{self.telescope_model.config_file_directory}",
200
+ ]
201
+ for key, value in options.items():
202
+ cmd.extend(["-C", f"{key}={value}"])
203
+ cmd.append(str(settings.config.corsika_dummy_file))
204
+
205
+ return cmd, self._log_file, self._log_file
197
206
 
198
207
  def _check_run_result(self, run_number=None): # pylint: disable=unused-argument
199
208
  """
@@ -217,20 +226,19 @@ class SimulatorRayTracing(SimtelRunner):
217
226
  raise RuntimeError("Photon list is empty.")
218
227
  return True
219
228
 
220
- def _write_out_single_pixel_camera_file(self):
221
- """Write out the single pixel camera file."""
222
- with self.telescope_model.config_file_directory.joinpath("single_pixel_camera.dat").open(
223
- "w"
224
- ) as file:
225
- file.write("# Single pixel camera\n")
226
- file.write('PixType 1 0 0 300 1 300 0.00 "funnel_perfect.dat"\n')
227
- file.write("Pixel 0 1 0. 0. 0 0 0 0x00 1\n")
228
- file.write("Trigger 1 of 0\n")
229
+ def _write_out_single_pixel_camera_files(self):
230
+ """Write out per-label single pixel camera + funnel files.
231
+
232
+ These files are referenced by sim_telarray and must not be shared across
233
+ parallel worker processes (otherwise they can be truncated mid-read).
234
+ """
235
+ self._single_pixel_camera_file = (
236
+ self._base_directory / f"single_pixel_camera_{self.label}.dat"
237
+ )
238
+ self._funnel_file = self._base_directory / f"funnel_perfect_{self.label}.dat"
229
239
 
230
- # need to also write out the funnel_perfect.dat file
231
- with self.telescope_model.config_file_directory.joinpath("funnel_perfect.dat").open(
232
- "w"
233
- ) as file:
240
+ # Funnel file (referenced by absolute path from the camera file).
241
+ with self._funnel_file.open("w", encoding="utf-8") as file:
234
242
  file.write(
235
243
  "# Perfect light collection where the angular efficiency of funnels is needed\n"
236
244
  )
@@ -239,6 +247,13 @@ class SimulatorRayTracing(SimtelRunner):
239
247
  file.write("60 1.0\n")
240
248
  file.write("90 1.0\n")
241
249
 
250
+ # Camera config.
251
+ with self._single_pixel_camera_file.open("w", encoding="utf-8") as file:
252
+ file.write("# Single pixel camera\n")
253
+ file.write(f'PixType 1 0 0 300 1 300 0.00 "{self._funnel_file}"\n')
254
+ file.write("Pixel 0 1 0. 0. 0 0 0 0x00 1\n")
255
+ file.write("Trigger 1 of 0\n")
256
+
242
257
  def _config_to_namedtuple(self, data_dict):
243
258
  """Convert dict to namedtuple for configuration."""
244
259
  config_data = namedtuple(