gammasimtools 0.9.0__py3-none-any.whl → 0.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. {gammasimtools-0.9.0.dist-info → gammasimtools-0.11.0.dist-info}/METADATA +4 -2
  2. {gammasimtools-0.9.0.dist-info → gammasimtools-0.11.0.dist-info}/RECORD +133 -117
  3. {gammasimtools-0.9.0.dist-info → gammasimtools-0.11.0.dist-info}/WHEEL +1 -1
  4. {gammasimtools-0.9.0.dist-info → gammasimtools-0.11.0.dist-info}/entry_points.txt +6 -1
  5. simtools/_version.py +9 -4
  6. simtools/applications/calculate_trigger_rate.py +15 -38
  7. simtools/applications/convert_all_model_parameters_from_simtel.py +9 -29
  8. simtools/applications/convert_geo_coordinates_of_array_elements.py +47 -45
  9. simtools/applications/convert_model_parameter_from_simtel.py +2 -3
  10. simtools/applications/db_add_file_to_db.py +1 -3
  11. simtools/applications/db_add_simulation_model_from_repository_to_db.py +110 -0
  12. simtools/applications/db_add_value_from_json_to_db.py +1 -2
  13. simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +6 -6
  14. simtools/applications/db_get_file_from_db.py +11 -12
  15. simtools/applications/db_get_parameter_from_db.py +26 -35
  16. simtools/applications/derive_mirror_rnda.py +1 -2
  17. simtools/applications/derive_photon_electron_spectrum.py +99 -0
  18. simtools/applications/derive_psf_parameters.py +1 -0
  19. simtools/applications/docs_produce_array_element_report.py +71 -0
  20. simtools/applications/docs_produce_model_parameter_reports.py +63 -0
  21. simtools/applications/generate_array_config.py +17 -17
  22. simtools/applications/generate_corsika_histograms.py +2 -2
  23. simtools/applications/generate_regular_arrays.py +19 -17
  24. simtools/applications/generate_simtel_array_histograms.py +11 -48
  25. simtools/applications/production_derive_limits.py +95 -0
  26. simtools/applications/production_generate_simulation_config.py +37 -33
  27. simtools/applications/production_scale_events.py +4 -9
  28. simtools/applications/run_application.py +165 -0
  29. simtools/applications/simulate_light_emission.py +0 -4
  30. simtools/applications/simulate_prod.py +1 -1
  31. simtools/applications/simulate_prod_htcondor_generator.py +26 -26
  32. simtools/applications/submit_data_from_external.py +12 -4
  33. simtools/applications/submit_model_parameter_from_external.py +18 -11
  34. simtools/applications/validate_camera_efficiency.py +2 -2
  35. simtools/applications/validate_file_using_schema.py +26 -22
  36. simtools/camera/single_photon_electron_spectrum.py +168 -0
  37. simtools/configuration/commandline_parser.py +37 -1
  38. simtools/configuration/configurator.py +8 -10
  39. simtools/constants.py +10 -3
  40. simtools/corsika/corsika_config.py +19 -17
  41. simtools/corsika/corsika_histograms.py +5 -7
  42. simtools/corsika/corsika_histograms_visualize.py +2 -4
  43. simtools/data_model/data_reader.py +0 -3
  44. simtools/data_model/metadata_collector.py +20 -12
  45. simtools/data_model/metadata_model.py +8 -124
  46. simtools/data_model/model_data_writer.py +81 -75
  47. simtools/data_model/schema.py +220 -0
  48. simtools/data_model/validate_data.py +79 -68
  49. simtools/db/db_handler.py +350 -492
  50. simtools/db/db_model_upload.py +139 -0
  51. simtools/dependencies.py +112 -0
  52. simtools/io_operations/hdf5_handler.py +54 -24
  53. simtools/layout/array_layout.py +38 -32
  54. simtools/model/array_model.py +13 -7
  55. simtools/model/model_parameter.py +55 -54
  56. simtools/model/site_model.py +2 -2
  57. simtools/production_configuration/calculate_statistical_errors_grid_point.py +119 -145
  58. simtools/production_configuration/event_scaler.py +9 -35
  59. simtools/production_configuration/generate_simulation_config.py +9 -44
  60. simtools/production_configuration/interpolation_handler.py +9 -15
  61. simtools/production_configuration/limits_calculation.py +202 -0
  62. simtools/reporting/docs_read_parameters.py +310 -0
  63. simtools/runners/corsika_simtel_runner.py +4 -4
  64. simtools/schemas/{integration_tests_config.metaschema.yml → application_workflow.metaschema.yml} +61 -27
  65. simtools/schemas/array_elements.yml +8 -0
  66. simtools/schemas/input/MST_mirror_2f_measurements.schema.yml +39 -0
  67. simtools/schemas/input/single_pe_spectrum.schema.yml +38 -0
  68. simtools/schemas/model_parameter.metaschema.yml +103 -2
  69. simtools/schemas/model_parameter_and_data_schema.metaschema.yml +4 -1
  70. simtools/schemas/model_parameters/array_element_position_utm.schema.yml +1 -1
  71. simtools/schemas/model_parameters/array_window.schema.yml +37 -0
  72. simtools/schemas/model_parameters/asum_clipping.schema.yml +0 -4
  73. simtools/schemas/model_parameters/channels_per_chip.schema.yml +1 -1
  74. simtools/schemas/model_parameters/correct_nsb_spectrum_to_telescope_altitude.schema.yml +1 -1
  75. simtools/schemas/model_parameters/corsika_cherenkov_photon_bunch_size.schema.yml +2 -0
  76. simtools/schemas/model_parameters/corsika_cherenkov_photon_wavelength_range.schema.yml +2 -0
  77. simtools/schemas/model_parameters/corsika_first_interaction_height.schema.yml +2 -0
  78. simtools/schemas/model_parameters/corsika_iact_io_buffer.schema.yml +4 -2
  79. simtools/schemas/model_parameters/corsika_iact_max_bunches.schema.yml +2 -0
  80. simtools/schemas/model_parameters/corsika_iact_split_auto.schema.yml +2 -0
  81. simtools/schemas/model_parameters/corsika_longitudinal_shower_development.schema.yml +2 -0
  82. simtools/schemas/model_parameters/corsika_particle_kinetic_energy_cutoff.schema.yml +2 -0
  83. simtools/schemas/model_parameters/corsika_starting_grammage.schema.yml +2 -0
  84. simtools/schemas/model_parameters/dsum_clipping.schema.yml +0 -2
  85. simtools/schemas/model_parameters/dsum_ignore_below.schema.yml +0 -2
  86. simtools/schemas/model_parameters/dsum_offset.schema.yml +0 -2
  87. simtools/schemas/model_parameters/dsum_pedsub.schema.yml +0 -2
  88. simtools/schemas/model_parameters/dsum_pre_clipping.schema.yml +0 -2
  89. simtools/schemas/model_parameters/dsum_prescale.schema.yml +0 -2
  90. simtools/schemas/model_parameters/dsum_presum_max.schema.yml +0 -2
  91. simtools/schemas/model_parameters/dsum_presum_shift.schema.yml +0 -2
  92. simtools/schemas/model_parameters/dsum_shaping.schema.yml +0 -2
  93. simtools/schemas/model_parameters/dsum_shaping_renormalize.schema.yml +0 -2
  94. simtools/schemas/model_parameters/dsum_threshold.schema.yml +0 -2
  95. simtools/schemas/model_parameters/dsum_zero_clip.schema.yml +0 -2
  96. simtools/schemas/model_parameters/fadc_compensate_pedestal.schema.yml +1 -1
  97. simtools/schemas/model_parameters/fadc_lg_compensate_pedestal.schema.yml +1 -1
  98. simtools/schemas/model_parameters/fadc_noise.schema.yml +3 -3
  99. simtools/schemas/model_parameters/fake_mirror_list.schema.yml +33 -0
  100. simtools/schemas/model_parameters/iobuf_maximum.schema.yml +1 -1
  101. simtools/schemas/model_parameters/iobuf_output_maximum.schema.yml +1 -1
  102. simtools/schemas/model_parameters/laser_photons.schema.yml +2 -2
  103. simtools/schemas/model_parameters/lightguide_efficiency_vs_incidence_angle.schema.yml +1 -1
  104. simtools/schemas/model_parameters/lightguide_efficiency_vs_wavelength.schema.yml +1 -1
  105. simtools/schemas/model_parameters/min_photoelectrons.schema.yml +1 -1
  106. simtools/schemas/model_parameters/min_photons.schema.yml +1 -1
  107. simtools/schemas/model_parameters/random_generator.schema.yml +1 -1
  108. simtools/schemas/model_parameters/sampled_output.schema.yml +1 -1
  109. simtools/schemas/model_parameters/save_pe_with_amplitude.schema.yml +1 -1
  110. simtools/schemas/model_parameters/secondary_mirror_degraded_reflection.schema.yml +1 -1
  111. simtools/schemas/model_parameters/store_photoelectrons.schema.yml +1 -1
  112. simtools/schemas/model_parameters/tailcut_scale.schema.yml +1 -1
  113. simtools/schemas/production_configuration_metrics.schema.yml +68 -0
  114. simtools/schemas/production_tables.schema.yml +41 -0
  115. simtools/simtel/simtel_config_reader.py +1 -2
  116. simtools/simtel/simtel_config_writer.py +6 -8
  117. simtools/simtel/simtel_io_histogram.py +32 -68
  118. simtools/simtel/simtel_io_histograms.py +17 -34
  119. simtools/simtel/simulator_array.py +2 -1
  120. simtools/simtel/simulator_camera_efficiency.py +6 -3
  121. simtools/simtel/simulator_light_emission.py +5 -6
  122. simtools/simtel/simulator_ray_tracing.py +3 -4
  123. simtools/testing/configuration.py +2 -1
  124. simtools/testing/helpers.py +6 -13
  125. simtools/testing/validate_output.py +141 -47
  126. simtools/utils/general.py +114 -14
  127. simtools/utils/names.py +299 -157
  128. simtools/utils/value_conversion.py +17 -13
  129. simtools/version.py +2 -2
  130. simtools/visualization/legend_handlers.py +2 -0
  131. simtools/applications/db_add_model_parameters_from_repository_to_db.py +0 -176
  132. simtools/db/db_array_elements.py +0 -130
  133. {gammasimtools-0.9.0.dist-info → gammasimtools-0.11.0.dist-info}/LICENSE +0 -0
  134. {gammasimtools-0.9.0.dist-info → gammasimtools-0.11.0.dist-info}/top_level.txt +0 -0
  135. /simtools/{camera_efficiency.py → camera/camera_efficiency.py} +0 -0
@@ -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,9 +10,9 @@ 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
15
+ from simtools.db import db_handler
16
16
  from simtools.io_operations import io_handler
17
17
  from simtools.utils import names, value_conversion
18
18
 
@@ -122,11 +122,12 @@ class ModelDataWriter:
122
122
  parameter_name,
123
123
  value,
124
124
  instrument,
125
- model_version,
125
+ parameter_version,
126
126
  output_file,
127
127
  output_path=None,
128
128
  use_plain_output_path=False,
129
129
  metadata_input_dict=None,
130
+ db_config=None,
130
131
  ):
131
132
  """
132
133
  Generate DB-style model parameter dict and write it to json file.
@@ -139,8 +140,8 @@ class ModelDataWriter:
139
140
  Value of the parameter.
140
141
  instrument: str
141
142
  Name of the instrument.
142
- model_version: str
143
- Version of the model.
143
+ parameter_version: str
144
+ Version of the parameter.
144
145
  output_file: str
145
146
  Name of output file.
146
147
  output_path: str or Path
@@ -149,6 +150,8 @@ class ModelDataWriter:
149
150
  Use plain output path.
150
151
  metadata_input_dict: dict
151
152
  Input to metadata collector.
153
+ db_config: dict
154
+ Database configuration. If not None, check if parameter with the same version exists.
152
155
 
153
156
  Returns
154
157
  -------
@@ -162,20 +165,73 @@ class ModelDataWriter:
162
165
  output_path=output_path,
163
166
  use_plain_output_path=use_plain_output_path,
164
167
  )
165
- _json_dict = writer.get_validated_parameter_dict(
166
- parameter_name, value, instrument, model_version
167
- )
168
- 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
169
174
  if metadata_input_dict is not None:
170
175
  metadata_input_dict["output_file"] = output_file
171
176
  metadata_input_dict["output_file_format"] = Path(output_file).suffix.lstrip(".")
177
+ metadata = MetadataCollector(args_dict=metadata_input_dict).get_top_level_metadata()
172
178
  writer.write_metadata_to_yml(
173
- metadata=MetadataCollector(args_dict=metadata_input_dict).get_top_level_metadata(),
174
- yml_file=output_path / f"{Path(output_file).stem}",
179
+ metadata=metadata, yml_file=output_path / f"{Path(output_file).stem}"
175
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)
176
187
  return _json_dict
177
188
 
178
- def get_validated_parameter_dict(self, parameter_name, value, instrument, model_version):
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
+
226
+ def get_validated_parameter_dict(
227
+ self,
228
+ parameter_name,
229
+ value,
230
+ instrument,
231
+ parameter_version,
232
+ unique_id=None,
233
+ schema_version=None,
234
+ ):
179
235
  """
180
236
  Get validated parameter dictionary.
181
237
 
@@ -187,8 +243,10 @@ class ModelDataWriter:
187
243
  Value of the parameter.
188
244
  instrument: str
189
245
  Name of the instrument.
190
- model_version: str
191
- Version of the model.
246
+ parameter_version: str
247
+ Version of the parameter.
248
+ schema_version: str
249
+ Version of the schema.
192
250
 
193
251
  Returns
194
252
  -------
@@ -196,29 +254,21 @@ class ModelDataWriter:
196
254
  Validated parameter dictionary.
197
255
  """
198
256
  self._logger.debug(f"Getting validated parameter dictionary for {instrument}")
199
- schema_file = self._read_model_parameter_schema(parameter_name)
200
-
201
- try: # e.g. instrument is 'North"
202
- site = names.validate_site_name(instrument)
203
- except ValueError: # e.g. instrument is 'LSTN-01'
204
- site = names.get_site_from_array_element_name(instrument)
205
-
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)
257
+ schema_file = schema.get_model_parameter_schema_file(parameter_name)
258
+ self.schema_dict = gen.collect_data_from_file(schema_file)
210
259
 
211
260
  value, unit = value_conversion.split_value_and_unit(value)
212
261
 
213
262
  data_dict = {
263
+ "schema_version": schema.get_model_parameter_schema_version(schema_version),
214
264
  "parameter": parameter_name,
215
265
  "instrument": instrument,
216
- "site": site,
217
- "version": model_version,
266
+ "site": names.get_site_from_array_element_name(instrument),
267
+ "parameter_version": parameter_version,
268
+ "unique_id": unique_id,
218
269
  "value": value,
219
270
  "unit": unit,
220
271
  "type": self._get_parameter_type(),
221
- "applicable": applicable,
222
272
  "file": self._parameter_is_a_file(),
223
273
  }
224
274
  return self.validate_and_transform(
@@ -227,22 +277,6 @@ class ModelDataWriter:
227
277
  is_model_parameter=True,
228
278
  )
229
279
 
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
280
  def _get_parameter_type(self):
247
281
  """
248
282
  Return parameter type from schema.
@@ -273,36 +307,6 @@ class ModelDataWriter:
273
307
  pass
274
308
  return False
275
309
 
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
310
  def _get_unit_from_schema(self):
307
311
  """
308
312
  Return unit(s) from schema dict.
@@ -471,7 +475,9 @@ class ModelDataWriter:
471
475
  If yml_file is not defined.
472
476
  """
473
477
  try:
474
- 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
+ )
475
481
  with open(yml_file, "w", encoding="UTF-8") as file:
476
482
  yaml.safe_dump(
477
483
  gen.change_dict_keys_case(metadata, keys_lower_case),
@@ -0,0 +1,220 @@
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")
116
+ or data.get(
117
+ "SCHEMA_VERSION", "0.1.0"
118
+ ), # default version to ensure backward compatibility
119
+ )
120
+
121
+ try:
122
+ jsonschema.validate(data, schema=json_schema, format_checker=format_checkers.format_checker)
123
+ except jsonschema.exceptions.ValidationError as exc:
124
+ _logger.error(f"Validation failed using schema: {json_schema} for data: {data}")
125
+ raise exc
126
+ if (
127
+ isinstance(data, dict)
128
+ and data.get("meta_schema_url")
129
+ and not gen.url_exists(data["meta_schema_url"])
130
+ ):
131
+ raise FileNotFoundError(f"Meta schema URL does not exist: {data['meta_schema_url']}")
132
+
133
+ _logger.debug(f"Successful validation of data using schema ({json_schema.get('name')})")
134
+
135
+
136
+ def load_schema(schema_file=None, schema_version=None):
137
+ """
138
+ Load parameter schema from file.
139
+
140
+ Parameters
141
+ ----------
142
+ schema_file: str
143
+ Path to schema file.
144
+ schema_version: str
145
+ Schema version.
146
+
147
+ Returns
148
+ -------
149
+ schema: dict
150
+ Schema dictionary.
151
+
152
+ Raises
153
+ ------
154
+ FileNotFoundError
155
+ if schema file is not found
156
+
157
+ """
158
+ schema_file = schema_file or METADATA_JSON_SCHEMA
159
+
160
+ for path in (schema_file, SCHEMA_PATH / schema_file):
161
+ try:
162
+ schema = gen.collect_data_from_file(file_name=path)
163
+ break
164
+ except FileNotFoundError:
165
+ continue
166
+ else:
167
+ raise FileNotFoundError(f"Schema file not found: {schema_file}")
168
+
169
+ if isinstance(schema, list): # schema file with several schemas defined
170
+ if schema_version is None:
171
+ raise ValueError(f"Schema version not given in {schema_file}.")
172
+ schema = next((doc for doc in schema if doc.get("version") == schema_version), None)
173
+ if schema is None:
174
+ raise ValueError(f"Schema version {schema_version} not found in {schema_file}.")
175
+ elif schema_version is not None and schema_version != schema.get("version"):
176
+ _logger.warning(f"Schema version {schema_version} does not match {schema.get('version')}")
177
+
178
+ _logger.debug(f"Loading schema from {schema_file}")
179
+ _add_array_elements("InstrumentTypeElement", schema)
180
+
181
+ return schema
182
+
183
+
184
+ def _add_array_elements(key, schema):
185
+ """
186
+ Add list of array elements to schema.
187
+
188
+ Avoids having to list all array elements in multiple schema.
189
+ Assumes an element [key]['enum'] is a list of elements.
190
+
191
+ Parameters
192
+ ----------
193
+ key: str
194
+ Key in schema dictionary
195
+ schema: dict
196
+ Schema dictionary
197
+
198
+ Returns
199
+ -------
200
+ dict
201
+ Schema dictionary with added array elements.
202
+
203
+ """
204
+ _list_of_array_elements = sorted(names.array_elements().keys())
205
+
206
+ def recursive_search(sub_schema, key):
207
+ if key in sub_schema:
208
+ if "enum" in sub_schema[key] and isinstance(sub_schema[key]["enum"], list):
209
+ sub_schema[key]["enum"] = list(
210
+ set(sub_schema[key]["enum"] + _list_of_array_elements)
211
+ )
212
+ else:
213
+ sub_schema[key]["enum"] = _list_of_array_elements
214
+ else:
215
+ for _, v in sub_schema.items():
216
+ if isinstance(v, dict):
217
+ recursive_search(v, key)
218
+
219
+ recursive_search(schema, key)
220
+ return schema