gammasimtools 0.23.0__py3-none-any.whl → 0.24.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.23.0.dist-info → gammasimtools-0.24.0.dist-info}/METADATA +1 -1
  2. {gammasimtools-0.23.0.dist-info → gammasimtools-0.24.0.dist-info}/RECORD +59 -58
  3. simtools/_version.py +2 -2
  4. simtools/application_control.py +4 -4
  5. simtools/applications/convert_geo_coordinates_of_array_elements.py +1 -1
  6. simtools/applications/db_add_file_to_db.py +2 -2
  7. simtools/applications/db_add_simulation_model_from_repository_to_db.py +1 -1
  8. simtools/applications/db_add_value_from_json_to_db.py +2 -2
  9. simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +1 -1
  10. simtools/applications/db_generate_compound_indexes.py +1 -1
  11. simtools/applications/db_get_array_layouts_from_db.py +2 -2
  12. simtools/applications/db_get_file_from_db.py +1 -1
  13. simtools/applications/db_get_parameter_from_db.py +1 -1
  14. simtools/applications/db_inspect_databases.py +4 -2
  15. simtools/applications/db_upload_model_repository.py +1 -1
  16. simtools/applications/derive_ctao_array_layouts.py +1 -1
  17. simtools/applications/generate_array_config.py +1 -1
  18. simtools/applications/maintain_simulation_model_add_production.py +11 -21
  19. simtools/applications/production_generate_grid.py +1 -1
  20. simtools/applications/submit_array_layouts.py +2 -2
  21. simtools/applications/validate_camera_fov.py +1 -1
  22. simtools/applications/validate_cumulative_psf.py +2 -2
  23. simtools/applications/validate_optics.py +1 -1
  24. simtools/configuration/commandline_parser.py +7 -9
  25. simtools/configuration/configurator.py +1 -1
  26. simtools/corsika/corsika_config.py +2 -4
  27. simtools/data_model/model_data_writer.py +1 -1
  28. simtools/data_model/schema.py +36 -34
  29. simtools/db/db_handler.py +61 -294
  30. simtools/db/db_model_upload.py +1 -1
  31. simtools/db/mongo_db.py +535 -0
  32. simtools/dependencies.py +33 -8
  33. simtools/layout/array_layout.py +7 -7
  34. simtools/layout/array_layout_utils.py +3 -3
  35. simtools/model/array_model.py +36 -67
  36. simtools/model/calibration_model.py +12 -9
  37. simtools/model/model_parameter.py +196 -159
  38. simtools/model/model_repository.py +159 -35
  39. simtools/model/model_utils.py +3 -3
  40. simtools/model/site_model.py +59 -27
  41. simtools/model/telescope_model.py +21 -13
  42. simtools/ray_tracing/mirror_panel_psf.py +4 -4
  43. simtools/ray_tracing/psf_parameter_optimisation.py +1 -1
  44. simtools/reporting/docs_auto_report_generator.py +1 -1
  45. simtools/reporting/docs_read_parameters.py +3 -2
  46. simtools/schemas/simulation_models_info.schema.yml +2 -1
  47. simtools/simtel/simtel_config_writer.py +97 -20
  48. simtools/simulator.py +2 -1
  49. simtools/testing/assertions.py +50 -6
  50. simtools/testing/validate_output.py +4 -8
  51. simtools/utils/value_conversion.py +10 -5
  52. simtools/version.py +24 -0
  53. simtools/visualization/plot_pixels.py +1 -1
  54. simtools/visualization/plot_psf.py +1 -1
  55. simtools/visualization/plot_tables.py +1 -1
  56. {gammasimtools-0.23.0.dist-info → gammasimtools-0.24.0.dist-info}/WHEEL +0 -0
  57. {gammasimtools-0.23.0.dist-info → gammasimtools-0.24.0.dist-info}/entry_points.txt +0 -0
  58. {gammasimtools-0.23.0.dist-info → gammasimtools-0.24.0.dist-info}/licenses/LICENSE +0 -0
  59. {gammasimtools-0.23.0.dist-info → gammasimtools-0.24.0.dist-info}/top_level.txt +0 -0
@@ -528,7 +528,7 @@ class SimtelConfigWriter:
528
528
  """
529
529
  file.write(self.TAB + "% Site parameters\n")
530
530
  for par, value in site_parameters.items():
531
- simtel_name, value = self._convert_model_parameters_to_simtel_format(
531
+ simtel_name, simtel_value = self._convert_model_parameters_to_simtel_format(
532
532
  names.get_simulation_software_name_from_parameter_name(
533
533
  par, software_name="sim_telarray"
534
534
  ),
@@ -537,7 +537,7 @@ class SimtelConfigWriter:
537
537
  telescope_model,
538
538
  )
539
539
  if simtel_name is not None:
540
- file.write(f"{self.TAB}{simtel_name} = {value}\n")
540
+ file.write(f"{self.TAB}{simtel_name} = {simtel_value}\n")
541
541
  for meta in self._get_sim_telarray_metadata(
542
542
  "site", site_parameters, None, additional_metadata
543
543
  ):
@@ -592,34 +592,111 @@ class SimtelConfigWriter:
592
592
  telescope_model: dict of TelescopeModel
593
593
  Telescope models.
594
594
  """
595
+ trigger_per_telescope_type = self._group_telescopes_by_type(telescope_model)
596
+ hardstereo_lines, non_hardstereo_groups, all_non_hardstereo_tels, multiplicity = (
597
+ self._process_telescope_triggers(array_triggers, trigger_per_telescope_type)
598
+ )
599
+
600
+ array_triggers_file = "array_triggers.dat"
601
+ with open(model_path / array_triggers_file, "w", encoding="utf-8") as file:
602
+ file.write("# Array trigger definition\n")
603
+ self._write_trigger_lines(
604
+ file, hardstereo_lines, non_hardstereo_groups, all_non_hardstereo_tels, multiplicity
605
+ )
606
+
607
+ return array_triggers_file
608
+
609
+ def _group_telescopes_by_type(self, telescope_model):
610
+ """Group telescopes by their type."""
595
611
  trigger_per_telescope_type = {}
596
612
  for count, tel_name in enumerate(telescope_model.keys()):
597
613
  telescope_type = names.get_array_element_type_from_name(tel_name)
598
614
  trigger_per_telescope_type.setdefault(telescope_type, []).append(count + 1)
615
+ return trigger_per_telescope_type
616
+
617
+ def _process_telescope_triggers(self, array_triggers, trigger_per_telescope_type):
618
+ """Process telescope triggers and group them by hardstereo and parameters."""
619
+ hardstereo_lines = []
620
+ non_hardstereo_groups = {}
621
+ all_non_hardstereo_tels = []
622
+ multiplicity = None
599
623
 
600
- trigger_lines = {}
601
624
  for tel_type, tel_list in trigger_per_telescope_type.items():
602
625
  trigger_dict = self._get_array_triggers_for_telescope_type(
603
626
  array_triggers, tel_type, len(tel_list)
604
627
  )
605
- trigger_lines[tel_type] = f"Trigger {trigger_dict['multiplicity']['value']} of "
606
- trigger_lines[tel_type] += ", ".join(map(str, tel_list))
607
- width = trigger_dict["width"]["value"] * u.Unit(trigger_dict["width"]["unit"]).to("ns")
608
- trigger_lines[tel_type] += f" width {width}"
609
- if trigger_dict.get("hard_stereo", {}).get("value"):
610
- trigger_lines[tel_type] += " hardstereo"
611
- if all(trigger_dict["min_separation"][key] is not None for key in ["value", "unit"]):
612
- min_sep = trigger_dict["min_separation"]["value"] * u.Unit(
613
- trigger_dict["min_separation"]["unit"]
614
- ).to("m")
615
- trigger_lines[tel_type] += f" minsep {min_sep}"
616
-
617
- array_triggers_file = "array_triggers.dat"
618
- with open(model_path / array_triggers_file, "w", encoding="utf-8") as file:
619
- file.write("# Array trigger definition\n")
620
- file.writelines(f"{line}\n" for line in trigger_lines.values())
628
+ width, minsep = self._extract_trigger_parameters(trigger_dict)
629
+ multiplicity = trigger_dict["multiplicity"]["value"] # Store for later use
621
630
 
622
- return array_triggers_file
631
+ if trigger_dict.get("hard_stereo", {}).get("value"):
632
+ line = self._build_trigger_line(
633
+ trigger_dict, tel_list, width, minsep, hardstereo=True
634
+ )
635
+ hardstereo_lines.append(line)
636
+ else:
637
+ key = (width, minsep)
638
+ non_hardstereo_groups.setdefault(key, []).extend(tel_list)
639
+ all_non_hardstereo_tels.extend(tel_list)
640
+
641
+ return hardstereo_lines, non_hardstereo_groups, all_non_hardstereo_tels, multiplicity
642
+
643
+ def _extract_trigger_parameters(self, trigger_dict):
644
+ """Extract width and min_separation parameters from trigger dictionary."""
645
+ width = trigger_dict["width"]["value"] * u.Unit(trigger_dict["width"]["unit"]).to("ns")
646
+ minsep = None
647
+ if all(trigger_dict["min_separation"][key] is not None for key in ["value", "unit"]):
648
+ minsep = trigger_dict["min_separation"]["value"] * u.Unit(
649
+ trigger_dict["min_separation"]["unit"]
650
+ ).to("m")
651
+ return width, minsep
652
+
653
+ def _build_trigger_line(self, trigger_dict, tel_list, width, minsep, hardstereo=False):
654
+ """Build a trigger line string."""
655
+ line = f"Trigger {trigger_dict['multiplicity']['value']} of "
656
+ line += ", ".join(map(str, tel_list))
657
+ line += f" width {width}"
658
+ if hardstereo:
659
+ line += " hardstereo"
660
+ if minsep is not None:
661
+ line += f" minsep {minsep}"
662
+ return line
663
+
664
+ def _write_trigger_lines(
665
+ self, file, hardstereo_lines, non_hardstereo_groups, all_non_hardstereo_tels, multiplicity
666
+ ):
667
+ """Write all trigger lines to file."""
668
+ # Write hardstereo lines first
669
+ for line in hardstereo_lines:
670
+ file.write(f"{line}\n")
671
+
672
+ # Write individual non-hardstereo groups if they have different parameters
673
+ if len(non_hardstereo_groups) > 1:
674
+ for (width, minsep), tel_list in non_hardstereo_groups.items():
675
+ line = f"Trigger {multiplicity} of "
676
+ line += ", ".join(map(str, tel_list))
677
+ line += f" width {width}"
678
+ if minsep is not None:
679
+ line += f" minsep {minsep}"
680
+ file.write(f"{line}\n")
681
+
682
+ # Write combined line with all non-hardstereo telescopes using shortest values
683
+ if all_non_hardstereo_tels:
684
+ min_width = min(width for width, minsep in non_hardstereo_groups.keys())
685
+ min_minsep = self._get_minimum_minsep(non_hardstereo_groups)
686
+
687
+ combined_line = f"Trigger {multiplicity} of "
688
+ combined_line += ", ".join(map(str, sorted(all_non_hardstereo_tels)))
689
+ combined_line += f" width {min_width}"
690
+ if min_minsep is not None:
691
+ combined_line += f" minsep {min_minsep}"
692
+ file.write(f"{combined_line}\n")
693
+
694
+ def _get_minimum_minsep(self, non_hardstereo_groups):
695
+ """Get minimum min_separation value from groups."""
696
+ minsep_values = [
697
+ minsep for width, minsep in non_hardstereo_groups.keys() if minsep is not None
698
+ ]
699
+ return min(minsep_values) if minsep_values else None
623
700
 
624
701
  def _get_array_triggers_for_telescope_type(
625
702
  self, array_triggers, telescope_type, num_telescopes_of_type
simtools/simulator.py CHANGED
@@ -125,7 +125,7 @@ class Simulator:
125
125
  label=self.label,
126
126
  site=self.args_dict.get("site"),
127
127
  layout_name=self.args_dict.get("array_layout_name"),
128
- mongo_db_config=self.db_config,
128
+ db_config=self.db_config,
129
129
  model_version=version,
130
130
  sim_telarray_seeds={
131
131
  "seed": self._get_seed_for_random_instrument_instances(
@@ -140,6 +140,7 @@ class Simulator:
140
140
  calibration_device_types=self._get_calibration_device_types(
141
141
  self.args_dict.get("run_mode")
142
142
  ),
143
+ overwrite_model_parameters=self.args_dict.get("overwrite_model_parameters", None),
143
144
  )
144
145
  for version in versions
145
146
  ]
@@ -8,6 +8,8 @@ from pathlib import Path
8
8
  import numpy as np
9
9
  import yaml
10
10
 
11
+ from simtools.simtel.simtel_io_metadata import read_sim_telarray_metadata
12
+
11
13
  _logger = logging.getLogger(__name__)
12
14
 
13
15
 
@@ -124,7 +126,35 @@ def assert_expected_output(file, expected_output):
124
126
  return True
125
127
 
126
128
 
127
- def check_output_from_sim_telarray(file, expected_output):
129
+ def assert_expected_simtel_metadata(file, expected_metadata):
130
+ """
131
+ Assert that expected metadata is present in the sim_telarray file.
132
+
133
+ Parameters
134
+ ----------
135
+ file: Path
136
+ Path to the sim_telarray file.
137
+ expected_metadata: dict
138
+ Expected metadata values.
139
+
140
+ """
141
+ global_meta, telescope_meta = read_sim_telarray_metadata(file)
142
+
143
+ for key, value in expected_metadata.items():
144
+ if key not in global_meta and key not in telescope_meta:
145
+ _logger.error(f"Metadata key {key} not found in sim_telarray file {file}")
146
+ return False
147
+ if key in global_meta and global_meta[key] != value:
148
+ _logger.error(
149
+ f"Metadata key {key} has value {global_meta[key]} instead of expected {value}"
150
+ )
151
+ return False
152
+ _logger.debug(f"Metadata key {key} matches expected value {value}")
153
+
154
+ return True
155
+
156
+
157
+ def check_output_from_sim_telarray(file, file_test):
128
158
  """
129
159
  Check that the sim_telarray simulation result is reasonable and matches the expected output.
130
160
 
@@ -132,20 +162,34 @@ def check_output_from_sim_telarray(file, expected_output):
132
162
  ----------
133
163
  file: Path
134
164
  Path to the sim_telarray file.
135
- expected_output: dict
136
- Expected output values.
165
+ file_test: dict
166
+ File test description including expected output and metadata.
137
167
 
138
168
  Raises
139
169
  ------
140
170
  ValueError
141
171
  If the file is not a zstd compressed file.
142
172
  """
173
+ if "expected_output" not in file_test and "expected_simtel_metadata" not in file_test:
174
+ _logger.debug(f"No expected output or metadata provided, skipping checks {file_test}")
175
+ return True
176
+
143
177
  if file.suffix != ".zst":
144
178
  raise ValueError(
145
179
  f"Expected output file {file} is not a zstd compressed file "
146
180
  f"(i.e., a sim_telarray file)."
147
181
  )
148
182
 
149
- return assert_n_showers_and_energy_range(file=file) and assert_expected_output(
150
- file=file, expected_output=expected_output
151
- )
183
+ assert_output = assert_metadata = True
184
+
185
+ if "expected_output" in file_test:
186
+ assert_output = assert_expected_output(
187
+ file=file, expected_output=file_test["expected_output"]
188
+ )
189
+
190
+ if "expected_simtel_metadata" in file_test:
191
+ assert_metadata = assert_expected_simtel_metadata(
192
+ file=file, expected_metadata=file_test["expected_simtel_metadata"]
193
+ )
194
+
195
+ return assert_n_showers_and_energy_range(file=file) and assert_output and assert_metadata
@@ -117,11 +117,7 @@ def _validate_output_path_and_file(config, integration_file_tests):
117
117
  except AssertionError as exc:
118
118
  raise AssertionError(f"Output file {output_file_path} does not exist. ") from exc
119
119
 
120
- if "expected_output" in file_test:
121
- assert assertions.check_output_from_sim_telarray(
122
- output_file_path,
123
- file_test["expected_output"],
124
- )
120
+ assert assertions.check_output_from_sim_telarray(output_file_path, file_test)
125
121
 
126
122
 
127
123
  def _validate_model_parameter_json_file(config, model_parameter_validation, db_config):
@@ -139,7 +135,7 @@ def _validate_model_parameter_json_file(config, model_parameter_validation, db_c
139
135
 
140
136
  """
141
137
  _logger.info(f"Checking model parameter json file: {model_parameter_validation}")
142
- db = db_handler.DatabaseHandler(mongo_db_config=db_config)
138
+ db = db_handler.DatabaseHandler(db_config=db_config)
143
139
 
144
140
  reference_parameter_name = model_parameter_validation.get("reference_parameter_name")
145
141
 
@@ -346,7 +342,7 @@ def _validate_simtel_cfg_files(config, simtel_cfg_file):
346
342
  f"Comparing simtel cfg files: {reference_file} and {test_file} "
347
343
  f"for model version {config['configuration']['model_version']}"
348
344
  )
349
- return _compare_simtel_cfg_files(reference_file, test_file)
345
+ assert _compare_simtel_cfg_files(reference_file, test_file)
350
346
 
351
347
 
352
348
  def _compare_simtel_cfg_files(reference_file, test_file):
@@ -382,7 +378,7 @@ def _compare_simtel_cfg_files(reference_file, test_file):
382
378
  return False
383
379
 
384
380
  for ref_line, test_line in zip(reference_cfg, test_cfg):
385
- if any(ignore in ref_line for ignore in ("config_release", "Label")):
381
+ if any(ignore in ref_line for ignore in ("config_release", "Label", "simtools_version")):
386
382
  continue
387
383
  if ref_line != test_line:
388
384
  _logger.error(
@@ -170,16 +170,21 @@ def get_value_as_quantity(value, unit):
170
170
 
171
171
  Raises
172
172
  ------
173
- u.UnitConversionError
173
+ ValueError
174
174
  If the value cannot be converted to the given unit.
175
175
  """
176
176
  if isinstance(value, u.Quantity):
177
177
  try:
178
178
  return value.to(unit)
179
- except u.UnitConversionError:
180
- _logger.error(f"Cannot convert {value.unit} to {unit}.")
181
- raise
182
- return value * unit
179
+ except u.UnitConversionError as exc:
180
+ raise ValueError(f"Cannot convert {value} with unit {value.unit} to {unit}.") from exc
181
+ elif not isinstance(value, int | float):
182
+ return value
183
+
184
+ if unit is None or unit == "null":
185
+ return value * u.dimensionless_unscaled
186
+
187
+ return value * u.Unit(unit)
183
188
 
184
189
 
185
190
  def _unit_as_string(unit):
simtools/version.py CHANGED
@@ -4,6 +4,7 @@
4
4
  # which is adapted from https://github.com/astropy/astropy/blob/master/astropy/version.py
5
5
  # see https://github.com/astropy/astropy/pull/10774 for a discussion on why this needed.
6
6
 
7
+ from packaging.specifiers import SpecifierSet
7
8
  from packaging.version import InvalidVersion, Version
8
9
 
9
10
  MAJOR_MINOR_PATCH = "major.minor.patch"
@@ -189,3 +190,26 @@ def compare_versions(version_string_1, version_string_2, level=MAJOR_MINOR_PATCH
189
190
  raise ValueError(f"Unknown level: {level}")
190
191
 
191
192
  return (ver1 > ver2) - (ver1 < ver2)
193
+
194
+
195
+ def check_version_constraint(version_string, constraint):
196
+ """
197
+ Check if a version satisfies a constraint.
198
+
199
+ Parameters
200
+ ----------
201
+ version_string : str
202
+ The version string to check (e.g., "6.0.2").
203
+ constraint : str
204
+ The version constraint to check against (e.g., ">=6.0.0").
205
+
206
+ Returns
207
+ -------
208
+ bool
209
+ True if the version satisfies the constraint, False otherwise.
210
+ """
211
+ spec = SpecifierSet(constraint.strip(), prereleases=True)
212
+ ver = Version(version_string)
213
+ if ver in spec:
214
+ return True
215
+ return False
@@ -44,7 +44,7 @@ def plot(config, output_file, db_config=None):
44
44
  None
45
45
  The function saves the plot to the specified output file.
46
46
  """
47
- db = db_handler.DatabaseHandler(mongo_db_config=db_config)
47
+ db = db_handler.DatabaseHandler(db_config=db_config)
48
48
  db.export_model_file(
49
49
  parameter=config["parameter"],
50
50
  site=config["site"],
@@ -640,7 +640,7 @@ def create_psf_vs_offaxis_plot(tel_model, site_model, args_dict, best_pars, outp
640
640
  logger.info(f"Creating {psf_label_cm} vs off-axis angle plot with best parameters...")
641
641
 
642
642
  # Apply best parameters to telescope model
643
- tel_model.change_multiple_parameters(**best_pars)
643
+ tel_model.overwrite_parameters(best_pars)
644
644
 
645
645
  # Create off-axis angle array
646
646
  max_offset = args_dict.get("max_offset", MAX_OFFSET_DEFAULT)
@@ -131,7 +131,7 @@ def _read_table_from_model_database(table_config, db_config):
131
131
  Table
132
132
  Astropy table
133
133
  """
134
- db = db_handler.DatabaseHandler(mongo_db_config=db_config)
134
+ db = db_handler.DatabaseHandler(db_config=db_config)
135
135
  return db.export_model_file(
136
136
  parameter=table_config["parameter"],
137
137
  site=table_config["site"],