gammasimtools 0.5.1__py3-none-any.whl → 0.6.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 (78) hide show
  1. {gammasimtools-0.5.1.dist-info → gammasimtools-0.6.1.dist-info}/METADATA +80 -28
  2. gammasimtools-0.6.1.dist-info/RECORD +91 -0
  3. {gammasimtools-0.5.1.dist-info → gammasimtools-0.6.1.dist-info}/WHEEL +1 -1
  4. {gammasimtools-0.5.1.dist-info → gammasimtools-0.6.1.dist-info}/entry_points.txt +4 -2
  5. simtools/_version.py +14 -2
  6. simtools/applications/add_file_to_db.py +2 -1
  7. simtools/applications/compare_cumulative_psf.py +10 -15
  8. simtools/applications/db_development_tools/add_new_parameter_to_db.py +12 -6
  9. simtools/applications/derive_mirror_rnda.py +95 -71
  10. simtools/applications/generate_corsika_histograms.py +216 -131
  11. simtools/applications/generate_default_metadata.py +110 -0
  12. simtools/applications/generate_simtel_array_histograms.py +192 -0
  13. simtools/applications/get_file_from_db.py +1 -1
  14. simtools/applications/get_parameter.py +3 -3
  15. simtools/applications/make_regular_arrays.py +89 -93
  16. simtools/applications/{plot_layout_array.py → plot_array_layout.py} +15 -14
  17. simtools/applications/print_array_elements.py +81 -34
  18. simtools/applications/produce_array_config.py +2 -2
  19. simtools/applications/production.py +39 -5
  20. simtools/applications/sim_showers_for_trigger_rates.py +26 -30
  21. simtools/applications/simulate_prod.py +49 -107
  22. simtools/applications/submit_data_from_external.py +8 -10
  23. simtools/applications/tune_psf.py +16 -18
  24. simtools/applications/validate_camera_efficiency.py +63 -9
  25. simtools/applications/validate_camera_fov.py +9 -13
  26. simtools/applications/validate_file_using_schema.py +127 -0
  27. simtools/applications/validate_optics.py +13 -15
  28. simtools/camera_efficiency.py +73 -80
  29. simtools/configuration/commandline_parser.py +52 -22
  30. simtools/configuration/configurator.py +98 -33
  31. simtools/constants.py +9 -0
  32. simtools/corsika/corsika_config.py +28 -22
  33. simtools/corsika/corsika_default_config.py +282 -0
  34. simtools/corsika/corsika_histograms.py +328 -282
  35. simtools/corsika/corsika_histograms_visualize.py +162 -163
  36. simtools/corsika/corsika_runner.py +8 -4
  37. simtools/corsika_simtel/corsika_simtel_runner.py +18 -23
  38. simtools/data_model/data_reader.py +129 -0
  39. simtools/data_model/metadata_collector.py +346 -118
  40. simtools/data_model/metadata_model.py +123 -218
  41. simtools/data_model/model_data_writer.py +79 -22
  42. simtools/data_model/validate_data.py +96 -46
  43. simtools/db_handler.py +67 -42
  44. simtools/io_operations/__init__.py +0 -0
  45. simtools/io_operations/hdf5_handler.py +112 -0
  46. simtools/{io_handler.py → io_operations/io_handler.py} +51 -22
  47. simtools/job_execution/job_manager.py +1 -1
  48. simtools/layout/{layout_array.py → array_layout.py} +168 -199
  49. simtools/layout/geo_coordinates.py +196 -0
  50. simtools/layout/telescope_position.py +12 -12
  51. simtools/model/array_model.py +16 -14
  52. simtools/model/camera.py +5 -8
  53. simtools/model/mirrors.py +136 -73
  54. simtools/model/model_utils.py +1 -69
  55. simtools/model/telescope_model.py +32 -25
  56. simtools/psf_analysis.py +26 -19
  57. simtools/ray_tracing.py +54 -26
  58. simtools/schemas/data.metaschema.yml +400 -0
  59. simtools/schemas/metadata.metaschema.yml +566 -0
  60. simtools/simtel/simtel_config_writer.py +14 -5
  61. simtools/simtel/simtel_histograms.py +266 -83
  62. simtools/simtel/simtel_runner.py +8 -7
  63. simtools/simtel/simtel_runner_array.py +7 -8
  64. simtools/simtel/simtel_runner_camera_efficiency.py +48 -2
  65. simtools/simtel/simtel_runner_ray_tracing.py +61 -25
  66. simtools/simulator.py +43 -50
  67. simtools/utils/general.py +232 -286
  68. simtools/utils/geometry.py +163 -0
  69. simtools/utils/names.py +294 -142
  70. simtools/visualization/legend_handlers.py +115 -9
  71. simtools/visualization/visualize.py +13 -13
  72. gammasimtools-0.5.1.dist-info/RECORD +0 -83
  73. simtools/applications/plot_simtel_histograms.py +0 -120
  74. simtools/applications/validate_schema_files.py +0 -135
  75. simtools/corsika/corsika_output_visualize.py +0 -345
  76. simtools/data_model/validate_schema.py +0 -285
  77. {gammasimtools-0.5.1.dist-info → gammasimtools-0.6.1.dist-info}/LICENSE +0 -0
  78. {gammasimtools-0.5.1.dist-info → gammasimtools-0.6.1.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,20 @@
1
1
  import copy
2
2
  import logging
3
3
 
4
- import matplotlib.pyplot as plt
5
4
  import numpy as np
5
+ from astropy import units as u
6
+ from ctapipe.io import write_table
6
7
  from eventio import EventIOFile, Histograms
7
8
  from eventio.search_utils import yield_toplevel_of_type
8
- from matplotlib.backends.backend_pdf import PdfPages
9
9
 
10
- __all__ = ["BadHistogramFormat", "SimtelHistograms"]
10
+ from simtools import version
11
+ from simtools.io_operations.hdf5_handler import fill_hdf5_table
12
+ from simtools.utils.names import sanitize_name
11
13
 
14
+ __all__ = ["InconsistentHistogramFormat", "SimtelHistograms"]
12
15
 
13
- class BadHistogramFormat(Exception):
16
+
17
+ class InconsistentHistogramFormat(Exception):
14
18
  """Exception for bad histogram format."""
15
19
 
16
20
 
@@ -32,27 +36,17 @@ class SimtelHistograms:
32
36
  Initialize SimtelHistograms
33
37
  """
34
38
  self._logger = logging.getLogger(__name__)
39
+ if not isinstance(histogram_files, list):
40
+ histogram_files = [histogram_files]
35
41
  self._histogram_files = histogram_files
36
42
  self._is_test = test
37
- self.combined_hists = None
38
-
39
- def plot_and_save_figures(self, fig_name):
40
- """
41
- Plot all histograms and save a single pdf file.
42
-
43
- Parameters
44
- ----------
45
- fig_name: str
46
- Name of the output figure file.
47
- """
48
- self._combine_histogram_files()
49
- self._plot_combined_histograms(fig_name)
43
+ self._list_of_histograms = None
44
+ self._combined_hists = None
45
+ self.__meta_dict = None
50
46
 
51
47
  @property
52
48
  def number_of_histograms(self):
53
49
  """Returns number of histograms."""
54
- if not hasattr(self, "combined_hists"):
55
- self._combine_histogram_files()
56
50
  return len(self.combined_hists)
57
51
 
58
52
  def get_histogram_title(self, i_hist):
@@ -69,83 +63,199 @@ class SimtelHistograms:
69
63
  str
70
64
  Histogram title.
71
65
  """
72
- if not hasattr(self, "combined_hists"):
73
- self._combine_histogram_files()
74
66
  return self.combined_hists[i_hist]["title"]
75
67
 
76
- def _combine_histogram_files(self):
77
- """Combine histograms from all files into one single list of histograms."""
78
- # Processing and combining histograms from multiple files
79
- self.combined_hists = []
80
-
81
- n_files = 0
82
- for file in self._histogram_files:
83
- count_file = True
84
- with EventIOFile(file) as f:
85
- for o in yield_toplevel_of_type(f, Histograms):
86
- try:
68
+ @property
69
+ def list_of_histograms(self):
70
+ """
71
+ Returns a list with the histograms for each file.
72
+
73
+ Returns
74
+ -------
75
+ list:
76
+ List of histograms.
77
+ """
78
+ if self._list_of_histograms is None:
79
+ self._list_of_histograms = []
80
+ for file in self._histogram_files:
81
+ with EventIOFile(file) as f:
82
+ for o in yield_toplevel_of_type(f, Histograms):
87
83
  hists = o.parse()
88
- except Exception:
89
- self._logger.warning(f"Problematic file {file}")
90
- count_file = False
91
- continue
92
-
93
- if len(self.combined_hists) == 0:
94
- # First file
95
- self.combined_hists = copy.copy(hists)
96
-
97
- else:
98
- # Remaining files
99
- for hist, this_combined_hist in zip(hists, self.combined_hists):
100
- # Checking consistency of histograms
101
- for key_to_test in [
102
- "lower_x",
103
- "upper_x",
104
- "n_bins_x",
105
- "title",
106
- ]:
107
- if hist[key_to_test] != this_combined_hist[key_to_test]:
108
- msg = "Trying to add histograms with inconsistent dimensions"
109
- self._logger.error(msg)
110
- raise BadHistogramFormat(msg)
111
-
112
- this_combined_hist["data"] = np.add(
113
- this_combined_hist["data"], hist["data"]
114
- )
115
-
116
- n_files += int(count_file)
117
-
118
- self._logger.debug(f"End of reading {n_files} files")
119
-
120
- def _plot_combined_histograms(self, fig_name):
121
- """
122
- Plot all histograms into pdf pages and save the figure as a pdf file.
84
+ self._list_of_histograms.append(hists)
85
+ return self._list_of_histograms
86
+
87
+ def _check_consistency(self, first_hist_file, second_hist_file):
88
+ """
89
+ Checks whether two histograms have the same format.
90
+ Raises an error in case they are not consistent.
123
91
 
124
92
  Parameters
125
93
  ----------
126
- fig_name: str
127
- Name of the output figure file.
94
+ first_hist_file: dict
95
+ One histogram from a single file.
96
+ second_hist_file: dict
97
+ One histogram from a single file.
98
+
99
+ Raises
100
+ ------
101
+ InconsistentHistogramFormat:
102
+ if the format of the histograms have inconsistent dimensions.
103
+ """
104
+ for key_to_test in [
105
+ "lower_x",
106
+ "upper_x",
107
+ "n_bins_x",
108
+ "title",
109
+ ]:
110
+ if first_hist_file[key_to_test] != second_hist_file[key_to_test]:
111
+ msg = "Trying to add histograms with inconsistent dimensions"
112
+ self._logger.error(msg)
113
+ raise InconsistentHistogramFormat(msg)
114
+
115
+ @property
116
+ def combined_hists(self):
117
+ """Add the values of the same type of histogram from the various lists into a single
118
+ histogram list."""
119
+ # Processing and combining histograms from multiple files
120
+ if self._combined_hists is None:
121
+ self._combined_hists = []
122
+ for i_hist, hists_one_file in enumerate(self.list_of_histograms):
123
+ if i_hist == 0:
124
+ # First file
125
+ self._combined_hists = copy.copy(hists_one_file)
126
+
127
+ else:
128
+ for hist, this_combined_hist in zip(hists_one_file, self._combined_hists):
129
+ self._check_consistency(hist, this_combined_hist)
130
+
131
+ this_combined_hist["data"] = np.add(
132
+ this_combined_hist["data"], hist["data"]
133
+ )
134
+
135
+ self._logger.debug(f"End of reading {len(self.list_of_histograms)} files")
136
+ return self._combined_hists
137
+
138
+ @combined_hists.setter
139
+ def combined_hists(self, new_combined_hists):
128
140
  """
141
+ Setter for combined_hists.
129
142
 
130
- pdf_pages = PdfPages(fig_name)
131
- for i_hist, histo in enumerate(self.combined_hists):
132
- # Test case: processing only 1/10 of the histograms
133
- if self._is_test and i_hist % 10 != 0:
134
- self._logger.debug(f"Skipping (test=True): {histo['title']}")
135
- continue
143
+ Parameters
144
+ ----------
145
+ new_combined_hists:
146
+ Combined histograms.
147
+ """
148
+ self._combined_hists = new_combined_hists
136
149
 
137
- self._logger.debug(f"Processing: {histo['title']}")
150
+ def _derive_trigger_rate_histograms(self, livetime):
151
+ """
152
+ Calculates the trigger rate histograms, i.e., the ratio in which the events
153
+ are triggered in each bin of impact distance and log energy for each histogram file for
154
+ the livetime defined by `livetime`.
155
+ The livetime gives the amount of time used in a small production to produce the histograms
156
+ used. It is assumed that the livetime is the same for all the histogram files used and that
157
+ the radius (x-axis in the histograms) is given in meters.
138
158
 
139
- fig = plt.figure(figsize=(8, 6))
140
- ax = plt.gca()
159
+ Parameters
160
+ ----------
161
+ livetime: astropy.Quantity
162
+ Time used in the simulation that produced the histograms. E.g., 1*u.h.
141
163
 
142
- self.plot_one_histogram(i_hist, ax)
164
+ Returns
165
+ -------
166
+ list:
167
+ List with the trigger rate histograms for each file.
168
+ """
169
+ if isinstance(livetime, u.Quantity):
170
+ livetime = livetime.to(u.s)
171
+ else:
172
+ livetime = livetime * u.s
173
+ events_histogram = {}
174
+ trigged_events_histogram = {}
175
+ # Save the appropriate histograms to a dictionary
176
+ for i_file, hists_one_file in enumerate(self.list_of_histograms):
177
+ for hist in hists_one_file:
178
+ if hist["id"] == 1:
179
+ events_histogram[i_file] = hist
180
+
181
+ elif hist["id"] == 2:
182
+ trigged_events_histogram[i_file] = hist
183
+
184
+ list_of_trigger_rate_hists = []
185
+ # Calculate the event rate histograms
186
+ for i_file, hists_one_file in enumerate(self.list_of_histograms):
187
+ event_rate_histogram = copy.copy(events_histogram[i_file])
188
+ area_dict = np.pi * (
189
+ (events_histogram[i_file]["upper_x"]) ** 2
190
+ - (events_histogram[i_file]["lower_x"]) ** 2
191
+ )
143
192
 
144
- plt.tight_layout()
145
- pdf_pages.savefig(fig)
146
- plt.close()
193
+ event_rate_histogram["data"] = (
194
+ np.zeros_like(trigged_events_histogram[i_file]["data"]) / livetime.unit
195
+ )
196
+ bins_with_events = events_histogram[i_file]["data"] != 0
197
+ event_rate_histogram["data"][bins_with_events] = (
198
+ trigged_events_histogram[i_file]["data"][bins_with_events]
199
+ / events_histogram[i_file]["data"][bins_with_events]
200
+ * area_dict
201
+ / livetime
202
+ )
147
203
 
148
- pdf_pages.close()
204
+ # Keeping only the necessary information for proceeding with integration
205
+ keys_to_keep = [
206
+ "data",
207
+ "lower_x",
208
+ "lower_y",
209
+ "upper_x",
210
+ "upper_y",
211
+ "entries",
212
+ "n_bins_x",
213
+ "n_bins_y",
214
+ ]
215
+ event_rate_histogram = {
216
+ key: event_rate_histogram[key]
217
+ for key in keys_to_keep
218
+ if key in event_rate_histogram
219
+ }
220
+
221
+ list_of_trigger_rate_hists.append(event_rate_histogram)
222
+ return list_of_trigger_rate_hists
223
+
224
+ def _integrate_trigger_rate_histograms(self, hists):
225
+ """
226
+ Integrates in energy the trigger rate histogram based on the histogram bin edges.
227
+
228
+ Parameters
229
+ ----------
230
+ hists: list
231
+ List with the final trigger rate for each histogram.
232
+ """
233
+
234
+ list_of_integrated_hists = []
235
+ for _, hist in enumerate(hists):
236
+ energy_axis = np.logspace(hist["lower_y"], hist["upper_y"], hist["n_bins_y"])
237
+ radius_axis = np.linspace(hist["lower_x"], hist["upper_x"], hist["n_bins_x"])
238
+ integrated_hist = np.zeros_like(radius_axis)
239
+ for i_radius, _ in enumerate(radius_axis):
240
+ integrated_hist[i_radius] = np.sum(
241
+ hist["data"][:-1, i_radius].value * np.diff(energy_axis)
242
+ )
243
+
244
+ list_of_integrated_hists.append(np.sum(integrated_hist) * hist["data"][0, 0].unit)
245
+ return list_of_integrated_hists
246
+
247
+ def trigger_rate_per_histogram(self, livetime):
248
+ """
249
+ Estimates the trigger rate for each histogram passed.
250
+
251
+ Parameters
252
+ ----------
253
+ livetime: astropy.Quantity
254
+ Time used in the simulation that produced the histograms.
255
+ """
256
+ hists = self._derive_trigger_rate_histograms(livetime=livetime)
257
+ trigger_rates = self._integrate_trigger_rate_histograms(hists)
258
+ return trigger_rates
149
259
 
150
260
  def plot_one_histogram(self, i_hist, ax):
151
261
  """
@@ -229,3 +339,76 @@ class SimtelHistograms:
229
339
  ax.hist(centers, bins=x_bins, weights=hist["data"])
230
340
  ax.set_xlim(xlim)
231
341
  return
342
+
343
+ @property
344
+ def _meta_dict(self):
345
+ """
346
+ Define the meta dictionary for exporting the histograms.
347
+
348
+ Returns
349
+ -------
350
+ dict
351
+ Meta dictionary for the hdf5 files with the histograms.
352
+ """
353
+
354
+ if self.__meta_dict is None:
355
+ self.__meta_dict = {
356
+ "simtools_version": version.__version__,
357
+ "note": "Only lower bin edges are given.",
358
+ }
359
+ return self.__meta_dict
360
+
361
+ def export_histograms(self, hdf5_file_name, overwrite=False):
362
+ """
363
+ Export the histograms to hdf5 files.
364
+
365
+ Parameters
366
+ ----------
367
+ hdf5_file_name: str
368
+ Name of the file to be saved with the hdf5 tables.
369
+ overwrite: bool
370
+ If True overwrites the histograms already saved in the hdf5 file.
371
+ """
372
+ for histogram in self.combined_hists:
373
+ x_bin_edges_list = np.linspace(
374
+ histogram["lower_x"],
375
+ histogram["upper_x"],
376
+ num=histogram["n_bins_x"] + 1,
377
+ endpoint=True,
378
+ )
379
+ if histogram["n_bins_y"] > 0:
380
+ y_bin_edges_list = np.linspace(
381
+ histogram["lower_y"],
382
+ histogram["upper_y"],
383
+ num=histogram["n_bins_y"] + 1,
384
+ endpoint=True,
385
+ )
386
+ else:
387
+ y_bin_edges_list = None
388
+
389
+ self._meta_dict["Title"] = sanitize_name(histogram["title"])
390
+
391
+ table = fill_hdf5_table(
392
+ hist=histogram["data"],
393
+ x_bin_edges=x_bin_edges_list,
394
+ y_bin_edges=y_bin_edges_list,
395
+ x_label=None,
396
+ y_label=None,
397
+ meta_data=self._meta_dict,
398
+ )
399
+
400
+ self._logger.debug(
401
+ f"Writing histogram with name {self._meta_dict['Title']} to " f"{hdf5_file_name}."
402
+ )
403
+ # overwrite takes precedence over append
404
+ if overwrite is True:
405
+ append = False
406
+ else:
407
+ append = True
408
+ write_table(
409
+ table,
410
+ hdf5_file_name,
411
+ f"/{self._meta_dict['Title']}",
412
+ append=append,
413
+ overwrite=overwrite,
414
+ )
@@ -45,7 +45,7 @@ class SimtelRunner:
45
45
  self._script_dir = None
46
46
  self._script_file = None
47
47
 
48
- self.RUNS_PER_SET = 1
48
+ self.runs_per_set = 1
49
49
 
50
50
  def __repr__(self):
51
51
  return f"SimtelRunner(label={self.label})\n"
@@ -140,8 +140,8 @@ class SimtelRunner:
140
140
  file.write(f"{line}\n")
141
141
  file.write("# End of extras\n\n")
142
142
 
143
- N = 1 if test else self.RUNS_PER_SET
144
- for _ in range(N):
143
+ n = 1 if test else self.runs_per_set
144
+ for _ in range(n):
145
145
  file.write(f"{command}\n\n")
146
146
 
147
147
  # Printing out runtime
@@ -178,10 +178,10 @@ class SimtelRunner:
178
178
  self._logger.info(f"Running (test) with command: {command}")
179
179
  self._run_simtel_and_check_output(command)
180
180
  else:
181
- self._logger.debug(f"Running ({self.RUNS_PER_SET}x) with command: {command}")
181
+ self._logger.debug(f"Running ({self.runs_per_set}x) with command: {command}")
182
182
  self._run_simtel_and_check_output(command)
183
183
 
184
- for _ in range(self.RUNS_PER_SET - 1):
184
+ for _ in range(self.runs_per_set - 1):
185
185
  self._run_simtel_and_check_output(command)
186
186
 
187
187
  self._check_run_result(run_number=run_number)
@@ -236,8 +236,9 @@ class SimtelRunner:
236
236
  return False
237
237
 
238
238
  @staticmethod
239
- def _config_option(par, value=None):
239
+ def _config_option(par, value=None, weak_option=False):
240
240
  """Util function for building sim_telarray command."""
241
- c = f" -C {par}"
241
+ option_syntax = "-W" if weak_option else "-C"
242
+ c = f" {option_syntax} {par}"
242
243
  c += f"={value}" if value is not None else ""
243
244
  return c
@@ -2,7 +2,7 @@ import logging
2
2
  from pathlib import Path
3
3
 
4
4
  import simtools.utils.general as gen
5
- from simtools import io_handler
5
+ from simtools.io_operations import io_handler
6
6
  from simtools.simtel.simtel_runner import InvalidOutputFile, SimtelRunner
7
7
 
8
8
  __all__ = ["SimtelRunnerArray"]
@@ -68,11 +68,11 @@ class SimtelRunnerArray(SimtelRunner):
68
68
  self._base_directory = self.io_handler.get_output_directory(self.label, "array-simulator")
69
69
 
70
70
  # Loading config_data
71
- _config_data_in = gen.collect_data_from_yaml_or_dict(config_file, config_data)
71
+ _config_data_in = gen.collect_data_from_file_or_dict(config_file, config_data)
72
72
  _parameter_file = self.io_handler.get_input_data_file(
73
73
  "parameters", "simtel-runner-array_parameters.yml"
74
74
  )
75
- _parameters = gen.collect_data_from_yaml_or_dict(_parameter_file, None)
75
+ _parameters = gen.collect_data_from_file_or_dict(_parameter_file, None)
76
76
  self.config = gen.validate_config_data(_config_data_in, _parameters)
77
77
 
78
78
  self._load_simtel_data_directories()
@@ -105,7 +105,7 @@ class SimtelRunnerArray(SimtelRunner):
105
105
 
106
106
  def get_info_for_file_name(self, run_number):
107
107
  """
108
- Get a dirctionary with the info necessary for building the sim_telarray file names.
108
+ Get a dictionary with the info necessary for building the sim_telarray file names.
109
109
 
110
110
  Returns
111
111
  -------
@@ -226,7 +226,7 @@ class SimtelRunnerArray(SimtelRunner):
226
226
  _resources = {}
227
227
 
228
228
  _resources["runtime"] = None
229
- with open(sub_log_file, "r") as file:
229
+ with open(sub_log_file, "r", encoding="utf-8") as file:
230
230
  for line in reversed(list(file)):
231
231
  if "RUNTIME" in line:
232
232
  _resources["runtime"] = int(line.split()[1])
@@ -274,7 +274,7 @@ class SimtelRunnerArray(SimtelRunner):
274
274
  command += super()._config_option("power_law", "2.5")
275
275
  command += super()._config_option("histogram_file", histogram_file)
276
276
  command += super()._config_option("output_file", output_file)
277
- command += super()._config_option("random_state", "auto")
277
+ command += super()._config_option("random_state", "none")
278
278
  command += super()._config_option("show", "all")
279
279
  command += f" {kwargs['input_file']}"
280
280
  command += f" > {self._log_file} 2>&1 || exit"
@@ -290,5 +290,4 @@ class SimtelRunnerArray(SimtelRunner):
290
290
  msg = "sim_telarray output file does not exist."
291
291
  self._logger.error(msg)
292
292
  raise InvalidOutputFile(msg)
293
-
294
- self._logger.debug("Everything looks fine with the sim_telarray output file.")
293
+ self._logger.debug(f"simtel_array output file {output_file} exists.")
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from pathlib import Path
2
3
 
3
4
  from simtools.simtel.simtel_runner import SimtelRunner
4
5
 
@@ -32,6 +33,7 @@ class SimtelRunnerCameraEfficiency(SimtelRunner):
32
33
  file_simtel=None,
33
34
  file_log=None,
34
35
  zenith_angle=None,
36
+ nsb_spectrum=None,
35
37
  ):
36
38
  """
37
39
  Initialize SimtelRunner.
@@ -47,6 +49,20 @@ class SimtelRunnerCameraEfficiency(SimtelRunner):
47
49
  self._file_simtel = file_simtel
48
50
  self._file_log = file_log
49
51
  self.zenith_angle = zenith_angle
52
+ self.nsb_spectrum = nsb_spectrum
53
+
54
+ @property
55
+ def nsb_spectrum(self):
56
+ """nsb_spectrum property"""
57
+ return self._nsb_spectrum
58
+
59
+ @nsb_spectrum.setter
60
+ def nsb_spectrum(self, nsb_spectrum):
61
+ """Setter for nsb_spectrum"""
62
+ if nsb_spectrum is not None:
63
+ self._nsb_spectrum = self._validate_or_fix_nsb_spectrum_file_format(nsb_spectrum)
64
+ else:
65
+ self._nsb_spectrum = None
50
66
 
51
67
  def _shall_run(self, **kwargs): # pylint: disable=unused-argument; applies only to this line
52
68
  """Tells if simulations should be run again based on the existence of output files."""
@@ -83,7 +99,7 @@ class SimtelRunnerCameraEfficiency(SimtelRunner):
83
99
  # Processing camera filter
84
100
  # A special case is testeff does not support 2D distributions
85
101
  camera_filter_file = self._telescope_model.get_parameter_value("camera_filter")
86
- if self._telescope_model.is_file_2D("camera_filter"):
102
+ if self._telescope_model.is_file_2d("camera_filter"):
87
103
  camera_filter_file = self._get_one_dim_distribution(
88
104
  "camera_filter", "camera_filter_incidence_angle"
89
105
  )
@@ -93,7 +109,7 @@ class SimtelRunnerCameraEfficiency(SimtelRunner):
93
109
  mirror_reflectivity = self._telescope_model.get_parameter_value("mirror_reflectivity")
94
110
  if mirror_class == 2:
95
111
  mirror_reflectivity_secondary = mirror_reflectivity
96
- if self._telescope_model.is_file_2D("mirror_reflectivity"):
112
+ if self._telescope_model.is_file_2d("mirror_reflectivity"):
97
113
  mirror_reflectivity = self._get_one_dim_distribution(
98
114
  "mirror_reflectivity", "primary_mirror_incidence_angle"
99
115
  )
@@ -102,6 +118,8 @@ class SimtelRunnerCameraEfficiency(SimtelRunner):
102
118
  )
103
119
 
104
120
  command = str(self._simtel_source_path.joinpath("sim_telarray/bin/testeff"))
121
+ if self.nsb_spectrum is not None:
122
+ command += f" -fnsb {self.nsb_spectrum}"
105
123
  command += " -nm -nsb-extra"
106
124
  command += f" -alt {self._telescope_model.get_parameter_value('altitude')}"
107
125
  command += f" -fatm {self._telescope_model.get_parameter_value('atmospheric_transmission')}"
@@ -190,3 +208,31 @@ class SimtelRunnerCameraEfficiency(SimtelRunner):
190
208
  )
191
209
 
192
210
  return one_dim_file
211
+
212
+ def _validate_or_fix_nsb_spectrum_file_format(self, nsb_spectrum_file):
213
+ """
214
+ Validate or fix the nsb spectrum file format.
215
+ The nsb spectrum file format required by sim_telarray has three columns:
216
+ wavelength (nm), ignored, NSB flux [1e9 * ph/m2/s/sr/nm],
217
+ where the second column is ignored by sim_telarray and the third is used for the NSB flux.
218
+ This function makes sure the file has at least three columns,
219
+ by copying the second column to the third.
220
+ """
221
+
222
+ validated_nsb_spectrum_file = (
223
+ self._telescope_model.get_config_directory() / Path(nsb_spectrum_file).name
224
+ )
225
+ with open(nsb_spectrum_file, "r", encoding="utf-8") as file:
226
+ lines = file.readlines()
227
+ with open(validated_nsb_spectrum_file, "w", encoding="utf-8") as file:
228
+ for line in lines:
229
+ if line.startswith("#"):
230
+ file.write(line)
231
+ continue
232
+ split_line = line.split()
233
+ if len(split_line) == 2:
234
+ split_line.append(split_line[1])
235
+ file.write(f"{split_line[0]} {split_line[1]} {split_line[2]}\n")
236
+ else:
237
+ file.write(line)
238
+ return validated_nsb_spectrum_file