gammasimtools 0.24.0__py3-none-any.whl → 0.26.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.24.0.dist-info → gammasimtools-0.26.0.dist-info}/METADATA +2 -1
  2. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/RECORD +134 -130
  3. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/entry_points.txt +3 -1
  4. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/licenses/LICENSE +1 -1
  5. simtools/_version.py +2 -2
  6. simtools/application_control.py +78 -0
  7. simtools/applications/calculate_incident_angles.py +0 -2
  8. simtools/applications/convert_geo_coordinates_of_array_elements.py +1 -2
  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 +2 -6
  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/derive_mirror_rnda.py +1 -3
  20. simtools/applications/derive_psf_parameters.py +5 -1
  21. simtools/applications/derive_pulse_shape_parameters.py +194 -0
  22. simtools/applications/derive_trigger_rates.py +1 -1
  23. simtools/applications/docs_produce_array_element_report.py +2 -8
  24. simtools/applications/docs_produce_calibration_reports.py +1 -3
  25. simtools/applications/docs_produce_model_parameter_reports.py +0 -2
  26. simtools/applications/docs_produce_simulation_configuration_report.py +1 -3
  27. simtools/applications/generate_array_config.py +0 -1
  28. simtools/applications/generate_corsika_histograms.py +48 -235
  29. simtools/applications/generate_regular_arrays.py +5 -35
  30. simtools/applications/generate_simtel_event_data.py +2 -2
  31. simtools/applications/maintain_simulation_model_add_production.py +2 -2
  32. simtools/applications/maintain_simulation_model_write_array_element_positions.py +87 -0
  33. simtools/applications/plot_array_layout.py +64 -108
  34. simtools/applications/plot_simulated_event_distributions.py +57 -0
  35. simtools/applications/plot_tabular_data.py +0 -1
  36. simtools/applications/plot_tabular_data_for_model_parameter.py +1 -6
  37. simtools/applications/production_derive_corsika_limits.py +1 -1
  38. simtools/applications/production_generate_grid.py +0 -1
  39. simtools/applications/run_application.py +1 -1
  40. simtools/applications/simulate_flasher.py +3 -4
  41. simtools/applications/simulate_illuminator.py +0 -1
  42. simtools/applications/simulate_pedestals.py +2 -6
  43. simtools/applications/simulate_prod.py +9 -28
  44. simtools/applications/simulate_prod_htcondor_generator.py +8 -1
  45. simtools/applications/submit_array_layouts.py +7 -7
  46. simtools/applications/submit_model_parameter_from_external.py +1 -3
  47. simtools/applications/validate_camera_efficiency.py +0 -1
  48. simtools/applications/validate_camera_fov.py +0 -1
  49. simtools/applications/validate_cumulative_psf.py +0 -2
  50. simtools/applications/validate_file_using_schema.py +49 -123
  51. simtools/applications/validate_optics.py +0 -13
  52. simtools/camera/camera_efficiency.py +1 -6
  53. simtools/camera/single_photon_electron_spectrum.py +2 -1
  54. simtools/configuration/commandline_parser.py +43 -8
  55. simtools/configuration/configurator.py +6 -11
  56. simtools/corsika/corsika_config.py +204 -99
  57. simtools/corsika/corsika_histograms.py +411 -1735
  58. simtools/corsika/primary_particle.py +1 -1
  59. simtools/data_model/metadata_collector.py +5 -2
  60. simtools/data_model/metadata_model.py +0 -4
  61. simtools/data_model/model_data_writer.py +27 -17
  62. simtools/data_model/schema.py +112 -5
  63. simtools/data_model/validate_data.py +80 -48
  64. simtools/db/db_handler.py +19 -8
  65. simtools/db/db_model_upload.py +2 -1
  66. simtools/db/mongo_db.py +133 -42
  67. simtools/dependencies.py +83 -44
  68. simtools/io/ascii_handler.py +4 -2
  69. simtools/io/table_handler.py +1 -1
  70. simtools/job_execution/htcondor_script_generator.py +0 -2
  71. simtools/layout/array_layout.py +4 -12
  72. simtools/layout/array_layout_utils.py +227 -58
  73. simtools/model/array_model.py +37 -18
  74. simtools/model/calibration_model.py +0 -4
  75. simtools/model/legacy_model_parameter.py +134 -0
  76. simtools/model/model_parameter.py +24 -14
  77. simtools/model/model_repository.py +18 -5
  78. simtools/model/model_utils.py +1 -6
  79. simtools/model/site_model.py +0 -4
  80. simtools/model/telescope_model.py +6 -11
  81. simtools/production_configuration/derive_corsika_limits.py +6 -11
  82. simtools/production_configuration/interpolation_handler.py +16 -16
  83. simtools/ray_tracing/incident_angles.py +5 -11
  84. simtools/ray_tracing/mirror_panel_psf.py +3 -7
  85. simtools/ray_tracing/psf_analysis.py +29 -27
  86. simtools/ray_tracing/psf_parameter_optimisation.py +822 -680
  87. simtools/ray_tracing/ray_tracing.py +6 -15
  88. simtools/reporting/docs_auto_report_generator.py +8 -13
  89. simtools/reporting/docs_read_parameters.py +70 -16
  90. simtools/runners/corsika_runner.py +15 -10
  91. simtools/runners/corsika_simtel_runner.py +9 -8
  92. simtools/runners/runner_services.py +17 -7
  93. simtools/runners/simtel_runner.py +11 -58
  94. simtools/runners/simtools_runner.py +2 -4
  95. simtools/schemas/model_parameters/flasher_pulse_exp_decay.schema.yml +2 -0
  96. simtools/schemas/model_parameters/flasher_pulse_shape.schema.yml +50 -0
  97. simtools/schemas/model_parameters/flasher_pulse_width.schema.yml +2 -0
  98. simtools/schemas/simulation_models_info.schema.yml +2 -0
  99. simtools/settings.py +154 -0
  100. simtools/sim_events/file_info.py +128 -0
  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 +273 -0
  105. simtools/simtel/simtel_config_writer.py +146 -22
  106. simtools/simtel/simtel_table_reader.py +6 -4
  107. simtools/simtel/simulator_array.py +62 -23
  108. simtools/simtel/simulator_camera_efficiency.py +4 -6
  109. simtools/simtel/simulator_light_emission.py +101 -19
  110. simtools/simtel/simulator_ray_tracing.py +4 -10
  111. simtools/simulator.py +360 -353
  112. simtools/telescope_trigger_rates.py +3 -4
  113. simtools/testing/assertions.py +115 -8
  114. simtools/testing/configuration.py +2 -3
  115. simtools/testing/helpers.py +2 -3
  116. simtools/testing/log_inspector.py +5 -1
  117. simtools/testing/sim_telarray_metadata.py +1 -1
  118. simtools/testing/validate_output.py +69 -23
  119. simtools/utils/general.py +37 -0
  120. simtools/utils/geometry.py +0 -77
  121. simtools/utils/names.py +7 -9
  122. simtools/version.py +37 -0
  123. simtools/visualization/legend_handlers.py +21 -10
  124. simtools/visualization/plot_array_layout.py +312 -41
  125. simtools/visualization/plot_corsika_histograms.py +143 -605
  126. simtools/visualization/plot_mirrors.py +834 -0
  127. simtools/visualization/plot_pixels.py +2 -4
  128. simtools/visualization/plot_psf.py +0 -1
  129. simtools/visualization/plot_simtel_event_histograms.py +4 -4
  130. simtools/visualization/plot_simtel_events.py +6 -11
  131. simtools/visualization/plot_tables.py +8 -19
  132. simtools/visualization/visualize.py +22 -2
  133. simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +0 -160
  134. simtools/applications/print_version.py +0 -53
  135. simtools/io/hdf5_handler.py +0 -139
  136. simtools/simtel/simtel_io_file_info.py +0 -62
  137. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/WHEEL +0 -0
  138. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/top_level.txt +0 -0
@@ -6,13 +6,12 @@ from pathlib import Path
6
6
  import numpy as np
7
7
  from astropy import units as u
8
8
 
9
+ from simtools import settings
9
10
  from simtools.corsika.primary_particle import PrimaryParticle
10
11
  from simtools.io import io_handler
11
12
  from simtools.model.model_parameter import ModelParameter
12
-
13
-
14
- class InvalidCorsikaInputError(Exception):
15
- """Exception for invalid corsika input."""
13
+ from simtools.sim_events import file_info
14
+ from simtools.utils import general as gen
16
15
 
17
16
 
18
17
  class CorsikaConfig:
@@ -30,8 +29,6 @@ class CorsikaConfig:
30
29
  Array model.
31
30
  args_dict : dict
32
31
  Configuration dictionary.
33
- db_config : dict
34
- MongoDB configuration.
35
32
  label : str
36
33
  Instance label.
37
34
  dummy_simulations : bool
@@ -39,66 +36,91 @@ class CorsikaConfig:
39
36
  (e.g., sim_telarray requires for some run modes a valid CORSIKA input file).
40
37
  """
41
38
 
42
- def __init__(self, array_model, args_dict, db_config=None, label=None, dummy_simulations=False):
39
+ def __init__(self, array_model, args_dict, label=None, dummy_simulations=False):
43
40
  """Initialize CorsikaConfig."""
44
41
  self._logger = logging.getLogger(__name__)
45
42
  self._logger.debug("Init CorsikaConfig")
46
43
 
47
44
  self.label = label
48
- self.zenith_angle = None
49
- self.azimuth_angle = None
45
+ self.shower_events = self.mc_events = None
46
+ self.zenith_angle = self.azimuth_angle = None
47
+ self.curved_atmosphere_min_zenith_angle = None
50
48
  self._run_number = None
51
49
  self.config_file_path = None
52
50
  self.primary_particle = args_dict # see setter for primary_particle
51
+ self.use_curved_atmosphere = args_dict # see setter for use_curved_atmosphere
53
52
  self.dummy_simulations = dummy_simulations
54
53
 
55
54
  self.io_handler = io_handler.IOHandler()
56
55
  self.array_model = array_model
57
- self.config = self.fill_corsika_configuration(args_dict, db_config)
56
+ self.config = self._fill_corsika_configuration(args_dict)
57
+ self._initialize_from_config(args_dict)
58
58
  self.is_file_updated = False
59
59
 
60
- def __repr__(self):
61
- """CorsikaConfig class representation."""
62
- return (
63
- f"<class {self.__class__.__name__}> "
64
- f"(site={self.array_model.site}, "
65
- f"layout={self.array_model.layout_name}, label={self.label})"
66
- )
67
-
68
60
  @property
69
61
  def primary_particle(self):
70
62
  """Primary particle."""
71
63
  return self._primary_particle
72
64
 
73
65
  @primary_particle.setter
74
- def primary_particle(self, args_dict):
66
+ def primary_particle(self, args):
75
67
  """
76
- Set primary particle from input dictionary.
68
+ Set primary particle from input dictionary or CORSIKA 7 particle ID.
77
69
 
78
70
  This is to make sure that when setting the primary particle,
79
71
  we get the full PrimaryParticle object expected.
80
72
 
81
73
  Parameters
82
74
  ----------
83
- args_dict: dict
75
+ args: dict, corsika particle ID, or None
84
76
  Configuration dictionary
85
77
  """
86
- self._primary_particle = self._set_primary_particle(args_dict)
78
+ if (
79
+ isinstance(args, dict)
80
+ and args.get("primary_id_type") is not None
81
+ and args.get("primary") is not None
82
+ ):
83
+ self._primary_particle = PrimaryParticle(
84
+ particle_id_type=args.get("primary_id_type"), particle_id=args.get("primary")
85
+ )
86
+ elif isinstance(args, int):
87
+ self._primary_particle = PrimaryParticle(
88
+ particle_id_type="corsika7_id", particle_id=args
89
+ )
90
+ else:
91
+ self._primary_particle = PrimaryParticle()
92
+
93
+ @property
94
+ def use_curved_atmosphere(self):
95
+ """Check if zenith angle condition for curved atmosphere usage for CORSIKA is met."""
96
+ return self._use_curved_atmosphere
97
+
98
+ @use_curved_atmosphere.setter
99
+ def use_curved_atmosphere(self, args):
100
+ """Check if zenith angle condition for curved atmosphere usage for CORSIKA is met."""
101
+ self._use_curved_atmosphere = False
102
+ if isinstance(args, bool):
103
+ self._use_curved_atmosphere = args
104
+ elif isinstance(args, dict):
105
+ try:
106
+ self._use_curved_atmosphere = (
107
+ args.get("zenith_angle", 0.0 * u.deg).to("deg").value
108
+ > args["curved_atmosphere_min_zenith_angle"].to("deg").value
109
+ )
110
+ except KeyError:
111
+ self._use_curved_atmosphere = False
87
112
 
88
- def fill_corsika_configuration(self, args_dict, db_config=None):
113
+ def _fill_corsika_configuration(self, args_dict):
89
114
  """
90
115
  Fill CORSIKA configuration.
91
116
 
92
- Dictionary keys are CORSIKA parameter names.
93
- Values are converted to CORSIKA-consistent units.
94
-
117
+ Dictionary keys are CORSIKA parameter names. Values are converted to
118
+ CORSIKA-consistent units.
95
119
 
96
120
  Parameters
97
121
  ----------
98
122
  args_dict : dict
99
123
  Configuration dictionary.
100
- db_config: dict
101
- Database configuration.
102
124
 
103
125
  Returns
104
126
  -------
@@ -108,32 +130,36 @@ class CorsikaConfig:
108
130
  if args_dict is None:
109
131
  return {}
110
132
 
111
- self.is_file_updated = False
112
- self.azimuth_angle = int(args_dict.get("azimuth_angle", 0.0 * u.deg).to("deg").value)
113
- self.zenith_angle = int(args_dict.get("zenith_angle", 0.0 * u.deg).to("deg").value)
114
-
115
- self._logger.debug(
116
- f"Setting CORSIKA parameters from database ({args_dict['model_version']})"
117
- )
118
-
119
133
  config = {}
120
134
  if self.dummy_simulations:
121
- config["USER_INPUT"] = self._corsika_configuration_for_dummy_simulations()
135
+ config["USER_INPUT"] = self._corsika_configuration_for_dummy_simulations(args_dict)
136
+ elif args_dict.get("corsika_file", None) is not None:
137
+ config["USER_INPUT"] = self._corsika_configuration_from_corsika_file(
138
+ args_dict["corsika_file"]
139
+ )
122
140
  else:
123
141
  config["USER_INPUT"] = self._corsika_configuration_from_user_input(args_dict)
124
142
 
125
- if db_config is None: # all following parameter require DB
143
+ config.update(
144
+ self._fill_corsika_configuration_from_db(
145
+ gen.ensure_iterable(args_dict.get("model_version"))
146
+ )
147
+ )
148
+ return config
149
+
150
+ def _fill_corsika_configuration_from_db(self, model_versions):
151
+ """Fill CORSIKA configuration from database."""
152
+ config = {}
153
+ # all following parameters require DB
154
+ if settings.config.db_config is None or not model_versions:
126
155
  return config
127
156
 
128
- # If the user provided multiple model versions, we take the first one
129
- # because for CORSIKA config we need only one and it doesn't matter which
130
- model_versions = args_dict.get("model_version", None)
131
- if not isinstance(model_versions, list):
132
- model_versions = [model_versions]
133
- self.assert_corsika_configurations_match(model_versions, db_config=db_config)
157
+ # For multiple model versions, check that CORSIKA parameters are identical
158
+ self.assert_corsika_configurations_match(model_versions)
134
159
  model_version = model_versions[0]
135
- self._logger.debug(f"Using model version {model_version} for CORSIKA parameters")
136
- db_model_parameters = ModelParameter(db_config=db_config, model_version=model_version)
160
+
161
+ self._logger.debug(f"Using model version {model_version} for CORSIKA parameters from DB")
162
+ db_model_parameters = ModelParameter(model_version=model_version)
137
163
  parameters_from_db = db_model_parameters.get_simulation_software_parameters("corsika")
138
164
 
139
165
  config["INTERACTION_FLAGS"] = self._corsika_configuration_interaction_flags(
@@ -144,10 +170,42 @@ class CorsikaConfig:
144
170
  )
145
171
  config["DEBUGGING_OUTPUT_PARAMETERS"] = self._corsika_configuration_debugging_parameters()
146
172
  config["IACT_PARAMETERS"] = self._corsika_configuration_iact_parameters(parameters_from_db)
147
-
148
173
  return config
149
174
 
150
- def assert_corsika_configurations_match(self, model_versions, db_config=None):
175
+ def _initialize_from_config(self, args_dict):
176
+ """
177
+ Initialize additional parameters either from command line args or from derived config.
178
+
179
+ Takes into account that in the case of a given CORSIKA input file, some parameters are read
180
+ from the file instead of the command line args.
181
+
182
+ """
183
+ self.primary_particle = int(self.config.get("USER_INPUT", {}).get("PRMPAR", [1])[0])
184
+ self.shower_events = int(self.config.get("USER_INPUT", {}).get("NSHOW", [0])[0])
185
+ self.mc_events = int(
186
+ self.shower_events * self.config.get("USER_INPUT", {}).get("CSCAT", [1])[0]
187
+ )
188
+
189
+ if args_dict.get("corsika_file", None) is not None:
190
+ azimuth = self._rotate_azimuth_by_180deg(
191
+ 0.5 * (self.config["USER_INPUT"]["PHIP"][0] + self.config["USER_INPUT"]["PHIP"][1]),
192
+ invert_operation=True,
193
+ )
194
+ zenith = 0.5 * (
195
+ self.config["USER_INPUT"]["THETAP"][0] + self.config["USER_INPUT"]["THETAP"][1]
196
+ )
197
+ else:
198
+ azimuth = args_dict.get("azimuth_angle", 0.0 * u.deg).to("deg").value
199
+ zenith = args_dict.get("zenith_angle", 20.0 * u.deg).to("deg").value
200
+
201
+ self.azimuth_angle = round(azimuth)
202
+ self.zenith_angle = round(zenith)
203
+
204
+ self.curved_atmosphere_min_zenith_angle = (
205
+ args_dict.get("curved_atmosphere_min_zenith_angle", 90.0 * u.deg).to("deg").value
206
+ )
207
+
208
+ def assert_corsika_configurations_match(self, model_versions):
151
209
  """
152
210
  Assert that CORSIKA configurations match across all model versions.
153
211
 
@@ -155,8 +213,6 @@ class CorsikaConfig:
155
213
  ----------
156
214
  model_versions : list
157
215
  List of model versions to check.
158
- db_config : dict, optional
159
- Database configuration.
160
216
 
161
217
  Raises
162
218
  ------
@@ -170,7 +226,7 @@ class CorsikaConfig:
170
226
 
171
227
  # Get parameters for all model versions
172
228
  for model_version in model_versions:
173
- db_model_parameters = ModelParameter(db_config=db_config, model_version=model_version)
229
+ db_model_parameters = ModelParameter(model_version=model_version)
174
230
  parameters_from_db_list.append(
175
231
  db_model_parameters.get_simulation_software_parameters("corsika")
176
232
  )
@@ -193,13 +249,13 @@ class CorsikaConfig:
193
249
  f" {model_versions[i]}: {current_value}\n"
194
250
  f" {model_versions[i + 1]}: {next_value}"
195
251
  )
196
- raise InvalidCorsikaInputError(
252
+ raise ValueError(
197
253
  f"CORSIKA parameter '{key}' differs between model versions "
198
254
  f"{model_versions[i]} and {model_versions[i + 1]}. "
199
255
  f"Values are {current_value} and {next_value} respectively."
200
256
  )
201
257
 
202
- def _corsika_configuration_for_dummy_simulations(self):
258
+ def _corsika_configuration_for_dummy_simulations(self, args_dict):
203
259
  """
204
260
  Return CORSIKA configuration for dummy simulations.
205
261
 
@@ -211,18 +267,73 @@ class CorsikaConfig:
211
267
  dict
212
268
  Dictionary with CORSIKA parameters for dummy simulations.
213
269
  """
270
+ theta, phi = self._get_corsika_theta_phi(args_dict)
214
271
  return {
215
272
  "EVTNR": [1],
216
273
  "NSHOW": [1],
217
274
  "PRMPAR": [1], # CORSIKA ID 1 for primary gamma
218
275
  "ESLOPE": [-2.0],
219
276
  "ERANGE": [0.1, 0.1],
220
- "THETAP": [20.0, 20.0],
221
- "PHIP": [0.0, 0.0],
277
+ "THETAP": [theta, theta],
278
+ "PHIP": [phi, phi],
222
279
  "VIEWCONE": [0.0, 0.0],
223
280
  "CSCAT": [1, 0.0, 10.0],
224
281
  }
225
282
 
283
+ def _corsika_configuration_from_corsika_file(self, corsika_input_file):
284
+ """
285
+ Get CORSIKA configuration run header of provided input files.
286
+
287
+ Reads configuration from the run and event headers from the CORSIKA input file
288
+ (unfortunately quite fine tuned to the pycorsikaio run and event
289
+ header implementation).
290
+
291
+ Parameters
292
+ ----------
293
+ corsika_input_file : str, path
294
+ Path to the CORSIKA input file.
295
+
296
+ Returns
297
+ -------
298
+ dict
299
+ Dictionary with CORSIKA parameters from input file.
300
+ """
301
+ run_header, event_header = file_info.get_corsika_run_and_event_headers(corsika_input_file)
302
+ self._logger.debug(f"CORSIKA run header from {corsika_input_file}")
303
+
304
+ def to_float32(value):
305
+ """Convert value to numpy float32."""
306
+ return np.float32(value) if value is not None else 0.0
307
+
308
+ def to_int32(value):
309
+ """Convert value to numpy int32."""
310
+ return np.int32(value) if value is not None else 0
311
+
312
+ if run_header["n_observation_levels"] > 0:
313
+ self._check_altitude_and_site(run_header["observation_height"][0])
314
+
315
+ return {
316
+ "EVTNR": [to_int32(event_header["event_number"])],
317
+ "NSHOW": [to_int32(run_header["n_showers"])],
318
+ "PRMPAR": [to_int32(event_header["particle_id"])],
319
+ "ESLOPE": [to_float32(run_header["energy_spectrum_slope"])],
320
+ "ERANGE": [to_float32(run_header["energy_min"]), to_float32(run_header["energy_max"])],
321
+ "THETAP": [
322
+ to_float32(event_header["theta_min"]),
323
+ to_float32(event_header["theta_max"]),
324
+ ],
325
+ "PHIP": [to_float32(event_header["phi_min"]), to_float32(event_header["phi_max"])],
326
+ "VIEWCONE": [
327
+ to_float32(event_header["viewcone_inner_angle"]),
328
+ to_float32(event_header["viewcone_outer_angle"]),
329
+ ],
330
+ "CSCAT": [
331
+ to_int32(event_header["n_reuse"]),
332
+ to_float32(event_header["reuse_x"]),
333
+ to_float32(event_header["reuse_y"]),
334
+ ],
335
+ }
336
+
226
337
  def _corsika_configuration_from_user_input(self, args_dict):
227
338
  """
228
339
  Get CORSIKA configuration from user input.
@@ -237,6 +348,7 @@ class CorsikaConfig:
237
348
  dict
238
349
  Dictionary with CORSIKA parameters.
239
350
  """
351
+ theta, phi = self._get_corsika_theta_phi(args_dict)
240
352
  return {
241
353
  "EVTNR": [args_dict["event_number_first_shower"]],
242
354
  "NSHOW": [args_dict["nshow"]],
@@ -246,24 +358,8 @@ class CorsikaConfig:
246
358
  args_dict["energy_range"][0].to("GeV").value,
247
359
  args_dict["energy_range"][1].to("GeV").value,
248
360
  ],
249
- "THETAP": [
250
- float(args_dict["zenith_angle"].to("deg").value),
251
- float(args_dict["zenith_angle"].to("deg").value),
252
- ],
253
- "PHIP": [
254
- self._rotate_azimuth_by_180deg(
255
- args_dict["azimuth_angle"].to("deg").value,
256
- correct_for_geomagnetic_field_alignment=args_dict[
257
- "correct_for_b_field_alignment"
258
- ],
259
- ),
260
- self._rotate_azimuth_by_180deg(
261
- args_dict["azimuth_angle"].to("deg").value,
262
- correct_for_geomagnetic_field_alignment=args_dict[
263
- "correct_for_b_field_alignment"
264
- ],
265
- ),
266
- ],
361
+ "THETAP": [theta, theta],
362
+ "PHIP": [phi, phi],
267
363
  "VIEWCONE": [
268
364
  args_dict["view_cone"][0].to("deg").value,
269
365
  args_dict["view_cone"][1].to("deg").value,
@@ -275,6 +371,26 @@ class CorsikaConfig:
275
371
  ],
276
372
  }
277
373
 
374
+ def _check_altitude_and_site(self, observation_height):
375
+ """Check that observation height from CORSIKA file matches site model."""
376
+ site_altitude = self.array_model.site_model.get_parameter_value("corsika_observation_level")
377
+ if not np.isclose(observation_height / 1.0e2, site_altitude, atol=1.0):
378
+ raise ValueError(
379
+ "Observatory altitude does not match CORSIKA file observation height: "
380
+ f"{site_altitude} m (site model) != {observation_height / 1.0e2} m (CORSIKA file)"
381
+ )
382
+
383
+ def _get_corsika_theta_phi(self, args_dict):
384
+ """Get CORSIKA theta and phi angles from args_dict."""
385
+ theta = args_dict.get("zenith_angle", 20.0 * u.deg).to("deg").value
386
+ phi = self._rotate_azimuth_by_180deg(
387
+ args_dict.get("azimuth_angle", 0.0 * u.deg).to("deg").value,
388
+ correct_for_geomagnetic_field_alignment=args_dict.get(
389
+ "correct_for_b_field_alignment", True
390
+ ),
391
+ )
392
+ return theta, phi
393
+
278
394
  def _corsika_configuration_interaction_flags(self, parameters_from_db):
279
395
  """
280
396
  Return CORSIKA interaction flags / parameters.
@@ -298,7 +414,8 @@ class CorsikaConfig:
298
414
  parameters_from_db["corsika_starting_grammage"]
299
415
  )
300
416
  ]
301
- parameters["TSTART"] = ["T"]
417
+ if not self.use_curved_atmosphere:
418
+ parameters["TSTART"] = ["T"]
302
419
  parameters["ECUTS"] = self._input_config_corsika_particle_kinetic_energy_cutoff(
303
420
  parameters_from_db["corsika_particle_kinetic_energy_cutoff"]
304
421
  )
@@ -444,16 +561,23 @@ class CorsikaConfig:
444
561
  return f"{int(value)}MB"
445
562
  return f"{int(entry['value'] * u.Unit(entry['unit']).to('byte'))}"
446
563
 
447
- def _rotate_azimuth_by_180deg(self, az, correct_for_geomagnetic_field_alignment=True):
564
+ def _rotate_azimuth_by_180deg(
565
+ self, az, correct_for_geomagnetic_field_alignment=True, invert_operation=False
566
+ ):
448
567
  """
449
568
  Convert azimuth angle to the CORSIKA coordinate system.
450
569
 
570
+ Corresponds to a rotation by 180 degrees, and optionally a correction for the
571
+ for the differences between the geographic and geomagnetic north pole.
572
+
451
573
  Parameters
452
574
  ----------
453
575
  az: float
454
576
  Azimuth angle in degrees.
455
577
  correct_for_geomagnetic_field_alignment: bool
456
578
  Whether to correct for the geomagnetic field alignment.
579
+ invert_operation: bool
580
+ Whether to invert the operation (i.e., convert from CORSIKA to geographic system).
457
581
 
458
582
  Returns
459
583
  -------
@@ -463,6 +587,8 @@ class CorsikaConfig:
463
587
  b_field_declination = 0
464
588
  if correct_for_geomagnetic_field_alignment:
465
589
  b_field_declination = self.array_model.site_model.get_parameter_value("geomag_rotation")
590
+ if invert_operation:
591
+ return (az - 180 - b_field_declination) % 360
466
592
  return (az + 180 + b_field_declination) % 360
467
593
 
468
594
  @property
@@ -470,27 +596,6 @@ class CorsikaConfig:
470
596
  """Primary particle name."""
471
597
  return self.primary_particle.name
472
598
 
473
- def _set_primary_particle(self, args_dict):
474
- """
475
- Set primary particle from input dictionary.
476
-
477
- Parameters
478
- ----------
479
- args_dict: dict
480
- Input dictionary.
481
-
482
- Returns
483
- -------
484
- PrimaryParticle
485
- Primary particle.
486
-
487
- """
488
- if not args_dict or args_dict.get("primary_id_type") is None:
489
- return PrimaryParticle()
490
- return PrimaryParticle(
491
- particle_id_type=args_dict.get("primary_id_type"), particle_id=args_dict.get("primary")
492
- )
493
-
494
599
  def get_config_parameter(self, par_name):
495
600
  """
496
601
  Get value of CORSIKA configuration parameter.
@@ -511,7 +616,7 @@ class CorsikaConfig:
511
616
  Value(s) of the parameter.
512
617
  """
513
618
  par_value = []
514
- for _, values in self.config.items():
619
+ for values in self.config.values():
515
620
  if par_name in values:
516
621
  par_value = values[par_name]
517
622
  if len(par_value) == 0:
@@ -674,7 +779,7 @@ class CorsikaConfig:
674
779
 
675
780
  base_name = (
676
781
  f"{self.primary_particle.name}_{run_number_in_file_name}"
677
- f"za{int(self.get_config_parameter('THETAP')[0]):03}deg_"
782
+ f"za{int(self.get_config_parameter('THETAP')[0]):02}deg_"
678
783
  f"azm{self.azimuth_angle:03}deg{view_cone}_"
679
784
  f"{self.array_model.site}_{self.array_model.layout_name}_"
680
785
  f"{self.array_model.model_version}{file_label}"
@@ -685,7 +790,7 @@ class CorsikaConfig:
685
790
  if file_type == "config":
686
791
  return f"corsika_config_{base_name}.input"
687
792
  if file_type == "output_generic":
688
- return f"{base_name}.zst"
793
+ return f"{base_name}.corsika.zst"
689
794
  if file_type == "multipipe":
690
795
  return f"multi_cta-{self.array_model.site}-{self.array_model.layout_name}.cfg"
691
796