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
@@ -44,11 +44,11 @@ class TriggeredEventData:
44
44
  angular_distance: list[float] = field(default_factory=list)
45
45
 
46
46
 
47
- class SimtelIOEventDataReader:
47
+ class EventDataReader:
48
48
  """Read reduced MC data set stored in astropy tables."""
49
49
 
50
50
  def __init__(self, event_data_file, telescope_list=None):
51
- """Initialize SimtelIOEventDataReader."""
51
+ """Initialize EventDataReader."""
52
52
  self._logger = logging.getLogger(__name__)
53
53
  self.telescope_list = telescope_list
54
54
 
@@ -61,7 +61,7 @@ class SimtelIOEventDataReader:
61
61
 
62
62
  Rearrange dictionary with tables names into a list of dictionaries
63
63
  under the assumption that the file contains the tables "SHOWERS",
64
- "TRIGGERS", and "FILE_INFO".
64
+ "TRIGGERS", and "FILE_INFO". Note that not all tables need to be present.
65
65
 
66
66
  Parameters
67
67
  ----------
@@ -88,13 +88,13 @@ class SimtelIOEventDataReader:
88
88
  except (ValueError, AttributeError):
89
89
  sorted_indices = [0] # Handle the case where the key is only "SHOWERS"
90
90
  for i in sorted_indices:
91
- data_sets.append(
92
- {
93
- "SHOWERS": dataset_dict["SHOWERS"][i],
94
- "TRIGGERS": dataset_dict["TRIGGERS"][i],
95
- "FILE_INFO": dataset_dict["FILE_INFO"][i],
96
- }
97
- )
91
+ entry = {
92
+ "SHOWERS": dataset_dict["SHOWERS"][i],
93
+ "FILE_INFO": dataset_dict["FILE_INFO"][i],
94
+ }
95
+ if i < len(dataset_dict["TRIGGERS"]) and dataset_dict["TRIGGERS"][i]:
96
+ entry["TRIGGERS"] = dataset_dict["TRIGGERS"][i]
97
+ data_sets.append(entry)
98
98
 
99
99
  return data_sets
100
100
 
@@ -248,20 +248,23 @@ class SimtelIOEventDataReader:
248
248
  tuple
249
249
  A tuple with file info table, shower, triggered shower, and triggered event data.
250
250
  """
251
- table_name_map = table_name_map or {}
252
251
 
253
- def get_name(key):
254
- return table_name_map.get(key, key)
252
+ def get_name(k):
253
+ return k if table_name_map is None else table_name_map.get(k)
255
254
 
256
- tables = table_handler.read_tables(
257
- event_data_file,
258
- table_names=[get_name(k) for k in ("SHOWERS", "TRIGGERS", "FILE_INFO")],
259
- )
255
+ table_names = [
256
+ name for k in ("SHOWERS", "TRIGGERS", "FILE_INFO") if (name := get_name(k)) is not None
257
+ ]
258
+ tables = table_handler.read_tables(event_data_file, table_names=table_names)
260
259
  self.reduced_file_info = self.get_reduced_simulation_file_info(
261
260
  tables[get_name("FILE_INFO")]
262
261
  )
263
262
 
264
263
  shower_data = self._table_to_shower_data(tables[get_name("SHOWERS")])
264
+ if tables.get(get_name("TRIGGERS")) is None:
265
+ self._logger.info("No triggered event data found in the file.")
266
+ return tables[get_name("FILE_INFO")], shower_data, None, None
267
+
265
268
  triggered_data = self._table_to_triggered_data(tables[get_name("TRIGGERS")])
266
269
  triggered_shower = self._get_triggered_shower_data(
267
270
  shower_data,
@@ -1,4 +1,4 @@
1
- """Generate a reduced dataset from sim_telarray output files using astropy tables."""
1
+ """Generate a reduced dataset from simulation files (CORSIKA/sim_telarray) using astropy tables."""
2
2
 
3
3
  import logging
4
4
  from dataclasses import dataclass
@@ -6,7 +6,7 @@ from dataclasses import dataclass
6
6
  import astropy.units as u
7
7
  import numpy as np
8
8
  from astropy.table import Table
9
- from eventio import EventIOFile
9
+ from eventio import EventIOFile, iact
10
10
  from eventio.simtel import (
11
11
  ArrayEvent,
12
12
  MCEvent,
@@ -17,7 +17,10 @@ from eventio.simtel import (
17
17
  )
18
18
 
19
19
  from simtools.corsika.primary_particle import PrimaryParticle
20
- from simtools.io.eventio_handler import get_combined_corsika_run_header
20
+ from simtools.sim_events.file_info import (
21
+ get_combined_eventio_run_header,
22
+ get_corsika_run_and_event_headers,
23
+ )
21
24
  from simtools.simtel.simtel_io_metadata import (
22
25
  get_sim_telarray_telescope_id_to_telescope_name_mapping,
23
26
  read_sim_telarray_metadata,
@@ -68,11 +71,11 @@ class TableSchemas:
68
71
  }
69
72
 
70
73
 
71
- class SimtelIOEventDataWriter:
74
+ class EventDataWriter:
72
75
  """
73
- Process sim_telarray events and write tables to file.
76
+ Process simulation events (CORSIKA/sim_telarray) and write tables to file.
74
77
 
75
- Extracts essential information from sim_telarray output files:
78
+ Extracts essential information from simulation files, including:
76
79
 
77
80
  - Shower parameters (energy, core location, direction)
78
81
  - Trigger patterns
@@ -108,7 +111,7 @@ class SimtelIOEventDataWriter:
108
111
  Returns
109
112
  -------
110
113
  list
111
- List of astropy tables containing processed data.
114
+ List of tables containing processed data.
112
115
  """
113
116
  for i, file in enumerate(self.input_files[: self.max_files]):
114
117
  self._logger.info(f"Processing file {i + 1}/{self.max_files}: {file}")
@@ -117,13 +120,15 @@ class SimtelIOEventDataWriter:
117
120
  return self.create_tables()
118
121
 
119
122
  def create_tables(self):
120
- """Create astropy tables from collected data."""
123
+ """Create tables from collected data."""
121
124
  tables = []
122
125
  for data, schema, name in [
123
126
  (self.shower_data, TableSchemas.shower_schema, "SHOWERS"),
124
127
  (self.trigger_data, TableSchemas.trigger_schema, "TRIGGERS"),
125
128
  (self.file_info, TableSchemas.file_info_schema, "FILE_INFO"),
126
129
  ]:
130
+ if len(data) == 0:
131
+ continue
127
132
  table = Table(rows=data, names=schema.keys())
128
133
  table.meta["EXTNAME"] = name
129
134
  self._add_units_to_table(table, schema)
@@ -149,40 +154,69 @@ class SimtelIOEventDataWriter:
149
154
  self._process_mc_event(eventio_object)
150
155
  elif isinstance(eventio_object, ArrayEvent):
151
156
  self._process_array_event(eventio_object, file_id)
157
+ elif isinstance(eventio_object, iact.EventHeader):
158
+ self._process_mc_shower_from_iact(eventio_object, file_id)
152
159
 
153
160
  def _process_mc_run_header(self, eventio_object):
154
- """Process MC run header and update data lists."""
161
+ """Process MC run header (sim_telarray file)."""
155
162
  mc_head = eventio_object.parse()
156
163
  self.n_use = mc_head["n_use"] # reuse factor n_use needed to extend the values below
157
164
  self._logger.info(f"Shower reuse factor: {self.n_use} (viewcone: {mc_head['viewcone']})")
158
165
 
159
166
  def _process_file_info(self, file_id, file):
160
167
  """Process file information and append to file info list."""
161
- run_info = get_combined_corsika_run_header(file)
162
- self.telescope_id_to_name = get_sim_telarray_telescope_id_to_telescope_name_mapping(file)
163
- particle = PrimaryParticle(
164
- particle_id_type="eventio_id", particle_id=run_info.get("primary_id", 1)
165
- )
168
+ run_info = get_combined_eventio_run_header(file)
169
+ if run_info: # sim_telarray file
170
+ self.telescope_id_to_name = get_sim_telarray_telescope_id_to_telescope_name_mapping(
171
+ file
172
+ )
173
+ corsika7_id = PrimaryParticle(
174
+ particle_id_type="eventio_id",
175
+ particle_id=run_info.get("primary_id", 1),
176
+ ).corsika7_id
177
+ nsb = self.get_nsb_level_from_sim_telarray_metadata(file)
178
+
179
+ e_min, e_max = run_info["E_range"]
180
+ view_cone_min, view_cone_max = run_info["viewcone"]
181
+ core_min, core_max = run_info["core_range"]
182
+ azimuth, el = np.degrees(run_info["direction"])
183
+ zenith = 90.0 - el
184
+ else: # CORSIKA IACT file
185
+ run_header, event_header = get_corsika_run_and_event_headers(file)
186
+ corsika7_id = int(event_header["particle_id"])
187
+ e_min = event_header["energy_min"]
188
+ e_max = event_header["energy_max"]
189
+ zenith = np.degrees(event_header["zenith"])
190
+ # Rotate to geographic north
191
+ azimuth = np.degrees(
192
+ event_header["azimuth"] - event_header["angle_array_x_magnetic_north"]
193
+ )
194
+ view_cone_min = event_header["viewcone_inner_angle"]
195
+ view_cone_max = event_header["viewcone_outer_angle"]
196
+ core_min = 0.0
197
+ core_max = run_header["x_scatter"] / 1.0e2 # cm to m
198
+ nsb = 0.0
199
+
166
200
  self.file_info.append(
167
201
  {
168
202
  "file_name": str(file),
169
203
  "file_id": file_id,
170
- "particle_id": particle.corsika7_id,
171
- "energy_min": run_info["E_range"][0],
172
- "energy_max": run_info["E_range"][1],
173
- "viewcone_min": run_info["viewcone"][0],
174
- "viewcone_max": run_info["viewcone"][1],
175
- "core_scatter_min": run_info["core_range"][0],
176
- "core_scatter_max": run_info["core_range"][1],
177
- "zenith": 90.0 - np.degrees(run_info["direction"][1]),
178
- "azimuth": np.degrees(run_info["direction"][0]),
179
- "nsb_level": self.get_nsb_level_from_sim_telarray_metadata(file),
204
+ "particle_id": corsika7_id,
205
+ "energy_min": e_min,
206
+ "energy_max": e_max,
207
+ "viewcone_min": view_cone_min,
208
+ "viewcone_max": view_cone_max,
209
+ "core_scatter_min": core_min,
210
+ "core_scatter_max": core_max,
211
+ "zenith": zenith,
212
+ "azimuth": azimuth,
213
+ "nsb_level": nsb,
180
214
  }
181
215
  )
182
216
 
183
217
  def _process_mc_shower(self, eventio_object, file_id):
184
218
  """
185
- Process MC shower and update shower event list.
219
+ Process MC shower from sim_telarray file and update shower event list.
186
220
 
187
221
  Duplicated entries 'self.n_use' times to match the number simulated events with
188
222
  different core positions.
@@ -204,6 +238,31 @@ class SimtelIOEventDataWriter:
204
238
  for _ in range(self.n_use)
205
239
  )
206
240
 
241
+ def _process_mc_shower_from_iact(self, eventio_object, file_id):
242
+ """
243
+ Process MC shower from IACT file and update shower event list.
244
+
245
+ Duplicated entries 'self.n_use' times to match the number simulated events with
246
+ different core positions.
247
+ """
248
+ shower_header = eventio_object.parse()
249
+ self.n_use = int(shower_header["n_reuse"])
250
+
251
+ self.shower_data.extend(
252
+ {
253
+ "shower_id": shower_header["event_number"],
254
+ "event_id": shower_header["event_number"] * 100 + i,
255
+ "file_id": file_id,
256
+ "simulated_energy": shower_header["total_energy"],
257
+ "x_core": shower_header["reuse_x"][i] / 1.0e2,
258
+ "y_core": shower_header["reuse_y"][i] / 1.0e2,
259
+ "shower_azimuth": np.degrees(shower_header["azimuth"]),
260
+ "shower_altitude": 90.0 - np.degrees(shower_header["zenith"]),
261
+ "area_weight": 1.0,
262
+ }
263
+ for i in range(self.n_use)
264
+ )
265
+
207
266
  def _process_mc_event(self, eventio_object):
208
267
  """
209
268
  Process MC event and update shower event list.
@@ -94,8 +94,13 @@ def _exp_decay(t, tau):
94
94
  numpy.ndarray
95
95
  Exponential values at ``t`` (unitless), zero for ``t < 0``.
96
96
  """
97
- tau = max(tau, 1e-9)
98
- return np.where(t >= 0, np.exp(-t / tau), 0.0)
97
+ tau = max(float(tau), 1e-9)
98
+ t_arr = np.asarray(t, dtype=float)
99
+ expo = -t_arr / tau
100
+ expo = np.minimum(expo, 0.0)
101
+ with np.errstate(over="ignore", under="ignore", invalid="ignore"):
102
+ e = np.exp(expo)
103
+ return np.where(t_arr >= 0, e, 0.0)
99
104
 
100
105
 
101
106
  def generate_gauss_expconv_pulse(
@@ -10,34 +10,13 @@ import numpy as np
10
10
 
11
11
  import simtools.utils.general as gen
12
12
  import simtools.version
13
- from simtools.io import ascii_handler
13
+ from simtools import dependencies, settings
14
14
  from simtools.simtel.pulse_shapes import generate_pulse_from_rise_fall_times
15
15
  from simtools.utils import names
16
16
 
17
17
  logger = logging.getLogger(__name__)
18
18
 
19
19
 
20
- def sim_telarray_random_seeds(seed, number):
21
- """
22
- Generate random seeds to be used in sim_telarray.
23
-
24
- Parameters
25
- ----------
26
- seed: int
27
- Seed for the random number generator.
28
- number: int
29
- Number of random seeds to generate.
30
-
31
- Returns
32
- -------
33
- list
34
- List of random seeds.
35
- """
36
- rng = np.random.default_rng(seed)
37
- max_int32 = np.iinfo(np.int32).max # sim_telarray requires 32 bit integers
38
- return list(rng.integers(low=1, high=max_int32, size=number, dtype=np.int32))
39
-
40
-
41
20
  class SimtelConfigWriter:
42
21
  """
43
22
  SimtelConfigWriter writes sim_telarray configuration files.
@@ -56,8 +35,6 @@ class SimtelConfigWriter:
56
35
  Layout name.
57
36
  label: str
58
37
  Instance label. Important for output file naming.
59
- simtel_path: str or Path
60
- Path to the sim_telarray installation directory.
61
38
  """
62
39
 
63
40
  TAB = " " * 3
@@ -70,11 +47,9 @@ class SimtelConfigWriter:
70
47
  telescope_model_name=None,
71
48
  telescope_design_model=None,
72
49
  label=None,
73
- simtel_path=None,
74
50
  ):
75
51
  """Initialize SimtelConfigWriter."""
76
52
  self._logger = logging.getLogger(__name__)
77
- self._logger.debug("Init SimtelConfigWriter")
78
53
 
79
54
  self._site = site
80
55
  self._model_version = model_version
@@ -82,7 +57,6 @@ class SimtelConfigWriter:
82
57
  self._layout_name = layout_name
83
58
  self._telescope_model_name = telescope_model_name
84
59
  self._telescope_design_model = telescope_design_model
85
- self._simtel_path = simtel_path
86
60
 
87
61
  def write_telescope_config_file(
88
62
  self, config_file_path, parameters, telescope_name=None, telescope_design_model=None
@@ -125,14 +99,14 @@ class SimtelConfigWriter:
125
99
  file.write(f"{meta}\n")
126
100
 
127
101
  @staticmethod
128
- def write_lightpulse_table_gauss_expconv(
102
+ def write_light_pulse_table_gauss_exp_conv(
129
103
  file_path,
130
- width_ns=None,
131
- exp_decay_ns=None,
104
+ width_ns,
105
+ exp_decay_ns,
106
+ fadc_sum_bins,
132
107
  dt_ns=0.1,
133
108
  rise_range=(0.1, 0.9),
134
109
  fall_range=(0.9, 0.1),
135
- fadc_sum_bins=None,
136
110
  time_margin_ns=10.0,
137
111
  ):
138
112
  """Write a pulse table for a Gaussian convolved with a causal exponential.
@@ -143,22 +117,19 @@ class SimtelConfigWriter:
143
117
  Destination path of the ASCII pulse table to write. Parent directory must exist.
144
118
  width_ns : float
145
119
  Target rise time in ns between the fractional levels defined by ``rise_range``.
146
- Defaults correspond to 10-90% rise time.
147
120
  exp_decay_ns : float
148
121
  Target fall time in ns between the fractional levels defined by ``fall_range``.
149
- Defaults correspond to 90-10% fall time.
150
- dt_ns : float, optional
151
- Time sampling step in ns for the generated pulse table. Default is 0.1.
152
- rise_range : tuple[float, float], optional
153
- Fractional amplitude bounds (low, high) for rise-time definition. Default (0.1, 0.9).
154
- fall_range : tuple[float, float], optional
155
- Fractional amplitude bounds (high, low) for fall-time definition. Default (0.9, 0.1).
156
122
  fadc_sum_bins : int
157
123
  Length of the FADC integration window (treated as ns here) used to derive
158
124
  the internal time sampling window of the solver as [-(margin), bins + margin].
125
+ dt_ns : float, optional
126
+ Time sampling step in ns for the generated pulse table.
127
+ rise_range : tuple[float, float], optional
128
+ Fractional amplitude bounds (low, high) for rise-time definition.
129
+ fall_range : tuple[float, float], optional
130
+ Fractional amplitude bounds (high, low) for fall-time definition.
159
131
  time_margin_ns : float, optional
160
132
  Margin in ns to add to both ends of the FADC window when ``fadc_sum_bins`` is given.
161
- Default is 5.0 ns.
162
133
 
163
134
  Returns
164
135
  -------
@@ -174,8 +145,10 @@ class SimtelConfigWriter:
174
145
  if width_ns is None or exp_decay_ns is None:
175
146
  raise ValueError("width_ns (rise 10-90) and exp_decay_ns (fall 90-10) are required")
176
147
  logger.info(
177
- f"Generating lightpulse table with rise10-90={width_ns} ns, "
178
- f"fall90-10={exp_decay_ns} ns, dt={dt_ns} ns"
148
+ "Generating pulse-shape table with "
149
+ f"rise{int(rise_range[0] * 100)}-{int(rise_range[1] * 100)}={width_ns} ns, "
150
+ f"fall{int(fall_range[0] * 100)}-{int(fall_range[1] * 100)}={exp_decay_ns} ns, "
151
+ f"dt={dt_ns} ns"
179
152
  )
180
153
  width = float(fadc_sum_bins)
181
154
  t_start_ns = -abs(time_margin_ns + width)
@@ -193,6 +166,42 @@ class SimtelConfigWriter:
193
166
 
194
167
  return SimtelConfigWriter._write_ascii_pulse_table(file_path, t, y)
195
168
 
169
+ @staticmethod
170
+ def write_angular_distribution_table_lambertian(
171
+ file_path,
172
+ max_angle_deg,
173
+ n_samples=100,
174
+ ):
175
+ """Write a Lambertian angular distribution table (I(t) ~ cos(t)).
176
+
177
+ Parameters
178
+ ----------
179
+ file_path : str or pathlib.Path
180
+ Destination path of the ASCII table to write. Parent directory must exist.
181
+ max_angle_deg : float
182
+ Maximum angle (deg) for the distribution sampling range [0, max_angle_deg].
183
+ n_samples : int, optional
184
+ Number of samples (including end point) from 0 to max_angle_deg. Default 100.
185
+
186
+ Returns
187
+ -------
188
+ pathlib.Path
189
+ Path to created angular distribution table.
190
+ """
191
+ logger.info(
192
+ f"Generating Lambertian angular distribution table up to {max_angle_deg} deg "
193
+ f"with {n_samples} samples"
194
+ )
195
+ angles = np.linspace(0.0, float(max_angle_deg), int(n_samples), dtype=float)
196
+ intensities = np.cos(np.deg2rad(angles))
197
+ intensities[intensities < 0] = 0.0
198
+ if intensities.max() > 0:
199
+ intensities /= intensities.max()
200
+
201
+ return SimtelConfigWriter._write_ascii_angle_distribution_table(
202
+ file_path, angles, intensities
203
+ )
204
+
196
205
  @staticmethod
197
206
  def _write_ascii_pulse_table(file_path, t, y):
198
207
  """Write two-column ASCII pulse table."""
@@ -202,6 +211,15 @@ class SimtelConfigWriter:
202
211
  fh.write(f"{ti:.6f} {yi:.8f}\n")
203
212
  return Path(file_path)
204
213
 
214
+ @staticmethod
215
+ def _write_ascii_angle_distribution_table(file_path, angles, intensities):
216
+ """Write two-column ASCII angular distribution table."""
217
+ with open(file_path, "w", encoding="utf-8") as fh:
218
+ fh.write("# angle[deg] relative_intensity\n")
219
+ for a, i in zip(angles, intensities):
220
+ fh.write(f"{a:.6f} {i:.8f}\n")
221
+ return Path(file_path)
222
+
205
223
  def _get_parameters_for_sim_telarray(self, parameters, config_file_path):
206
224
  """
207
225
  Convert parameter dictionary to sim_telarray configuration file format.
@@ -257,28 +275,24 @@ class SimtelConfigWriter:
257
275
  Model parameters in sim_telarray format including flasher parameters.
258
276
 
259
277
  """
260
- if "flasher_pulse_shape" not in parameters and "flasher_pulse_width" not in parameters:
278
+ if "flasher_pulse_shape" not in parameters:
261
279
  return simtel_par
262
280
 
263
281
  mapping = {
264
282
  "gauss": "laser_pulse_sigtime",
265
283
  "tophat": "laser_pulse_twidth",
284
+ "gauss-exponential": "laser_pulse_sigtime",
266
285
  }
267
286
 
268
- shape = parameters.get("flasher_pulse_shape", {}).get("value", "").lower()
269
- if "exponential" in shape:
270
- simtel_par["laser_pulse_exptime"] = parameters.get("flasher_pulse_exp_decay", {}).get(
271
- "value", 0.0
272
- )
273
- else:
274
- simtel_par["laser_pulse_exptime"] = 0.0
287
+ shape_value = parameters.get("flasher_pulse_shape", {}).get("value")
288
+ shape = shape_value[0].lower()
289
+ width = shape_value[1]
290
+ exp_decay = shape_value[2]
275
291
 
276
- width = parameters.get("flasher_pulse_width", {}).get("value", 0.0)
292
+ simtel_par["laser_pulse_exptime"] = exp_decay if ("exponential" in shape) else 0.0
277
293
 
278
294
  simtel_par.update(dict.fromkeys(mapping.values(), 0.0))
279
- if shape == "gauss-exponential":
280
- simtel_par["laser_pulse_sigtime"] = width
281
- elif shape in mapping:
295
+ if shape in mapping:
282
296
  simtel_par[mapping[shape]] = width
283
297
  else:
284
298
  self._logger.warning(f"Flasher pulse shape '{shape}' without width definition")
@@ -456,39 +470,6 @@ class SimtelConfigWriter:
456
470
  file.write(f"# include <{tel_config_file}>\n\n")
457
471
  file.write("#endif \n\n") # configuration files need to end with \n\n
458
472
 
459
- if additional_metadata and additional_metadata.get("random_instrument_instances"):
460
- self._write_random_seeds_file(additional_metadata, config_file_directory)
461
-
462
- def _write_random_seeds_file(self, sim_telarray_seeds, config_file_directory):
463
- """
464
- Write list of random number used to generate random instances of instrument.
465
-
466
- Parameters
467
- ----------
468
- random_instrument_instances: int
469
- Number of random instances of the instrument.
470
- """
471
- self._logger.info(
472
- "Writing random seed file "
473
- f"{config_file_directory}/{sim_telarray_seeds['seed_file_name']}"
474
- f" (global seed {sim_telarray_seeds['seed']})"
475
- )
476
- if sim_telarray_seeds["random_instrument_instances"] > 1024:
477
- raise ValueError("Number of random instances of instrument must be less than 1024")
478
- random_integers = sim_telarray_random_seeds(
479
- sim_telarray_seeds["seed"], sim_telarray_seeds["random_instrument_instances"]
480
- )
481
- with open(
482
- config_file_directory / sim_telarray_seeds["seed_file_name"], "w", encoding="utf-8"
483
- ) as file:
484
- file.write(
485
- "# Random seeds for instrument configuration generated with seed "
486
- f"{sim_telarray_seeds['seed']}"
487
- f" (model version {self._model_version}, site {self._site})\n"
488
- )
489
- for number in random_integers:
490
- file.write(f"{number}\n")
491
-
492
473
  def write_single_mirror_list_file(
493
474
  self, mirror_number, mirrors, single_mirror_list_file, set_focal_length_to_zero=False
494
475
  ):
@@ -576,17 +557,24 @@ class SimtelConfigWriter:
576
557
  "simtools_model_production_version": self._model_version,
577
558
  }
578
559
  try:
579
- build_opts = ascii_handler.collect_data_from_file(
580
- Path(self._simtel_path) / "build_opts.yml"
581
- )
560
+ build_opts = dependencies.get_build_options()
582
561
  for key, value in build_opts.items():
583
562
  meta_items[f"simtools_{key}"] = value
584
563
  except (FileNotFoundError, TypeError):
585
564
  pass # don't expect build_opts.yml to be present on all systems
586
565
 
566
+ # CORSIKA executable without _flat/_curved suffix (do not know here if curved or flat)
567
+ try:
568
+ meta_items["simtools_corsika_exec"] = settings.config.corsika_exe.name.removesuffix(
569
+ "_flat"
570
+ )
571
+ except AttributeError as exc:
572
+ raise AttributeError("CORSIKA executable path is not set in settings.") from exc
573
+
587
574
  file.write(f"{self.TAB}% Simtools parameters\n")
588
575
  for key, value in meta_items.items():
589
- file.write(f"{self.TAB}metaparam global set {key} = {value}\n")
576
+ if not isinstance(value, list):
577
+ file.write(f"{self.TAB}metaparam global set {key} = {value}\n")
590
578
 
591
579
  def _write_site_parameters(
592
580
  self, file, site_parameters, model_path, telescope_model, additional_metadata=None