gammasimtools 0.19.0__py3-none-any.whl → 0.21.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 (59) hide show
  1. {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/METADATA +1 -3
  2. {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/RECORD +54 -51
  3. {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/entry_points.txt +3 -3
  4. simtools/_version.py +2 -2
  5. simtools/applications/calculate_incident_angles.py +182 -0
  6. simtools/applications/db_add_simulation_model_from_repository_to_db.py +17 -14
  7. simtools/applications/db_add_value_from_json_to_db.py +6 -9
  8. simtools/applications/db_generate_compound_indexes.py +7 -3
  9. simtools/applications/db_get_file_from_db.py +11 -23
  10. simtools/applications/derive_psf_parameters.py +58 -39
  11. simtools/applications/derive_trigger_rates.py +91 -0
  12. simtools/applications/generate_corsika_histograms.py +7 -184
  13. simtools/applications/maintain_simulation_model_add_production.py +105 -0
  14. simtools/applications/plot_simtel_events.py +5 -189
  15. simtools/applications/print_version.py +8 -7
  16. simtools/applications/validate_file_using_schema.py +7 -4
  17. simtools/configuration/commandline_parser.py +17 -11
  18. simtools/corsika/corsika_histograms.py +81 -0
  19. simtools/data_model/validate_data.py +8 -3
  20. simtools/db/db_handler.py +122 -31
  21. simtools/db/db_model_upload.py +51 -30
  22. simtools/dependencies.py +10 -5
  23. simtools/layout/array_layout_utils.py +37 -5
  24. simtools/model/array_model.py +18 -1
  25. simtools/model/model_repository.py +118 -63
  26. simtools/model/site_model.py +25 -0
  27. simtools/production_configuration/derive_corsika_limits.py +9 -34
  28. simtools/ray_tracing/incident_angles.py +706 -0
  29. simtools/ray_tracing/psf_parameter_optimisation.py +999 -565
  30. simtools/schemas/model_parameter_and_data_schema.metaschema.yml +2 -2
  31. simtools/schemas/model_parameters/nsb_reference_spectrum.schema.yml +1 -1
  32. simtools/schemas/model_parameters/nsb_spectrum.schema.yml +22 -29
  33. simtools/schemas/model_parameters/stars.schema.yml +1 -1
  34. simtools/schemas/production_tables.schema.yml +5 -0
  35. simtools/simtel/simtel_config_writer.py +18 -20
  36. simtools/simtel/simtel_io_event_histograms.py +253 -516
  37. simtools/simtel/simtel_io_event_reader.py +51 -2
  38. simtools/simtel/simtel_io_event_writer.py +31 -11
  39. simtools/simtel/simtel_io_metadata.py +1 -1
  40. simtools/simtel/simtel_table_reader.py +3 -3
  41. simtools/simulator.py +1 -4
  42. simtools/telescope_trigger_rates.py +119 -0
  43. simtools/testing/log_inspector.py +13 -11
  44. simtools/utils/geometry.py +20 -0
  45. simtools/version.py +89 -0
  46. simtools/{corsika/corsika_histograms_visualize.py → visualization/plot_corsika_histograms.py} +109 -0
  47. simtools/visualization/plot_incident_angles.py +431 -0
  48. simtools/visualization/plot_psf.py +673 -0
  49. simtools/visualization/plot_simtel_event_histograms.py +376 -0
  50. simtools/visualization/{simtel_event_plots.py → plot_simtel_events.py} +284 -87
  51. simtools/visualization/visualize.py +1 -3
  52. simtools/applications/calculate_trigger_rate.py +0 -187
  53. simtools/applications/generate_sim_telarray_histograms.py +0 -196
  54. simtools/applications/maintain_simulation_model_add_production_table.py +0 -71
  55. simtools/simtel/simtel_io_histogram.py +0 -623
  56. simtools/simtel/simtel_io_histograms.py +0 -556
  57. {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/WHEEL +0 -0
  58. {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/licenses/LICENSE +0 -0
  59. {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/top_level.txt +0 -0
@@ -3,9 +3,9 @@
3
3
  r"""
4
4
  Plot simulated events.
5
5
 
6
- This application produces figures from one or more sim_telarray (.simtel.zst) files
7
- by calling functions in ``simtools.visualization.simtel_event_plots``. It is meant to
8
- run after simulations (e.g., simtools-simulate-flasher, simtools-simulate-illuminator).
6
+ Produces figures from one or more sim_telarray (.simtel.zst) files
7
+ It is meant to run after simulations (e.g., simtools-simulate-flasher,
8
+ simtools-simulate-illuminator).
9
9
 
10
10
  What it does
11
11
  ------------
@@ -81,29 +81,8 @@ from pathlib import Path
81
81
 
82
82
  import simtools.utils.general as gen
83
83
  from simtools.configuration import configurator
84
- from simtools.corsika.corsika_histograms_visualize import save_figs_to_pdf
85
- from simtools.data_model.metadata_collector import MetadataCollector
86
84
  from simtools.io import io_handler
87
- from simtools.visualization.simtel_event_plots import (
88
- plot_simtel_event_image,
89
- plot_simtel_integrated_pedestal_image,
90
- plot_simtel_integrated_signal_image,
91
- plot_simtel_peak_timing,
92
- plot_simtel_step_traces,
93
- plot_simtel_time_traces,
94
- plot_simtel_waveform_matrix,
95
- )
96
-
97
- PLOT_CHOICES = {
98
- "event_image": "event_image",
99
- "time_traces": "time_traces",
100
- "waveform_matrix": "waveform_matrix",
101
- "step_traces": "step_traces",
102
- "integrated_signal_image": "integrated_signal_image",
103
- "integrated_pedestal_image": "integrated_pedestal_image",
104
- "peak_timing": "peak_timing",
105
- "all": "all",
106
- }
85
+ from simtools.visualization.plot_simtel_events import PLOT_CHOICES, generate_and_save_plots
107
86
 
108
87
 
109
88
  def _parse(label: str):
@@ -128,7 +107,6 @@ def _parse(label: str):
128
107
  default=["event_image"],
129
108
  choices=sorted(PLOT_CHOICES),
130
109
  )
131
- # common plotting options
132
110
  config.parser.add_argument("--tel_id", type=int, default=None, help="Telescope ID")
133
111
  config.parser.add_argument(
134
112
  "--n_pixels", type=int, default=3, help="For time_traces: number of pixel traces"
@@ -179,7 +157,6 @@ def _parse(label: str):
179
157
  default=None,
180
158
  help="0-based index of the event to plot; default is the first event",
181
159
  )
182
- # outputs
183
160
  config.parser.add_argument(
184
161
  "--output_file",
185
162
  type=str,
@@ -199,139 +176,6 @@ def _parse(label: str):
199
176
  return config.initialize(db_config=False, require_command_line=True)
200
177
 
201
178
 
202
- def _save_png(fig, out_dir: Path, stem: str, suffix: str, dpi: int):
203
- """Save ``fig`` as a PNG into ``out_dir`` using ``stem`` and ``suffix``.
204
-
205
- Errors during saving are logged as warnings and otherwise ignored.
206
- """
207
- png_path = out_dir.joinpath(f"{stem}_{suffix}.png")
208
- try:
209
- fig.savefig(png_path, dpi=dpi, bbox_inches="tight")
210
- except Exception as ex: # pylint:disable=broad-except
211
- logging.getLogger(__name__).warning("Failed to save PNG %s: %s", png_path, ex)
212
-
213
-
214
- def _make_output_paths(
215
- ioh: io_handler.IOHandler, base: str | None, input_file: Path
216
- ) -> tuple[Path, Path]:
217
- """Return (out_dir, pdf_path) based on base and input_file."""
218
- out_dir = ioh.get_output_directory(label=Path(__file__).stem)
219
- if base:
220
- pdf_path = ioh.get_output_file(f"{base}_{input_file.stem}")
221
- else:
222
- pdf_path = ioh.get_output_file(input_file.stem)
223
- pdf_path = Path(f"{pdf_path}.pdf") if pdf_path.suffix != ".pdf" else Path(pdf_path)
224
- return out_dir, pdf_path
225
-
226
-
227
- def _collect_figures_for_file(
228
- filename: Path,
229
- plots: list[str],
230
- args: dict,
231
- out_dir: Path,
232
- base_stem: str,
233
- save_pngs: bool,
234
- dpi: int,
235
- ):
236
- """Generate the selected plots for a single sim_telarray file.
237
-
238
- Returns a list of figures. If ``save_pngs`` is True, also writes PNGs to
239
- ``out_dir`` using ``base_stem`` for filenames.
240
- """
241
- logger = logging.getLogger(__name__)
242
- figures: list[object] = []
243
-
244
- def add(fig, tag: str):
245
- if fig is not None:
246
- figures.append(fig)
247
- if save_pngs:
248
- _save_png(fig, out_dir, base_stem, tag, dpi)
249
- else:
250
- logger.warning("Plot '%s' returned no figure for %s", tag, filename)
251
-
252
- plots_to_run = (
253
- [
254
- "event_image",
255
- "time_traces",
256
- "waveform_matrix",
257
- "step_traces",
258
- "integrated_signal_image",
259
- "integrated_pedestal_image",
260
- "peak_timing",
261
- ]
262
- if "all" in plots
263
- else list(plots)
264
- )
265
-
266
- def _call_peak_timing():
267
- try:
268
- fig_stats = plot_simtel_peak_timing(
269
- filename,
270
- tel_id=args.get("tel_id"),
271
- sum_threshold=args.get("sum_threshold", 10.0),
272
- peak_width=args.get("peak_width", 8),
273
- examples=args.get("examples", 3),
274
- timing_bins=args.get("timing_bins"),
275
- return_stats=True,
276
- event_index=args.get("event_index"),
277
- )
278
- return fig_stats[0] if isinstance(fig_stats, tuple) else fig_stats
279
- except TypeError:
280
- return plot_simtel_peak_timing(
281
- filename,
282
- tel_id=args.get("tel_id"),
283
- sum_threshold=args.get("sum_threshold", 10.0),
284
- peak_width=args.get("peak_width", 8),
285
- examples=args.get("examples", 3),
286
- timing_bins=args.get("timing_bins"),
287
- event_index=args.get("event_index"),
288
- )
289
-
290
- # function name -> (callable, defaults)
291
- dispatch: dict[str, tuple[object, dict[str, object]]] = {
292
- "event_image": (
293
- plot_simtel_event_image,
294
- {"distance": None, "event_index": None},
295
- ),
296
- "time_traces": (
297
- plot_simtel_time_traces,
298
- {"tel_id": None, "n_pixels": 3, "event_index": None},
299
- ),
300
- "waveform_matrix": (
301
- plot_simtel_waveform_matrix,
302
- {"tel_id": None, "vmax": None, "event_index": None},
303
- ),
304
- "step_traces": (
305
- plot_simtel_step_traces,
306
- {"tel_id": None, "pixel_step": None, "max_pixels": None, "event_index": None},
307
- ),
308
- "integrated_signal_image": (
309
- plot_simtel_integrated_signal_image,
310
- {"tel_id": None, "half_width": 8, "event_index": None},
311
- ),
312
- "integrated_pedestal_image": (
313
- plot_simtel_integrated_pedestal_image,
314
- {"tel_id": None, "half_width": 8, "offset": 16, "event_index": None},
315
- ),
316
- }
317
-
318
- for plot_name in plots_to_run:
319
- if plot_name == "peak_timing":
320
- add(_call_peak_timing(), "peak_timing")
321
- continue
322
- entry = dispatch.get(plot_name)
323
- if entry is None:
324
- logger.warning("Unknown plot selection '%s'", plot_name)
325
- continue
326
- func, defaults = entry
327
- # Build kwargs with user args overriding defaults
328
- kwargs = {k: args.get(k, v) for k, v in defaults.items()}
329
- fig = func(filename, **kwargs) # type: ignore[misc]
330
- add(fig, plot_name)
331
-
332
- return figures
333
-
334
-
335
179
  def main():
336
180
  """Generate plots from sim_telarray files."""
337
181
  label = Path(__file__).stem
@@ -341,38 +185,10 @@ def main():
341
185
  logger.setLevel(gen.get_log_level_from_user(args.get("log_level", "INFO")))
342
186
 
343
187
  ioh = io_handler.IOHandler()
344
-
345
188
  simtel_files = [Path(p).expanduser() for p in gen.ensure_iterable(args["simtel_files"])]
346
189
  plots = list(gen.ensure_iterable(args.get("plots")))
347
190
 
348
- for simtel in simtel_files:
349
- out_dir, pdf_path = _make_output_paths(ioh, args.get("output_file"), simtel)
350
- figures = _collect_figures_for_file(
351
- filename=simtel,
352
- plots=plots,
353
- args=args,
354
- out_dir=out_dir,
355
- base_stem=simtel.stem,
356
- save_pngs=bool(args.get("save_pngs", False)),
357
- dpi=int(args.get("dpi", 300)),
358
- )
359
-
360
- if not figures:
361
- logger.warning("No figures produced for %s", simtel)
362
- continue
363
-
364
- # Save a multipage PDF
365
- try:
366
- save_figs_to_pdf(figures, pdf_path)
367
- logger.info("Saved PDF: %s", pdf_path)
368
- except Exception as ex: # pylint:disable=broad-except
369
- logger.error("Failed to save PDF %s: %s", pdf_path, ex)
370
-
371
- # Dump run metadata alongside PDF
372
- try:
373
- MetadataCollector.dump(args, pdf_path, add_activity_name=True)
374
- except Exception as ex: # pylint:disable=broad-except
375
- logger.warning("Failed to write metadata for %s: %s", pdf_path, ex)
191
+ generate_and_save_plots(simtel_files=simtel_files, plots=plots, args=args, ioh=ioh)
376
192
 
377
193
 
378
194
  if __name__ == "__main__":
@@ -39,7 +39,7 @@ def _parse(label, description, usage):
39
39
  """
40
40
  config = configurator.Configurator(label=label, description=description, usage=usage)
41
41
 
42
- return config.initialize(db_config=True, output=True)
42
+ return config.initialize(db_config=True, output=True, require_command_line=False)
43
43
 
44
44
 
45
45
  def main():
@@ -68,12 +68,13 @@ def main():
68
68
  key, value = version_entry.split(": ", 1)
69
69
  version_dict[key] = value
70
70
 
71
- ascii_handler.write_data_to_file(
72
- data=version_dict,
73
- output_file=io_handler_instance.get_output_file(
74
- args_dict.get("output_file", "simtools_version.json"), label=label
75
- ),
76
- )
71
+ if not args_dict.get("output_file_from_default", False):
72
+ ascii_handler.write_data_to_file(
73
+ data=version_dict,
74
+ output_file=io_handler_instance.get_output_file(
75
+ args_dict.get("output_file", "simtools_version.json"), label=label
76
+ ),
77
+ )
77
78
 
78
79
 
79
80
  if __name__ == "__main__":
@@ -145,10 +145,13 @@ def validate_dict_using_schema(args_dict, logger):
145
145
  except FileNotFoundError as exc:
146
146
  raise FileNotFoundError(f"Error reading schema file from {file_name}") from exc
147
147
  data = data if isinstance(data, list) else [data]
148
- for data_dict in data:
149
- schema.validate_dict_using_schema(
150
- data_dict, _get_schema_file_name(args_dict, data_dict)
151
- )
148
+ try:
149
+ for data_dict in data:
150
+ schema.validate_dict_using_schema(
151
+ data_dict, _get_schema_file_name(args_dict, data_dict)
152
+ )
153
+ except Exception as exc:
154
+ raise ValueError(f"Validation of file {file_name} failed") from exc
152
155
  logger.info(f"Successful validation of file {file_name}")
153
156
 
154
157
 
@@ -222,6 +222,13 @@ class CommandLineParser(argparse.ArgumentParser):
222
222
  required=False,
223
223
  default=None,
224
224
  )
225
+ _job_group.add_argument(
226
+ "--db_simulation_model_version",
227
+ help="version of simulation model database",
228
+ type=str.strip,
229
+ required=False,
230
+ default=None,
231
+ )
225
232
 
226
233
  def initialize_simulation_model_arguments(self, model_options):
227
234
  """
@@ -740,28 +747,27 @@ class CommandLineParser(argparse.ArgumentParser):
740
747
  @staticmethod
741
748
  def parse_quantity_pair(string):
742
749
  """
743
- Parse a string representing a pair of astropy quantities separated by a space.
744
-
745
- Args:
746
- string: The input string (e.g., "0 deg 1.5 deg").
750
+ Parse a string representing a pair of astropy quantities.
747
751
 
748
752
  Returns
749
753
  -------
750
- tuple: A tuple containing two astropy.units.Quantity objects.
754
+ tuple
755
+ A tuple of two astropy.units.Quantity objects.
751
756
 
752
757
  Raises
753
758
  ------
754
- ValueError: If the string is not formatted correctly (e.g., missing space).
759
+ ValueError
760
+ If the string cannot be parsed into exactly two quantities.
755
761
  """
756
- pattern = r"(\d+\.?\d*)\s*([a-zA-Z]+)"
762
+ pattern = r"(?>[\d\.eE+-]+)\s*(?>[A-Za-z]+)"
757
763
  matches = re.findall(pattern, string)
758
764
  if len(matches) != 2:
759
765
  raise ValueError("Input string does not contain exactly two quantities.")
760
766
 
761
- return (
762
- u.Quantity(float(matches[0][0]), matches[0][1]),
763
- u.Quantity(float(matches[1][0]), matches[1][1]),
764
- )
767
+ try:
768
+ return tuple(u.Quantity(m) for m in matches)
769
+ except Exception as exc:
770
+ raise ValueError(f"Could not parse quantities: {exc}") from exc
765
771
 
766
772
  @staticmethod
767
773
  def parse_integer_and_quantity(input_string):
@@ -22,6 +22,7 @@ from simtools.io.ascii_handler import collect_data_from_file
22
22
  from simtools.io.hdf5_handler import fill_hdf5_table
23
23
  from simtools.utils.geometry import convert_2d_to_radial_distr, rotate
24
24
  from simtools.utils.names import sanitize_name
25
+ from simtools.visualization import plot_corsika_histograms as visualize
25
26
 
26
27
  X_AXIS_STRING = "x axis"
27
28
  Y_AXIS_STRING = "y axis"
@@ -111,6 +112,86 @@ class CorsikaHistograms:
111
112
  self.read_event_information()
112
113
  self._initialize_header()
113
114
 
115
+ def parse_telescope_indices(self, indices_arg):
116
+ """Return telescope indices as ndarray[int] or None.
117
+
118
+ Accepts None, a sequence of strings/ints. Raises ValueError on invalid input.
119
+ """
120
+ if indices_arg is None:
121
+ return None
122
+ try:
123
+ return np.array(indices_arg).astype(int)
124
+ except ValueError as exc:
125
+ msg = (
126
+ f"{indices_arg} not a valid input. Please use integer numbers for telescope_indices"
127
+ )
128
+ self._logger.error(msg)
129
+ raise ValueError(msg) from exc
130
+
131
+ def should_overwrite(
132
+ self, write_hdf5: bool, event1d: list | None, event2d: list | None
133
+ ) -> bool:
134
+ """Return True if output HDF5 exists and any writing flag is requested."""
135
+ exists = Path(self.hdf5_file_name).exists()
136
+ if exists and (write_hdf5 or bool(event1d) or bool(event2d)):
137
+ self._logger.warning(
138
+ f"Output hdf5 file {self.hdf5_file_name} already exists. Overwriting it."
139
+ )
140
+ return True
141
+ return False
142
+
143
+ def run_export_pipeline(
144
+ self,
145
+ *,
146
+ individual_telescopes: bool,
147
+ hist_config,
148
+ indices_arg,
149
+ write_pdf: bool,
150
+ write_hdf5: bool,
151
+ event1d: list | None,
152
+ event2d: list | None,
153
+ test: bool = False,
154
+ ) -> dict:
155
+ """Run the full histogram export pipeline and return output artifact paths.
156
+
157
+ Returns a dict with optional keys: pdf_photons, pdf_event1d, pdf_event2d.
158
+ """
159
+ outputs: dict[str, Path | None] = {
160
+ "pdf_photons": None,
161
+ "pdf_event_1d": None,
162
+ "pdf_event_2d": None,
163
+ }
164
+
165
+ indices = self.parse_telescope_indices(indices_arg)
166
+ overwrite = self.should_overwrite(write_hdf5, event1d, event2d)
167
+
168
+ self.set_histograms(
169
+ telescope_indices=indices,
170
+ individual_telescopes=individual_telescopes,
171
+ hist_config=hist_config,
172
+ )
173
+
174
+ if write_pdf:
175
+ pdf_path = visualize.export_all_photon_figures_pdf(self, test=test)
176
+ outputs["pdf_photons"] = pdf_path
177
+ if write_hdf5:
178
+ self.export_histograms(overwrite=overwrite)
179
+
180
+ if event1d is not None:
181
+ outputs["pdf_event_1d"] = visualize.derive_event_1d_histograms(
182
+ self, event1d, pdf=write_pdf, hdf5=write_hdf5, overwrite=not write_hdf5
183
+ )
184
+ if event2d is not None:
185
+ outputs["pdf_event_2d"] = visualize.derive_event_2d_histograms(
186
+ self,
187
+ event2d,
188
+ pdf=write_pdf,
189
+ hdf5=write_hdf5,
190
+ overwrite=not (write_hdf5 or bool(event1d)),
191
+ )
192
+
193
+ return outputs
194
+
114
195
  @property
115
196
  def hdf5_file_name(self):
116
197
  """
@@ -187,9 +187,14 @@ class DataValidator:
187
187
  value_as_list, unit_as_list = self._get_value_and_units_as_lists()
188
188
 
189
189
  for index, (value, unit) in enumerate(zip(value_as_list, unit_as_list)):
190
- value_as_list[index], unit_as_list[index] = self._validate_value_and_unit(
191
- value, unit, index
192
- )
190
+ try:
191
+ value_as_list[index], unit_as_list[index] = self._validate_value_and_unit(
192
+ value, unit, index
193
+ )
194
+ except TypeError as ex:
195
+ raise TypeError(
196
+ f"Error validating dictionary using {self.schema_file_name}"
197
+ ) from ex
193
198
 
194
199
  if len(value_as_list) == 1:
195
200
  self.data_dict["value"], self.data_dict["unit"] = value_as_list[0], unit_as_list[0]