gammasimtools 0.10.0__py3-none-any.whl → 0.11.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 (84) hide show
  1. {gammasimtools-0.10.0.dist-info → gammasimtools-0.11.0.dist-info}/METADATA +3 -1
  2. {gammasimtools-0.10.0.dist-info → gammasimtools-0.11.0.dist-info}/RECORD +84 -77
  3. {gammasimtools-0.10.0.dist-info → gammasimtools-0.11.0.dist-info}/WHEEL +1 -1
  4. {gammasimtools-0.10.0.dist-info → gammasimtools-0.11.0.dist-info}/entry_points.txt +4 -0
  5. simtools/_version.py +9 -4
  6. simtools/applications/convert_all_model_parameters_from_simtel.py +0 -1
  7. simtools/applications/convert_model_parameter_from_simtel.py +0 -1
  8. simtools/applications/db_add_file_to_db.py +0 -1
  9. simtools/applications/db_get_parameter_from_db.py +7 -28
  10. simtools/applications/derive_mirror_rnda.py +1 -2
  11. simtools/applications/derive_psf_parameters.py +1 -0
  12. simtools/applications/docs_produce_array_element_report.py +71 -0
  13. simtools/applications/docs_produce_model_parameter_reports.py +63 -0
  14. simtools/applications/generate_corsika_histograms.py +2 -2
  15. simtools/applications/generate_regular_arrays.py +4 -2
  16. simtools/applications/production_derive_limits.py +95 -0
  17. simtools/applications/production_generate_simulation_config.py +15 -29
  18. simtools/applications/production_scale_events.py +2 -7
  19. simtools/applications/run_application.py +165 -0
  20. simtools/applications/simulate_light_emission.py +0 -4
  21. simtools/applications/submit_model_parameter_from_external.py +11 -6
  22. simtools/applications/validate_file_using_schema.py +3 -3
  23. simtools/configuration/commandline_parser.py +29 -0
  24. simtools/configuration/configurator.py +8 -10
  25. simtools/corsika/corsika_config.py +11 -10
  26. simtools/corsika/corsika_histograms.py +4 -6
  27. simtools/corsika/corsika_histograms_visualize.py +2 -4
  28. simtools/data_model/metadata_collector.py +18 -9
  29. simtools/data_model/model_data_writer.py +67 -15
  30. simtools/data_model/schema.py +10 -3
  31. simtools/data_model/validate_data.py +70 -24
  32. simtools/db/db_handler.py +42 -12
  33. simtools/dependencies.py +112 -0
  34. simtools/layout/array_layout.py +5 -4
  35. simtools/model/model_parameter.py +35 -2
  36. simtools/production_configuration/calculate_statistical_errors_grid_point.py +5 -6
  37. simtools/production_configuration/event_scaler.py +3 -19
  38. simtools/production_configuration/generate_simulation_config.py +4 -12
  39. simtools/production_configuration/interpolation_handler.py +2 -5
  40. simtools/production_configuration/limits_calculation.py +202 -0
  41. simtools/reporting/docs_read_parameters.py +310 -0
  42. simtools/runners/corsika_simtel_runner.py +1 -3
  43. simtools/schemas/{integration_tests_config.metaschema.yml → application_workflow.metaschema.yml} +51 -27
  44. simtools/schemas/array_elements.yml +8 -0
  45. simtools/schemas/model_parameter.metaschema.yml +96 -0
  46. simtools/schemas/model_parameter_and_data_schema.metaschema.yml +2 -1
  47. simtools/schemas/model_parameters/correct_nsb_spectrum_to_telescope_altitude.schema.yml +1 -1
  48. simtools/schemas/model_parameters/corsika_cherenkov_photon_bunch_size.schema.yml +2 -0
  49. simtools/schemas/model_parameters/corsika_cherenkov_photon_wavelength_range.schema.yml +2 -0
  50. simtools/schemas/model_parameters/corsika_first_interaction_height.schema.yml +2 -0
  51. simtools/schemas/model_parameters/corsika_iact_io_buffer.schema.yml +2 -0
  52. simtools/schemas/model_parameters/corsika_iact_max_bunches.schema.yml +2 -0
  53. simtools/schemas/model_parameters/corsika_iact_split_auto.schema.yml +2 -0
  54. simtools/schemas/model_parameters/corsika_longitudinal_shower_development.schema.yml +2 -0
  55. simtools/schemas/model_parameters/corsika_particle_kinetic_energy_cutoff.schema.yml +2 -0
  56. simtools/schemas/model_parameters/corsika_starting_grammage.schema.yml +2 -0
  57. simtools/schemas/model_parameters/iobuf_maximum.schema.yml +1 -1
  58. simtools/schemas/model_parameters/iobuf_output_maximum.schema.yml +1 -1
  59. simtools/schemas/model_parameters/lightguide_efficiency_vs_incidence_angle.schema.yml +1 -1
  60. simtools/schemas/model_parameters/lightguide_efficiency_vs_wavelength.schema.yml +1 -1
  61. simtools/schemas/model_parameters/min_photoelectrons.schema.yml +1 -1
  62. simtools/schemas/model_parameters/min_photons.schema.yml +1 -1
  63. simtools/schemas/model_parameters/random_generator.schema.yml +1 -1
  64. simtools/schemas/model_parameters/sampled_output.schema.yml +1 -1
  65. simtools/schemas/model_parameters/save_pe_with_amplitude.schema.yml +1 -1
  66. simtools/schemas/model_parameters/store_photoelectrons.schema.yml +1 -1
  67. simtools/schemas/model_parameters/tailcut_scale.schema.yml +1 -1
  68. simtools/schemas/production_tables.schema.yml +1 -1
  69. simtools/simtel/simtel_config_reader.py +1 -2
  70. simtools/simtel/simtel_config_writer.py +1 -2
  71. simtools/simtel/simtel_io_histogram.py +0 -1
  72. simtools/simtel/simtel_io_histograms.py +2 -4
  73. simtools/simtel/simulator_camera_efficiency.py +1 -3
  74. simtools/simtel/simulator_light_emission.py +2 -5
  75. simtools/simtel/simulator_ray_tracing.py +1 -3
  76. simtools/testing/configuration.py +2 -1
  77. simtools/testing/validate_output.py +23 -13
  78. simtools/utils/general.py +12 -2
  79. simtools/utils/names.py +290 -152
  80. simtools/utils/value_conversion.py +17 -13
  81. simtools/version.py +2 -2
  82. simtools/visualization/legend_handlers.py +2 -0
  83. {gammasimtools-0.10.0.dist-info → gammasimtools-0.11.0.dist-info}/LICENSE +0 -0
  84. {gammasimtools-0.10.0.dist-info → gammasimtools-0.11.0.dist-info}/top_level.txt +0 -0
@@ -944,10 +944,10 @@ class CorsikaHistograms:
944
944
 
945
945
  hist_2d_values_list, x_position_list, y_position_list = self.get_2d_photon_position_distr()
946
946
 
947
- for i_hist, _ in enumerate(x_position_list):
947
+ for i_hist, x_pos in enumerate(x_position_list):
948
948
  hist_1d, bin_edges_1d = convert_2d_to_radial_distr(
949
949
  hist_2d_values_list[i_hist],
950
- x_position_list[i_hist], # pylint: disable=unnecessary-list-index-lookup
950
+ x_pos,
951
951
  y_position_list[i_hist],
952
952
  bins=bins,
953
953
  max_dist=max_dist,
@@ -1248,8 +1248,7 @@ class CorsikaHistograms:
1248
1248
  meta_data=self._meta_dict,
1249
1249
  )
1250
1250
  self._logger.info(
1251
- f"Writing 1D histogram with name {hdf5_table_name} to "
1252
- f"{self.hdf5_file_name}."
1251
+ f"Writing 1D histogram with name {hdf5_table_name} to {self.hdf5_file_name}."
1253
1252
  )
1254
1253
  # overwrite takes precedence over append
1255
1254
  if overwrite is True:
@@ -1382,8 +1381,7 @@ class CorsikaHistograms:
1382
1381
  )
1383
1382
 
1384
1383
  self._logger.info(
1385
- f"Writing 2D histogram with name {hdf5_table_name} to "
1386
- f"{self.hdf5_file_name}."
1384
+ f"Writing 2D histogram with name {hdf5_table_name} to {self.hdf5_file_name}."
1387
1385
  )
1388
1386
  # Always appending to table due to the file previously created
1389
1387
  # by self._export_1d_histograms.
@@ -93,8 +93,7 @@ def _kernel_plot_2d_photons(histograms_instance, property_name, log_z=False):
93
93
  all_figs.append(fig)
94
94
  if histograms_instance.individual_telescopes is False:
95
95
  ax.set_title(
96
- f"{histograms_instance.dict_2d_distributions[property_name]['file name']}"
97
- "_all_tels"
96
+ f"{histograms_instance.dict_2d_distributions[property_name]['file name']}_all_tels"
98
97
  )
99
98
  else:
100
99
  ax.text(
@@ -285,8 +284,7 @@ def _kernel_plot_1d_photons(histograms_instance, property_name, log_y=True):
285
284
  ax.set_yscale("log")
286
285
  if histograms_instance.individual_telescopes is False:
287
286
  ax.set_title(
288
- f"{histograms_instance.dict_1d_distributions[property_name]['file name']}"
289
- "_all_tels"
287
+ f"{histograms_instance.dict_1d_distributions[property_name]['file name']}_all_tels"
290
288
  )
291
289
  else:
292
290
  ax.set_title(
@@ -6,7 +6,6 @@ implementation of the observatory metadata model.
6
6
 
7
7
  """
8
8
 
9
- import datetime
10
9
  import getpass
11
10
  import logging
12
11
  import uuid
@@ -15,6 +14,7 @@ from pathlib import Path
15
14
  import simtools.constants
16
15
  import simtools.utils.general as gen
17
16
  import simtools.version
17
+ from simtools.constants import METADATA_JSON_SCHEMA
18
18
  from simtools.data_model import metadata_model, schema
19
19
  from simtools.io_operations import io_handler
20
20
  from simtools.utils import names
@@ -96,9 +96,9 @@ class MetadataCollector:
96
96
 
97
97
  """
98
98
  try:
99
- self.top_level_meta[self.observatory]["activity"][
100
- "end"
101
- ] = datetime.datetime.now().isoformat(timespec="seconds")
99
+ self.top_level_meta[self.observatory]["activity"]["end"] = (
100
+ gen.now_date_time_in_isoformat()
101
+ )
102
102
  except KeyError:
103
103
  pass
104
104
  return self.top_level_meta
@@ -199,8 +199,17 @@ class MetadataCollector:
199
199
  contact_dict: dict
200
200
  Dictionary for contact metadata fields.
201
201
  """
202
- if contact_dict.get("name", None) is None:
202
+ contact_dict["name"] = contact_dict.get("name") or self.args_dict.get("user_name")
203
+ if contact_dict["name"] is None:
204
+ self._logger.warning("No user name provided, take user info from system level.")
203
205
  contact_dict["name"] = getpass.getuser()
206
+ meta_dict = {
207
+ "email": "user_mail",
208
+ "orcid": "user_orcid",
209
+ "organization": "user_organization",
210
+ }
211
+ for key, value in meta_dict.items():
212
+ contact_dict[key] = contact_dict.get(key) or self.args_dict.get(value)
204
213
 
205
214
  def _fill_context_meta(self, context_dict):
206
215
  """
@@ -266,7 +275,7 @@ class MetadataCollector:
266
275
  self._logger.error("Unknown metadata file format: %s", metadata_file_name)
267
276
  raise gen.InvalidConfigDataError
268
277
 
269
- schema.validate_dict_using_schema(_input_metadata, None)
278
+ schema.validate_dict_using_schema(_input_metadata, schema_file=METADATA_JSON_SCHEMA)
270
279
 
271
280
  return gen.change_dict_keys_case(
272
281
  self._process_metadata_from_file(_input_metadata),
@@ -326,7 +335,7 @@ class MetadataCollector:
326
335
  self.schema_dict = self.get_data_model_schema_dict()
327
336
 
328
337
  product_dict["id"] = str(uuid.uuid4())
329
- product_dict["creation_time"] = datetime.datetime.now().isoformat(timespec="seconds")
338
+ product_dict["creation_time"] = gen.now_date_time_in_isoformat()
330
339
  product_dict["description"] = self.schema_dict.get("description", None)
331
340
 
332
341
  # DATA:CATEGORY
@@ -366,7 +375,7 @@ class MetadataCollector:
366
375
  )
367
376
  if instrument_dict["ID"]:
368
377
  instrument_dict["class"] = names.get_collection_name_from_array_element_name(
369
- instrument_dict["ID"]
378
+ instrument_dict["ID"], False
370
379
  )
371
380
 
372
381
  def _fill_process_meta(self, process_dict):
@@ -394,7 +403,7 @@ class MetadataCollector:
394
403
  activity_dict["name"] = self.args_dict.get("label", None)
395
404
  activity_dict["type"] = "software"
396
405
  activity_dict["id"] = self.args_dict.get("activity_id", "UNDEFINED_ACTIVITY_ID")
397
- activity_dict["start"] = datetime.datetime.now().isoformat(timespec="seconds")
406
+ activity_dict["start"] = gen.now_date_time_in_isoformat()
398
407
  activity_dict["end"] = activity_dict["start"]
399
408
  activity_dict["software"]["name"] = "simtools"
400
409
  activity_dict["software"]["version"] = simtools.version.__version__
@@ -12,6 +12,7 @@ from astropy.io.registry.base import IORegistryError
12
12
  import simtools.utils.general as gen
13
13
  from simtools.data_model import schema, validate_data
14
14
  from simtools.data_model.metadata_collector import MetadataCollector
15
+ from simtools.db import db_handler
15
16
  from simtools.io_operations import io_handler
16
17
  from simtools.utils import names, value_conversion
17
18
 
@@ -126,6 +127,7 @@ class ModelDataWriter:
126
127
  output_path=None,
127
128
  use_plain_output_path=False,
128
129
  metadata_input_dict=None,
130
+ db_config=None,
129
131
  ):
130
132
  """
131
133
  Generate DB-style model parameter dict and write it to json file.
@@ -148,6 +150,8 @@ class ModelDataWriter:
148
150
  Use plain output path.
149
151
  metadata_input_dict: dict
150
152
  Input to metadata collector.
153
+ db_config: dict
154
+ Database configuration. If not None, check if parameter with the same version exists.
151
155
 
152
156
  Returns
153
157
  -------
@@ -161,21 +165,72 @@ class ModelDataWriter:
161
165
  output_path=output_path,
162
166
  use_plain_output_path=use_plain_output_path,
163
167
  )
164
- _json_dict = writer.get_validated_parameter_dict(
165
- parameter_name, value, instrument, parameter_version
166
- )
167
- writer.write_dict_to_model_parameter_json(output_file, _json_dict)
168
+ if db_config is not None:
169
+ writer.check_db_for_existing_parameter(
170
+ parameter_name, instrument, parameter_version, db_config
171
+ )
172
+
173
+ unique_id = None
168
174
  if metadata_input_dict is not None:
169
175
  metadata_input_dict["output_file"] = output_file
170
176
  metadata_input_dict["output_file_format"] = Path(output_file).suffix.lstrip(".")
177
+ metadata = MetadataCollector(args_dict=metadata_input_dict).get_top_level_metadata()
171
178
  writer.write_metadata_to_yml(
172
- metadata=MetadataCollector(args_dict=metadata_input_dict).get_top_level_metadata(),
173
- yml_file=output_path / f"{Path(output_file).stem}",
179
+ metadata=metadata, yml_file=output_path / f"{Path(output_file).stem}"
174
180
  )
181
+ unique_id = metadata.get("cta", {}).get("product", {}).get("id")
182
+
183
+ _json_dict = writer.get_validated_parameter_dict(
184
+ parameter_name, value, instrument, parameter_version, unique_id
185
+ )
186
+ writer.write_dict_to_model_parameter_json(output_file, _json_dict)
175
187
  return _json_dict
176
188
 
189
+ def check_db_for_existing_parameter(
190
+ self, parameter_name, instrument, parameter_version, db_config
191
+ ):
192
+ """
193
+ Check if a parameter with the same version exists in the simulation model database.
194
+
195
+ Parameters
196
+ ----------
197
+ parameter_name: str
198
+ Name of the parameter.
199
+ instrument: str
200
+ Name of the instrument.
201
+ parameter_version: str
202
+ Version of the parameter.
203
+ db_config: dict
204
+ Database configuration.
205
+
206
+ Raises
207
+ ------
208
+ ValueError
209
+ If parameter with the same version exists in the database.
210
+ """
211
+ db = db_handler.DatabaseHandler(mongo_db_config=db_config)
212
+ try:
213
+ db.get_model_parameter(
214
+ parameter=parameter_name,
215
+ parameter_version=parameter_version,
216
+ site=names.get_site_from_array_element_name(instrument),
217
+ array_element_name=instrument,
218
+ )
219
+ except ValueError:
220
+ pass # parameter does not exist - expected behavior
221
+ else:
222
+ raise ValueError(
223
+ f"Parameter {parameter_name} with version {parameter_version} already exists."
224
+ )
225
+
177
226
  def get_validated_parameter_dict(
178
- self, parameter_name, value, instrument, parameter_version, schema_version=None
227
+ self,
228
+ parameter_name,
229
+ value,
230
+ instrument,
231
+ parameter_version,
232
+ unique_id=None,
233
+ schema_version=None,
179
234
  ):
180
235
  """
181
236
  Get validated parameter dictionary.
@@ -202,20 +257,15 @@ class ModelDataWriter:
202
257
  schema_file = schema.get_model_parameter_schema_file(parameter_name)
203
258
  self.schema_dict = gen.collect_data_from_file(schema_file)
204
259
 
205
- try: # e.g. instrument is 'North"
206
- site = names.validate_site_name(instrument)
207
- except ValueError: # e.g. instrument is 'LSTN-01'
208
- site = names.get_site_from_array_element_name(instrument)
209
-
210
260
  value, unit = value_conversion.split_value_and_unit(value)
211
261
 
212
262
  data_dict = {
213
263
  "schema_version": schema.get_model_parameter_schema_version(schema_version),
214
264
  "parameter": parameter_name,
215
265
  "instrument": instrument,
216
- "site": site,
266
+ "site": names.get_site_from_array_element_name(instrument),
217
267
  "parameter_version": parameter_version,
218
- "unique_id": None,
268
+ "unique_id": unique_id,
219
269
  "value": value,
220
270
  "unit": unit,
221
271
  "type": self._get_parameter_type(),
@@ -425,7 +475,9 @@ class ModelDataWriter:
425
475
  If yml_file is not defined.
426
476
  """
427
477
  try:
428
- yml_file = Path(yml_file or self.product_data_file).with_suffix(".metadata.yml")
478
+ yml_file = names.file_name_with_version(
479
+ yml_file or self.product_data_file, ".metadata.yml"
480
+ )
429
481
  with open(yml_file, "w", encoding="UTF-8") as file:
430
482
  yaml.safe_dump(
431
483
  gen.change_dict_keys_case(metadata, keys_lower_case),
@@ -112,15 +112,22 @@ def validate_dict_using_schema(data, schema_file=None, json_schema=None):
112
112
  if json_schema is None:
113
113
  json_schema = load_schema(
114
114
  schema_file,
115
- data.get("schema_version", "0.1.0"), # default version to ensure backward compatibility
115
+ data.get("schema_version")
116
+ or data.get(
117
+ "SCHEMA_VERSION", "0.1.0"
118
+ ), # default version to ensure backward compatibility
116
119
  )
117
120
 
118
121
  try:
119
122
  jsonschema.validate(data, schema=json_schema, format_checker=format_checkers.format_checker)
120
123
  except jsonschema.exceptions.ValidationError as exc:
121
- _logger.error(f"Validation failed using schema: {json_schema}")
124
+ _logger.error(f"Validation failed using schema: {json_schema} for data: {data}")
122
125
  raise exc
123
- if data.get("meta_schema_url") and not gen.url_exists(data["meta_schema_url"]):
126
+ if (
127
+ isinstance(data, dict)
128
+ and data.get("meta_schema_url")
129
+ and not gen.url_exists(data["meta_schema_url"])
130
+ ):
124
131
  raise FileNotFoundError(f"Meta schema URL does not exist: {data['meta_schema_url']}")
125
132
 
126
133
  _logger.debug(f"Successful validation of data using schema ({json_schema.get('name')})")
@@ -12,7 +12,7 @@ from astropy.utils.diff import report_diff_values
12
12
 
13
13
  import simtools.utils.general as gen
14
14
  from simtools.data_model import schema
15
- from simtools.utils import value_conversion
15
+ from simtools.utils import names, value_conversion
16
16
 
17
17
  __all__ = ["DataValidator"]
18
18
 
@@ -79,7 +79,7 @@ class DataValidator:
79
79
 
80
80
  """
81
81
  if self.data_file_name:
82
- self.validate_data_file()
82
+ self.validate_data_file(is_model_parameter)
83
83
  if isinstance(self.data_dict, dict):
84
84
  return self._validate_data_dict(is_model_parameter, lists_as_strings)
85
85
  if isinstance(self.data_table, Table):
@@ -87,11 +87,16 @@ class DataValidator:
87
87
  self._logger.error("No data or data table to validate")
88
88
  raise TypeError
89
89
 
90
- def validate_data_file(self):
90
+ def validate_data_file(self, is_model_parameter=None):
91
91
  """
92
92
  Open data file and read data from file.
93
93
 
94
94
  Doing this successfully is understood as file validation.
95
+
96
+ Parameters
97
+ ----------
98
+ is_model_parameter: bool
99
+ This is a model parameter file.
95
100
  """
96
101
  try:
97
102
  if Path(self.data_file_name).suffix in (".yml", ".yaml", ".json"):
@@ -102,14 +107,30 @@ class DataValidator:
102
107
  self._logger.info(f"Validating tabled data from: {self.data_file_name}")
103
108
  except (AttributeError, TypeError):
104
109
  pass
110
+ if is_model_parameter:
111
+ self.validate_parameter_and_file_name()
105
112
 
106
113
  def validate_parameter_and_file_name(self):
107
- """Validate that file name and key 'parameter_name' in data dict are the same."""
108
- if not str(Path(self.data_file_name).stem).startswith(self.data_dict.get("parameter")):
109
- raise ValueError(
110
- f"Parameter name in data dict {self.data_dict.get('parameter')} and "
111
- f"file name {Path(self.data_file_name).stem} do not match."
112
- )
114
+ """
115
+ Validate model parameter file name.
116
+
117
+ Expect that the following convention is used:
118
+
119
+ - file name starts with the parameter name
120
+ - file name ends with parameter version string
121
+
122
+ """
123
+ file_stem = Path(self.data_file_name).stem
124
+ param = self.data_dict.get("parameter")
125
+ param_version = self.data_dict.get("parameter_version")
126
+ if not file_stem.startswith(param):
127
+ raise ValueError(f"Mismatch: parameter '{param}' vs. file '{file_stem}'.")
128
+
129
+ if param_version and not file_stem.endswith(param_version):
130
+ raise ValueError(f"Mismatch: version '{param_version}' vs. file '{file_stem}'.")
131
+
132
+ if param_version is None:
133
+ self._logger.warning(f"File '{file_stem}' has no parameter version defined.")
113
134
 
114
135
  @staticmethod
115
136
  def validate_model_parameter(par_dict):
@@ -170,7 +191,17 @@ class DataValidator:
170
191
  else:
171
192
  self.data_dict["value"], self.data_dict["unit"] = value_as_list, unit_as_list
172
193
 
173
- self._check_version_string(self.data_dict.get("version"))
194
+ if self.data_dict.get("instrument"):
195
+ if self.data_dict["instrument"] == self.data_dict["site"]: # site parameters
196
+ names.validate_site_name(self.data_dict["site"])
197
+ else:
198
+ names.validate_array_element_name(self.data_dict["instrument"])
199
+ self._check_site_and_array_element_consistency(
200
+ self.data_dict.get("instrument"), self.data_dict.get("site")
201
+ )
202
+
203
+ for version_string in ("version", "parameter_version", "model_version"):
204
+ self._check_version_string(self.data_dict.get(version_string))
174
205
 
175
206
  if lists_as_strings:
176
207
  self._convert_results_to_model_format()
@@ -766,20 +797,14 @@ class DataValidator:
766
797
  Converts strings to numerical values or lists of values, if required.
767
798
 
768
799
  """
769
- value = self.data_dict["value"]
770
- if not isinstance(value, str):
771
- return
772
-
773
- # assume float value if type is not defined
774
- _is_float = self.data_dict.get("type", "float").startswith(("float", "double"))
775
-
776
- if value.isnumeric():
777
- self.data_dict["value"] = float(value) if _is_float else int(value)
778
- else:
779
- self.data_dict["value"] = gen.convert_string_to_list(value, is_float=_is_float)
780
-
781
- if self.data_dict["unit"] is not None:
782
- self.data_dict["unit"] = gen.convert_string_to_list(self.data_dict["unit"])
800
+ self.data_dict["value"], _ = value_conversion.split_value_and_unit(
801
+ self.data_dict["value"],
802
+ "int" in self.data_dict.get("type", "float"),
803
+ )
804
+ if isinstance(self.data_dict["unit"], str):
805
+ self.data_dict["unit"] = gen.convert_string_to_list(
806
+ self.data_dict["unit"], force_comma_separation=True
807
+ )
783
808
 
784
809
  def _convert_results_to_model_format(self):
785
810
  """
@@ -814,3 +839,24 @@ class DataValidator:
814
839
  if not re.match(semver_regex, version):
815
840
  raise ValueError(f"Invalid version string '{version}'")
816
841
  self._logger.debug(f"Valid version string '{version}'")
842
+
843
+ def _check_site_and_array_element_consistency(self, instrument, site):
844
+ """
845
+ Check that site and array element names are consistent.
846
+
847
+ An example for an inconsistency is 'LSTN' at site 'South'
848
+ """
849
+ if not all([instrument, site]) or "OBS" in instrument:
850
+ return
851
+
852
+ def to_sorted_list(value):
853
+ """Return value as sorted list."""
854
+ return [value] if isinstance(value, str) else sorted(value)
855
+
856
+ instrument_site = to_sorted_list(names.get_site_from_array_element_name(instrument))
857
+ site = to_sorted_list(site)
858
+
859
+ if instrument_site != site:
860
+ raise ValueError(
861
+ f"Site '{site}' and instrument site '{instrument_site}' are inconsistent."
862
+ )
simtools/db/db_handler.py CHANGED
@@ -178,7 +178,6 @@ class DatabaseHandler:
178
178
  parameter_version,
179
179
  site,
180
180
  array_element_name,
181
- collection,
182
181
  ):
183
182
  """
184
183
  Get a model parameter using the parameter version.
@@ -193,8 +192,6 @@ class DatabaseHandler:
193
192
  Site name.
194
193
  array_element_name: str
195
194
  Name of the array element model (e.g. MSTN, SSTS).
196
- collection: str
197
- Collection of array element (e.g. telescopes, calibration_devices).
198
195
 
199
196
  Returns
200
197
  -------
@@ -209,7 +206,9 @@ class DatabaseHandler:
209
206
  query["instrument"] = array_element_name
210
207
  if site is not None:
211
208
  query["site"] = site
212
- return self._read_mongo_db(query=query, collection_name=collection)
209
+ return self._read_mongo_db(
210
+ query=query, collection_name=names.get_collection_name_from_parameter_name(parameter)
211
+ )
213
212
 
214
213
  def get_model_parameters(
215
214
  self,
@@ -228,7 +227,7 @@ class DatabaseHandler:
228
227
  site: str
229
228
  Site name.
230
229
  array_element_name: str
231
- Name of the array element model (e.g. LSTN-01, MSTS-design, ILLN-01).
230
+ Name of the array element model (e.g. LSTN-01, MSTx-FlashCam, ILLN-01).
232
231
  model_version: str, list
233
232
  Version(s) of the model.
234
233
  collection: str
@@ -476,7 +475,7 @@ class DatabaseHandler:
476
475
  """
477
476
  collection = self.get_collection(self._get_db_name(), "production_tables")
478
477
  return sorted(
479
- [post["model_version"] for post in collection.find({"collection": collection_name})]
478
+ {post["model_version"] for post in collection.find({"collection": collection_name})}
480
479
  )
481
480
 
482
481
  def get_array_elements(self, model_version, collection="telescopes"):
@@ -499,6 +498,32 @@ class DatabaseHandler:
499
498
  production_table = self._read_production_table_from_mongo_db(collection, model_version)
500
499
  return sorted([entry for entry in production_table["parameters"] if "-design" not in entry])
501
500
 
501
+ def get_design_model(self, model_version, array_element_name, collection="telescopes"):
502
+ """
503
+ Get the design model used for a given array element and a given model version.
504
+
505
+ Parameters
506
+ ----------
507
+ model_version: str
508
+ Version of the model.
509
+ array_element_name: str
510
+ Name of the array element model (e.g. MSTN, SSTS).
511
+ collection: str
512
+ Which collection to get the array elements from:
513
+ i.e. telescopes, calibration_devices.
514
+
515
+ Returns
516
+ -------
517
+ str
518
+ Design model for a given array element.
519
+ """
520
+ production_table = self._read_production_table_from_mongo_db(collection, model_version)
521
+ try:
522
+ return production_table["design_model"][array_element_name]
523
+ except KeyError:
524
+ # for eg. array_element_name == 'LSTN-design' returns 'LSTN-design'
525
+ return array_element_name
526
+
502
527
  def get_array_elements_of_type(self, array_element_type, model_version, collection):
503
528
  """
504
529
  Get array elements of a certain type (e.g. 'LSTN') for a DB collection.
@@ -837,15 +862,20 @@ class DatabaseHandler:
837
862
  return ["xSTx-design"] # placeholder to ignore 'instrument' field in query.
838
863
  if collection == "sites":
839
864
  return [f"OBS-{site}"]
840
- if "-design" in array_element_name:
865
+ if names.is_design_type(array_element_name):
841
866
  return [array_element_name]
867
+ if collection == "configuration_sim_telarray":
868
+ # get design model from 'telescope' or 'calibration_device' production tables
869
+ production_table = self._read_production_table_from_mongo_db(
870
+ names.get_collection_name_from_array_element_name(array_element_name),
871
+ production_table["model_version"],
872
+ )
842
873
  try:
843
874
  return [
844
875
  production_table["design_model"][array_element_name],
845
876
  array_element_name,
846
877
  ]
847
- except KeyError:
848
- return [
849
- f"{names.get_array_element_type_from_name(array_element_name)}-design",
850
- array_element_name,
851
- ]
878
+ except KeyError as exc:
879
+ raise KeyError(
880
+ f"Failed generated array element list for db query for {array_element_name}"
881
+ ) from exc
@@ -0,0 +1,112 @@
1
+ """
2
+ Simtools dependencies version management.
3
+
4
+ This modules provides two main functionalities:
5
+
6
+ - retrieve the versions of simtools dependencies (e.g., databases, sim_telarray, CORSIKA)
7
+ - provide space for future implementations of version management
8
+
9
+ """
10
+
11
+ import logging
12
+ import os
13
+ import re
14
+ import subprocess
15
+ from pathlib import Path
16
+
17
+ import simtools.utils.general as gen
18
+ from simtools.db.db_handler import DatabaseHandler
19
+
20
+ _logger = logging.getLogger(__name__)
21
+
22
+
23
+ def get_version_string(db_config=None):
24
+ """Print the versions of the dependencies."""
25
+ return (
26
+ f"Database version: {get_database_version(db_config)}\n"
27
+ f"sim_telarray version: {get_sim_telarray_version()}\n"
28
+ f"CORSIKA version: {get_corsika_version()}\n"
29
+ )
30
+
31
+
32
+ def get_database_version(db_config):
33
+ """
34
+ Get the version of the simulation model data base used.
35
+
36
+ Parameters
37
+ ----------
38
+ db_config : dict
39
+ Dictionary containing the database configuration.
40
+
41
+ Returns
42
+ -------
43
+ str
44
+ Version of the simulation model data base used.
45
+
46
+ """
47
+ if db_config is None:
48
+ return None
49
+ db = DatabaseHandler(db_config)
50
+ return db.mongo_db_config.get("db_simulation_model")
51
+
52
+
53
+ def get_sim_telarray_version():
54
+ """
55
+ Get the version of the sim_telarray package using 'sim_telarray --version'.
56
+
57
+ Returns
58
+ -------
59
+ str
60
+ Version of the sim_telarray package.
61
+ """
62
+ sim_telarray_path = os.getenv("SIMTOOLS_SIMTEL_PATH")
63
+ if sim_telarray_path is None:
64
+ _logger.warning("Environment variable SIMTOOLS_SIMTEL_PATH is not set.")
65
+ return None
66
+ sim_telarray_path = Path(sim_telarray_path) / "sim_telarray" / "bin" / "sim_telarray"
67
+
68
+ # expect stdout with e.g. a line 'Release: 2024.271.0 from 2024-09-27'
69
+ result = subprocess.run(
70
+ [sim_telarray_path, "--version"],
71
+ capture_output=True,
72
+ text=True,
73
+ check=False,
74
+ )
75
+ match = re.search(r"^Release:\s+(.+)", result.stdout, re.MULTILINE)
76
+
77
+ if match:
78
+ return match.group(1).split()[0]
79
+ raise ValueError(f"sim_telarray release not found in {result.stdout}")
80
+
81
+
82
+ def get_corsika_version():
83
+ """
84
+ Get the version of the corsika package.
85
+
86
+ Returns
87
+ -------
88
+ str
89
+ Version of the corsika package.
90
+ """
91
+ try:
92
+ build_opts = get_build_options()
93
+ except (FileNotFoundError, TypeError):
94
+ _logger.warning("CORSIKA version not implemented yet.")
95
+ return None
96
+ return build_opts.get("corsika_version")
97
+
98
+
99
+ def get_build_options():
100
+ """
101
+ Return CORSIKA / sim_telarray build options.
102
+
103
+ Expects a build_opts.yml file in the sim_telarray directory.
104
+ """
105
+ try:
106
+ return gen.collect_data_from_file(
107
+ Path(os.getenv("SIMTOOLS_SIMTEL_PATH")) / "build_opts.yml"
108
+ )
109
+ except FileNotFoundError as exc:
110
+ raise FileNotFoundError("No build_opts.yml file found.") from exc
111
+ except TypeError as exc:
112
+ raise TypeError("SIMTOOLS_SIMTEL_PATH not defined.") from exc