gammasimtools 0.25.0__py3-none-any.whl → 0.27.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/METADATA +6 -1
  2. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/RECORD +135 -130
  3. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/WHEEL +1 -1
  4. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/entry_points.txt +3 -2
  5. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/licenses/LICENSE +1 -1
  6. simtools/_version.py +2 -2
  7. simtools/application_control.py +35 -7
  8. simtools/applications/convert_geo_coordinates_of_array_elements.py +3 -3
  9. simtools/applications/db_add_file_to_db.py +1 -1
  10. simtools/applications/db_add_simulation_model_from_repository_to_db.py +1 -1
  11. simtools/applications/db_add_value_from_json_to_db.py +1 -1
  12. simtools/applications/db_generate_compound_indexes.py +1 -1
  13. simtools/applications/db_get_array_layouts_from_db.py +3 -7
  14. simtools/applications/db_get_file_from_db.py +1 -1
  15. simtools/applications/db_get_parameter_from_db.py +1 -1
  16. simtools/applications/db_inspect_databases.py +1 -1
  17. simtools/applications/db_upload_model_repository.py +1 -1
  18. simtools/applications/derive_ctao_array_layouts.py +1 -2
  19. simtools/applications/{calculate_incident_angles.py → derive_incident_angle.py} +16 -18
  20. simtools/applications/derive_mirror_rnda.py +112 -180
  21. simtools/applications/derive_psf_parameters.py +0 -1
  22. simtools/applications/derive_pulse_shape_parameters.py +0 -1
  23. simtools/applications/derive_trigger_rates.py +1 -1
  24. simtools/applications/docs_produce_array_element_report.py +2 -8
  25. simtools/applications/docs_produce_calibration_reports.py +1 -3
  26. simtools/applications/docs_produce_model_parameter_reports.py +0 -2
  27. simtools/applications/docs_produce_simulation_configuration_report.py +1 -3
  28. simtools/applications/generate_array_config.py +0 -1
  29. simtools/applications/generate_corsika_histograms.py +79 -229
  30. simtools/applications/generate_regular_arrays.py +76 -69
  31. simtools/applications/generate_simtel_event_data.py +2 -2
  32. simtools/applications/maintain_simulation_model_add_production.py +2 -2
  33. simtools/applications/maintain_simulation_model_write_array_element_positions.py +87 -0
  34. simtools/applications/plot_array_layout.py +5 -111
  35. simtools/applications/plot_simulated_event_distributions.py +57 -0
  36. simtools/applications/plot_tabular_data.py +0 -1
  37. simtools/applications/plot_tabular_data_for_model_parameter.py +1 -6
  38. simtools/applications/production_derive_corsika_limits.py +1 -1
  39. simtools/applications/production_generate_grid.py +0 -1
  40. simtools/applications/run_application.py +1 -1
  41. simtools/applications/simulate_flasher.py +3 -15
  42. simtools/applications/simulate_illuminator.py +2 -11
  43. simtools/applications/simulate_pedestals.py +1 -5
  44. simtools/applications/simulate_prod.py +8 -11
  45. simtools/applications/simulate_prod_htcondor_generator.py +1 -1
  46. simtools/applications/submit_array_layouts.py +2 -4
  47. simtools/applications/submit_data_from_external.py +2 -1
  48. simtools/applications/submit_model_parameter_from_external.py +1 -3
  49. simtools/applications/validate_camera_efficiency.py +28 -28
  50. simtools/applications/validate_camera_fov.py +0 -1
  51. simtools/applications/validate_cumulative_psf.py +1 -5
  52. simtools/applications/validate_optics.py +2 -14
  53. simtools/atmosphere.py +83 -0
  54. simtools/camera/camera_efficiency.py +171 -53
  55. simtools/camera/single_photon_electron_spectrum.py +8 -7
  56. simtools/configuration/commandline_parser.py +82 -11
  57. simtools/configuration/configurator.py +6 -11
  58. simtools/constants.py +5 -0
  59. simtools/corsika/corsika_config.py +100 -202
  60. simtools/corsika/corsika_histograms.py +561 -1708
  61. simtools/corsika/primary_particle.py +1 -1
  62. simtools/data_model/metadata_collector.py +5 -2
  63. simtools/data_model/metadata_model.py +0 -4
  64. simtools/data_model/model_data_writer.py +59 -64
  65. simtools/data_model/schema.py +2 -0
  66. simtools/data_model/validate_data.py +1 -3
  67. simtools/db/db_handler.py +23 -10
  68. simtools/db/mongo_db.py +2 -2
  69. simtools/dependencies.py +81 -38
  70. simtools/io/ascii_handler.py +55 -5
  71. simtools/io/io_handler.py +23 -12
  72. simtools/io/table_handler.py +1 -1
  73. simtools/job_execution/job_manager.py +154 -79
  74. simtools/job_execution/process_pool.py +137 -0
  75. simtools/layout/array_layout.py +4 -13
  76. simtools/layout/array_layout_utils.py +348 -57
  77. simtools/model/array_model.py +23 -63
  78. simtools/model/calibration_model.py +4 -8
  79. simtools/model/legacy_model_parameter.py +134 -0
  80. simtools/model/model_parameter.py +147 -86
  81. simtools/model/model_utils.py +40 -6
  82. simtools/model/site_model.py +4 -8
  83. simtools/model/telescope_model.py +10 -16
  84. simtools/production_configuration/derive_corsika_limits.py +6 -11
  85. simtools/production_configuration/interpolation_handler.py +16 -16
  86. simtools/ray_tracing/incident_angles.py +92 -17
  87. simtools/ray_tracing/mirror_panel_psf.py +338 -222
  88. simtools/ray_tracing/psf_analysis.py +62 -48
  89. simtools/ray_tracing/psf_parameter_optimisation.py +3 -3
  90. simtools/ray_tracing/ray_tracing.py +43 -25
  91. simtools/reporting/docs_auto_report_generator.py +8 -13
  92. simtools/reporting/docs_read_parameters.py +2 -8
  93. simtools/runners/corsika_runner.py +52 -195
  94. simtools/runners/corsika_simtel_runner.py +77 -108
  95. simtools/runners/runner_services.py +214 -213
  96. simtools/runners/simtel_runner.py +27 -160
  97. simtools/runners/simtools_runner.py +11 -73
  98. simtools/schemas/application_workflow.metaschema.yml +8 -0
  99. simtools/settings.py +173 -0
  100. simtools/{io/eventio_handler.py → sim_events/file_info.py} +3 -3
  101. simtools/{simtel/simtel_io_event_histograms.py → sim_events/histograms.py} +25 -15
  102. simtools/{simtel/simtel_io_event_reader.py → sim_events/reader.py} +20 -17
  103. simtools/{simtel/simtel_io_event_writer.py → sim_events/writer.py} +84 -25
  104. simtools/simtel/pulse_shapes.py +7 -2
  105. simtools/simtel/simtel_config_writer.py +79 -91
  106. simtools/simtel/simtel_seeds.py +184 -0
  107. simtools/simtel/simtel_table_reader.py +6 -4
  108. simtools/simtel/simulator_array.py +114 -109
  109. simtools/simtel/simulator_camera_efficiency.py +68 -46
  110. simtools/simtel/simulator_light_emission.py +164 -132
  111. simtools/simtel/simulator_ray_tracing.py +80 -71
  112. simtools/simulator.py +137 -355
  113. simtools/telescope_trigger_rates.py +3 -4
  114. simtools/testing/assertions.py +84 -33
  115. simtools/testing/configuration.py +1 -2
  116. simtools/testing/helpers.py +2 -3
  117. simtools/testing/log_inspector.py +1 -0
  118. simtools/testing/sim_telarray_metadata.py +14 -12
  119. simtools/testing/validate_output.py +121 -42
  120. simtools/utils/general.py +43 -17
  121. simtools/utils/geometry.py +0 -77
  122. simtools/utils/names.py +5 -5
  123. simtools/utils/random.py +36 -0
  124. simtools/visualization/legend_handlers.py +7 -6
  125. simtools/visualization/plot_array_layout.py +91 -16
  126. simtools/visualization/plot_corsika_histograms.py +145 -605
  127. simtools/visualization/plot_incident_angles.py +48 -1
  128. simtools/visualization/plot_mirrors.py +1 -4
  129. simtools/visualization/plot_pixels.py +2 -4
  130. simtools/visualization/plot_psf.py +160 -19
  131. simtools/visualization/plot_simtel_event_histograms.py +4 -4
  132. simtools/visualization/plot_simtel_events.py +6 -11
  133. simtools/visualization/plot_tables.py +8 -19
  134. simtools/visualization/visualize.py +22 -2
  135. simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +0 -160
  136. simtools/applications/print_version.py +0 -53
  137. simtools/io/hdf5_handler.py +0 -139
  138. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/top_level.txt +0 -0
@@ -351,6 +351,7 @@ def _plot_component_angles(
351
351
  out_path,
352
352
  bin_width_deg,
353
353
  log,
354
+ model_version=None,
354
355
  ):
355
356
  arrays = _gather_angle_arrays(results_by_offset, column, log)
356
357
  if not arrays:
@@ -365,6 +366,17 @@ def _plot_component_angles(
365
366
  ax.set_title(f"Incident angle {title_suffix} vs off-axis angle")
366
367
  ax.grid(True, alpha=0.3)
367
368
  ax.legend()
369
+ if model_version:
370
+ ax.text(
371
+ 0.03,
372
+ 0.97,
373
+ f"Model version: {model_version}",
374
+ transform=ax.transAxes,
375
+ fontsize=8,
376
+ verticalalignment="top",
377
+ horizontalalignment="left",
378
+ bbox={"boxstyle": "round", "facecolor": "white", "alpha": 0.5},
379
+ )
368
380
  plt.tight_layout()
369
381
  plt.savefig(out_path, dpi=300)
370
382
  plt.close(fig)
@@ -378,8 +390,30 @@ def plot_incident_angles(
378
390
  radius_bin_width_m=0.01,
379
391
  debug_plots=False,
380
392
  logger=None,
393
+ model_version=None,
381
394
  ):
382
- """Plot overlaid histograms of focal, primary, secondary angles, and primary hit radius."""
395
+ """Plot overlaid histograms of focal, primary, secondary angles, and primary hit radius.
396
+
397
+ Parameters
398
+ ----------
399
+ results_by_offset : dict
400
+ Mapping from off-axis angle to result tables containing angle and radius columns.
401
+ output_dir : path-like
402
+ Base output directory where the ``plots`` subdirectory will be created.
403
+ label : str
404
+ Label used to distinguish this set of plots in the output filenames.
405
+ bin_width_deg : float, optional
406
+ Bin width in degrees for the angle-of-incidence histograms.
407
+ radius_bin_width_m : float, optional
408
+ Bin width in meters for the primary mirror hit-radius histograms.
409
+ debug_plots : bool, optional
410
+ If True, generate additional diagnostic plots.
411
+ logger : logging.Logger or None, optional
412
+ Logger instance to use for messages. If None, a module-level logger is used.
413
+ model_version : str or None, optional
414
+ Semantic model version identifier to annotate the generated plots. If None,
415
+ no model version text is added to the figures.
416
+ """
383
417
  log = logger or logging.getLogger(__name__)
384
418
  if not results_by_offset:
385
419
  log.warning("No results provided for multi-offset plot")
@@ -402,6 +436,17 @@ def plot_incident_angles(
402
436
  ax.set_title("Incident angle distribution vs off-axis angle")
403
437
  ax.grid(True, alpha=0.3)
404
438
  ax.legend()
439
+ if model_version:
440
+ ax.text(
441
+ 0.03,
442
+ 0.97,
443
+ f"Model version: {model_version}",
444
+ transform=ax.transAxes,
445
+ fontsize=8,
446
+ verticalalignment="top",
447
+ horizontalalignment="left",
448
+ bbox={"boxstyle": "round", "facecolor": "white", "alpha": 0.5},
449
+ )
405
450
  plt.tight_layout()
406
451
  plt.savefig(out_dir / f"incident_angles_multi_{label}.png", dpi=300)
407
452
  plt.close(fig)
@@ -414,6 +459,7 @@ def plot_incident_angles(
414
459
  out_path=out_dir / f"incident_angles_primary_multi_{label}.png",
415
460
  bin_width_deg=bin_width_deg,
416
461
  log=log,
462
+ model_version=model_version,
417
463
  )
418
464
  _plot_component_angles(
419
465
  results_by_offset=results_by_offset,
@@ -422,6 +468,7 @@ def plot_incident_angles(
422
468
  out_path=out_dir / f"incident_angles_secondary_multi_{label}.png",
423
469
  bin_width_deg=bin_width_deg,
424
470
  log=log,
471
+ model_version=model_version,
425
472
  )
426
473
 
427
474
  # Debug plots
@@ -63,7 +63,7 @@ def _detect_segmentation_type(data_file_path):
63
63
  return "standard"
64
64
 
65
65
 
66
- def plot(config, output_file, db_config=None):
66
+ def plot(config, output_file):
67
67
  """
68
68
  Plot mirror panel layout based on configuration.
69
69
 
@@ -79,8 +79,6 @@ def plot(config, output_file, db_config=None):
79
79
  - title: str, optional, plot title
80
80
  output_file : str or Path
81
81
  Path where to save the plot (without extension)
82
- db_config : dict, optional
83
- Database configuration dictionary
84
82
 
85
83
  Returns
86
84
  -------
@@ -91,7 +89,6 @@ def plot(config, output_file, db_config=None):
91
89
  site=config["site"],
92
90
  telescope_name=config["telescope"],
93
91
  model_version=config.get("model_version"),
94
- db_config=db_config,
95
92
  ignore_software_version=True,
96
93
  )
97
94
 
@@ -21,7 +21,7 @@ from simtools.visualization import visualize
21
21
  logger = logging.getLogger(__name__)
22
22
 
23
23
 
24
- def plot(config, output_file, db_config=None):
24
+ def plot(config, output_file):
25
25
  """
26
26
  Plot pixel layout based on configuration.
27
27
 
@@ -36,15 +36,13 @@ def plot(config, output_file, db_config=None):
36
36
  - telescope : str, name of the telescope
37
37
  output_file : str
38
38
  Path where to save the plot
39
- db_config : dict, optional
40
- Database configuration.
41
39
 
42
40
  Returns
43
41
  -------
44
42
  None
45
43
  The function saves the plot to the specified output file.
46
44
  """
47
- db = db_handler.DatabaseHandler(db_config=db_config)
45
+ db = db_handler.DatabaseHandler()
48
46
  db.export_model_file(
49
47
  parameter=config["parameter"],
50
48
  site=config["site"],
@@ -6,6 +6,7 @@ including parameter comparison plots, convergence plots, and PSF diameter vs off
6
6
  """
7
7
 
8
8
  import logging
9
+ from pathlib import Path
9
10
 
10
11
  import astropy.units as u
11
12
  import matplotlib.pyplot as plt
@@ -141,22 +142,27 @@ def _create_base_plot_figure(data_to_plot, simulated_data=None):
141
142
 
142
143
 
143
144
  def _build_parameter_title(pars, is_best):
144
- """Build parameter title string for plots."""
145
+ """Build parameter title string for plots, handling optional parameter groups."""
145
146
  title_prefix = "* " if is_best else ""
146
- return (
147
- f"{title_prefix}reflection = "
148
- f"{pars['mirror_reflection_random_angle'][0]:.5f}, "
149
- f"{pars['mirror_reflection_random_angle'][1]:.5f}, "
150
- f"{pars['mirror_reflection_random_angle'][2]:.5f}\n"
151
- f"align_vertical = {pars['mirror_align_random_vertical'][0]:.5f}, "
152
- f"{pars['mirror_align_random_vertical'][1]:.5f}, "
153
- f"{pars['mirror_align_random_vertical'][2]:.5f}, "
154
- f"{pars['mirror_align_random_vertical'][3]:.5f}\n"
155
- f"align_horizontal = {pars['mirror_align_random_horizontal'][0]:.5f}, "
156
- f"{pars['mirror_align_random_horizontal'][1]:.5f}, "
157
- f"{pars['mirror_align_random_horizontal'][2]:.5f}, "
158
- f"{pars['mirror_align_random_horizontal'][3]:.5f}"
159
- )
147
+ title_lines = []
148
+
149
+ if "mirror_reflection_random_angle" in pars:
150
+ refl = pars["mirror_reflection_random_angle"]
151
+ title_lines.append(f"reflection = {refl[0]:.5f}, {refl[1]:.5f}, {refl[2]:.5f}")
152
+
153
+ if "mirror_align_random_vertical" in pars:
154
+ vert = pars["mirror_align_random_vertical"]
155
+ title_lines.append(
156
+ f"align_vertical = {vert[0]:.5f}, {vert[1]:.5f}, {vert[2]:.5f}, {vert[3]:.5f}"
157
+ )
158
+
159
+ if "mirror_align_random_horizontal" in pars:
160
+ horiz = pars["mirror_align_random_horizontal"]
161
+ title_lines.append(
162
+ f"align_horizontal = {horiz[0]:.5f}, {horiz[1]:.5f}, {horiz[2]:.5f}, {horiz[3]:.5f}"
163
+ )
164
+
165
+ return title_prefix + "\n".join(title_lines)
160
166
 
161
167
 
162
168
  def _add_metric_text_box(ax, metrics_text, is_best):
@@ -640,7 +646,7 @@ def create_psf_vs_offaxis_plot(tel_model, site_model, args_dict, best_pars, outp
640
646
  logger.info(f"Creating {psf_label_cm} vs off-axis angle plot with best parameters...")
641
647
 
642
648
  # Apply best parameters to telescope model
643
- tel_model.overwrite_parameters(best_pars)
649
+ tel_model.overwrite_parameters(best_pars, flat_dict=True)
644
650
 
645
651
  # Create off-axis angle array
646
652
  max_offset = args_dict.get("max_offset", MAX_OFFSET_DEFAULT)
@@ -654,7 +660,7 @@ def create_psf_vs_offaxis_plot(tel_model, site_model, args_dict, best_pars, outp
654
660
  ray = RayTracing(
655
661
  telescope_model=tel_model,
656
662
  site_model=site_model,
657
- simtel_path=args_dict["simtel_path"],
663
+ label=args_dict.get("label") or getattr(tel_model, "label", None),
658
664
  zenith_angle=args_dict["zenith"] * u.deg,
659
665
  source_distance=args_dict["src_distance"] * u.km,
660
666
  off_axis_angle=off_axis_angles * u.deg,
@@ -662,9 +668,9 @@ def create_psf_vs_offaxis_plot(tel_model, site_model, args_dict, best_pars, outp
662
668
 
663
669
  logger.info(f"Running ray tracing for {len(off_axis_angles)} off-axis angles...")
664
670
  ray.simulate(test=args_dict.get("test", False), force=True)
665
- ray.analyze(force=True)
671
+ ray.analyze(force=True, containment_fraction=fraction)
666
672
 
667
- for key in ["d80_cm", "d80_deg"]:
673
+ for key in ["psf_cm", "psf_deg"]:
668
674
  plt.figure(figsize=(10, 6), tight_layout=True)
669
675
 
670
676
  ray.plot(key, marker="o", linestyle="-", color="blue", linewidth=2, markersize=6)
@@ -773,3 +779,138 @@ def create_optimization_plots(args_dict, gd_results, tel_model, data_to_plot, ou
773
779
  use_ks_statistic=False,
774
780
  )
775
781
  pdf_pages.close()
782
+
783
+
784
+ def create_summary_psf_comparison_plot(
785
+ tel_model, optimized_params, data_to_plot, output_dir, final_rmsd, simulated_data
786
+ ):
787
+ """
788
+ Create a standalone plot comparing measured vs simulated PSF with final optimized parameters.
789
+
790
+ This creates a single plot showing the cumulative PSF comparison
791
+ before and after the optimization.
792
+
793
+ Parameters
794
+ ----------
795
+ tel_model : TelescopeModel
796
+ Telescope model with optimized parameters
797
+ optimized_params : dict
798
+ Dictionary of optimized parameter values
799
+ data_to_plot : dict
800
+ Measured PSF data
801
+ output_dir : Path
802
+ Directory for output files
803
+ final_rmsd : float
804
+ Final RMSD value at the end of optimization
805
+ simulated_data : dict
806
+ Final simulated PSF data with optimized parameters
807
+
808
+ Returns
809
+ -------
810
+ Path
811
+ Path to the created plot file
812
+ """
813
+ fig, ax = _create_base_plot_figure(data_to_plot, simulated_data)
814
+
815
+ title_lines = ["Final Optimized Parameters:"]
816
+ if "mirror_reflection_random_angle" in optimized_params:
817
+ refl = optimized_params["mirror_reflection_random_angle"]
818
+ title_lines.append(
819
+ f"mirror_reflection_random_angle = [{refl[0]:.6f}, {refl[1]:.6f}, {refl[2]:.6f}]"
820
+ )
821
+
822
+ if "mirror_align_random_vertical" in optimized_params:
823
+ vert = optimized_params["mirror_align_random_vertical"]
824
+ title_lines.append(
825
+ f"mirror_align_random_vertical = "
826
+ f"[{vert[0]:.6f}, {vert[1]:.6f}, {vert[2]:.6f}, {vert[3]:.6f}]"
827
+ )
828
+
829
+ if "mirror_align_random_horizontal" in optimized_params:
830
+ horiz = optimized_params["mirror_align_random_horizontal"]
831
+ title_lines.append(
832
+ f"mirror_align_random_horizontal = "
833
+ f"[{horiz[0]:.6f}, {horiz[1]:.6f}, {horiz[2]:.6f}, {horiz[3]:.6f}]"
834
+ )
835
+
836
+ ax.set_title("\n".join(title_lines), fontsize=9, loc="left")
837
+
838
+ rmsd_text = f"RMSD = {final_rmsd:.4f} ({final_rmsd * 100:.2f}%)"
839
+ ax.text(
840
+ 0.98,
841
+ 0.02,
842
+ rmsd_text,
843
+ transform=ax.transAxes,
844
+ fontsize=12,
845
+ verticalalignment="bottom",
846
+ horizontalalignment="right",
847
+ bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.8},
848
+ )
849
+
850
+ output_file = Path(output_dir) / f"{tel_model.name}_final_psf_comparison.png"
851
+ fig.savefig(output_file, dpi=150, bbox_inches="tight")
852
+ plt.close(fig)
853
+
854
+ logger.info(f"Final PSF comparison plot saved to {output_file}")
855
+ return output_file
856
+
857
+
858
+ def plot_psf_histogram(measured, simulated, args_dict):
859
+ """Write histogram comparing measured vs simulated PSF diameter distributions."""
860
+ output_dir = Path(args_dict.get("output_path", "."))
861
+ out_name = args_dict.get("psf_hist")
862
+ if not out_name:
863
+ return None
864
+ out_path = Path(out_name)
865
+ if not out_path.is_absolute():
866
+ out_path = output_dir / out_path
867
+ out_path.parent.mkdir(parents=True, exist_ok=True)
868
+ measured = np.asarray(measured, dtype=float)
869
+ simulated = np.asarray(simulated, dtype=float)
870
+ measured = measured[np.isfinite(measured)]
871
+ simulated = simulated[np.isfinite(simulated)]
872
+ if measured.size == 0 or simulated.size == 0:
873
+ return None
874
+ bins = 25
875
+ all_vals = np.concatenate([measured, simulated])
876
+ x_min = float(np.nanmin(all_vals))
877
+ x_max = float(np.nanmax(all_vals))
878
+ if not np.isfinite(x_min) or not np.isfinite(x_max) or x_max <= x_min:
879
+ return None
880
+ bin_edges = np.linspace(x_min, x_max, bins + 1)
881
+ meas_mean = float(np.mean(measured))
882
+ meas_rms = float(np.std(measured, ddof=0))
883
+ sim_mean = float(np.mean(simulated))
884
+ sim_rms = float(np.std(simulated, ddof=0))
885
+ fig, ax = plt.subplots(figsize=(7.5, 4.5), constrained_layout=True)
886
+ ax.hist(
887
+ measured,
888
+ bins=bin_edges,
889
+ alpha=0.55,
890
+ color="tab:red",
891
+ edgecolor="white",
892
+ label=f"Measured (mean={meas_mean:.2f} mm, rms={meas_rms:.2f} mm)",
893
+ )
894
+ ax.hist(
895
+ simulated,
896
+ bins=bin_edges,
897
+ alpha=0.55,
898
+ color="tab:blue",
899
+ edgecolor="white",
900
+ label=f"Simulated (mean={sim_mean:.2f} mm, rms={sim_rms:.2f} mm)",
901
+ )
902
+ ax.axvline(meas_mean, color="tab:red", linestyle="--", linewidth=1)
903
+ ax.axvline(sim_mean, color="tab:blue", linestyle="--", linewidth=1)
904
+ tel = args_dict.get("telescope", "")
905
+ model_version = args_dict.get("model_version", "")
906
+ fraction = args_dict.get("fraction")
907
+ label = get_psf_diameter_label(fraction, unit="mm") if fraction is not None else "PSF"
908
+ suffix = " ".join([s for s in (tel, model_version) if s])
909
+ ax.set_xlabel(label)
910
+ ax.set_ylabel("Count")
911
+ ax.set_title(f"{label} ({suffix})" if suffix else label)
912
+ ax.legend(loc="best", fontsize=9, frameon=True)
913
+ fig.savefig(out_path)
914
+ plt.close(fig)
915
+ logger.info("PSF histogram written to %s", str(out_path))
916
+ return str(out_path)
@@ -1,4 +1,4 @@
1
- """Plot simtel event histograms filled with SimtelIOEventHistograms."""
1
+ """Plot simtel event histograms filled with EventDataHistograms."""
2
2
 
3
3
  import logging
4
4
 
@@ -6,7 +6,7 @@ import matplotlib.pyplot as plt
6
6
  import numpy as np
7
7
  from matplotlib.colors import LogNorm
8
8
 
9
- from simtools.simtel.simtel_io_event_histograms import SimtelIOEventHistograms
9
+ from simtools.sim_events.histograms import EventDataHistograms
10
10
 
11
11
  _logger = logging.getLogger(__name__)
12
12
 
@@ -17,7 +17,7 @@ def plot(histograms, output_path=None, limits=None, rebin_factor=2, array_name=N
17
17
 
18
18
  Parameters
19
19
  ----------
20
- histograms: SimtelIOEventHistograms
20
+ histograms: EventDataHistograms
21
21
  Instance containing the histograms to plot.
22
22
  output_path: Path or str, optional
23
23
  Directory to save plots. If None, plots will be displayed.
@@ -230,7 +230,7 @@ def _create_rebinned_plot(plot_args, filename, output_path, rebin_factor):
230
230
  data = plot_args["data"]
231
231
  bins = plot_args["bins"]
232
232
 
233
- rebinned_data, rebinned_x_bins, rebinned_y_bins = SimtelIOEventHistograms.rebin_2d_histogram(
233
+ rebinned_data, rebinned_x_bins, rebinned_y_bins = EventDataHistograms.rebin_2d_histogram(
234
234
  data, bins[0], bins[1], rebin_factor
235
235
  )
236
236
 
@@ -13,8 +13,7 @@ from ctapipe.visualization import CameraDisplay
13
13
  from scipy import signal as _signal
14
14
 
15
15
  from simtools.data_model.metadata_collector import MetadataCollector
16
- from simtools.visualization.plot_corsika_histograms import save_figs_to_pdf
17
- from simtools.visualization.visualize import save_figure
16
+ from simtools.visualization.visualize import save_figure, save_figures_to_single_document
18
17
 
19
18
  _logger = logging.getLogger(__name__)
20
19
 
@@ -665,24 +664,21 @@ def plot_simtel_peak_timing(
665
664
 
666
665
  fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4), dpi=300)
667
666
 
668
- edges = _histogram_edges(n_samp, timing_bins)
669
- et_name = getattr(getattr(event.trigger, "event_type", None), "name", "?")
670
667
  tel = source.subarray.tel[tel_id]
671
668
  tel_label = getattr(tel, "name", f"CT{tel_id}")
672
669
  _draw_peak_hist(
673
670
  ax1,
674
671
  peak_samples,
675
- edges,
672
+ _histogram_edges(n_samp, timing_bins),
676
673
  mean_sample,
677
674
  std_sample,
678
675
  tel_label,
679
- et_name,
676
+ getattr(getattr(event.trigger, "event_type", None), "name", "?"),
680
677
  pix_ids.size,
681
678
  found_count,
682
679
  )
683
680
 
684
- readout = source.subarray.tel[tel_id].camera.readout
685
- t = _time_axis_from_readout(readout, n_samp)
681
+ t = _time_axis_from_readout(source.subarray.tel[tel_id].camera.readout, n_samp)
686
682
 
687
683
  ex_ids = pix_ids[: max(1, int(examples))]
688
684
  for pid in ex_ids:
@@ -698,13 +694,12 @@ def plot_simtel_peak_timing(
698
694
  fig.tight_layout()
699
695
 
700
696
  if return_stats:
701
- stats = {
697
+ return fig, {
702
698
  "considered": int(pix_ids.size),
703
699
  "found": int(found_count),
704
700
  "mean": float(mean_sample),
705
701
  "std": float(std_sample),
706
702
  }
707
- return fig, stats
708
703
  return fig
709
704
 
710
705
 
@@ -991,7 +986,7 @@ def generate_and_save_plots(
991
986
  continue
992
987
 
993
988
  try:
994
- save_figs_to_pdf(figures, pdf_path)
989
+ save_figures_to_single_document(figures, pdf_path)
995
990
  _logger.info("Saved PDF: %s", pdf_path)
996
991
  except Exception as ex: # pylint:disable=broad-except
997
992
  _logger.error("Failed to save PDF %s: %s", pdf_path, ex)
@@ -16,7 +16,7 @@ from simtools.visualization import visualize
16
16
  _logger = logging.getLogger(__name__)
17
17
 
18
18
 
19
- def plot(config, output_file, db_config=None, data_path=None):
19
+ def plot(config, output_file, data_path=None):
20
20
  """
21
21
  Plot tabular data from data or from model parameter files.
22
22
 
@@ -26,12 +26,10 @@ def plot(config, output_file, db_config=None, data_path=None):
26
26
  Configuration dictionary for plotting.
27
27
  output_file: str
28
28
  Output file.
29
- db_config: dict, optional
30
- Database configuration dictionary for accessing the model parameter database.
31
29
  data_path: Path or str, optional
32
30
  Path to the data files (optional). Expect all files to be in the same directory.
33
31
  """
34
- data = read_table_data(config, db_config, data_path)
32
+ data = read_table_data(config, data_path)
35
33
 
36
34
  fig = visualize.plot_1d(
37
35
  data,
@@ -42,7 +40,7 @@ def plot(config, output_file, db_config=None, data_path=None):
42
40
  return output_file
43
41
 
44
42
 
45
- def read_table_data(config, db_config, data_path=None):
43
+ def read_table_data(config, data_path=None):
46
44
  """
47
45
  Read table data from file or parameter database.
48
46
 
@@ -50,8 +48,6 @@ def read_table_data(config, db_config, data_path=None):
50
48
  ----------
51
49
  config: dict
52
50
  Configuration dictionary for plotting.
53
- db_config: dict
54
- Database configuration dictionary for accessing the model parameter database.
55
51
  data_path: Path or str, optional
56
52
  Path to the data files (optional). Expect all files to be in the same directory.
57
53
 
@@ -64,7 +60,7 @@ def read_table_data(config, db_config, data_path=None):
64
60
 
65
61
  for _config in config["tables"]:
66
62
  if "parameter" in _config:
67
- table = _read_table_from_model_database(_config, db_config)
63
+ table = _read_table_from_model_database(_config)
68
64
  elif "file_name" in _config:
69
65
  file_name = (
70
66
  _config["file_name"]
@@ -117,7 +113,7 @@ def _process_table_data(table, _config):
117
113
  )
118
114
 
119
115
 
120
- def _read_table_from_model_database(table_config, db_config):
116
+ def _read_table_from_model_database(table_config):
121
117
  """
122
118
  Read table data from model parameter database.
123
119
 
@@ -131,7 +127,7 @@ def _read_table_from_model_database(table_config, db_config):
131
127
  Table
132
128
  Astropy table
133
129
  """
134
- db = db_handler.DatabaseHandler(db_config=db_config)
130
+ db = db_handler.DatabaseHandler()
135
131
  return db.export_model_file(
136
132
  parameter=table_config["parameter"],
137
133
  site=table_config["site"],
@@ -170,15 +166,11 @@ def _validate_config_columns(config, valid_columns, logger):
170
166
 
171
167
  def _get_valid_columns(table):
172
168
  """Return columns that exist and have valid data (not all NaN)."""
173
- valid_columns = []
174
- for col in table.colnames:
175
- if not all(np.isnan(table[col])):
176
- valid_columns.append(col)
177
- return valid_columns
169
+ return [col for col in table.colnames if not all(np.isnan(table[col]))]
178
170
 
179
171
 
180
172
  def generate_plot_configurations(
181
- parameter, parameter_version, site, telescope, output_path, plot_type, db_config
173
+ parameter, parameter_version, site, telescope, output_path, plot_type
182
174
  ):
183
175
  """
184
176
  Generate plot configurations for a model parameter from schema files.
@@ -197,8 +189,6 @@ def generate_plot_configurations(
197
189
  Output path for the plots.
198
190
  plot_type: str
199
191
  Plot type or "all" for all plots.
200
- db_config: dict
201
- Database configuration.
202
192
 
203
193
  Returns
204
194
  -------
@@ -226,7 +216,6 @@ def generate_plot_configurations(
226
216
  "telescope": telescope,
227
217
  "parameter_version": parameter_version,
228
218
  },
229
- db_config=db_config,
230
219
  )
231
220
  valid_columns = _get_valid_columns(table)
232
221
 
@@ -11,6 +11,7 @@ import matplotlib.pyplot as plt
11
11
  from astropy.table import QTable
12
12
  from cycler import cycler
13
13
  from matplotlib import gridspec
14
+ from matplotlib.backends.backend_pdf import PdfPages
14
15
 
15
16
  COLORS = {}
16
17
  COLORS["classic"] = [
@@ -615,7 +616,7 @@ def plot_hist_2d(data, **kwargs):
615
616
  return fig
616
617
 
617
618
 
618
- def save_figure(fig, output_file, figure_format=None, log_title="", dpi="figure"):
619
+ def save_figure(fig, output_file, figure_format=("pdf", "png"), log_title="", dpi="figure"):
619
620
  """
620
621
  Save figure to output file(s).
621
622
 
@@ -630,10 +631,29 @@ def save_figure(fig, output_file, figure_format=None, log_title="", dpi="figure"
630
631
  title: str
631
632
  Title of the figure to be added to the log message.
632
633
  """
633
- figure_format = figure_format or ["pdf", "png"]
634
634
  for fmt in figure_format:
635
635
  _file = Path(output_file).with_suffix(f".{fmt}")
636
636
  fig.savefig(_file, format=fmt, bbox_inches="tight", dpi=dpi)
637
637
  logging.info(f"Saved plot {log_title} to {_file}")
638
638
 
639
639
  fig.clf()
640
+
641
+
642
+ def save_figures_to_single_document(figs, output_file_name):
643
+ """
644
+ Save multiple figures to a single PDF document.
645
+
646
+ Parameters
647
+ ----------
648
+ figs: list
649
+ List of plt.figure instances to save.
650
+ output_file_name: Path, str
651
+ PDF file name
652
+ """
653
+ _logger.info(f"Saving {len(figs)} figures to {output_file_name}")
654
+ pdf_pages = PdfPages(Path(output_file_name).absolute().as_posix())
655
+ for fig in figs:
656
+ plt.tight_layout()
657
+ pdf_pages.savefig(fig)
658
+
659
+ pdf_pages.close()