gammasimtools 0.10.0__py3-none-any.whl → 0.12.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.12.0.dist-info}/METADATA +3 -1
  2. {gammasimtools-0.10.0.dist-info → gammasimtools-0.12.0.dist-info}/RECORD +84 -77
  3. {gammasimtools-0.10.0.dist-info → gammasimtools-0.12.0.dist-info}/WHEEL +1 -1
  4. {gammasimtools-0.10.0.dist-info → gammasimtools-0.12.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 +30 -1
  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 +46 -14
  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 +20 -14
  81. simtools/version.py +2 -2
  82. simtools/visualization/legend_handlers.py +2 -0
  83. {gammasimtools-0.10.0.dist-info → gammasimtools-0.12.0.dist-info}/LICENSE +0 -0
  84. {gammasimtools-0.10.0.dist-info → gammasimtools-0.12.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
@@ -40,7 +40,7 @@ jsonschema_db_dict = {
40
40
  "db_api_user": {"type": "string", "description": "API username"},
41
41
  "db_api_pw": {"type": "string", "description": "Password for the API user"},
42
42
  "db_api_authentication_database": {
43
- "type": "string",
43
+ "type": ["string", "null"],
44
44
  "default": "admin",
45
45
  "description": "DB with user info (optional)",
46
46
  },
@@ -119,7 +119,9 @@ class DatabaseHandler:
119
119
  port=self.mongo_db_config["db_api_port"],
120
120
  username=self.mongo_db_config["db_api_user"],
121
121
  password=self.mongo_db_config["db_api_pw"],
122
- authSource=self.mongo_db_config.get("db_api_authentication_database", "admin"),
122
+ authSource=self.mongo_db_config.get("db_api_authentication_database")
123
+ if self.mongo_db_config.get("db_api_authentication_database")
124
+ else "admin",
123
125
  directConnection=direct_connection,
124
126
  ssl=not direct_connection,
125
127
  tlsallowinvalidhostnames=True,
@@ -178,7 +180,6 @@ class DatabaseHandler:
178
180
  parameter_version,
179
181
  site,
180
182
  array_element_name,
181
- collection,
182
183
  ):
183
184
  """
184
185
  Get a model parameter using the parameter version.
@@ -193,8 +194,6 @@ class DatabaseHandler:
193
194
  Site name.
194
195
  array_element_name: str
195
196
  Name of the array element model (e.g. MSTN, SSTS).
196
- collection: str
197
- Collection of array element (e.g. telescopes, calibration_devices).
198
197
 
199
198
  Returns
200
199
  -------
@@ -209,7 +208,9 @@ class DatabaseHandler:
209
208
  query["instrument"] = array_element_name
210
209
  if site is not None:
211
210
  query["site"] = site
212
- return self._read_mongo_db(query=query, collection_name=collection)
211
+ return self._read_mongo_db(
212
+ query=query, collection_name=names.get_collection_name_from_parameter_name(parameter)
213
+ )
213
214
 
214
215
  def get_model_parameters(
215
216
  self,
@@ -228,7 +229,7 @@ class DatabaseHandler:
228
229
  site: str
229
230
  Site name.
230
231
  array_element_name: str
231
- Name of the array element model (e.g. LSTN-01, MSTS-design, ILLN-01).
232
+ Name of the array element model (e.g. LSTN-01, MSTx-FlashCam, ILLN-01).
232
233
  model_version: str, list
233
234
  Version(s) of the model.
234
235
  collection: str
@@ -476,7 +477,7 @@ class DatabaseHandler:
476
477
  """
477
478
  collection = self.get_collection(self._get_db_name(), "production_tables")
478
479
  return sorted(
479
- [post["model_version"] for post in collection.find({"collection": collection_name})]
480
+ {post["model_version"] for post in collection.find({"collection": collection_name})}
480
481
  )
481
482
 
482
483
  def get_array_elements(self, model_version, collection="telescopes"):
@@ -499,6 +500,32 @@ class DatabaseHandler:
499
500
  production_table = self._read_production_table_from_mongo_db(collection, model_version)
500
501
  return sorted([entry for entry in production_table["parameters"] if "-design" not in entry])
501
502
 
503
+ def get_design_model(self, model_version, array_element_name, collection="telescopes"):
504
+ """
505
+ Get the design model used for a given array element and a given model version.
506
+
507
+ Parameters
508
+ ----------
509
+ model_version: str
510
+ Version of the model.
511
+ array_element_name: str
512
+ Name of the array element model (e.g. MSTN, SSTS).
513
+ collection: str
514
+ Which collection to get the array elements from:
515
+ i.e. telescopes, calibration_devices.
516
+
517
+ Returns
518
+ -------
519
+ str
520
+ Design model for a given array element.
521
+ """
522
+ production_table = self._read_production_table_from_mongo_db(collection, model_version)
523
+ try:
524
+ return production_table["design_model"][array_element_name]
525
+ except KeyError:
526
+ # for eg. array_element_name == 'LSTN-design' returns 'LSTN-design'
527
+ return array_element_name
528
+
502
529
  def get_array_elements_of_type(self, array_element_type, model_version, collection):
503
530
  """
504
531
  Get array elements of a certain type (e.g. 'LSTN') for a DB collection.
@@ -837,15 +864,20 @@ class DatabaseHandler:
837
864
  return ["xSTx-design"] # placeholder to ignore 'instrument' field in query.
838
865
  if collection == "sites":
839
866
  return [f"OBS-{site}"]
840
- if "-design" in array_element_name:
867
+ if names.is_design_type(array_element_name):
841
868
  return [array_element_name]
869
+ if collection == "configuration_sim_telarray":
870
+ # get design model from 'telescope' or 'calibration_device' production tables
871
+ production_table = self._read_production_table_from_mongo_db(
872
+ names.get_collection_name_from_array_element_name(array_element_name),
873
+ production_table["model_version"],
874
+ )
842
875
  try:
843
876
  return [
844
877
  production_table["design_model"][array_element_name],
845
878
  array_element_name,
846
879
  ]
847
- except KeyError:
848
- return [
849
- f"{names.get_array_element_type_from_name(array_element_name)}-design",
850
- array_element_name,
851
- ]
880
+ except KeyError as exc:
881
+ raise KeyError(
882
+ f"Failed generated array element list for db query for {array_element_name}"
883
+ ) from exc