gammasimtools 0.24.0__py3-none-any.whl → 0.26.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.24.0.dist-info → gammasimtools-0.26.0.dist-info}/METADATA +2 -1
  2. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/RECORD +134 -130
  3. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/entry_points.txt +3 -1
  4. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/licenses/LICENSE +1 -1
  5. simtools/_version.py +2 -2
  6. simtools/application_control.py +78 -0
  7. simtools/applications/calculate_incident_angles.py +0 -2
  8. simtools/applications/convert_geo_coordinates_of_array_elements.py +1 -2
  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 +2 -6
  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/derive_mirror_rnda.py +1 -3
  20. simtools/applications/derive_psf_parameters.py +5 -1
  21. simtools/applications/derive_pulse_shape_parameters.py +194 -0
  22. simtools/applications/derive_trigger_rates.py +1 -1
  23. simtools/applications/docs_produce_array_element_report.py +2 -8
  24. simtools/applications/docs_produce_calibration_reports.py +1 -3
  25. simtools/applications/docs_produce_model_parameter_reports.py +0 -2
  26. simtools/applications/docs_produce_simulation_configuration_report.py +1 -3
  27. simtools/applications/generate_array_config.py +0 -1
  28. simtools/applications/generate_corsika_histograms.py +48 -235
  29. simtools/applications/generate_regular_arrays.py +5 -35
  30. simtools/applications/generate_simtel_event_data.py +2 -2
  31. simtools/applications/maintain_simulation_model_add_production.py +2 -2
  32. simtools/applications/maintain_simulation_model_write_array_element_positions.py +87 -0
  33. simtools/applications/plot_array_layout.py +64 -108
  34. simtools/applications/plot_simulated_event_distributions.py +57 -0
  35. simtools/applications/plot_tabular_data.py +0 -1
  36. simtools/applications/plot_tabular_data_for_model_parameter.py +1 -6
  37. simtools/applications/production_derive_corsika_limits.py +1 -1
  38. simtools/applications/production_generate_grid.py +0 -1
  39. simtools/applications/run_application.py +1 -1
  40. simtools/applications/simulate_flasher.py +3 -4
  41. simtools/applications/simulate_illuminator.py +0 -1
  42. simtools/applications/simulate_pedestals.py +2 -6
  43. simtools/applications/simulate_prod.py +9 -28
  44. simtools/applications/simulate_prod_htcondor_generator.py +8 -1
  45. simtools/applications/submit_array_layouts.py +7 -7
  46. simtools/applications/submit_model_parameter_from_external.py +1 -3
  47. simtools/applications/validate_camera_efficiency.py +0 -1
  48. simtools/applications/validate_camera_fov.py +0 -1
  49. simtools/applications/validate_cumulative_psf.py +0 -2
  50. simtools/applications/validate_file_using_schema.py +49 -123
  51. simtools/applications/validate_optics.py +0 -13
  52. simtools/camera/camera_efficiency.py +1 -6
  53. simtools/camera/single_photon_electron_spectrum.py +2 -1
  54. simtools/configuration/commandline_parser.py +43 -8
  55. simtools/configuration/configurator.py +6 -11
  56. simtools/corsika/corsika_config.py +204 -99
  57. simtools/corsika/corsika_histograms.py +411 -1735
  58. simtools/corsika/primary_particle.py +1 -1
  59. simtools/data_model/metadata_collector.py +5 -2
  60. simtools/data_model/metadata_model.py +0 -4
  61. simtools/data_model/model_data_writer.py +27 -17
  62. simtools/data_model/schema.py +112 -5
  63. simtools/data_model/validate_data.py +80 -48
  64. simtools/db/db_handler.py +19 -8
  65. simtools/db/db_model_upload.py +2 -1
  66. simtools/db/mongo_db.py +133 -42
  67. simtools/dependencies.py +83 -44
  68. simtools/io/ascii_handler.py +4 -2
  69. simtools/io/table_handler.py +1 -1
  70. simtools/job_execution/htcondor_script_generator.py +0 -2
  71. simtools/layout/array_layout.py +4 -12
  72. simtools/layout/array_layout_utils.py +227 -58
  73. simtools/model/array_model.py +37 -18
  74. simtools/model/calibration_model.py +0 -4
  75. simtools/model/legacy_model_parameter.py +134 -0
  76. simtools/model/model_parameter.py +24 -14
  77. simtools/model/model_repository.py +18 -5
  78. simtools/model/model_utils.py +1 -6
  79. simtools/model/site_model.py +0 -4
  80. simtools/model/telescope_model.py +6 -11
  81. simtools/production_configuration/derive_corsika_limits.py +6 -11
  82. simtools/production_configuration/interpolation_handler.py +16 -16
  83. simtools/ray_tracing/incident_angles.py +5 -11
  84. simtools/ray_tracing/mirror_panel_psf.py +3 -7
  85. simtools/ray_tracing/psf_analysis.py +29 -27
  86. simtools/ray_tracing/psf_parameter_optimisation.py +822 -680
  87. simtools/ray_tracing/ray_tracing.py +6 -15
  88. simtools/reporting/docs_auto_report_generator.py +8 -13
  89. simtools/reporting/docs_read_parameters.py +70 -16
  90. simtools/runners/corsika_runner.py +15 -10
  91. simtools/runners/corsika_simtel_runner.py +9 -8
  92. simtools/runners/runner_services.py +17 -7
  93. simtools/runners/simtel_runner.py +11 -58
  94. simtools/runners/simtools_runner.py +2 -4
  95. simtools/schemas/model_parameters/flasher_pulse_exp_decay.schema.yml +2 -0
  96. simtools/schemas/model_parameters/flasher_pulse_shape.schema.yml +50 -0
  97. simtools/schemas/model_parameters/flasher_pulse_width.schema.yml +2 -0
  98. simtools/schemas/simulation_models_info.schema.yml +2 -0
  99. simtools/settings.py +154 -0
  100. simtools/sim_events/file_info.py +128 -0
  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 +273 -0
  105. simtools/simtel/simtel_config_writer.py +146 -22
  106. simtools/simtel/simtel_table_reader.py +6 -4
  107. simtools/simtel/simulator_array.py +62 -23
  108. simtools/simtel/simulator_camera_efficiency.py +4 -6
  109. simtools/simtel/simulator_light_emission.py +101 -19
  110. simtools/simtel/simulator_ray_tracing.py +4 -10
  111. simtools/simulator.py +360 -353
  112. simtools/telescope_trigger_rates.py +3 -4
  113. simtools/testing/assertions.py +115 -8
  114. simtools/testing/configuration.py +2 -3
  115. simtools/testing/helpers.py +2 -3
  116. simtools/testing/log_inspector.py +5 -1
  117. simtools/testing/sim_telarray_metadata.py +1 -1
  118. simtools/testing/validate_output.py +69 -23
  119. simtools/utils/general.py +37 -0
  120. simtools/utils/geometry.py +0 -77
  121. simtools/utils/names.py +7 -9
  122. simtools/version.py +37 -0
  123. simtools/visualization/legend_handlers.py +21 -10
  124. simtools/visualization/plot_array_layout.py +312 -41
  125. simtools/visualization/plot_corsika_histograms.py +143 -605
  126. simtools/visualization/plot_mirrors.py +834 -0
  127. simtools/visualization/plot_pixels.py +2 -4
  128. simtools/visualization/plot_psf.py +0 -1
  129. simtools/visualization/plot_simtel_event_histograms.py +4 -4
  130. simtools/visualization/plot_simtel_events.py +6 -11
  131. simtools/visualization/plot_tables.py +8 -19
  132. simtools/visualization/visualize.py +22 -2
  133. simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +0 -160
  134. simtools/applications/print_version.py +0 -53
  135. simtools/io/hdf5_handler.py +0 -139
  136. simtools/simtel/simtel_io_file_info.py +0 -62
  137. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/WHEEL +0 -0
  138. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/top_level.txt +0 -0
@@ -185,7 +185,7 @@ class PrimaryParticle:
185
185
  "silicon": {"corsika7_id": 2814},
186
186
  "iron": {"corsika7_id": 5626},
187
187
  }
188
- for _, ids in particles.items():
188
+ for ids in particles.values():
189
189
  ids["pdg_id"] = Corsika7ID(ids["corsika7_id"]).to_pdgid().numerator
190
190
  ids["pdg_name"] = Particle.findall(pdgid=ids["pdg_id"])[0].name
191
191
 
@@ -360,6 +360,9 @@ class MetadataCollector:
360
360
  "Metadata extraction from sim_telarray files is not supported yet."
361
361
  )
362
362
  continue
363
+ elif Path(metadata_file).name.endswith((".corsika.zst", ".corsika")):
364
+ self._logger.warning("Metadata extraction from CORSIKA files is not supported yet.")
365
+ continue
363
366
  else:
364
367
  raise ValueError(f"Unknown metadata file format: {metadata_file}")
365
368
 
@@ -422,7 +425,7 @@ class MetadataCollector:
422
425
  product_dict["creation_time"] = gen.now_date_time_in_isoformat()
423
426
  product_dict["description"] = self.schema_dict.get("description", None)
424
427
 
425
- # DATA:CATEGORY
428
+ # metadata DATA:CATEGORY
426
429
  product_dict["data"]["category"] = "SIM"
427
430
  product_dict["data"]["level"] = "R1"
428
431
  product_dict["data"]["type"] = "Service"
@@ -431,7 +434,7 @@ class MetadataCollector:
431
434
  except KeyError:
432
435
  pass
433
436
 
434
- # DATA:MODEL
437
+ # metadata DATA:MODEL
435
438
  product_dict["data"]["model"]["name"] = (
436
439
  self.schema_dict.get("name")
437
440
  or self.args_dict.get("metadata_product_data_name")
@@ -8,13 +8,9 @@ Follows CTAO top-level data model definition.
8
8
 
9
9
  """
10
10
 
11
- import logging
12
-
13
11
  import simtools.data_model.schema
14
12
  import simtools.utils.general as gen
15
13
 
16
- _logger = logging.getLogger(__name__)
17
-
18
14
 
19
15
  def get_default_metadata_dict(
20
16
  schema_file=None, observatory="CTA", schema_version="latest", lower_case=True
@@ -98,9 +98,10 @@ class ModelDataWriter:
98
98
  output_file,
99
99
  output_path=None,
100
100
  metadata_input_dict=None,
101
- db_config=None,
102
101
  unit=None,
103
102
  meta_parameter=False,
103
+ model_parameter_schema_version=None,
104
+ check_db_for_existing_parameter=True,
104
105
  ):
105
106
  """
106
107
  Generate DB-style model parameter dict and write it to json file.
@@ -121,10 +122,14 @@ class ModelDataWriter:
121
122
  Path to output file.
122
123
  metadata_input_dict: dict
123
124
  Input to metadata collector.
124
- db_config: dict
125
- Database configuration. If not None, check if parameter with the same version exists.
126
125
  unit: str
127
126
  Unit of the parameter value (if applicable and value is not of type astropy Quantity).
127
+ meta_parameter: bool
128
+ Setting for meta parameter flag.
129
+ model_parameter_schema_version: str, None
130
+ Version of the model parameter schema (if None, use schema version from schema dict).
131
+ check_db_for_existing_parameter: bool
132
+ If True, check if parameter with same version exists in DB before writing.
128
133
 
129
134
  Returns
130
135
  -------
@@ -137,10 +142,8 @@ class ModelDataWriter:
137
142
  args_dict=None,
138
143
  output_path=output_path,
139
144
  )
140
- if db_config is not None:
141
- writer.check_db_for_existing_parameter(
142
- parameter_name, instrument, parameter_version, db_config
143
- )
145
+ if check_db_for_existing_parameter:
146
+ writer.check_db_for_existing_parameter(parameter_name, instrument, parameter_version)
144
147
 
145
148
  unique_id = None
146
149
  if metadata_input_dict is not None:
@@ -158,15 +161,14 @@ class ModelDataWriter:
158
161
  instrument,
159
162
  parameter_version,
160
163
  unique_id,
164
+ model_parameter_schema_version=model_parameter_schema_version,
161
165
  unit=unit,
162
166
  meta_parameter=meta_parameter,
163
167
  )
164
168
  writer.write_dict_to_model_parameter_json(output_file, _json_dict)
165
169
  return _json_dict
166
170
 
167
- def check_db_for_existing_parameter(
168
- self, parameter_name, instrument, parameter_version, db_config
169
- ):
171
+ def check_db_for_existing_parameter(self, parameter_name, instrument, parameter_version):
170
172
  """
171
173
  Check if a parameter with the same version exists in the simulation model database.
172
174
 
@@ -178,15 +180,15 @@ class ModelDataWriter:
178
180
  Name of the instrument.
179
181
  parameter_version: str
180
182
  Version of the parameter.
181
- db_config: dict
182
- Database configuration.
183
183
 
184
184
  Raises
185
185
  ------
186
186
  ValueError
187
187
  If parameter with the same version exists in the database.
188
188
  """
189
- db = db_handler.DatabaseHandler(db_config=db_config)
189
+ db = db_handler.DatabaseHandler()
190
+ if not db.is_configured():
191
+ return
190
192
  try:
191
193
  db.get_model_parameter(
192
194
  parameter=parameter_name,
@@ -211,6 +213,7 @@ class ModelDataWriter:
211
213
  schema_version=None,
212
214
  unit=None,
213
215
  meta_parameter=False,
216
+ model_parameter_schema_version=None,
214
217
  ):
215
218
  """
216
219
  Get validated parameter dictionary.
@@ -233,6 +236,8 @@ class ModelDataWriter:
233
236
  Unit of the parameter value (if applicable and value is not an astropy Quantity).
234
237
  meta_parameter: bool
235
238
  Setting for meta parameter flag.
239
+ model_parameter_schema_version: str, None
240
+ Version of the model parameter schema (if None, use schema version from schema dict).
236
241
 
237
242
  Returns
238
243
  -------
@@ -240,7 +245,9 @@ class ModelDataWriter:
240
245
  Validated parameter dictionary.
241
246
  """
242
247
  self._logger.debug(f"Getting validated parameter dictionary for {instrument}")
243
- self.schema_dict, schema_file = self._read_schema_dict(parameter_name, schema_version)
248
+ self.schema_dict, schema_file = self._read_schema_dict(
249
+ parameter_name, model_parameter_schema_version
250
+ )
244
251
 
245
252
  if unit is None:
246
253
  value, unit = value_conversion.split_value_and_unit(value)
@@ -257,7 +264,8 @@ class ModelDataWriter:
257
264
  "type": self._get_parameter_type(),
258
265
  "file": self._parameter_is_a_file(),
259
266
  "meta_parameter": meta_parameter,
260
- "model_parameter_schema_version": self.schema_dict.get("schema_version", "0.1.0"),
267
+ "model_parameter_schema_version": model_parameter_schema_version
268
+ or self.schema_dict.get("schema_version", "0.1.0"),
261
269
  }
262
270
  return self.validate_and_transform(
263
271
  product_data_dict=data_dict,
@@ -361,8 +369,10 @@ class ModelDataWriter:
361
369
  """
362
370
  try:
363
371
  unit_list = []
364
- for data in self.schema_dict["data"]:
365
- unit_list.append(data["unit"] if data["unit"] != "dimensionless" else None)
372
+ unit_list = [
373
+ data["unit"] if data["unit"] != "dimensionless" else None
374
+ for data in self.schema_dict["data"]
375
+ ]
366
376
  return unit_list if len(unit_list) > 1 else unit_list[0]
367
377
  except (KeyError, IndexError):
368
378
  pass
@@ -146,11 +146,10 @@ def validate_dict_using_schema(
146
146
 
147
147
  def _validate_meta_schema_url(data):
148
148
  """Validate meta_schema_url if present in data."""
149
- if (
150
- isinstance(data, dict)
151
- and data.get("meta_schema_url")
152
- and not gen.url_exists(data["meta_schema_url"])
153
- ):
149
+ if not isinstance(data, dict):
150
+ return
151
+
152
+ if data.get("meta_schema_url") is not None and not gen.url_exists(data["meta_schema_url"]):
154
153
  raise FileNotFoundError(f"Meta schema URL does not exist: {data['meta_schema_url']}")
155
154
 
156
155
 
@@ -355,3 +354,111 @@ def validate_deprecation_and_version(data, software_name=None, ignore_software_v
355
354
  _logger.warning(f"{msg}, but version check is ignored.")
356
355
  else:
357
356
  raise ValueError(msg)
357
+
358
+
359
+ def validate_schema_from_files(
360
+ file_directory, file_name=None, schema_file=None, ignore_software_version=False
361
+ ):
362
+ """
363
+ Validate a schema file or several files in a directory.
364
+
365
+ Files to be validated are taken from file_directory and file_name pattern.
366
+ The schema is either given as command line argument, read from the meta_schema_url or from
367
+ the metadata section of the data dictionary.
368
+
369
+ Parameters
370
+ ----------
371
+ file_directory : str or Path, optional
372
+ Directory with files to be validated.
373
+ file_name : str or Path, optional
374
+ File name pattern to be validated.
375
+ schema_file : str, optional
376
+ Schema file name provided directly.
377
+ ignore_software_version : bool
378
+ If True, ignore software version check.
379
+ """
380
+ if file_directory and file_name:
381
+ file_list = sorted(Path(file_directory).rglob(file_name))
382
+ else:
383
+ file_list = [Path(file_name)] if file_name else []
384
+
385
+ for _file_name in file_list:
386
+ try:
387
+ data = ascii_handler.collect_data_from_file(file_name=_file_name)
388
+ except FileNotFoundError as exc:
389
+ raise FileNotFoundError(f"Error reading schema file from {_file_name}") from exc
390
+ data = data if isinstance(data, list) else [data]
391
+ try:
392
+ for data_dict in data:
393
+ validate_dict_using_schema(
394
+ data_dict,
395
+ _get_schema_file_name(schema_file, _file_name, data_dict),
396
+ ignore_software_version=ignore_software_version,
397
+ )
398
+ except Exception as exc:
399
+ raise ValueError(f"Validation of file {_file_name} failed") from exc
400
+ _logger.info(f"Successful validation of file {_file_name}")
401
+
402
+
403
+ def _get_schema_file_name(schema_file=None, file_name=None, data_dict=None):
404
+ """
405
+ Get schema file name from metadata, data dict, or from file.
406
+
407
+ Parameters
408
+ ----------
409
+ schema_file : str, optional
410
+ Schema file name provided directly.
411
+ file_name : str or Path, optional
412
+ File name to extract schema information from.
413
+ data_dict : dict, optional
414
+ Dictionary with metaschema information.
415
+
416
+ Returns
417
+ -------
418
+ str or None
419
+ Schema file name.
420
+ """
421
+ if schema_file is not None:
422
+ return schema_file
423
+
424
+ if data_dict and (url := data_dict.get("meta_schema_url")):
425
+ return url
426
+
427
+ if file_name:
428
+ return _extract_schema_from_file(file_name)
429
+
430
+ return None
431
+
432
+
433
+ def _extract_schema_url_from_metadata_dict(metadata, observatory="cta"):
434
+ """Extract schema URL from metadata dictionary."""
435
+ for key in (observatory, observatory.lower()):
436
+ url = metadata.get(key, {}).get("product", {}).get("data", {}).get("model", {}).get("url")
437
+ if url:
438
+ return url
439
+ return None
440
+
441
+
442
+ def _extract_schema_from_file(file_name, observatory="cta"):
443
+ """
444
+ Extract schema file name from a metadata or data file.
445
+
446
+ Parameters
447
+ ----------
448
+ file_name : str or Path
449
+ File name to extract schema information from.
450
+ observatory : str
451
+ Observatory name (default: "cta").
452
+
453
+ Returns
454
+ -------
455
+ str or None
456
+ Schema file name or None if not found.
457
+
458
+ """
459
+ try:
460
+ metadata = ascii_handler.collect_data_from_file(file_name=file_name, yaml_document=0)
461
+ except FileNotFoundError:
462
+ return None
463
+
464
+ return _extract_schema_url_from_metadata_dict(metadata, observatory)
@@ -14,6 +14,7 @@ import simtools.utils.general as gen
14
14
  from simtools.data_model import schema
15
15
  from simtools.io import ascii_handler
16
16
  from simtools.utils import names, value_conversion
17
+ from simtools.version import is_valid_semantic_version
17
18
 
18
19
 
19
20
  class DataValidator:
@@ -46,7 +47,7 @@ class DataValidator:
46
47
  check_exact_data_type=True,
47
48
  ):
48
49
  """Initialize validation class and read required reference data columns."""
49
- self._logger = logging.getLogger(__name__)
50
+ self.logger = logging.getLogger(__name__)
50
51
 
51
52
  self.data_file_name = data_file
52
53
  self.schema_file_name = schema_file
@@ -83,7 +84,7 @@ class DataValidator:
83
84
  return self._validate_data_dict(is_model_parameter, lists_as_strings)
84
85
  if isinstance(self.data_table, Table):
85
86
  return self._validate_data_table()
86
- self._logger.error("No data or data table to validate")
87
+ self.logger.error("No data or data table to validate")
87
88
  raise TypeError
88
89
 
89
90
  def validate_data_file(self, is_model_parameter=None):
@@ -100,10 +101,10 @@ class DataValidator:
100
101
  try:
101
102
  if Path(self.data_file_name).suffix in (".yml", ".yaml", ".json"):
102
103
  self.data_dict = ascii_handler.collect_data_from_file(self.data_file_name)
103
- self._logger.info(f"Validating data from: {self.data_file_name}")
104
+ self.logger.info(f"Validating data from: {self.data_file_name}")
104
105
  else:
105
106
  self.data_table = Table.read(self.data_file_name, guess=True, delimiter=r"\s")
106
- self._logger.info(f"Validating tabled data from: {self.data_file_name}")
107
+ self.logger.info(f"Validating tabled data from: {self.data_file_name}")
107
108
  except (AttributeError, TypeError):
108
109
  pass
109
110
  if is_model_parameter:
@@ -129,7 +130,7 @@ class DataValidator:
129
130
  raise ValueError(f"Mismatch: version '{param_version}' vs. file '{file_stem}'.")
130
131
 
131
132
  if param_version is None:
132
- self._logger.warning(f"File '{file_stem}' has no parameter version defined.")
133
+ self.logger.warning(f"File '{file_stem}' has no parameter version defined.")
133
134
 
134
135
  @staticmethod
135
136
  def validate_model_parameter(par_dict):
@@ -153,6 +154,46 @@ class DataValidator:
153
154
  )
154
155
  return data_validator.validate_and_transform(is_model_parameter=True)
155
156
 
157
+ @staticmethod
158
+ def validate_data_files(
159
+ file_directory=None,
160
+ file_name=None,
161
+ is_model_parameter=True,
162
+ check_exact_data_type=False,
163
+ schema_file=None,
164
+ ):
165
+ """
166
+ Validate data or model parameters in files in a directory or a single file.
167
+
168
+ Parameters
169
+ ----------
170
+ file_directory: str or Path
171
+ Directory with files to be validated.
172
+ file_name: str or Path
173
+ Name of the file to be validated.
174
+ is_model_parameter: bool
175
+ This is a model parameter (add some data preparation).
176
+ check_exact_data_type: bool
177
+ Require exact data type for validation.
178
+ """
179
+ if file_directory:
180
+ file_list = sorted(Path(file_directory).rglob("*.json"))
181
+ elif file_name:
182
+ file_list = [Path(file_name)]
183
+ else:
184
+ return
185
+
186
+ for data_file in file_list:
187
+ parameter_name = re.sub(r"-\d+\.\d+\.\d+", "", data_file.stem)
188
+ schema_path = schema_file or schema.get_model_parameter_schema_file(f"{parameter_name}")
189
+ data_validator = DataValidator(
190
+ schema_file=schema_path,
191
+ data_file=data_file,
192
+ check_exact_data_type=check_exact_data_type,
193
+ )
194
+ data_validator.validate_and_transform(is_model_parameter)
195
+ data_validator.logger.info(f"Validated data file {data_file} with schema {schema_path}")
196
+
156
197
  def _validate_data_dict(self, is_model_parameter=False, lists_as_strings=False):
157
198
  """
158
199
  Validate values in a dictionary.
@@ -208,8 +249,10 @@ class DataValidator:
208
249
  self.data_dict.get("instrument"), self.data_dict.get("site")
209
250
  )
210
251
 
211
- for version_string in ("version", "parameter_version", "model_version"):
212
- self._check_version_string(self.data_dict.get(version_string))
252
+ for version_type in ("version", "parameter_version", "model_version"):
253
+ version_string = self.data_dict.get(version_type, "0.0.0")
254
+ if not is_valid_semantic_version(version_string):
255
+ raise ValueError(f"Invalid version string '{version_string}'")
213
256
 
214
257
  if lists_as_strings:
215
258
  self._convert_results_to_model_format()
@@ -278,7 +321,7 @@ class DataValidator:
278
321
  "table_columns", None
279
322
  )
280
323
  except IndexError:
281
- self._logger.error(f"Error reading validation schema from {self.schema_file_name}")
324
+ self.logger.error(f"Error reading validation schema from {self.schema_file_name}")
282
325
  raise
283
326
 
284
327
  if self._data_description is not None:
@@ -327,7 +370,7 @@ class DataValidator:
327
370
  for entry in self._data_description:
328
371
  if entry.get("required", False):
329
372
  if entry["name"] in self.data_table.columns:
330
- self._logger.debug(f"Found required data column {entry['name']}")
373
+ self.logger.debug(f"Found required data column {entry['name']}")
331
374
  else:
332
375
  raise KeyError(f"Missing required column {entry['name']}")
333
376
 
@@ -353,18 +396,18 @@ class DataValidator:
353
396
  _columns_by_which_to_reverse_sort.append(entry["name"])
354
397
 
355
398
  if len(_columns_by_which_to_sort) > 0:
356
- self._logger.debug(f"Sorting data columns: {_columns_by_which_to_sort}")
399
+ self.logger.debug(f"Sorting data columns: {_columns_by_which_to_sort}")
357
400
  try:
358
401
  self.data_table.sort(_columns_by_which_to_sort)
359
402
  except AttributeError:
360
- self._logger.error("No data table defined for sorting")
403
+ self.logger.error("No data table defined for sorting")
361
404
  raise
362
405
  elif len(_columns_by_which_to_reverse_sort) > 0:
363
- self._logger.debug(f"Reverse sorting data columns: {_columns_by_which_to_reverse_sort}")
406
+ self.logger.debug(f"Reverse sorting data columns: {_columns_by_which_to_reverse_sort}")
364
407
  try:
365
408
  self.data_table.sort(_columns_by_which_to_reverse_sort, reverse=True)
366
409
  except AttributeError:
367
- self._logger.error("No data table defined for reverse sorting")
410
+ self.logger.error("No data table defined for reverse sorting")
368
411
  raise
369
412
 
370
413
  def _check_data_for_duplicates(self):
@@ -379,7 +422,7 @@ class DataValidator:
379
422
  """
380
423
  _column_with_unique_requirement = self._get_unique_column_requirement()
381
424
  if len(_column_with_unique_requirement) == 0:
382
- self._logger.debug("No data columns with unique value requirement")
425
+ self.logger.debug("No data columns with unique value requirement")
383
426
  return
384
427
  _data_table_unique_for_key_column = unique(
385
428
  self.data_table, keys=_column_with_unique_requirement
@@ -412,10 +455,10 @@ class DataValidator:
412
455
 
413
456
  for entry in self._data_description:
414
457
  if "input_processing" in entry and "remove_duplicates" in entry["input_processing"]:
415
- self._logger.debug(f"Removing duplicates for column {entry['name']}")
458
+ self.logger.debug(f"Removing duplicates for column {entry['name']}")
416
459
  _unique_required_column.append(entry["name"])
417
460
 
418
- self._logger.debug(f"Unique required columns: {_unique_required_column}")
461
+ self.logger.debug(f"Unique required columns: {_unique_required_column}")
419
462
  return _unique_required_column
420
463
 
421
464
  def _get_reference_unit(self, column_name):
@@ -470,7 +513,7 @@ class DataValidator:
470
513
  dtype=dtype,
471
514
  allow_subtypes=(not self.check_exact_data_type),
472
515
  ):
473
- self._logger.error(
516
+ self.logger.error(
474
517
  f"Invalid data type in column '{column_name}'. "
475
518
  f"Expected type '{reference_dtype}', found '{dtype}' "
476
519
  f"(exact type: {self.check_exact_data_type})"
@@ -505,9 +548,9 @@ class DataValidator:
505
548
  data = np.array(data)
506
549
 
507
550
  if np.isnan(data).any():
508
- self._logger.info(f"Column {col_name} contains NaN.")
551
+ self.logger.info(f"Column {col_name} contains NaN.")
509
552
  if np.isinf(data).any():
510
- self._logger.info(f"Column {col_name} contains infinite value.")
553
+ self.logger.info(f"Column {col_name} contains infinite value.")
511
554
 
512
555
  entry = self._get_data_description(col_name)
513
556
  if "allow_nan" in entry.get("input_processing", {}):
@@ -593,7 +636,7 @@ class DataValidator:
593
636
  # ensure that the data type is preserved (e.g., integers)
594
637
  return (type(data)(u.Unit(column_unit).to(reference_unit) * data), reference_unit)
595
638
  except (u.core.UnitConversionError, ValueError) as exc:
596
- self._logger.error(
639
+ self.logger.error(
597
640
  f"Invalid unit in data column '{col_name}'. "
598
641
  f"Expected type '{reference_unit}', found '{column_unit}'"
599
642
  )
@@ -696,9 +739,9 @@ class DataValidator:
696
739
  try:
697
740
  col_index = int(col_name)
698
741
  if col_index < max_logs:
699
- self._logger.debug(message)
742
+ self.logger.debug(message)
700
743
  except (ValueError, TypeError):
701
- self._logger.debug(message)
744
+ self.logger.debug(message)
702
745
 
703
746
  @staticmethod
704
747
  def _interval_check(data, axis_range, range_type):
@@ -817,7 +860,7 @@ class DataValidator:
817
860
  except IndexError as exc:
818
861
  if len(self._data_description) == 1: # all columns are described by the same schema
819
862
  return self._data_description[0]
820
- self._logger.error(
863
+ self.logger.error(
821
864
  f"Data column '{column_name}' not found in reference column definition"
822
865
  )
823
866
  raise exc
@@ -835,7 +878,7 @@ class DataValidator:
835
878
  try:
836
879
  return _entry[_index]
837
880
  except IndexError:
838
- self._logger.error(
881
+ self.logger.error(
839
882
  f"Data column '{column_name}' not found in reference column definition"
840
883
  )
841
884
  raise
@@ -868,42 +911,31 @@ class DataValidator:
868
911
  if isinstance(self.data_dict["unit"], list):
869
912
  self.data_dict["unit"] = gen.convert_list_to_string(self.data_dict["unit"])
870
913
 
871
- def _check_version_string(self, version):
872
- """
873
- Check that version string follows semantic versioning.
874
-
875
- Parameters
876
- ----------
877
- version: str
878
- version string
879
-
880
- Raises
881
- ------
882
- ValueError
883
- if version string does not follow semantic versioning
884
-
885
- """
886
- if version is None:
887
- return
888
- semver_regex = r"^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$"
889
- if not re.match(semver_regex, version):
890
- raise ValueError(f"Invalid version string '{version}'")
891
- self._logger.debug(f"Valid version string '{version}'")
892
-
893
914
  def _check_site_and_array_element_consistency(self, instrument, site):
894
915
  """
895
916
  Check that site and array element names are consistent.
896
917
 
897
918
  An example for an inconsistency is 'LSTN' at site 'South'
898
919
  """
899
- if not all([instrument, site]) or "OBS" in instrument:
920
+ if not (instrument and site):
921
+ return
922
+
923
+ instruments = [instrument] if isinstance(instrument, str) else instrument
924
+ if any(inst.startswith("OBS") for inst in instruments):
900
925
  return
901
926
 
902
927
  def to_sorted_list(value):
903
928
  """Return value as sorted list."""
904
929
  return [value] if isinstance(value, str) else sorted(value)
905
930
 
906
- instrument_site = to_sorted_list(names.get_site_from_array_element_name(instrument))
931
+ instrument_sites = [names.get_site_from_array_element_name(inst) for inst in instruments]
932
+ # names.get_site_from_array_element_name might return a list
933
+ flat_sites = [
934
+ s
935
+ for sublist in instrument_sites
936
+ for s in (sublist if isinstance(sublist, list) else [sublist])
937
+ ]
938
+ instrument_site = to_sorted_list(set(flat_sites))
907
939
  site = to_sorted_list(site)
908
940
 
909
941
  if instrument_site != site:
simtools/db/db_handler.py CHANGED
@@ -4,6 +4,7 @@ import logging
4
4
  from collections import defaultdict
5
5
  from pathlib import Path
6
6
 
7
+ from simtools import settings
7
8
  from simtools.data_model import validate_data
8
9
  from simtools.db.mongo_db import MongoDBHandler
9
10
  from simtools.io import io_handler
@@ -20,11 +21,6 @@ class DatabaseHandler:
20
21
 
21
22
  - db_simulation_model_version (from db_config): version of the simulation model database
22
23
  - model_version (from production_tables): version of the model contained in the database
23
-
24
- Parameters
25
- ----------
26
- db_config: dict
27
- Dictionary with the DB configuration.
28
24
  """
29
25
 
30
26
  ALLOWED_FILE_EXTENSIONS = [".dat", ".txt", ".lis", ".cfg", ".yml", ".yaml", ".ecsv"]
@@ -33,13 +29,17 @@ class DatabaseHandler:
33
29
  model_parameters_cached = {}
34
30
  model_versions_cached = {}
35
31
 
36
- def __init__(self, db_config=None):
32
+ def __init__(self):
37
33
  """Initialize the DatabaseHandler class."""
38
34
  self._logger = logging.getLogger(__name__)
39
35
 
40
- self.db_config = MongoDBHandler.validate_db_config(db_config)
36
+ self.db_config = (
37
+ MongoDBHandler.validate_db_config(dict(settings.config.db_config))
38
+ if settings.config.db_config
39
+ else None
40
+ )
41
41
  self.io_handler = io_handler.IOHandler()
42
- self.mongo_db_handler = MongoDBHandler(db_config) if self.db_config else None
42
+ self.mongo_db_handler = MongoDBHandler(self.db_config) if self.db_config else None
43
43
 
44
44
  self.db_name = (
45
45
  MongoDBHandler.get_db_name(
@@ -50,6 +50,17 @@ class DatabaseHandler:
50
50
  else None
51
51
  )
52
52
 
53
+ def is_configured(self):
54
+ """
55
+ Check if the DatabaseHandler is configured.
56
+
57
+ Returns
58
+ -------
59
+ bool
60
+ True if the DatabaseHandler is configured, False otherwise.
61
+ """
62
+ return self.mongo_db_handler is not None
63
+
53
64
  def get_db_name(self, db_name=None, db_simulation_model_version=None, model_name=None):
54
65
  """Build DB name from configuration."""
55
66
  if db_name:
@@ -186,7 +186,8 @@ def _read_production_tables(model_path):
186
186
  models = [model_path.name]
187
187
  if (model_path / "info.yml").exists():
188
188
  info = ascii_handler.collect_data_from_file(file_name=model_path / "info.yml")
189
- models.extend(info.get("model_version_history", []))
189
+ if info.get("model_update") == "patch_update":
190
+ models.extend(info.get("model_version_history", []))
190
191
  # sort oldest --> newest
191
192
  models = sorted(set(models), key=Version, reverse=False)
192
193
  for model in models: