gammasimtools 0.8.2__py3-none-any.whl → 0.9.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 (65) hide show
  1. {gammasimtools-0.8.2.dist-info → gammasimtools-0.9.0.dist-info}/METADATA +3 -3
  2. {gammasimtools-0.8.2.dist-info → gammasimtools-0.9.0.dist-info}/RECORD +64 -59
  3. {gammasimtools-0.8.2.dist-info → gammasimtools-0.9.0.dist-info}/WHEEL +1 -1
  4. {gammasimtools-0.8.2.dist-info → gammasimtools-0.9.0.dist-info}/entry_points.txt +2 -0
  5. simtools/_version.py +2 -2
  6. simtools/applications/convert_all_model_parameters_from_simtel.py +1 -1
  7. simtools/applications/convert_geo_coordinates_of_array_elements.py +8 -9
  8. simtools/applications/convert_model_parameter_from_simtel.py +1 -1
  9. simtools/applications/db_add_model_parameters_from_repository_to_db.py +2 -10
  10. simtools/applications/db_add_value_from_json_to_db.py +1 -9
  11. simtools/applications/db_get_array_layouts_from_db.py +3 -1
  12. simtools/applications/db_get_parameter_from_db.py +1 -1
  13. simtools/applications/derive_mirror_rnda.py +10 -1
  14. simtools/applications/derive_psf_parameters.py +1 -1
  15. simtools/applications/generate_array_config.py +1 -5
  16. simtools/applications/generate_regular_arrays.py +9 -6
  17. simtools/applications/plot_array_layout.py +3 -1
  18. simtools/applications/plot_tabular_data.py +84 -0
  19. simtools/applications/production_scale_events.py +1 -2
  20. simtools/applications/simulate_light_emission.py +2 -2
  21. simtools/applications/simulate_prod.py +24 -59
  22. simtools/applications/simulate_prod_htcondor_generator.py +95 -0
  23. simtools/applications/submit_data_from_external.py +1 -1
  24. simtools/applications/validate_camera_efficiency.py +1 -1
  25. simtools/applications/validate_camera_fov.py +3 -7
  26. simtools/applications/validate_cumulative_psf.py +3 -7
  27. simtools/applications/validate_file_using_schema.py +31 -21
  28. simtools/applications/validate_optics.py +3 -4
  29. simtools/camera_efficiency.py +1 -4
  30. simtools/configuration/commandline_parser.py +7 -13
  31. simtools/configuration/configurator.py +6 -19
  32. simtools/data_model/metadata_collector.py +18 -0
  33. simtools/data_model/metadata_model.py +18 -5
  34. simtools/data_model/model_data_writer.py +1 -1
  35. simtools/data_model/validate_data.py +67 -10
  36. simtools/db/db_handler.py +92 -315
  37. simtools/io_operations/legacy_data_handler.py +61 -0
  38. simtools/job_execution/htcondor_script_generator.py +133 -0
  39. simtools/job_execution/job_manager.py +77 -50
  40. simtools/model/camera.py +4 -2
  41. simtools/model/model_parameter.py +40 -10
  42. simtools/model/site_model.py +1 -1
  43. simtools/ray_tracing/mirror_panel_psf.py +47 -27
  44. simtools/runners/corsika_runner.py +14 -3
  45. simtools/runners/runner_services.py +3 -3
  46. simtools/runners/simtel_runner.py +27 -8
  47. simtools/schemas/integration_tests_config.metaschema.yml +15 -5
  48. simtools/schemas/model_parameter.metaschema.yml +90 -2
  49. simtools/schemas/model_parameters/effective_focal_length.schema.yml +31 -1
  50. simtools/simtel/simtel_table_reader.py +410 -0
  51. simtools/simtel/simulator_camera_efficiency.py +6 -4
  52. simtools/simtel/simulator_light_emission.py +2 -2
  53. simtools/simtel/simulator_ray_tracing.py +1 -2
  54. simtools/simulator.py +80 -33
  55. simtools/testing/configuration.py +12 -8
  56. simtools/testing/helpers.py +5 -5
  57. simtools/testing/validate_output.py +26 -26
  58. simtools/utils/general.py +50 -3
  59. simtools/utils/names.py +2 -2
  60. simtools/utils/value_conversion.py +9 -1
  61. simtools/visualization/plot_tables.py +106 -0
  62. simtools/visualization/visualize.py +43 -5
  63. simtools/db/db_from_repo_handler.py +0 -106
  64. {gammasimtools-0.8.2.dist-info → gammasimtools-0.9.0.dist-info}/LICENSE +0 -0
  65. {gammasimtools-0.8.2.dist-info → gammasimtools-0.9.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,133 @@
1
+ """HT Condor script generator for simulation production."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ import astropy.units as u
7
+
8
+ _logger = logging.getLogger(__name__)
9
+
10
+
11
+ def generate_submission_script(args_dict):
12
+ """
13
+ Generate the HT Condor submission script.
14
+
15
+ Parameters
16
+ ----------
17
+ args_dict: dict
18
+ Arguments dictionary.
19
+ """
20
+ _logger.info("Generating HT Condor submission scripts ")
21
+
22
+ work_dir = Path(args_dict["output_path"])
23
+ log_dir = work_dir / "logs"
24
+ work_dir.mkdir(parents=True, exist_ok=True)
25
+ log_dir.mkdir(parents=True, exist_ok=True)
26
+ submit_file_name = "simulate_prod.submit"
27
+
28
+ with open(work_dir / f"{submit_file_name}.condor", "w", encoding="utf-8") as submit_file_handle:
29
+ submit_file_handle.write(
30
+ _get_submit_file(
31
+ f"{submit_file_name}.sh",
32
+ args_dict["apptainer_image"],
33
+ args_dict["priority"],
34
+ +args_dict["number_of_runs"],
35
+ )
36
+ )
37
+
38
+ with open(work_dir / f"{submit_file_name}.sh", "w", encoding="utf-8") as submit_script_handle:
39
+ submit_script_handle.write(_get_submit_script(args_dict))
40
+
41
+ Path(work_dir / f"{submit_file_name}.sh").chmod(0o755)
42
+
43
+
44
+ def _get_submit_file(executable, apptainer_image, priority, n_jobs):
45
+ """
46
+ Return HT Condor submit file.
47
+
48
+ Database access variables are passed through the environment file.
49
+
50
+ Parameters
51
+ ----------
52
+ executable: str
53
+ Name of the executable script.
54
+ apptainer_image: str
55
+ Path to the Apptainer image.
56
+ priority: int
57
+ Priority of the job.
58
+ n_jobs: int
59
+ Number of jobs to queue.
60
+
61
+ Returns
62
+ -------
63
+ str
64
+ HT Condor submit file content.
65
+ """
66
+ return f"""universe = container
67
+ container_image = {apptainer_image}
68
+ transfer_container = false
69
+
70
+ executable = {executable}
71
+ error = logs/err.$(cluster)_$(process)
72
+ output = logs/out.$(cluster)_$(process)
73
+ log = logs/log.$(cluster)_$(process)
74
+
75
+ priority = {priority}
76
+ arguments = "$(process) env.txt"
77
+
78
+ queue {n_jobs}
79
+ """
80
+
81
+
82
+ def _get_submit_script(args_dict):
83
+ """
84
+ Return HT Condor submit script.
85
+
86
+ Parameters
87
+ ----------
88
+ args_dict: dict
89
+ Arguments dictionary.
90
+
91
+ Returns
92
+ -------
93
+ str
94
+ HT Condor submit script content.
95
+ """
96
+ azimuth_angle_string = f"{args_dict['azimuth_angle'].to(u.deg).value}"
97
+ zenith_angle_string = f"{args_dict['zenith_angle'].to(u.deg).value}"
98
+ energy_range = args_dict["energy_range"]
99
+ energy_range_string = (
100
+ f'"{energy_range[0].to(u.GeV).value} GeV {energy_range[1].to(u.GeV).value} GeV"'
101
+ )
102
+ core_scatter = args_dict["core_scatter"]
103
+ core_scatter_string = f'"{core_scatter[0]} {core_scatter[1].to(u.m).value} m"'
104
+
105
+ label = args_dict["label"] if args_dict["label"] else "simulate-prod"
106
+
107
+ return f"""#!/usr/bin/env bash
108
+
109
+ # Process ID used to generate run number
110
+ process_id="$1"
111
+ # Load environment variables (for DB access)
112
+ set -a; source "$2"
113
+
114
+ simtools-simulate-prod \\
115
+ --simulation_software {args_dict["simulation_software"]} \\
116
+ --label {label} \\
117
+ --model_version {args_dict["model_version"]} \\
118
+ --site {args_dict["site"]} \\
119
+ --array_layout_name {args_dict["array_layout_name"]} \\
120
+ --primary {args_dict["primary"]} \\
121
+ --azimuth_angle {azimuth_angle_string} \\
122
+ --zenith_angle {zenith_angle_string} \\
123
+ --nshow {args_dict["nshow"]} \\
124
+ --energy_range {energy_range_string} \\
125
+ --core_scatter {core_scatter_string} \\
126
+ --run_number_start $((process_id + {args_dict["run_number_start"]})) \\
127
+ --number_of_runs 1 \\
128
+ --submit_engine \"local\" \\
129
+ --data_directory /tmp/simtools-data \\
130
+ --output_path /tmp/simtools-output \\
131
+ --log_level {args_dict["log_level"]} \\
132
+ --pack_for_grid_register simtools-output
133
+ """
@@ -1,7 +1,7 @@
1
1
  """Interface to workload managers like gridengine or HTCondor."""
2
2
 
3
3
  import logging
4
- import os
4
+ import subprocess
5
5
  from pathlib import Path
6
6
 
7
7
  import simtools.utils.general as gen
@@ -65,11 +65,9 @@ class JobManager:
65
65
  ValueError
66
66
  if invalid submit engine.
67
67
  """
68
- if value is None:
69
- value = "local"
70
- if value not in self.engines:
71
- raise ValueError(f"Invalid submit command: {value}")
72
- self._submit_engine = value
68
+ self._submit_engine = value or "local"
69
+ if self._submit_engine not in self.engines:
70
+ raise ValueError(f"Invalid submit command: {self._submit_engine}")
73
71
 
74
72
  def check_submission_system(self):
75
73
  """
@@ -77,14 +75,17 @@ class JobManager:
77
75
 
78
76
  Raises
79
77
  ------
80
- MissingWorkloadManagerError
78
+ JobExecutionError
81
79
  if workflow manager is not found.
82
80
  """
83
- if self.submit_engine is None or self.submit_engine == "local":
84
- return
85
-
86
- if gen.program_is_executable(self.engines[self.submit_engine]):
87
- return
81
+ try:
82
+ if self.submit_engine in (None, "local") or gen.program_is_executable(
83
+ self.engines[self.submit_engine]
84
+ ):
85
+ return
86
+ except KeyError:
87
+ pass
88
+ raise JobExecutionError(f"Submit engine {self.submit_engine} not found")
88
89
 
89
90
  def submit(self, run_script=None, run_out_file=None, log_file=None):
90
91
  """
@@ -109,12 +110,14 @@ class JobManager:
109
110
  self._logger.info(f"Job error stream {self.run_out_file + '.err'}")
110
111
  self._logger.info(f"Job log stream {self.run_out_file + '.job'}")
111
112
 
112
- if self.submit_engine == "gridengine":
113
- self._submit_gridengine()
114
- elif self.submit_engine == "htcondor":
115
- self._submit_htcondor()
116
- elif self.submit_engine == "local":
117
- self._submit_local(log_file)
113
+ submit_result = 0
114
+ if self.submit_engine == "local":
115
+ submit_result = self._submit_local(log_file)
116
+ else:
117
+ submit_result = getattr(self, f"_submit_{self.submit_engine}")()
118
+
119
+ if submit_result != 0:
120
+ raise JobExecutionError(f"Job submission failed with return code {submit_result}")
118
121
 
119
122
  def _submit_local(self, log_file):
120
123
  """
@@ -125,50 +128,72 @@ class JobManager:
125
128
  log_file: str or Path
126
129
  The log file of the actual simulator (CORSIKA or sim_telarray).
127
130
  Provided in order to print the log excerpt in case of run time error.
131
+
132
+ Returns
133
+ -------
134
+ int
135
+ Return code of the executed script
128
136
  """
129
137
  self._logger.info("Running script locally")
130
138
 
131
- shell_command = f"{self.run_script} > {self.run_out_file}.out 2> {self.run_out_file}.err"
132
-
133
- if not self.test:
134
- sys_output = os.system(shell_command)
135
- if sys_output != 0:
136
- msg = gen.get_log_excerpt(f"{self.run_out_file}.err")
137
- self._logger.error(msg)
138
- if log_file.exists() and gen.get_file_age(log_file) < 5:
139
- msg = gen.get_log_excerpt(log_file)
140
- self._logger.error(msg)
141
- raise JobExecutionError("See excerpt from log file above\n")
142
- else:
139
+ if self.test:
143
140
  self._logger.info("Testing (local)")
141
+ return 0
142
+
143
+ result = None
144
+ try:
145
+ with (
146
+ open(f"{self.run_out_file}.out", "w", encoding="utf-8") as stdout,
147
+ open(f"{self.run_out_file}.err", "w", encoding="utf-8") as stderr,
148
+ ):
149
+ result = subprocess.run(
150
+ f"{self.run_script}",
151
+ shell=True,
152
+ check=True,
153
+ text=True,
154
+ stdout=stdout,
155
+ stderr=stderr,
156
+ )
157
+ except subprocess.CalledProcessError as exc:
158
+ self._logger.error(gen.get_log_excerpt(f"{self.run_out_file}.err"))
159
+ if log_file.exists() and gen.get_file_age(log_file) < 5:
160
+ self._logger.error(gen.get_log_excerpt(log_file))
161
+ raise JobExecutionError("See excerpt from log file above\n") from exc
162
+
163
+ return result.returncode if result else 0
144
164
 
145
165
  def _submit_htcondor(self):
146
166
  """Submit a job described by a shell script to HTcondor."""
147
167
  _condor_file = self.run_script + ".condor"
168
+ lines = [
169
+ f"Executable = {self.run_script}",
170
+ f"Output = {self.run_out_file}.out",
171
+ f"Error = {self.run_out_file}.err",
172
+ f"Log = {self.run_out_file}.job",
173
+ ]
174
+ if self.submit_options:
175
+ lines.extend(option.lstrip() for option in self.submit_options.split(","))
176
+ lines.append("queue 1")
148
177
  try:
149
178
  with open(_condor_file, "w", encoding="utf-8") as file:
150
- file.write(f"Executable = {self.run_script}\n")
151
- file.write(f"Output = {self.run_out_file + '.out'}\n")
152
- file.write(f"Error = {self.run_out_file + '.err'}\n")
153
- file.write(f"Log = {self.run_out_file + '.job'}\n")
154
- if self.submit_options:
155
- submit_option_list = self.submit_options.split(",")
156
- for option in submit_option_list:
157
- file.write(option.lstrip() + "\n")
158
- file.write("queue 1\n")
179
+ file.write("\n".join(lines) + "\n")
159
180
  except FileNotFoundError as exc:
160
181
  self._logger.error(f"Failed creating condor submission file {_condor_file}")
161
182
  raise JobExecutionError from exc
162
183
 
163
- self._execute(self.submit_engine, self.engines[self.submit_engine] + " " + _condor_file)
184
+ return self._execute(self.submit_engine, [self.engines[self.submit_engine], _condor_file])
164
185
 
165
186
  def _submit_gridengine(self):
166
187
  """Submit a job described by a shell script to gridengine."""
167
- this_sub_cmd = self.engines[self.submit_engine]
168
- this_sub_cmd = this_sub_cmd + " -o " + self.run_out_file + ".out"
169
- this_sub_cmd = this_sub_cmd + " -e " + self.run_out_file + ".err"
170
-
171
- self._execute(self.submit_engine, this_sub_cmd + " " + self.run_script)
188
+ this_sub_cmd = [
189
+ self.engines[self.submit_engine],
190
+ "-o",
191
+ self.run_out_file + ".out",
192
+ "-e",
193
+ self.run_out_file + ".err",
194
+ self.run_script,
195
+ ]
196
+ return self._execute(self.submit_engine, this_sub_cmd)
172
197
 
173
198
  def _execute(self, engine, shell_command):
174
199
  """
@@ -178,13 +203,15 @@ class JobManager:
178
203
  ----------
179
204
  engine : str
180
205
  Engine to use.
181
- shell_command : str
182
- Shell command to execute.
206
+ shell_command : list
207
+ List of shell command plus arguments.
183
208
  """
184
209
  self._logger.info(f"Submitting script to {engine}")
185
210
  self._logger.debug(shell_command)
211
+ result = None
186
212
  if not self.test:
187
- os.system(shell_command)
213
+ result = subprocess.run(shell_command, shell=True, check=True)
188
214
  else:
189
- self._logger.info(f"Testing ({engine})")
190
- self._logger.info(shell_command)
215
+ self._logger.info(f"Testing ({engine}: {shell_command})")
216
+
217
+ return result.returncode if result else 0
simtools/model/camera.py CHANGED
@@ -5,8 +5,6 @@ from pathlib import Path
5
5
 
6
6
  import astropy.units as u
7
7
  import numpy as np
8
- from scipy.spatial import cKDTree as KDTree
9
- from scipy.spatial import distance
10
8
 
11
9
  from simtools.utils.geometry import rotate
12
10
 
@@ -304,6 +302,8 @@ class Camera:
304
302
  float
305
303
  The camera fill factor.
306
304
  """
305
+ from scipy.spatial import distance # pylint: disable=import-outside-toplevel
306
+
307
307
  if self.pixels["pixel_spacing"] == 9999:
308
308
  points = np.array([self.pixels["x"], self.pixels["y"]]).T
309
309
  pixel_distances = distance.cdist(points, points, "euclidean")
@@ -403,6 +403,8 @@ class Camera:
403
403
  list of lists
404
404
  Array of neighbor indices in a list for each pixel
405
405
  """
406
+ from scipy.spatial import cKDTree as KDTree # pylint: disable=import-outside-toplevel
407
+
406
408
  tree = KDTree(np.column_stack([x_pos, y_pos]))
407
409
  neighbors = tree.query_ball_tree(tree, radius)
408
410
  return [list(np.setdiff1d(neigh, [i])) for i, neigh in enumerate(neighbors)]
@@ -6,10 +6,12 @@ import shutil
6
6
  from copy import copy
7
7
 
8
8
  import astropy.units as u
9
+ from astropy.table import Table
9
10
 
10
11
  import simtools.utils.general as gen
11
12
  from simtools.db import db_handler
12
13
  from simtools.io_operations import io_handler
14
+ from simtools.simtel import simtel_table_reader
13
15
  from simtools.simtel.simtel_config_writer import SimtelConfigWriter
14
16
  from simtools.utils import names
15
17
 
@@ -273,7 +275,7 @@ class ModelParameter:
273
275
 
274
276
  def _set_config_file_directory_and_name(self):
275
277
  """Set and create the directory and the name of the config file."""
276
- if self.name is None:
278
+ if self.name is None and self.site is None:
277
279
  return
278
280
 
279
281
  self._config_file_directory = self.io_handler.get_output_directory(
@@ -281,15 +283,14 @@ class ModelParameter:
281
283
  )
282
284
 
283
285
  # Setting file name and the location
284
- if self.site is not None and self.name is not None:
285
- config_file_name = names.simtel_config_file_name(
286
- self.site,
287
- self.model_version,
288
- telescope_model_name=self.name,
289
- label=self.label,
290
- extra_label=self._extra_label,
291
- )
292
- self._config_file_path = self.config_file_directory.joinpath(config_file_name)
286
+ config_file_name = names.simtel_config_file_name(
287
+ self.site,
288
+ self.model_version,
289
+ telescope_model_name=self.name,
290
+ label=self.label,
291
+ extra_label=self._extra_label,
292
+ )
293
+ self._config_file_path = self.config_file_directory.joinpath(config_file_name)
293
294
 
294
295
  self._logger.debug(f"Config file path: {self._config_file_path}")
295
296
 
@@ -515,6 +516,35 @@ class ModelParameter:
515
516
  self.db.export_model_files(pars_from_db, self.config_file_directory)
516
517
  self._is_exported_model_files_up_to_date = True
517
518
 
519
+ def get_model_file_as_table(self, par_name):
520
+ """
521
+ Return tabular data from file as astropy table.
522
+
523
+ Parameters
524
+ ----------
525
+ par_name: str
526
+ Name of the parameter.
527
+
528
+ Returns
529
+ -------
530
+ Table
531
+ Astropy table.
532
+ """
533
+ _par_entry = {}
534
+ try:
535
+ _par_entry[par_name] = self._parameters[par_name]
536
+ except KeyError as exc:
537
+ raise ValueError(f"Parameter {par_name} not found in the model.") from exc
538
+ self.db.export_model_files(_par_entry, self.config_file_directory)
539
+ if _par_entry[par_name]["value"].endswith("ecsv"):
540
+ return Table.read(
541
+ self.config_file_directory.joinpath(_par_entry[par_name]["value"]),
542
+ format="ascii.ecsv",
543
+ )
544
+ return simtel_table_reader.read_simtel_table(
545
+ par_name, self.config_file_directory.joinpath(_par_entry[par_name]["value"])
546
+ )
547
+
518
548
  def export_config_file(self):
519
549
  """Export the config file used by sim_telarray."""
520
550
  # Exporting model file
@@ -80,7 +80,7 @@ class SiteModel(ModelParameter):
80
80
  Site-related CORSIKA parameters as dict
81
81
  """
82
82
  if config_file_style:
83
- model_directory = model_directory or Path("")
83
+ model_directory = model_directory or Path()
84
84
  return {
85
85
  "OBSLEV": [
86
86
  self.get_parameter_value_with_unit("corsika_observation_level").to_value("cm")
@@ -138,43 +138,63 @@ class MirrorPanelPSF:
138
138
  self.rnda_opt, save_figures=save_figures
139
139
  )
140
140
 
141
- def _optimize_reflection_angle(self, step_size=0.1):
142
- """Optimize the random reflection angle to minimize the difference in D80 containment."""
141
+ def _optimize_reflection_angle(self, step_size=0.1, max_iteration=100):
142
+ """
143
+ Optimize the random reflection angle to minimize the difference in D80 containment.
144
+
145
+ Parameters
146
+ ----------
147
+ step_size: float
148
+ Initial step size for optimization.
149
+ max_iteration: int
150
+ Maximum number of iterations.
151
+
152
+ Raises
153
+ ------
154
+ ValueError
155
+ If the optimization reaches the maximum number of iterations without converging.
156
+
157
+ """
158
+ relative_tolerance_d80 = self.args_dict["rtol_psf_containment"]
159
+ self._logger.info(
160
+ "Optimizing random reflection angle "
161
+ f"(relative tolerance = {relative_tolerance_d80}, "
162
+ f"step size = {step_size}, max iteration = {max_iteration})"
163
+ )
143
164
 
144
165
  def collect_results(rnda, mean, sig):
145
166
  self.results_rnda.append(rnda)
146
167
  self.results_mean.append(mean)
147
168
  self.results_sig.append(sig)
148
169
 
149
- stop = False
150
- mean_d80, sig_d80 = self.run_simulations_and_analysis(self.rnda_start)
170
+ reference_d80 = self.args_dict["psf_measurement_containment_mean"]
151
171
  rnda = self.rnda_start
152
- sign_delta = np.sign(mean_d80 - self.args_dict["psf_measurement_containment_mean"])
153
- collect_results(rnda, mean_d80, sig_d80)
154
- while not stop:
155
- rnda = rnda - (step_size * self.rnda_start * sign_delta)
156
- if rnda < 0:
157
- rnda = 0
158
- collect_results(rnda, mean_d80, sig_d80)
159
- break
172
+ prev_error_d80 = float("inf")
173
+ iteration = 0
174
+
175
+ while True:
160
176
  mean_d80, sig_d80 = self.run_simulations_and_analysis(rnda)
161
- new_sign_delta = np.sign(mean_d80 - self.args_dict["psf_measurement_containment_mean"])
162
- stop = new_sign_delta != sign_delta
163
- sign_delta = new_sign_delta
177
+ error_d80 = abs(1 - mean_d80 / reference_d80)
164
178
  collect_results(rnda, mean_d80, sig_d80)
165
179
 
166
- self._interpolate_optimal_rnda()
180
+ if error_d80 < relative_tolerance_d80:
181
+ break
182
+
183
+ if mean_d80 < reference_d80:
184
+ rnda += step_size * self.rnda_start
185
+ else:
186
+ rnda -= step_size * self.rnda_start
167
187
 
168
- def _interpolate_optimal_rnda(self):
169
- """Interpolate to find the optimal random reflection angle."""
170
- self.results_rnda, self.results_mean, self.results_sig = gen.sort_arrays(
171
- self.results_rnda, self.results_mean, self.results_sig
172
- )
173
- self.rnda_opt = np.interp(
174
- x=self.args_dict["psf_measurement_containment_mean"],
175
- xp=self.results_mean,
176
- fp=self.results_rnda,
177
- )
188
+ if error_d80 >= prev_error_d80:
189
+ step_size = step_size / 2
190
+ prev_error_d80 = error_d80
191
+ iteration += 1
192
+ if iteration > max_iteration:
193
+ raise ValueError(
194
+ f"Maximum iterations ({max_iteration}) reached without convergence."
195
+ )
196
+
197
+ self.rnda_opt = rnda
178
198
 
179
199
  def _get_starting_value(self):
180
200
  """Get optimization starting value from command line or previous model."""
@@ -275,6 +295,6 @@ class MirrorPanelPSF:
275
295
  )
276
296
  writer.ModelDataWriter.dump(
277
297
  args_dict=self.args_dict,
278
- metadata=MetadataCollector(args_dict=self.args_dict).top_level_meta,
298
+ metadata=MetadataCollector(args_dict=self.args_dict).get_top_level_metadata(),
279
299
  product_data=result_table,
280
300
  )
@@ -1,7 +1,7 @@
1
1
  """Generate run scripts and directories for CORSIKA simulations."""
2
2
 
3
3
  import logging
4
- import os
4
+ import stat
5
5
  from pathlib import Path
6
6
 
7
7
  from simtools.io_operations import io_handler
@@ -99,6 +99,15 @@ class CorsikaRunner:
99
99
  file_type="config_tmp", run_number=self.corsika_config.run_number
100
100
  )
101
101
  corsika_input_tmp_file = self._directory["inputs"].joinpath(corsika_input_tmp_name)
102
+ # CORSIKA log file naming (temporary and final)
103
+ corsika_log_tmp_file = (
104
+ self._directory["data"]
105
+ .joinpath(f"run{self.corsika_config.run_number:06}")
106
+ .joinpath(f"run{self.corsika_config.run_number}.log")
107
+ )
108
+ corsika_log_file = self.get_file_name(
109
+ file_type="corsika_log", run_number=self.corsika_config.run_number
110
+ )
102
111
 
103
112
  if use_pfp:
104
113
  pfp_command = self._get_pfp_command(corsika_input_tmp_file, corsika_input_file)
@@ -138,11 +147,13 @@ class CorsikaRunner:
138
147
  file.write(f"cp {corsika_input_file} {corsika_input_tmp_file}")
139
148
  file.write("\n# Running corsika_autoinputs\n")
140
149
  file.write(autoinputs_command)
150
+ file.write("\n# Moving log files to the corsika log directory\n")
151
+ file.write(f"gzip {corsika_log_tmp_file}\n")
152
+ file.write(f"mv -v {corsika_log_tmp_file}.gz {corsika_log_file}\n")
141
153
 
142
154
  file.write('\necho "RUNTIME: $SECONDS"\n')
143
155
 
144
- os.system(f"chmod ug+x {script_file_path}")
145
-
156
+ script_file_path.chmod(script_file_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP)
146
157
  return script_file_path
147
158
 
148
159
  def get_resources(self, run_number=None):
@@ -163,6 +163,7 @@ class RunnerServices:
163
163
  log_suffixes = {
164
164
  "log": ".log.gz",
165
165
  "histogram": ".hdata.zst",
166
+ "corsika_log": ".corsika.log.gz",
166
167
  }
167
168
  return self.directory["logs"].joinpath(f"{file_name}{log_suffixes[file_type]}")
168
169
 
@@ -187,7 +188,6 @@ class RunnerServices:
187
188
  data_suffixes = {
188
189
  "output": ".zst",
189
190
  "corsika_output": ".zst",
190
- "corsika_log": ".log",
191
191
  "simtel_output": ".simtel.zst",
192
192
  }
193
193
  run_dir = self._get_run_number_string(run_number)
@@ -245,10 +245,10 @@ class RunnerServices:
245
245
  """
246
246
  file_name = self._get_file_basename(run_number)
247
247
 
248
- if file_type in ["log", "histogram"]:
248
+ if file_type in ["log", "histogram", "corsika_log"]:
249
249
  return self._get_log_file_path(file_type, file_name)
250
250
 
251
- if file_type in ["output", "corsika_output", "corsika_log", "simtel_output"]:
251
+ if file_type in ["output", "corsika_output", "simtel_output"]:
252
252
  return self._get_data_file_path(file_type, file_name, run_number)
253
253
 
254
254
  if file_type in ("sub_log", "sub_script"):