gammasimtools 0.9.0__py3-none-any.whl → 0.10.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 (96) hide show
  1. {gammasimtools-0.9.0.dist-info → gammasimtools-0.10.0.dist-info}/METADATA +2 -2
  2. {gammasimtools-0.9.0.dist-info → gammasimtools-0.10.0.dist-info}/RECORD +94 -85
  3. {gammasimtools-0.9.0.dist-info → gammasimtools-0.10.0.dist-info}/entry_points.txt +2 -1
  4. simtools/_version.py +2 -2
  5. simtools/applications/calculate_trigger_rate.py +15 -38
  6. simtools/applications/convert_all_model_parameters_from_simtel.py +9 -28
  7. simtools/applications/convert_geo_coordinates_of_array_elements.py +47 -45
  8. simtools/applications/convert_model_parameter_from_simtel.py +2 -2
  9. simtools/applications/db_add_file_to_db.py +1 -2
  10. simtools/applications/db_add_simulation_model_from_repository_to_db.py +110 -0
  11. simtools/applications/db_add_value_from_json_to_db.py +1 -2
  12. simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +6 -6
  13. simtools/applications/db_get_file_from_db.py +11 -12
  14. simtools/applications/db_get_parameter_from_db.py +44 -32
  15. simtools/applications/derive_photon_electron_spectrum.py +99 -0
  16. simtools/applications/generate_array_config.py +17 -17
  17. simtools/applications/generate_regular_arrays.py +15 -15
  18. simtools/applications/generate_simtel_array_histograms.py +11 -48
  19. simtools/applications/production_generate_simulation_config.py +25 -7
  20. simtools/applications/production_scale_events.py +2 -2
  21. simtools/applications/simulate_prod.py +1 -1
  22. simtools/applications/simulate_prod_htcondor_generator.py +26 -26
  23. simtools/applications/submit_data_from_external.py +12 -4
  24. simtools/applications/submit_model_parameter_from_external.py +8 -6
  25. simtools/applications/validate_camera_efficiency.py +2 -2
  26. simtools/applications/validate_file_using_schema.py +23 -19
  27. simtools/camera/single_photon_electron_spectrum.py +168 -0
  28. simtools/configuration/commandline_parser.py +8 -1
  29. simtools/constants.py +10 -3
  30. simtools/corsika/corsika_config.py +8 -7
  31. simtools/corsika/corsika_histograms.py +1 -1
  32. simtools/data_model/data_reader.py +0 -3
  33. simtools/data_model/metadata_collector.py +3 -4
  34. simtools/data_model/metadata_model.py +8 -124
  35. simtools/data_model/model_data_writer.py +17 -63
  36. simtools/data_model/schema.py +213 -0
  37. simtools/data_model/validate_data.py +9 -44
  38. simtools/db/db_handler.py +323 -495
  39. simtools/db/db_model_upload.py +139 -0
  40. simtools/io_operations/hdf5_handler.py +54 -24
  41. simtools/layout/array_layout.py +33 -28
  42. simtools/model/array_model.py +13 -7
  43. simtools/model/model_parameter.py +22 -54
  44. simtools/model/site_model.py +2 -2
  45. simtools/production_configuration/calculate_statistical_errors_grid_point.py +119 -144
  46. simtools/production_configuration/event_scaler.py +7 -17
  47. simtools/production_configuration/generate_simulation_config.py +5 -32
  48. simtools/production_configuration/interpolation_handler.py +8 -11
  49. simtools/runners/corsika_simtel_runner.py +3 -1
  50. simtools/schemas/input/MST_mirror_2f_measurements.schema.yml +39 -0
  51. simtools/schemas/input/single_pe_spectrum.schema.yml +38 -0
  52. simtools/schemas/integration_tests_config.metaschema.yml +10 -0
  53. simtools/schemas/model_parameter.metaschema.yml +7 -2
  54. simtools/schemas/model_parameter_and_data_schema.metaschema.yml +2 -0
  55. simtools/schemas/model_parameters/array_element_position_utm.schema.yml +1 -1
  56. simtools/schemas/model_parameters/array_window.schema.yml +37 -0
  57. simtools/schemas/model_parameters/asum_clipping.schema.yml +0 -4
  58. simtools/schemas/model_parameters/channels_per_chip.schema.yml +1 -1
  59. simtools/schemas/model_parameters/corsika_iact_io_buffer.schema.yml +2 -2
  60. simtools/schemas/model_parameters/dsum_clipping.schema.yml +0 -2
  61. simtools/schemas/model_parameters/dsum_ignore_below.schema.yml +0 -2
  62. simtools/schemas/model_parameters/dsum_offset.schema.yml +0 -2
  63. simtools/schemas/model_parameters/dsum_pedsub.schema.yml +0 -2
  64. simtools/schemas/model_parameters/dsum_pre_clipping.schema.yml +0 -2
  65. simtools/schemas/model_parameters/dsum_prescale.schema.yml +0 -2
  66. simtools/schemas/model_parameters/dsum_presum_max.schema.yml +0 -2
  67. simtools/schemas/model_parameters/dsum_presum_shift.schema.yml +0 -2
  68. simtools/schemas/model_parameters/dsum_shaping.schema.yml +0 -2
  69. simtools/schemas/model_parameters/dsum_shaping_renormalize.schema.yml +0 -2
  70. simtools/schemas/model_parameters/dsum_threshold.schema.yml +0 -2
  71. simtools/schemas/model_parameters/dsum_zero_clip.schema.yml +0 -2
  72. simtools/schemas/model_parameters/fadc_compensate_pedestal.schema.yml +1 -1
  73. simtools/schemas/model_parameters/fadc_lg_compensate_pedestal.schema.yml +1 -1
  74. simtools/schemas/model_parameters/fadc_noise.schema.yml +3 -3
  75. simtools/schemas/model_parameters/fake_mirror_list.schema.yml +33 -0
  76. simtools/schemas/model_parameters/laser_photons.schema.yml +2 -2
  77. simtools/schemas/model_parameters/secondary_mirror_degraded_reflection.schema.yml +1 -1
  78. simtools/schemas/production_configuration_metrics.schema.yml +68 -0
  79. simtools/schemas/production_tables.schema.yml +41 -0
  80. simtools/simtel/simtel_config_writer.py +5 -6
  81. simtools/simtel/simtel_io_histogram.py +32 -67
  82. simtools/simtel/simtel_io_histograms.py +15 -30
  83. simtools/simtel/simulator_array.py +2 -1
  84. simtools/simtel/simulator_camera_efficiency.py +5 -0
  85. simtools/simtel/simulator_light_emission.py +3 -1
  86. simtools/simtel/simulator_ray_tracing.py +2 -1
  87. simtools/testing/helpers.py +6 -13
  88. simtools/testing/validate_output.py +131 -47
  89. simtools/utils/general.py +102 -12
  90. simtools/utils/names.py +24 -20
  91. simtools/applications/db_add_model_parameters_from_repository_to_db.py +0 -176
  92. simtools/db/db_array_elements.py +0 -130
  93. {gammasimtools-0.9.0.dist-info → gammasimtools-0.10.0.dist-info}/LICENSE +0 -0
  94. {gammasimtools-0.9.0.dist-info → gammasimtools-0.10.0.dist-info}/WHEEL +0 -0
  95. {gammasimtools-0.9.0.dist-info → gammasimtools-0.10.0.dist-info}/top_level.txt +0 -0
  96. /simtools/{camera_efficiency.py → camera/camera_efficiency.py} +0 -0
@@ -239,7 +239,14 @@ class CommandLineParser(argparse.ArgumentParser):
239
239
  if "model_version" in model_options:
240
240
  _job_group.add_argument(
241
241
  "--model_version",
242
- help="model version",
242
+ help="production model version",
243
+ type=str,
244
+ default=None,
245
+ )
246
+ if "parameter_version" in model_options:
247
+ _job_group.add_argument(
248
+ "--parameter_version",
249
+ help="model parameter version",
243
250
  type=str,
244
251
  default=None,
245
252
  )
simtools/constants.py CHANGED
@@ -2,8 +2,15 @@
2
2
 
3
3
  from importlib.resources import files
4
4
 
5
+ # Schema path
6
+ SCHEMA_PATH = files("simtools") / "schemas"
5
7
  # Path to metadata jsonschema
6
- METADATA_JSON_SCHEMA = files("simtools") / "schemas/metadata.metaschema.yml"
7
-
8
+ METADATA_JSON_SCHEMA = SCHEMA_PATH / "metadata.metaschema.yml"
9
+ # Path to model parameter metaschema
10
+ MODEL_PARAMETER_METASCHEMA = SCHEMA_PATH / "model_parameter.metaschema.yml"
11
+ # Path to model parameter description metaschema
12
+ MODEL_PARAMETER_DESCRIPTION_METASCHEMA = (
13
+ SCHEMA_PATH / "model_parameter_and_data_schema.metaschema.yml"
14
+ )
8
15
  # Path to model parameter schema files
9
- MODEL_PARAMETER_SCHEMA_PATH = files("simtools") / "schemas/model_parameters"
16
+ MODEL_PARAMETER_SCHEMA_PATH = SCHEMA_PATH / "model_parameters"
@@ -6,7 +6,6 @@ from pathlib import Path
6
6
  import numpy as np
7
7
  from astropy import units as u
8
8
 
9
- import simtools.utils.general as gen
10
9
  from simtools.corsika.primary_particle import PrimaryParticle
11
10
  from simtools.io_operations import io_handler
12
11
  from simtools.model.model_parameter import ModelParameter
@@ -111,8 +110,6 @@ class CorsikaConfig:
111
110
  if args_dict is None:
112
111
  return {}
113
112
 
114
- self._logger.debug("Setting CORSIKA parameters ")
115
-
116
113
  self._is_file_updated = False
117
114
  self.azimuth_angle = int(args_dict["azimuth_angle"].to("deg").value)
118
115
  self.zenith_angle = args_dict["zenith_angle"].to("deg").value
@@ -243,7 +240,7 @@ class CorsikaConfig:
243
240
 
244
241
  def _input_config_corsika_particle_kinetic_energy_cutoff(self, entry):
245
242
  """Return ECUTS parameter CORSIKA format."""
246
- e_cuts = gen.convert_string_to_list(entry["value"])
243
+ e_cuts = entry["value"]
247
244
  return [
248
245
  f"{e_cuts[0]*u.Unit(entry['unit']).to('GeV')} "
249
246
  f"{e_cuts[1]*u.Unit(entry['unit']).to('GeV')} "
@@ -280,7 +277,7 @@ class CorsikaConfig:
280
277
 
281
278
  def _input_config_corsika_cherenkov_wavelength(self, entry):
282
279
  """Return CWAVLG parameter CORSIKA format."""
283
- wavelength_range = gen.convert_string_to_list(entry["value"])
280
+ wavelength_range = entry["value"]
284
281
  return [
285
282
  f"{wavelength_range[0]*u.Unit(entry['unit']).to('nm')}",
286
283
  f"{wavelength_range[1]*u.Unit(entry['unit']).to('nm')}",
@@ -318,8 +315,12 @@ class CorsikaConfig:
318
315
  }
319
316
 
320
317
  def _input_config_io_buff(self, entry):
321
- """Return IO_BUFFER parameter CORSIKA format."""
322
- return f"{entry['value']}{entry['unit']}"
318
+ """Return IO_BUFFER parameter CORSIKA format (Byte or MB required)."""
319
+ value = entry["value"] * u.Unit(entry["unit"]).to("Mbyte")
320
+ # check if value is integer-like
321
+ if value.is_integer():
322
+ return f"{int(value)}MB"
323
+ return f"{int(entry['value'] * u.Unit(entry['unit']).to('byte'))}"
323
324
 
324
325
  def _rotate_azimuth_by_180deg(self, az, correct_for_geomagnetic_field_alignment=True):
325
326
  """
@@ -678,7 +678,7 @@ class CorsikaHistograms:
678
678
  ----------
679
679
  new_individual_telescopes: bool
680
680
  if False, the histograms are supposed to be filled for all telescopes.
681
- if True, one histogram is set for each telescope sepparately.
681
+ if True, one histogram is set for each telescope separately.
682
682
  """
683
683
  if new_individual_telescopes is None:
684
684
  self._individual_telescopes = False
@@ -112,9 +112,6 @@ def read_value_from_file(file_name, schema_file=None, validate=False):
112
112
  _logger.info("Reading data from %s", file_name)
113
113
 
114
114
  if validate:
115
- if schema_file is None and "meta_schema_url" in data:
116
- schema_file = data["meta_schema_url"]
117
- _logger.debug(f"Using schema from meta_schema_url: {schema_file}")
118
115
  if schema_file is None:
119
116
  _collector = MetadataCollector(None, metadata_file_name=file_name)
120
117
  schema_file = _collector.get_data_model_schema_file_name()
@@ -10,13 +10,12 @@ import datetime
10
10
  import getpass
11
11
  import logging
12
12
  import uuid
13
- from importlib.resources import files
14
13
  from pathlib import Path
15
14
 
16
15
  import simtools.constants
17
16
  import simtools.utils.general as gen
18
17
  import simtools.version
19
- from simtools.data_model import metadata_model
18
+ from simtools.data_model import metadata_model, schema
20
19
  from simtools.io_operations import io_handler
21
20
  from simtools.utils import names
22
21
 
@@ -135,7 +134,7 @@ class MetadataCollector:
135
134
  # from data model name
136
135
  if self.data_model_name:
137
136
  self._logger.debug(f"Schema file from data model name: {self.data_model_name}")
138
- return f"{files('simtools')}/schemas/model_parameters/{self.data_model_name}.schema.yml"
137
+ return str(schema.get_model_parameter_schema_file(self.data_model_name))
139
138
 
140
139
  # from input metadata
141
140
  try:
@@ -267,7 +266,7 @@ class MetadataCollector:
267
266
  self._logger.error("Unknown metadata file format: %s", metadata_file_name)
268
267
  raise gen.InvalidConfigDataError
269
268
 
270
- metadata_model.validate_schema(_input_metadata, None)
269
+ schema.validate_dict_using_schema(_input_metadata, None)
271
270
 
272
271
  return gen.change_dict_keys_case(
273
272
  self._process_metadata_from_file(_input_metadata),
@@ -9,48 +9,12 @@ Follows CTAO top-level data model definition.
9
9
  """
10
10
 
11
11
  import logging
12
- from importlib.resources import files
13
12
 
14
- import jsonschema
15
-
16
- import simtools.constants
17
- import simtools.utils.general as gen
18
- from simtools.data_model import format_checkers
19
- from simtools.utils import names
13
+ import simtools.data_model.schema
20
14
 
21
15
  _logger = logging.getLogger(__name__)
22
16
 
23
17
 
24
- def validate_schema(data, schema_file):
25
- """
26
- Validate dictionary against schema.
27
-
28
- Parameters
29
- ----------
30
- data
31
- dictionary to be validated
32
- schema_file (dict)
33
- schema used for validation
34
-
35
- Raises
36
- ------
37
- jsonschema.exceptions.ValidationError
38
- if validation fails
39
-
40
- """
41
- schema, schema_file = _load_schema(
42
- schema_file,
43
- data.get("schema_version", "0.1.0"), # default version to ensure backward compatibility
44
- )
45
-
46
- try:
47
- jsonschema.validate(data, schema=schema, format_checker=format_checkers.format_checker)
48
- except jsonschema.exceptions.ValidationError:
49
- _logger.error(f"Failed using {schema}")
50
- raise
51
- _logger.debug(f"Successful validation of data using schema from {schema_file}")
52
-
53
-
54
18
  def get_default_metadata_dict(schema_file=None, observatory="CTA"):
55
19
  """
56
20
  Return metadata schema with default values.
@@ -71,90 +35,10 @@ def get_default_metadata_dict(schema_file=None, observatory="CTA"):
71
35
 
72
36
 
73
37
  """
74
- schema, _ = _load_schema(schema_file)
38
+ schema = simtools.data_model.schema.load_schema(schema_file)
75
39
  return _fill_defaults(schema["definitions"], observatory)
76
40
 
77
41
 
78
- def _load_schema(schema_file=None, schema_version=None):
79
- """
80
- Load parameter schema from file from simpipe metadata schema.
81
-
82
- Returns
83
- -------
84
- schema_file: str
85
- File name schema is loaded from. If schema_file is not given,
86
- the default schema file name is returned.
87
- schema_version: str
88
- Schema version.
89
-
90
- Raises
91
- ------
92
- FileNotFoundError
93
- if schema file is not found
94
-
95
- """
96
- if schema_file is None:
97
- schema_file = files("simtools").joinpath(simtools.constants.METADATA_JSON_SCHEMA)
98
-
99
- try:
100
- schema = gen.collect_data_from_file(file_name=schema_file)
101
- except FileNotFoundError:
102
- schema_file = files("simtools").joinpath("schemas") / schema_file
103
- schema = gen.collect_data_from_file(file_name=schema_file)
104
-
105
- if isinstance(schema, list): # schema file with several schemas defined
106
- if schema_version is None:
107
- raise ValueError(f"Schema version not given in {schema_file}.")
108
- schema = next((doc for doc in schema if doc.get("version") == schema_version), None)
109
- if schema is None:
110
- raise ValueError(f"Schema version {schema_version} not found in {schema_file}.")
111
- elif schema_version is not None and schema_version != schema.get("version"):
112
- _logger.warning(f"Schema version {schema_version} does not match {schema.get('version')}")
113
-
114
- _logger.debug(f"Loading schema from {schema_file}")
115
- _add_array_elements("InstrumentTypeElement", schema)
116
-
117
- return schema, schema_file
118
-
119
-
120
- def _add_array_elements(key, schema):
121
- """
122
- Add list of array elements to schema.
123
-
124
- This assumes an element [key]['enum'] is a list of elements.
125
-
126
- Parameters
127
- ----------
128
- key: str
129
- Key in schema dictionary
130
- schema: dict
131
- Schema dictionary
132
-
133
- Returns
134
- -------
135
- dict
136
- Schema dictionary with added array elements.
137
-
138
- """
139
- _list_of_array_elements = sorted(names.array_elements().keys())
140
-
141
- def recursive_search(sub_schema, key):
142
- if key in sub_schema:
143
- if "enum" in sub_schema[key] and isinstance(sub_schema[key]["enum"], list):
144
- sub_schema[key]["enum"] = list(
145
- set(sub_schema[key]["enum"] + _list_of_array_elements)
146
- )
147
- else:
148
- sub_schema[key]["enum"] = _list_of_array_elements
149
- else:
150
- for _, v in sub_schema.items():
151
- if isinstance(v, dict):
152
- recursive_search(v, key)
153
-
154
- recursive_search(schema, key)
155
- return schema
156
-
157
-
158
42
  def _resolve_references(yaml_data, observatory="CTA"):
159
43
  """
160
44
  Resolve references in yaml data and expand the received dictionary accordingly.
@@ -227,21 +111,21 @@ def _fill_defaults(schema, observatory="CTA"):
227
111
  return defaults
228
112
 
229
113
 
230
- def _fill_defaults_recursive(subschema, current_dict):
114
+ def _fill_defaults_recursive(sub_schema, current_dict):
231
115
  """
232
- Recursively fill default values from the subschema into the current dictionary.
116
+ Recursively fill default values from the sub_schema into the current dictionary.
233
117
 
234
118
  Parameters
235
119
  ----------
236
- subschema: dict
237
- Subschema describing part of the input data.
120
+ sub_schema: dict
121
+ Sub schema describing part of the input data.
238
122
  current_dict: dict
239
123
  Current dictionary to fill with default values.
240
124
  """
241
- if "properties" not in subschema:
125
+ if "properties" not in sub_schema:
242
126
  _raise_missing_properties_error()
243
127
 
244
- for prop, prop_schema in subschema["properties"].items():
128
+ for prop, prop_schema in sub_schema["properties"].items():
245
129
  _process_property(prop, prop_schema, current_dict)
246
130
 
247
131
 
@@ -10,8 +10,7 @@ import yaml
10
10
  from astropy.io.registry.base import IORegistryError
11
11
 
12
12
  import simtools.utils.general as gen
13
- from simtools.constants import MODEL_PARAMETER_SCHEMA_PATH
14
- from simtools.data_model import validate_data
13
+ from simtools.data_model import schema, validate_data
15
14
  from simtools.data_model.metadata_collector import MetadataCollector
16
15
  from simtools.io_operations import io_handler
17
16
  from simtools.utils import names, value_conversion
@@ -122,7 +121,7 @@ class ModelDataWriter:
122
121
  parameter_name,
123
122
  value,
124
123
  instrument,
125
- model_version,
124
+ parameter_version,
126
125
  output_file,
127
126
  output_path=None,
128
127
  use_plain_output_path=False,
@@ -139,8 +138,8 @@ class ModelDataWriter:
139
138
  Value of the parameter.
140
139
  instrument: str
141
140
  Name of the instrument.
142
- model_version: str
143
- Version of the model.
141
+ parameter_version: str
142
+ Version of the parameter.
144
143
  output_file: str
145
144
  Name of output file.
146
145
  output_path: str or Path
@@ -163,7 +162,7 @@ class ModelDataWriter:
163
162
  use_plain_output_path=use_plain_output_path,
164
163
  )
165
164
  _json_dict = writer.get_validated_parameter_dict(
166
- parameter_name, value, instrument, model_version
165
+ parameter_name, value, instrument, parameter_version
167
166
  )
168
167
  writer.write_dict_to_model_parameter_json(output_file, _json_dict)
169
168
  if metadata_input_dict is not None:
@@ -175,7 +174,9 @@ class ModelDataWriter:
175
174
  )
176
175
  return _json_dict
177
176
 
178
- def get_validated_parameter_dict(self, parameter_name, value, instrument, model_version):
177
+ def get_validated_parameter_dict(
178
+ self, parameter_name, value, instrument, parameter_version, schema_version=None
179
+ ):
179
180
  """
180
181
  Get validated parameter dictionary.
181
182
 
@@ -187,8 +188,10 @@ class ModelDataWriter:
187
188
  Value of the parameter.
188
189
  instrument: str
189
190
  Name of the instrument.
190
- model_version: str
191
- Version of the model.
191
+ parameter_version: str
192
+ Version of the parameter.
193
+ schema_version: str
194
+ Version of the schema.
192
195
 
193
196
  Returns
194
197
  -------
@@ -196,29 +199,26 @@ class ModelDataWriter:
196
199
  Validated parameter dictionary.
197
200
  """
198
201
  self._logger.debug(f"Getting validated parameter dictionary for {instrument}")
199
- schema_file = self._read_model_parameter_schema(parameter_name)
202
+ schema_file = schema.get_model_parameter_schema_file(parameter_name)
203
+ self.schema_dict = gen.collect_data_from_file(schema_file)
200
204
 
201
205
  try: # e.g. instrument is 'North"
202
206
  site = names.validate_site_name(instrument)
203
207
  except ValueError: # e.g. instrument is 'LSTN-01'
204
208
  site = names.get_site_from_array_element_name(instrument)
205
209
 
206
- try:
207
- applicable = self._get_parameter_applicability(instrument)
208
- except ValueError:
209
- applicable = True # Default to True (expect that this field goes in future)
210
-
211
210
  value, unit = value_conversion.split_value_and_unit(value)
212
211
 
213
212
  data_dict = {
213
+ "schema_version": schema.get_model_parameter_schema_version(schema_version),
214
214
  "parameter": parameter_name,
215
215
  "instrument": instrument,
216
216
  "site": site,
217
- "version": model_version,
217
+ "parameter_version": parameter_version,
218
+ "unique_id": None,
218
219
  "value": value,
219
220
  "unit": unit,
220
221
  "type": self._get_parameter_type(),
221
- "applicable": applicable,
222
222
  "file": self._parameter_is_a_file(),
223
223
  }
224
224
  return self.validate_and_transform(
@@ -227,22 +227,6 @@ class ModelDataWriter:
227
227
  is_model_parameter=True,
228
228
  )
229
229
 
230
- def _read_model_parameter_schema(self, parameter_name):
231
- """
232
- Read model parameter schema.
233
-
234
- Parameters
235
- ----------
236
- parameter_name: str
237
- Name of the parameter.
238
- """
239
- schema_file = MODEL_PARAMETER_SCHEMA_PATH / f"{parameter_name}.schema.yml"
240
- try:
241
- self.schema_dict = gen.collect_data_from_file(file_name=schema_file)
242
- except FileNotFoundError as exc:
243
- raise FileNotFoundError(f"Schema file not found: {schema_file}") from exc
244
- return schema_file
245
-
246
230
  def _get_parameter_type(self):
247
231
  """
248
232
  Return parameter type from schema.
@@ -273,36 +257,6 @@ class ModelDataWriter:
273
257
  pass
274
258
  return False
275
259
 
276
- def _get_parameter_applicability(self, telescope_name):
277
- """
278
- Check if a parameter is applicable for a given telescope using schema files.
279
-
280
- First check for exact telescope name (e.g., LSTN-01), if not listed in the schema
281
- use telescope type (LSTN).
282
-
283
- Parameters
284
- ----------
285
- telescope_name: str
286
- Telescope name (e.g., LSTN-01)
287
-
288
- Returns
289
- -------
290
- bool
291
- True if parameter is applicable to telescope.
292
-
293
- """
294
- try:
295
- if telescope_name in self.schema_dict["instrument"]["type"]:
296
- return True
297
- except KeyError as exc:
298
- self._logger.error("Schema file does not contain 'instrument:type' key.")
299
- raise exc
300
-
301
- return (
302
- names.get_array_element_type_from_name(telescope_name)
303
- in self.schema_dict["instrument"]["type"]
304
- )
305
-
306
260
  def _get_unit_from_schema(self):
307
261
  """
308
262
  Return unit(s) from schema dict.
@@ -0,0 +1,213 @@
1
+ """Module providing functionality to read and validate dictionaries using schema."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ import jsonschema
7
+
8
+ import simtools.utils.general as gen
9
+ from simtools.constants import (
10
+ METADATA_JSON_SCHEMA,
11
+ MODEL_PARAMETER_METASCHEMA,
12
+ MODEL_PARAMETER_SCHEMA_PATH,
13
+ SCHEMA_PATH,
14
+ )
15
+ from simtools.data_model import format_checkers
16
+ from simtools.utils import names
17
+
18
+ _logger = logging.getLogger(__name__)
19
+
20
+
21
+ def get_get_model_parameter_schema_files(schema_directory=MODEL_PARAMETER_SCHEMA_PATH):
22
+ """
23
+ Return list of parameters and schema files located in schema file directory.
24
+
25
+ Returns
26
+ -------
27
+ list
28
+ List of parameters found in schema file directory.
29
+ list
30
+ List of schema files found in schema file directory.
31
+
32
+ """
33
+ schema_files = sorted(Path(schema_directory).rglob("*.schema.yml"))
34
+ if not schema_files:
35
+ raise FileNotFoundError(f"No schema files found in {schema_directory}")
36
+ parameters = []
37
+ for schema_file in schema_files:
38
+ schema_dict = gen.collect_data_from_file(file_name=schema_file)
39
+ parameters.append(schema_dict.get("name"))
40
+ return parameters, schema_files
41
+
42
+
43
+ def get_model_parameter_schema_file(parameter):
44
+ """
45
+ Return schema file path for a given model parameter.
46
+
47
+ Parameters
48
+ ----------
49
+ parameter: str
50
+ Model parameter name.
51
+
52
+ Returns
53
+ -------
54
+ Path
55
+ Schema file path.
56
+
57
+ """
58
+ schema_file = MODEL_PARAMETER_SCHEMA_PATH / f"{parameter}.schema.yml"
59
+ if not schema_file.exists():
60
+ raise FileNotFoundError(f"Schema file not found: {schema_file}")
61
+ return schema_file
62
+
63
+
64
+ def get_model_parameter_schema_version(schema_version=None):
65
+ """
66
+ Validate and return schema versions.
67
+
68
+ If no schema_version is given, the most recent version is provided.
69
+
70
+ Parameters
71
+ ----------
72
+ schema_version: str
73
+ Schema version.
74
+
75
+ Returns
76
+ -------
77
+ str
78
+ Schema version.
79
+
80
+ """
81
+ schemas = gen.collect_data_from_file(MODEL_PARAMETER_METASCHEMA)
82
+
83
+ if schema_version is None and schemas:
84
+ return schemas[0].get("version")
85
+
86
+ if any(schema.get("version") == schema_version for schema in schemas):
87
+ return schema_version
88
+
89
+ raise ValueError(f"Schema version {schema_version} not found in {MODEL_PARAMETER_METASCHEMA}.")
90
+
91
+
92
+ def validate_dict_using_schema(data, schema_file=None, json_schema=None):
93
+ """
94
+ Validate a data dictionary against a schema.
95
+
96
+ Parameters
97
+ ----------
98
+ data
99
+ dictionary to be validated
100
+ schema_file (dict)
101
+ schema used for validation
102
+
103
+ Raises
104
+ ------
105
+ jsonschema.exceptions.ValidationError
106
+ if validation fails
107
+
108
+ """
109
+ if json_schema is None and schema_file is None:
110
+ _logger.warning(f"No schema provided for validation of {data}")
111
+ return
112
+ if json_schema is None:
113
+ json_schema = load_schema(
114
+ schema_file,
115
+ data.get("schema_version", "0.1.0"), # default version to ensure backward compatibility
116
+ )
117
+
118
+ try:
119
+ jsonschema.validate(data, schema=json_schema, format_checker=format_checkers.format_checker)
120
+ except jsonschema.exceptions.ValidationError as exc:
121
+ _logger.error(f"Validation failed using schema: {json_schema}")
122
+ raise exc
123
+ if data.get("meta_schema_url") and not gen.url_exists(data["meta_schema_url"]):
124
+ raise FileNotFoundError(f"Meta schema URL does not exist: {data['meta_schema_url']}")
125
+
126
+ _logger.debug(f"Successful validation of data using schema ({json_schema.get('name')})")
127
+
128
+
129
+ def load_schema(schema_file=None, schema_version=None):
130
+ """
131
+ Load parameter schema from file.
132
+
133
+ Parameters
134
+ ----------
135
+ schema_file: str
136
+ Path to schema file.
137
+ schema_version: str
138
+ Schema version.
139
+
140
+ Returns
141
+ -------
142
+ schema: dict
143
+ Schema dictionary.
144
+
145
+ Raises
146
+ ------
147
+ FileNotFoundError
148
+ if schema file is not found
149
+
150
+ """
151
+ schema_file = schema_file or METADATA_JSON_SCHEMA
152
+
153
+ for path in (schema_file, SCHEMA_PATH / schema_file):
154
+ try:
155
+ schema = gen.collect_data_from_file(file_name=path)
156
+ break
157
+ except FileNotFoundError:
158
+ continue
159
+ else:
160
+ raise FileNotFoundError(f"Schema file not found: {schema_file}")
161
+
162
+ if isinstance(schema, list): # schema file with several schemas defined
163
+ if schema_version is None:
164
+ raise ValueError(f"Schema version not given in {schema_file}.")
165
+ schema = next((doc for doc in schema if doc.get("version") == schema_version), None)
166
+ if schema is None:
167
+ raise ValueError(f"Schema version {schema_version} not found in {schema_file}.")
168
+ elif schema_version is not None and schema_version != schema.get("version"):
169
+ _logger.warning(f"Schema version {schema_version} does not match {schema.get('version')}")
170
+
171
+ _logger.debug(f"Loading schema from {schema_file}")
172
+ _add_array_elements("InstrumentTypeElement", schema)
173
+
174
+ return schema
175
+
176
+
177
+ def _add_array_elements(key, schema):
178
+ """
179
+ Add list of array elements to schema.
180
+
181
+ Avoids having to list all array elements in multiple schema.
182
+ Assumes an element [key]['enum'] is a list of elements.
183
+
184
+ Parameters
185
+ ----------
186
+ key: str
187
+ Key in schema dictionary
188
+ schema: dict
189
+ Schema dictionary
190
+
191
+ Returns
192
+ -------
193
+ dict
194
+ Schema dictionary with added array elements.
195
+
196
+ """
197
+ _list_of_array_elements = sorted(names.array_elements().keys())
198
+
199
+ def recursive_search(sub_schema, key):
200
+ if key in sub_schema:
201
+ if "enum" in sub_schema[key] and isinstance(sub_schema[key]["enum"], list):
202
+ sub_schema[key]["enum"] = list(
203
+ set(sub_schema[key]["enum"] + _list_of_array_elements)
204
+ )
205
+ else:
206
+ sub_schema[key]["enum"] = _list_of_array_elements
207
+ else:
208
+ for _, v in sub_schema.items():
209
+ if isinstance(v, dict):
210
+ recursive_search(v, key)
211
+
212
+ recursive_search(schema, key)
213
+ return schema