gammasimtools 0.23.0__py3-none-any.whl → 0.24.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 (59) hide show
  1. {gammasimtools-0.23.0.dist-info → gammasimtools-0.24.0.dist-info}/METADATA +1 -1
  2. {gammasimtools-0.23.0.dist-info → gammasimtools-0.24.0.dist-info}/RECORD +59 -58
  3. simtools/_version.py +2 -2
  4. simtools/application_control.py +4 -4
  5. simtools/applications/convert_geo_coordinates_of_array_elements.py +1 -1
  6. simtools/applications/db_add_file_to_db.py +2 -2
  7. simtools/applications/db_add_simulation_model_from_repository_to_db.py +1 -1
  8. simtools/applications/db_add_value_from_json_to_db.py +2 -2
  9. simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +1 -1
  10. simtools/applications/db_generate_compound_indexes.py +1 -1
  11. simtools/applications/db_get_array_layouts_from_db.py +2 -2
  12. simtools/applications/db_get_file_from_db.py +1 -1
  13. simtools/applications/db_get_parameter_from_db.py +1 -1
  14. simtools/applications/db_inspect_databases.py +4 -2
  15. simtools/applications/db_upload_model_repository.py +1 -1
  16. simtools/applications/derive_ctao_array_layouts.py +1 -1
  17. simtools/applications/generate_array_config.py +1 -1
  18. simtools/applications/maintain_simulation_model_add_production.py +11 -21
  19. simtools/applications/production_generate_grid.py +1 -1
  20. simtools/applications/submit_array_layouts.py +2 -2
  21. simtools/applications/validate_camera_fov.py +1 -1
  22. simtools/applications/validate_cumulative_psf.py +2 -2
  23. simtools/applications/validate_optics.py +1 -1
  24. simtools/configuration/commandline_parser.py +7 -9
  25. simtools/configuration/configurator.py +1 -1
  26. simtools/corsika/corsika_config.py +2 -4
  27. simtools/data_model/model_data_writer.py +1 -1
  28. simtools/data_model/schema.py +36 -34
  29. simtools/db/db_handler.py +61 -294
  30. simtools/db/db_model_upload.py +1 -1
  31. simtools/db/mongo_db.py +535 -0
  32. simtools/dependencies.py +33 -8
  33. simtools/layout/array_layout.py +7 -7
  34. simtools/layout/array_layout_utils.py +3 -3
  35. simtools/model/array_model.py +36 -67
  36. simtools/model/calibration_model.py +12 -9
  37. simtools/model/model_parameter.py +196 -159
  38. simtools/model/model_repository.py +159 -35
  39. simtools/model/model_utils.py +3 -3
  40. simtools/model/site_model.py +59 -27
  41. simtools/model/telescope_model.py +21 -13
  42. simtools/ray_tracing/mirror_panel_psf.py +4 -4
  43. simtools/ray_tracing/psf_parameter_optimisation.py +1 -1
  44. simtools/reporting/docs_auto_report_generator.py +1 -1
  45. simtools/reporting/docs_read_parameters.py +3 -2
  46. simtools/schemas/simulation_models_info.schema.yml +2 -1
  47. simtools/simtel/simtel_config_writer.py +97 -20
  48. simtools/simulator.py +2 -1
  49. simtools/testing/assertions.py +50 -6
  50. simtools/testing/validate_output.py +4 -8
  51. simtools/utils/value_conversion.py +10 -5
  52. simtools/version.py +24 -0
  53. simtools/visualization/plot_pixels.py +1 -1
  54. simtools/visualization/plot_psf.py +1 -1
  55. simtools/visualization/plot_tables.py +1 -1
  56. {gammasimtools-0.23.0.dist-info → gammasimtools-0.24.0.dist-info}/WHEEL +0 -0
  57. {gammasimtools-0.23.0.dist-info → gammasimtools-0.24.0.dist-info}/entry_points.txt +0 -0
  58. {gammasimtools-0.23.0.dist-info → gammasimtools-0.24.0.dist-info}/licenses/LICENSE +0 -0
  59. {gammasimtools-0.23.0.dist-info → gammasimtools-0.24.0.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,18 @@
1
1
  #!/usr/bin/python3
2
- """Base class for simulation model parameters."""
2
+ """Base class for simulation model parameters (e.g., for SiteModel or TelescopeModel)."""
3
3
 
4
4
  import logging
5
5
  import shutil
6
- from copy import copy
6
+ from copy import copy, deepcopy
7
7
 
8
8
  import astropy.units as u
9
9
 
10
10
  import simtools.utils.general as gen
11
+ from simtools.data_model import schema
11
12
  from simtools.db import db_handler
12
13
  from simtools.io import ascii_handler, io_handler
13
14
  from simtools.simtel.simtel_config_writer import SimtelConfigWriter
14
- from simtools.utils import names
15
+ from simtools.utils import names, value_conversion
15
16
 
16
17
 
17
18
  class InvalidModelParameterError(Exception):
@@ -27,8 +28,8 @@ class ModelParameter:
27
28
 
28
29
  Parameters
29
30
  ----------
30
- db: DatabaseHandler
31
- Database handler.
31
+ db_config:
32
+ Database configuration dictionary.
32
33
  model_version: str
33
34
  Version of the model (ex. 5.0.0).
34
35
  site: str
@@ -38,35 +39,37 @@ class ModelParameter:
38
39
  collection: str
39
40
  instrument class (e.g. telescopes, calibration_devices)
40
41
  as stored under collection in the DB.
41
- mongo_db_config: dict
42
- MongoDB configuration.
43
42
  label: str
43
+ Instance label. Used for output file naming.
44
+ overwrite_model_parameters: str, optional
45
+ File name to overwrite model parameters from DB with provided values.
44
46
  Instance label. Important for output file naming.
45
-
47
+ ignore_software_version: bool
48
+ If True, ignore software version checks for deprecated parameters.
49
+ Useful for documentation generation.
46
50
  """
47
51
 
48
52
  def __init__(
49
53
  self,
50
- mongo_db_config,
54
+ db_config,
51
55
  model_version,
52
56
  site=None,
53
57
  array_element_name=None,
54
58
  collection="telescopes",
55
- db=None,
56
59
  label=None,
60
+ overwrite_model_parameters=None,
61
+ ignore_software_version=False,
57
62
  ):
58
63
  self._logger = logging.getLogger(__name__)
59
- self._extra_label = None
60
64
  self.io_handler = io_handler.IOHandler()
61
- self.db = (
62
- db if db is not None else db_handler.DatabaseHandler(mongo_db_config=mongo_db_config)
63
- )
65
+ self.db = db_handler.DatabaseHandler(db_config=db_config)
64
66
 
65
- self._parameters = {}
67
+ self.parameters = {}
66
68
  self._simulation_config_parameters = {sw: {} for sw in names.simulation_software()}
67
69
  self.collection = collection
68
70
  self.label = label
69
71
  self.model_version = model_version
72
+ self.ignore_software_version = ignore_software_version
70
73
  self.site = names.validate_site_name(site) if site is not None else None
71
74
  self.name = (
72
75
  names.validate_array_element_name(array_element_name)
@@ -78,56 +81,17 @@ class ModelParameter:
78
81
  )
79
82
  self._config_file_directory = None
80
83
  self._config_file_path = None
81
- self._load_parameters_from_db()
82
-
83
- self.simtel_config_writer = None
84
+ self.overwrite_model_parameters = overwrite_model_parameters
84
85
  self._added_parameter_files = None
85
- self._is_config_file_up_to_date = False
86
86
  self._is_exported_model_files_up_to_date = False
87
87
 
88
- @property
89
- def model_version(self):
90
- """Model version."""
91
- return self._model_version
92
-
93
- @model_version.setter
94
- def model_version(self, model_version):
95
- """
96
- Set model version.
97
-
98
- Parameters
99
- ----------
100
- model_version: str or list
101
- Model version (e.g., "6.0.0").
102
- If a list is passed, it must contain exactly one element,
103
- and only that element will be used.
104
-
105
- Raises
106
- ------
107
- ValueError
108
- If more than one model version is passed.
109
- """
110
- if isinstance(model_version, list):
111
- raise ValueError(
112
- f"Only one model version can be passed to {self.__class__.__name__}, not a list."
113
- )
114
- self._model_version = model_version
115
-
116
- @property
117
- def parameters(self):
118
- """
119
- Model parameters dictionary.
88
+ self._load_parameters_from_db()
120
89
 
121
- Returns
122
- -------
123
- dict
124
- Dictionary containing all model parameters
125
- """
126
- return self._parameters
90
+ self.simtel_config_writer = None
127
91
 
128
92
  def _get_parameter_dict(self, par_name):
129
93
  """
130
- Get model parameter dictionary as stored in the DB.
94
+ Get model parameter dictionary for a specific parameter as stored in the DB.
131
95
 
132
96
  No conversion to values are applied for the use in simtools
133
97
  (e.g., no conversion from the string representation of lists
@@ -155,7 +119,7 @@ class ModelParameter:
155
119
  f"Parameter {par_name} was not found in the model {self.name}, {self.site}."
156
120
  ) from e
157
121
 
158
- def get_parameter_value(self, par_name, parameter_dict=None):
122
+ def get_parameter_value(self, par_name):
159
123
  """
160
124
  Get the value of a model parameter.
161
125
 
@@ -166,9 +130,6 @@ class ModelParameter:
166
130
  ----------
167
131
  par_name: str
168
132
  Name of the parameter.
169
- parameter_dict: dict
170
- Dictionary with complete DB entry for the given parameter
171
- (including the 'value', 'units' fields).
172
133
 
173
134
  Returns
174
135
  -------
@@ -176,50 +137,28 @@ class ModelParameter:
176
137
 
177
138
  Raises
178
139
  ------
179
- KeyError
140
+ InvalidModelParameterError
180
141
  If par_name does not match any parameter in this model.
181
142
  """
182
- parameter_dict = parameter_dict if parameter_dict else self._get_parameter_dict(par_name)
183
143
  try:
184
- _parameter = parameter_dict["value"]
144
+ value = self._get_parameter_dict(par_name)["value"]
185
145
  except KeyError as exc:
186
- self._logger.error(f"Parameter {par_name} does not have a value")
187
- raise exc
188
- if isinstance(_parameter, str):
189
- _is_float = False
146
+ raise InvalidModelParameterError(f"Parameter {par_name} does not have a value") from exc
147
+
148
+ if isinstance(value, str):
190
149
  try:
191
150
  _is_float = self.get_parameter_type(par_name).startswith("float")
192
- except (InvalidModelParameterError, TypeError): # float - in case we don't know
151
+ except (
152
+ InvalidModelParameterError,
153
+ TypeError,
154
+ AttributeError,
155
+ ): # float - in case we don't know
193
156
  _is_float = True
194
- _parameter = gen.convert_string_to_list(_parameter, is_float=_is_float)
195
- _parameter = _parameter if len(_parameter) > 1 else _parameter[0]
196
-
197
- return _parameter
198
-
199
- def _create_quantity_for_value(self, value, unit):
200
- """
201
- Create an astropy quantity for a single value and unit.
202
-
203
- Parameters
204
- ----------
205
- value: numeric or str
206
- The value to create a quantity for.
207
- unit: str or None
208
- The unit string or None.
157
+ value = gen.convert_string_to_list(value, is_float=_is_float)
158
+ if len(value) == 1:
159
+ value = value[0]
209
160
 
210
- Returns
211
- -------
212
- astropy.Quantity or original value
213
- Astropy quantity for numeric values with units,
214
- original value for non-numeric values.
215
- """
216
- if not isinstance(value, int | float):
217
- return value
218
-
219
- if unit is None or unit == "null":
220
- return value * u.dimensionless_unscaled
221
-
222
- return value * u.Unit(unit)
161
+ return value
223
162
 
224
163
  def get_parameter_value_with_unit(self, par_name):
225
164
  """
@@ -237,7 +176,7 @@ class ModelParameter:
237
176
 
238
177
  """
239
178
  _parameter = self._get_parameter_dict(par_name)
240
- _value = self.get_parameter_value(par_name, _parameter)
179
+ _value = self.get_parameter_value(par_name)
241
180
 
242
181
  try:
243
182
  if isinstance(_parameter.get("unit"), str):
@@ -251,7 +190,9 @@ class ModelParameter:
251
190
 
252
191
  # Create list of quantities for multiple values with different units
253
192
  return [
254
- self._create_quantity_for_value(_value[i], _unit[i] if i < len(_unit) else None)
193
+ value_conversion.get_value_as_quantity(
194
+ _value[i], _unit[i] if i < len(_unit) else None
195
+ )
255
196
  for i in range(len(_value))
256
197
  ]
257
198
 
@@ -272,13 +213,11 @@ class ModelParameter:
272
213
 
273
214
  Returns
274
215
  -------
275
- str or None
276
- type of the parameter (None if no type is defined)
277
-
216
+ str
217
+ type of the parameter
278
218
  """
279
- parameter_dict = self._get_parameter_dict(par_name)
280
219
  try:
281
- return parameter_dict["type"]
220
+ return self._get_parameter_dict(par_name)["type"]
282
221
  except KeyError:
283
222
  self._logger.debug(f"Parameter {par_name} does not have a type.")
284
223
  return None
@@ -298,9 +237,8 @@ class ModelParameter:
298
237
  True if file flag is set.
299
238
 
300
239
  """
301
- parameter_dict = self._get_parameter_dict(par_name)
302
240
  try:
303
- return parameter_dict["file"]
241
+ return self._get_parameter_dict(par_name)["file"]
304
242
  except KeyError:
305
243
  self._logger.debug(f"Parameter {par_name} does not have a file associated with it.")
306
244
  return False
@@ -321,11 +259,6 @@ class ModelParameter:
321
259
  """
322
260
  return self._get_parameter_dict(par_name)["parameter_version"]
323
261
 
324
- def print_parameters(self):
325
- """Print parameters and their values for debugging purposes."""
326
- for par in self.parameters:
327
- print(f"{par} = {self.get_parameter_value(par)}")
328
-
329
262
  def _set_config_file_directory_and_name(self):
330
263
  """Set and create the directory and the name of the config file."""
331
264
  if self.name is None and self.site is None:
@@ -341,7 +274,6 @@ class ModelParameter:
341
274
  self.model_version,
342
275
  telescope_model_name=self.name,
343
276
  label=self.label,
344
- extra_label=self._extra_label,
345
277
  )
346
278
  self._config_file_path = self.config_file_directory.joinpath(config_file_name)
347
279
 
@@ -379,34 +311,71 @@ class ModelParameter:
379
311
  pass
380
312
 
381
313
  def _load_parameters_from_db(self):
382
- """Read parameters from DB and store them in _parameters."""
314
+ """
315
+ Read parameters from Database.
316
+
317
+ This is the main function to load the model parameters from the DB.
318
+ """
383
319
  if self.db is None:
384
320
  return
385
321
 
386
322
  if self.name or self.site:
387
- self._parameters = self.db.get_model_parameters(
388
- self.site, self.name, self.collection, self.model_version
323
+ # copy parameters dict, is it may be modified later on
324
+ self.parameters = deepcopy(
325
+ self.db.get_model_parameters(
326
+ self.site, self.name, self.collection, self.model_version
327
+ )
389
328
  )
329
+ if self.overwrite_model_parameters:
330
+ self.overwrite_parameters_from_file(self.overwrite_model_parameters)
331
+ self._check_model_parameter_software_versions(self.parameters.keys())
390
332
 
391
333
  self._load_simulation_software_parameter()
334
+ for software_name, parameters in self._simulation_config_parameters.items():
335
+ self._check_model_parameter_software_versions(
336
+ parameters.keys(), software_name=software_name
337
+ )
392
338
 
393
- @property
394
- def extra_label(self):
395
- """Return the extra label if defined, if not return ''."""
396
- return self._extra_label if self._extra_label is not None else ""
339
+ def _check_model_parameter_software_versions(self, parameter_list, software_name=None):
340
+ """
341
+ Ensure that model parameters are compatible with the installed software versions.
342
+
343
+ Compares software versions listed in schema files with the installed software versions
344
+ (e.g., sim_telarray, CORSIKA).
397
345
 
398
- def change_parameter(self, par_name, value):
346
+ Parameters
347
+ ----------
348
+ parameter_list: list
349
+ List containing model parameter names.
350
+ software_name: str
351
+ Name of the software for which the parameters are checked.
352
+ """
353
+ for par_name in parameter_list:
354
+ if par_name in (parameter_schema := names.model_parameters()):
355
+ schema.validate_deprecation_and_version(
356
+ data=parameter_schema[par_name],
357
+ software_name=software_name,
358
+ ignore_software_version=self.ignore_software_version,
359
+ )
360
+
361
+ def overwrite_model_parameter(self, par_name, value, parameter_version=None):
399
362
  """
400
- Change the value of an existing parameter.
363
+ Overwrite the parameter dictionary for a specific parameter in the model.
364
+
365
+ This function does not modify the DB, it affects only the current instance of
366
+ the model parameter dictionary.
401
367
 
402
- This function does not modify the DB, it affects only the current instance.
368
+ If the parameter version is given only, the parameter dictionary is updated
369
+ from the database for the given version.
403
370
 
404
371
  Parameters
405
372
  ----------
406
373
  par_name: str
407
374
  Name of the parameter.
408
375
  value:
409
- Value of the parameter.
376
+ New value for the parameter.
377
+ parameter_version: str, optional
378
+ New version for the parameter.
410
379
 
411
380
  Raises
412
381
  ------
@@ -416,31 +385,48 @@ class ModelParameter:
416
385
  if par_name not in self.parameters:
417
386
  raise InvalidModelParameterError(f"Parameter {par_name} not in the model")
418
387
 
419
- value = gen.convert_string_to_list(value) if isinstance(value, str) else value
420
-
421
- par_type = self.get_parameter_type(par_name)
422
- if not gen.validate_data_type(
423
- reference_dtype=par_type,
424
- value=value,
425
- dtype=None,
426
- allow_subtypes=True,
427
- ):
428
- raise ValueError(f"Could not cast {value} of type {type(value)} to {par_type}.")
388
+ if value is None and parameter_version:
389
+ _para_dict = self.db.get_model_parameter(
390
+ parameter=par_name,
391
+ site=self.site,
392
+ array_element_name=self.name,
393
+ parameter_version=parameter_version,
394
+ )
395
+ if _para_dict:
396
+ self.parameters[par_name] = _para_dict.get(par_name)
397
+ self._logger.debug(
398
+ f"Changing parameter {par_name} to version {parameter_version} with value "
399
+ f"{self.parameters[par_name]['value']}"
400
+ )
401
+ else:
402
+ value = gen.convert_string_to_list(value) if isinstance(value, str) else value
403
+
404
+ par_type = self.get_parameter_type(par_name)
405
+ if not gen.validate_data_type(
406
+ reference_dtype=par_type,
407
+ value=value,
408
+ dtype=None,
409
+ allow_subtypes=True,
410
+ ):
411
+ raise ValueError(f"Could not cast {value} of type {type(value)} to {par_type}.")
429
412
 
430
- self._logger.debug(
431
- f"Changing parameter {par_name} from {self.get_parameter_value(par_name)} to {value}"
432
- )
433
- self.parameters[par_name]["value"] = value
413
+ self._logger.debug(
414
+ f"Changing parameter {par_name} from {self.get_parameter_value(par_name)} "
415
+ f"to {value}"
416
+ )
417
+ self.parameters[par_name]["value"] = value
418
+ if parameter_version:
419
+ self.parameters[par_name]["parameter_version"] = parameter_version
434
420
 
435
421
  # In case parameter is a file, the model files will be outdated
436
422
  if self.get_parameter_file_flag(par_name):
437
423
  self._is_exported_model_files_up_to_date = False
438
424
 
439
- self._is_config_file_up_to_date = False
440
-
441
- def change_multiple_parameters_from_file(self, file_name):
425
+ def overwrite_parameters_from_file(self, file_name):
442
426
  """
443
- Change values of multiple existing parameters in the model from a file.
427
+ Overwrite parameters from a file.
428
+
429
+ File is expected to follow the format described in 'simulation_models_info.schema.yml'.
444
430
 
445
431
  This function does not modify the DB, it affects only the current instance.
446
432
  This feature is intended for developers and lacks validation.
@@ -450,34 +436,85 @@ class ModelParameter:
450
436
  file_name: str
451
437
  File containing the parameters to be changed.
452
438
  """
453
- self._logger.warning(
454
- "Changing multiple parameters from file is a feature for developers."
455
- "Insufficient validation of parameters."
439
+ changes_data = schema.validate_dict_using_schema(
440
+ data=ascii_handler.collect_data_from_file(file_name=file_name),
441
+ schema_file="simulation_models_info.schema.yml",
442
+ ).get("changes", {})
443
+
444
+ key_for_changes = self._get_key_for_parameter_changes(self.site, self.name, changes_data)
445
+ self.overwrite_parameters(changes_data.get(key_for_changes, {}) if key_for_changes else {})
446
+
447
+ def _get_key_for_parameter_changes(self, site, array_element_name, changes_data):
448
+ """
449
+ Get the key for parameter changes based on site and array element name.
450
+
451
+ For array elements, the following cases are taken into account:
452
+
453
+ - array element name in changes_data: specific array element is returned
454
+ - design type in changes_data: specific design type is returned if array
455
+ element matches this design
456
+
457
+ Parameters
458
+ ----------
459
+ site: str
460
+ Site name.
461
+ array_element_name: str
462
+ Array element name.
463
+ changes_data: dict
464
+ Dictionary containing the changes data.
465
+
466
+ Returns
467
+ -------
468
+ str
469
+ Key for parameter changes.
470
+ """
471
+ if site and not array_element_name:
472
+ return f"OBS-{site}"
473
+
474
+ if array_element_name in changes_data:
475
+ return array_element_name
476
+
477
+ design_type = self.db.get_design_model(
478
+ model_version=self.model_version,
479
+ array_element_name=array_element_name,
480
+ collection=self.collection,
456
481
  )
457
- self._logger.debug(f"Changing parameters from file {file_name}")
458
- self.change_multiple_parameters(**ascii_handler.collect_data_from_file(file_name=file_name))
482
+ if design_type in changes_data:
483
+ return design_type
484
+
485
+ return None
459
486
 
460
- def change_multiple_parameters(self, **kwargs):
487
+ def overwrite_parameters(self, changes):
461
488
  """
462
489
  Change the value of multiple existing parameters in the model.
463
490
 
464
491
  This function does not modify the DB, it affects only the current instance.
465
492
 
493
+ Allows for two types of 'changes' dictionary:
494
+
495
+ - simple: '{parameter_name: new_value, ...}'
496
+ - model repository style:
497
+ '{parameter_name: {"value": new_value, "version": new_version}, ...}'
498
+
466
499
  Parameters
467
500
  ----------
468
- **kwargs
469
- Parameters should be passed as parameter_name=value.
501
+ changes: dict
502
+ Parameters to be changed.
503
+ """
504
+ for par_name, par_value in changes.items():
505
+ if par_name in self.parameters:
506
+ if isinstance(par_value, dict) and ("value" in par_value or "version" in par_value):
507
+ self.overwrite_model_parameter(
508
+ par_name, par_value.get("value"), par_value.get("version")
509
+ )
510
+ else:
511
+ self.overwrite_model_parameter(par_name, par_value)
470
512
 
513
+ def overwrite_model_file(self, par_name, file_path):
471
514
  """
472
- for par, value in kwargs.items():
473
- if par in self.parameters:
474
- self.change_parameter(par, value)
515
+ Overwrite the existing model file in the config file directory.
475
516
 
476
- self._is_config_file_up_to_date = False
477
-
478
- def export_parameter_file(self, par_name, file_path):
479
- """
480
- Export a file to the config file directory.
517
+ Keeps track of updated model file with '_added_parameter_files' attribute.
481
518
 
482
519
  Parameters
483
520
  ----------
@@ -492,7 +529,7 @@ class ModelParameter:
492
529
 
493
530
  def export_model_files(self, destination_path=None, update_if_necessary=False):
494
531
  """
495
- Export the model files into the config file directory.
532
+ Export model files from the database into the config file directory.
496
533
 
497
534
  Parameters
498
535
  ----------