gammasimtools 0.26.0__py3-none-any.whl → 0.27.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 (70) hide show
  1. {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.0.dist-info}/METADATA +5 -1
  2. {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.0.dist-info}/RECORD +70 -66
  3. {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.0.dist-info}/WHEEL +1 -1
  4. {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.0.dist-info}/entry_points.txt +1 -1
  5. simtools/_version.py +2 -2
  6. simtools/applications/convert_geo_coordinates_of_array_elements.py +2 -1
  7. simtools/applications/db_get_array_layouts_from_db.py +1 -1
  8. simtools/applications/{calculate_incident_angles.py → derive_incident_angle.py} +16 -16
  9. simtools/applications/derive_mirror_rnda.py +111 -177
  10. simtools/applications/generate_corsika_histograms.py +38 -1
  11. simtools/applications/generate_regular_arrays.py +73 -36
  12. simtools/applications/simulate_flasher.py +3 -13
  13. simtools/applications/simulate_illuminator.py +2 -10
  14. simtools/applications/simulate_pedestals.py +1 -1
  15. simtools/applications/simulate_prod.py +8 -7
  16. simtools/applications/submit_data_from_external.py +2 -1
  17. simtools/applications/validate_camera_efficiency.py +28 -27
  18. simtools/applications/validate_cumulative_psf.py +1 -3
  19. simtools/applications/validate_optics.py +2 -1
  20. simtools/atmosphere.py +83 -0
  21. simtools/camera/camera_efficiency.py +171 -48
  22. simtools/camera/single_photon_electron_spectrum.py +6 -6
  23. simtools/configuration/commandline_parser.py +47 -9
  24. simtools/constants.py +5 -0
  25. simtools/corsika/corsika_config.py +88 -185
  26. simtools/corsika/corsika_histograms.py +246 -69
  27. simtools/data_model/model_data_writer.py +46 -49
  28. simtools/data_model/schema.py +2 -0
  29. simtools/db/db_handler.py +4 -2
  30. simtools/db/mongo_db.py +2 -2
  31. simtools/io/ascii_handler.py +51 -3
  32. simtools/io/io_handler.py +23 -12
  33. simtools/job_execution/job_manager.py +154 -79
  34. simtools/job_execution/process_pool.py +137 -0
  35. simtools/layout/array_layout.py +0 -1
  36. simtools/layout/array_layout_utils.py +143 -21
  37. simtools/model/array_model.py +22 -50
  38. simtools/model/calibration_model.py +4 -4
  39. simtools/model/model_parameter.py +123 -73
  40. simtools/model/model_utils.py +40 -1
  41. simtools/model/site_model.py +4 -4
  42. simtools/model/telescope_model.py +4 -5
  43. simtools/ray_tracing/incident_angles.py +87 -6
  44. simtools/ray_tracing/mirror_panel_psf.py +337 -217
  45. simtools/ray_tracing/psf_analysis.py +57 -42
  46. simtools/ray_tracing/psf_parameter_optimisation.py +3 -2
  47. simtools/ray_tracing/ray_tracing.py +37 -10
  48. simtools/runners/corsika_runner.py +52 -191
  49. simtools/runners/corsika_simtel_runner.py +74 -100
  50. simtools/runners/runner_services.py +214 -213
  51. simtools/runners/simtel_runner.py +27 -155
  52. simtools/runners/simtools_runner.py +9 -69
  53. simtools/schemas/application_workflow.metaschema.yml +8 -0
  54. simtools/settings.py +19 -0
  55. simtools/simtel/simtel_config_writer.py +0 -55
  56. simtools/simtel/simtel_seeds.py +184 -0
  57. simtools/simtel/simulator_array.py +115 -103
  58. simtools/simtel/simulator_camera_efficiency.py +66 -42
  59. simtools/simtel/simulator_light_emission.py +110 -123
  60. simtools/simtel/simulator_ray_tracing.py +78 -63
  61. simtools/simulator.py +135 -346
  62. simtools/testing/sim_telarray_metadata.py +13 -11
  63. simtools/testing/validate_output.py +87 -19
  64. simtools/utils/general.py +6 -17
  65. simtools/utils/random.py +36 -0
  66. simtools/visualization/plot_corsika_histograms.py +2 -0
  67. simtools/visualization/plot_incident_angles.py +48 -1
  68. simtools/visualization/plot_psf.py +160 -18
  69. {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.0.dist-info}/licenses/LICENSE +0 -0
  70. {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.0.dist-info}/top_level.txt +0 -0
@@ -483,7 +483,56 @@ def _get_array_name(array_name):
483
483
  return array_name[1:], int(array_name[0])
484
484
 
485
485
 
486
- def create_regular_array(array_name, site, telescope_distance):
486
+ def _create_star_array(tel_name, pos_x, pos_y, pos_z, n_telescopes, tel_type, site, distance):
487
+ """Create star-shaped array positions along x and y axes."""
488
+ axis_sequence = ["x", "y", "-x", "-y"]
489
+ step = 1
490
+ for i in range(n_telescopes):
491
+ tel_name.append(
492
+ names.generate_array_element_name_from_type_site_id(tel_type, site, f"{i + 1:02d}")
493
+ )
494
+ axis = axis_sequence[i % 4]
495
+ dist = distance * step
496
+ if axis == "x":
497
+ pos_x.append(dist)
498
+ pos_y.append(0 * u.m)
499
+ elif axis == "-x":
500
+ pos_x.append(-dist)
501
+ pos_y.append(0 * u.m)
502
+ elif axis == "y":
503
+ pos_x.append(0 * u.m)
504
+ pos_y.append(dist)
505
+ elif axis == "-y":
506
+ pos_x.append(0 * u.m)
507
+ pos_y.append(-dist)
508
+ pos_z.append(0 * u.m)
509
+
510
+ if (i + 1) % 4 == 0:
511
+ step += 1
512
+
513
+
514
+ def _create_square_array(tel_name, pos_x, pos_y, pos_z, n_tel, tel_type, site, distance):
515
+ """Create square array positions."""
516
+ if n_tel == 1:
517
+ tel_name.append(names.generate_array_element_name_from_type_site_id(tel_type, site, "01"))
518
+ pos_x.append(0 * u.m)
519
+ pos_y.append(0 * u.m)
520
+ pos_z.append(0 * u.m)
521
+ elif n_tel == 4:
522
+ for i in range(1, 5):
523
+ tel_name.append(
524
+ names.generate_array_element_name_from_type_site_id(tel_type, site, f"0{i}")
525
+ )
526
+ pos_x.append(distance * (-1) ** (i // 2))
527
+ pos_y.append(distance * (-1) ** (i % 2))
528
+ pos_z.append(0 * u.m)
529
+ else:
530
+ raise ValueError(f"Unsupported number of telescopes for square array: {n_tel}.")
531
+
532
+
533
+ def create_regular_array(
534
+ array_name, site, n_telescopes, telescope_type, telescope_distance, shape="square"
535
+ ):
487
536
  """
488
537
  Create a regular array layout table.
489
538
 
@@ -493,8 +542,14 @@ def create_regular_array(array_name, site, telescope_distance):
493
542
  Name of the regular array (e.g. "4MST").
494
543
  site : str
495
544
  Site identifier.
496
- telescope_distance : dict
497
- Dictionary with telescope distances per telescope type.
545
+ n_telescopes : int
546
+ Number of telescopes in the array.
547
+ telescope_type : str
548
+ Type of telescope (e.g. "MST").
549
+ telescope_distance : Quantity
550
+ Distance between telescopes in the array.
551
+ shape : str
552
+ Shape of the array: "square" or "star" (default: "square").
498
553
 
499
554
  Returns
500
555
  -------
@@ -502,26 +557,31 @@ def create_regular_array(array_name, site, telescope_distance):
502
557
  Table with the regular array layout.
503
558
  """
504
559
  tel_name, pos_x, pos_y, pos_z = [], [], [], []
505
- tel_size, n_tel = _get_array_name(array_name)
506
- tel_size = array_name[1:4]
507
560
 
508
- # Single telescope at the center
509
- if n_tel == 1:
510
- tel_name.append(names.generate_array_element_name_from_type_site_id(tel_size, site, "01"))
511
- pos_x.append(0 * u.m)
512
- pos_y.append(0 * u.m)
513
- pos_z.append(0 * u.m)
514
- # 4 telescopes in a regular square grid
515
- elif n_tel == 4:
516
- for i in range(1, 5):
517
- tel_name.append(
518
- names.generate_array_element_name_from_type_site_id(tel_size, site, f"0{i}")
519
- )
520
- pos_x.append(telescope_distance[tel_size] * (-1) ** (i // 2))
521
- pos_y.append(telescope_distance[tel_size] * (-1) ** (i % 2))
522
- pos_z.append(0 * u.m)
561
+ if shape == "square":
562
+ _create_square_array(
563
+ tel_name,
564
+ pos_x,
565
+ pos_y,
566
+ pos_z,
567
+ n_telescopes,
568
+ telescope_type,
569
+ site,
570
+ telescope_distance,
571
+ )
572
+ elif shape == "star":
573
+ _create_star_array(
574
+ tel_name,
575
+ pos_x,
576
+ pos_y,
577
+ pos_z,
578
+ n_telescopes,
579
+ telescope_type,
580
+ site,
581
+ telescope_distance,
582
+ )
523
583
  else:
524
- raise ValueError(f"Unsupported number of telescopes: {n_tel}.")
584
+ raise ValueError(f"Unsupported array shape: {shape}. Allowed: 'square' or 'star'.")
525
585
 
526
586
  table = QTable(meta={"array_name": array_name, "site": site})
527
587
  table["telescope_name"] = tel_name
@@ -591,3 +651,65 @@ def write_array_elements_from_file_to_repository(
591
651
  output_path=repository_path / instrument,
592
652
  output_file=f"{parameter_name}.json",
593
653
  )
654
+
655
+
656
+ def write_array_elements_info_yaml(
657
+ array_table, site, model_version, output_file, parameter_version="2.0.0"
658
+ ):
659
+ """
660
+ Write YAML file for array layout in a format compatible with overwrite.
661
+
662
+ Parameters
663
+ ----------
664
+ array_table : astropy.table.Table
665
+ Table with telescope positions.
666
+ site : str
667
+ Site identifier (e.g., 'North' or 'South').
668
+ model_version : str
669
+ Model version.
670
+ output_file : Path
671
+ Path to the output YAML file.
672
+ parameter_version : str
673
+ Parameter version to use in the YAML file.
674
+ """
675
+ telescope_names = [str(name) for name in array_table["telescope_name"]]
676
+
677
+ data = {
678
+ "model_version": model_version,
679
+ "model_update": "patch_update",
680
+ "model_version_history": [model_version],
681
+ "description": f"Regular array layout: {array_table.meta['array_name']}",
682
+ "changes": {
683
+ f"OBS-{site}": {
684
+ "array_layouts": {
685
+ "version": parameter_version,
686
+ "value": [],
687
+ "unit": None,
688
+ }
689
+ }
690
+ },
691
+ }
692
+
693
+ data["changes"][f"OBS-{site}"]["array_layouts"]["value"].append(
694
+ {"name": array_table.meta["array_name"], "elements": telescope_names}
695
+ )
696
+
697
+ for tel_name, pos_x, pos_y, pos_z in zip(
698
+ telescope_names,
699
+ array_table["position_x"],
700
+ array_table["position_y"],
701
+ array_table["position_z"],
702
+ ):
703
+ data["changes"][tel_name] = {
704
+ "array_element_position_ground": {
705
+ "version": parameter_version,
706
+ "value": [
707
+ float(pos_x.to(u.m).value),
708
+ float(pos_y.to(u.m).value),
709
+ float(pos_z.to(u.m).value),
710
+ ],
711
+ "unit": "m",
712
+ }
713
+ }
714
+
715
+ ascii_handler.write_data_to_file(data, output_file)
@@ -9,9 +9,10 @@ from astropy.table import QTable
9
9
  from simtools.data_model import data_reader, schema
10
10
  from simtools.io import io_handler
11
11
  from simtools.model.calibration_model import CalibrationModel
12
+ from simtools.model.model_utils import read_overwrite_model_parameter_dict
12
13
  from simtools.model.site_model import SiteModel
13
14
  from simtools.model.telescope_model import TelescopeModel
14
- from simtools.simtel.simtel_config_writer import SimtelConfigWriter
15
+ from simtools.simtel import simtel_config_writer, simtel_seeds
15
16
  from simtools.utils import general, names
16
17
 
17
18
 
@@ -50,7 +51,6 @@ class ArrayModel:
50
51
  ):
51
52
  """Initialize ArrayModel."""
52
53
  self._logger = logging.getLogger(__name__)
53
- self._logger.debug("Init ArrayModel")
54
54
  self.model_version = model_version
55
55
  self.label = label
56
56
  self.layout_name = (
@@ -62,7 +62,9 @@ class ArrayModel:
62
62
  self._config_file_directory = None
63
63
  self.io_handler = io_handler.IOHandler()
64
64
 
65
- self.overwrite_model_parameters = overwrite_model_parameters
65
+ self.overwrite_model_parameter_dict = read_overwrite_model_parameter_dict(
66
+ overwrite_model_parameters
67
+ )
66
68
 
67
69
  self.array_elements, self.site_model, self.telescope_models, self.calibration_models = (
68
70
  self._initialize(site, array_elements, calibration_device_types)
@@ -70,7 +72,7 @@ class ArrayModel:
70
72
 
71
73
  self._telescope_model_files_exported = False
72
74
  self._array_model_file_exported = False
73
- self._sim_telarray_seeds = None
75
+ self.sim_telarray_seed = None
74
76
 
75
77
  def _initialize(self, site, array_elements_config, calibration_device_types):
76
78
  """
@@ -99,7 +101,7 @@ class ArrayModel:
99
101
  site=names.validate_site_name(site),
100
102
  model_version=self.model_version,
101
103
  label=self.label,
102
- overwrite_model_parameters=self.overwrite_model_parameters,
104
+ overwrite_model_parameter_dict=self.overwrite_model_parameter_dict,
103
105
  )
104
106
 
105
107
  # Case 1: array_elements is a file name
@@ -127,41 +129,6 @@ class ArrayModel:
127
129
 
128
130
  return array_elements, site_model, telescope_models, calibration_models
129
131
 
130
- @property
131
- def sim_telarray_seeds(self):
132
- """
133
- Return sim_telarray seeds.
134
-
135
- Returns
136
- -------
137
- dict
138
- Dictionary with sim_telarray seeds.
139
- """
140
- return self._sim_telarray_seeds
141
-
142
- @sim_telarray_seeds.setter
143
- def sim_telarray_seeds(self, value):
144
- """
145
- Set sim_telarray seeds.
146
-
147
- Parameters
148
- ----------
149
- value: dict
150
- Dictionary with sim_telarray seeds.
151
- """
152
- if isinstance(value, dict):
153
- required_keys = {
154
- "seed",
155
- "random_instrument_instances",
156
- "seed_file_name",
157
- }
158
- if not required_keys.issubset(value):
159
- raise ValueError(
160
- "sim_telarray_seeds dictionary must contain the following keys: "
161
- f"{required_keys}"
162
- )
163
- self._sim_telarray_seeds = value
164
-
165
132
  @property
166
133
  def config_file_path(self):
167
134
  """
@@ -242,7 +209,7 @@ class ArrayModel:
242
209
  telescope_name=element_name,
243
210
  model_version=self.model_version,
244
211
  label=self.label,
245
- overwrite_model_parameters=self.overwrite_model_parameters,
212
+ overwrite_model_parameter_dict=self.overwrite_model_parameter_dict,
246
213
  )
247
214
  calibration_models[element_name] = self._build_calibration_models(
248
215
  telescope_models[element_name],
@@ -274,7 +241,7 @@ class ArrayModel:
274
241
  calibration_device_model_name=device_name,
275
242
  model_version=self.model_version,
276
243
  label=self.label,
277
- overwrite_model_parameters=self.overwrite_model_parameters,
244
+ overwrite_model_parameter_dict=self.overwrite_model_parameter_dict,
278
245
  )
279
246
  return calibration_models
280
247
 
@@ -289,7 +256,6 @@ class ArrayModel:
289
256
  for tel_model in self.telescope_models.values():
290
257
  name = tel_model.name
291
258
  if name not in exported_models:
292
- self._logger.debug(f"Exporting configuration file for telescope {name}")
293
259
  tel_model.write_sim_telarray_config_file(
294
260
  additional_models=self.calibration_models.get(tel_model.name)
295
261
  )
@@ -306,7 +272,7 @@ class ArrayModel:
306
272
  self.site_model.export_model_files()
307
273
 
308
274
  self._logger.info(f"Writing array configuration file into {self.config_file_path}")
309
- simtel_writer = SimtelConfigWriter(
275
+ simtel_writer = simtel_config_writer.SimtelConfigWriter(
310
276
  site=self.site_model.site,
311
277
  layout_name=self.layout_name,
312
278
  model_version=self.model_version,
@@ -531,10 +497,16 @@ class ArrayModel:
531
497
  dict
532
498
  Dictionary with additional metadata.
533
499
  """
534
- metadata = {}
535
- if self.sim_telarray_seeds is not None:
536
- metadata.update(self.sim_telarray_seeds)
537
-
538
- metadata["nsb_integrated_flux"] = self.site_model.get_nsb_integrated_flux()
500
+ return {
501
+ "nsb_integrated_flux": self.site_model.get_nsb_integrated_flux(),
502
+ }
539
503
 
540
- return metadata
504
+ def initialize_seeds(self, zenith_angle=None, azimuth_angle=None):
505
+ """Initialize sim_telarray seeds for instrument and shower simulations."""
506
+ self.sim_telarray_seed = simtel_seeds.SimtelSeeds(
507
+ output_path=self.get_config_directory(),
508
+ site=self.site_model.site,
509
+ model_version=self.model_version,
510
+ zenith_angle=zenith_angle,
511
+ azimuth_angle=azimuth_angle,
512
+ )
@@ -21,8 +21,8 @@ class CalibrationModel(ModelParameter):
21
21
  Model version.
22
22
  label: str, optional
23
23
  Instance label. Important for output file naming.
24
- overwrite_model_parameters: str, optional
25
- File name to overwrite model parameters from DB with provided values.
24
+ overwrite_model_parameter_dict: dict, optional
25
+ Dictionary to overwrite model parameters from DB with provided values.
26
26
  """
27
27
 
28
28
  def __init__(
@@ -31,7 +31,7 @@ class CalibrationModel(ModelParameter):
31
31
  calibration_device_model_name,
32
32
  model_version,
33
33
  label=None,
34
- overwrite_model_parameters=None,
34
+ overwrite_model_parameter_dict=None,
35
35
  ):
36
36
  """Initialize CalibrationModel."""
37
37
  super().__init__(
@@ -40,7 +40,7 @@ class CalibrationModel(ModelParameter):
40
40
  collection="calibration_devices",
41
41
  model_version=model_version,
42
42
  label=label,
43
- overwrite_model_parameters=overwrite_model_parameters,
43
+ overwrite_model_parameter_dict=overwrite_model_parameter_dict,
44
44
  )
45
45
 
46
46
  self._logger = logging.getLogger(__name__)
@@ -4,13 +4,14 @@
4
4
  import logging
5
5
  import shutil
6
6
  from copy import copy, deepcopy
7
+ from pathlib import Path
7
8
 
8
9
  import astropy.units as u
9
10
 
10
11
  import simtools.utils.general as gen
11
12
  from simtools.data_model import schema
12
13
  from simtools.db import db_handler
13
- from simtools.io import ascii_handler, io_handler
14
+ from simtools.io import io_handler
14
15
  from simtools.model import legacy_model_parameter
15
16
  from simtools.simtel.simtel_config_writer import SimtelConfigWriter
16
17
  from simtools.utils import names, value_conversion
@@ -40,8 +41,8 @@ class ModelParameter:
40
41
  as stored under collection in the DB.
41
42
  label: str
42
43
  Instance label. Used for output file naming.
43
- overwrite_model_parameters: str, optional
44
- File name to overwrite model parameters from DB with provided values.
44
+ overwrite_model_parameter_dict: dict, optional
45
+ Dictionary to overwrite model parameters from DB with provided values.
45
46
  Instance label. Important for output file naming.
46
47
  ignore_software_version: bool
47
48
  If True, ignore software version checks for deprecated parameters.
@@ -55,7 +56,7 @@ class ModelParameter:
55
56
  array_element_name=None,
56
57
  collection="telescopes",
57
58
  label=None,
58
- overwrite_model_parameters=None,
59
+ overwrite_model_parameter_dict=None,
59
60
  ignore_software_version=False,
60
61
  ):
61
62
  self._logger = logging.getLogger(__name__)
@@ -81,7 +82,7 @@ class ModelParameter:
81
82
  )
82
83
  self._config_file_directory = None
83
84
  self._config_file_path = None
84
- self.overwrite_model_parameters = overwrite_model_parameters
85
+ self.overwrite_model_parameter_dict = overwrite_model_parameter_dict
85
86
  self._added_parameter_files = None
86
87
  self._is_exported_model_files_up_to_date = False
87
88
 
@@ -276,8 +277,6 @@ class ModelParameter:
276
277
  )
277
278
  self._config_file_path = self.config_file_directory.joinpath(config_file_name)
278
279
 
279
- self._logger.debug(f"Config file path: {self._config_file_path}")
280
-
281
280
  def get_simulation_software_parameters(self, simulation_software):
282
281
  """
283
282
  Get simulation software parameters.
@@ -325,8 +324,7 @@ class ModelParameter:
325
324
  self.site, self.name, self.collection, self.model_version
326
325
  )
327
326
  )
328
- if self.overwrite_model_parameters:
329
- self.overwrite_parameters_from_file(self.overwrite_model_parameters)
327
+ self.overwrite_parameters(self.overwrite_model_parameter_dict)
330
328
  self._check_model_parameter_versions(self.parameters)
331
329
 
332
330
  self._load_simulation_software_parameter()
@@ -396,22 +394,20 @@ class ModelParameter:
396
394
  raise InvalidModelParameterError(f"Parameter {par_name} not in the model")
397
395
 
398
396
  if value is None and parameter_version:
399
- _para_dict = self.db.get_model_parameter(
400
- parameter=par_name,
401
- site=self.site,
402
- array_element_name=self.name,
403
- parameter_version=parameter_version,
404
- )
405
- if _para_dict:
406
- self.parameters[par_name] = _para_dict.get(par_name)
407
- self._logger.debug(
408
- f"Changing parameter {par_name} to version {parameter_version} with value "
409
- f"{self.parameters[par_name]['value']}"
410
- )
397
+ self._overwrite_model_parameter_from_db(par_name, parameter_version)
411
398
  else:
412
- value = gen.convert_string_to_list(value) if isinstance(value, str) else value
399
+ self._overwrite_model_parameter_from_value(par_name, value, parameter_version)
413
400
 
414
- par_type = self.get_parameter_type(par_name)
401
+ # In case parameter is a file, the model files will be outdated
402
+ if self.get_parameter_file_flag(par_name):
403
+ self._is_exported_model_files_up_to_date = False
404
+
405
+ def _overwrite_model_parameter_from_value(self, par_name, value, parameter_version=None):
406
+ """Overwrite model parameter from provided value only."""
407
+ value = gen.convert_string_to_list(value) if isinstance(value, str) else value
408
+ par_type = self.get_parameter_type(par_name)
409
+
410
+ if par_type in ("list", "dict"):
415
411
  if not gen.validate_data_type(
416
412
  reference_dtype=par_type,
417
413
  value=value,
@@ -419,40 +415,40 @@ class ModelParameter:
419
415
  allow_subtypes=True,
420
416
  ):
421
417
  raise ValueError(f"Could not cast {value} of type {type(value)} to {par_type}.")
418
+ else:
419
+ for value_element in gen.ensure_iterable(value):
420
+ if not gen.validate_data_type(
421
+ reference_dtype=par_type,
422
+ value=value_element,
423
+ dtype=None,
424
+ allow_subtypes=True,
425
+ ):
426
+ raise ValueError(
427
+ f"Could not cast {value_element} of type "
428
+ f"{type(value_element)} to {par_type}."
429
+ )
422
430
 
423
- self._logger.debug(
424
- f"Changing parameter {par_name} from {self.get_parameter_value(par_name)} "
425
- f"to {value}"
426
- )
427
- self.parameters[par_name]["value"] = value
428
- if parameter_version:
429
- self.parameters[par_name]["parameter_version"] = parameter_version
430
-
431
- # In case parameter is a file, the model files will be outdated
432
- if self.get_parameter_file_flag(par_name):
433
- self._is_exported_model_files_up_to_date = False
434
-
435
- def overwrite_parameters_from_file(self, file_name):
436
- """
437
- Overwrite parameters from a file.
438
-
439
- File is expected to follow the format described in 'simulation_models_info.schema.yml'.
440
-
441
- This function does not modify the DB, it affects only the current instance.
442
- This feature is intended for developers and lacks validation.
443
-
444
- Parameters
445
- ----------
446
- file_name: str
447
- File containing the parameters to be changed.
448
- """
449
- changes_data = schema.validate_dict_using_schema(
450
- data=ascii_handler.collect_data_from_file(file_name=file_name),
451
- schema_file="simulation_models_info.schema.yml",
452
- ).get("changes", {})
453
-
454
- key_for_changes = self._get_key_for_parameter_changes(self.site, self.name, changes_data)
455
- self.overwrite_parameters(changes_data.get(key_for_changes, {}) if key_for_changes else {})
431
+ self._logger.debug(
432
+ f"Changing parameter {par_name} from {self.get_parameter_value(par_name)} to {value}"
433
+ )
434
+ self.parameters[par_name]["value"] = value
435
+ if parameter_version:
436
+ self.parameters[par_name]["parameter_version"] = parameter_version
437
+
438
+ def _overwrite_model_parameter_from_db(self, par_name, parameter_version):
439
+ """Overwrite model parameter from DB for a specific version."""
440
+ _para_dict = self.db.get_model_parameter(
441
+ parameter=par_name,
442
+ site=self.site,
443
+ array_element_name=self.name,
444
+ parameter_version=parameter_version,
445
+ )
446
+ if _para_dict:
447
+ self.parameters[par_name] = _para_dict.get(par_name)
448
+ self._logger.debug(
449
+ f"Changing parameter {par_name} to version {parameter_version} with value "
450
+ f"{self.parameters[par_name]['value']}"
451
+ )
456
452
 
457
453
  def _get_key_for_parameter_changes(self, site, array_element_name, changes_data):
458
454
  """
@@ -494,7 +490,7 @@ class ModelParameter:
494
490
 
495
491
  return None
496
492
 
497
- def overwrite_parameters(self, changes):
493
+ def overwrite_parameters(self, changes, flat_dict=False):
498
494
  """
499
495
  Change the value of multiple existing parameters in the model.
500
496
 
@@ -502,23 +498,43 @@ class ModelParameter:
502
498
 
503
499
  Allows for two types of 'changes' dictionary:
504
500
 
505
- - simple: '{parameter_name: new_value, ...}'
506
- - model repository style:
507
- '{parameter_name: {"value": new_value, "version": new_version}, ...}'
501
+ - simple (flat_dict=True): '{parameter_name: new_value, ...}'
502
+ - model repository style (flat_dict=False):
503
+ '{array_element: {parameter_name: {"value": new_value, "version": new_version}, ...}}'
508
504
 
509
505
  Parameters
510
506
  ----------
511
507
  changes: dict
512
508
  Parameters to be changed.
513
509
  """
510
+ if not changes:
511
+ return
512
+ if not flat_dict:
513
+ key_for_changes = self._get_key_for_parameter_changes(self.site, self.name, changes)
514
+ changes = changes.get(key_for_changes, {})
515
+ if not changes:
516
+ return
517
+
518
+ if flat_dict:
519
+ self._logger.debug(f"Overwriting parameters with changes: {changes}")
520
+ else:
521
+ self._logger.debug(
522
+ f"Overwriting parameters for {key_for_changes} with changes: {changes}"
523
+ )
524
+
514
525
  for par_name, par_value in changes.items():
515
- if par_name in self.parameters:
516
- if isinstance(par_value, dict) and ("value" in par_value or "version" in par_value):
517
- self.overwrite_model_parameter(
518
- par_name, par_value.get("value"), par_value.get("version")
519
- )
520
- else:
521
- self.overwrite_model_parameter(par_name, par_value)
526
+ if par_name not in self.parameters:
527
+ self._logger.warning(
528
+ f"Parameter {par_name} not found in model {self.name}, cannot overwrite it."
529
+ )
530
+ continue
531
+
532
+ if isinstance(par_value, dict) and ("value" in par_value or "version" in par_value):
533
+ self.overwrite_model_parameter(
534
+ par_name, par_value.get("value"), par_value.get("version")
535
+ )
536
+ else:
537
+ self.overwrite_model_parameter(par_name, par_value)
522
538
 
523
539
  def overwrite_model_file(self, par_name, file_path):
524
540
  """
@@ -566,7 +582,29 @@ class ModelParameter:
566
582
  )
567
583
  self._is_exported_model_files_up_to_date = True
568
584
 
569
- def write_sim_telarray_config_file(self, additional_models=None):
585
+ def get_config_file_path(self, label=None):
586
+ """Return config file path for a given label.
587
+
588
+ Parameters
589
+ ----------
590
+ label : str or None
591
+ Label used for output file naming. If None, use this model's label.
592
+
593
+ Returns
594
+ -------
595
+ pathlib.Path
596
+ Path to the sim_telarray configuration file.
597
+ """
598
+ config_file_name = names.simtel_config_file_name(
599
+ self.site,
600
+ telescope_model_name=self.name,
601
+ label=self.label if label is None else label,
602
+ )
603
+ return self.config_file_directory.joinpath(config_file_name)
604
+
605
+ def write_sim_telarray_config_file(
606
+ self, additional_models=None, label=None, config_file_path=None
607
+ ):
570
608
  """
571
609
  Write the sim_telarray configuration file.
572
610
 
@@ -574,15 +612,26 @@ class ModelParameter:
574
612
  ----------
575
613
  additional_models: TelescopeModel or SiteModel
576
614
  Model object for additional parameter to be written to the config file.
615
+ label: str or None
616
+ Optional label override used for output file naming.
617
+ config_file_path: pathlib.Path or str or None
618
+ Optional explicit path of the config file. If not given, it is derived from ``label``.
577
619
  """
578
620
  self.parameters.update(self._simulation_config_parameters.get("sim_telarray", {}))
579
621
  self.export_model_files(update_if_necessary=True)
580
622
 
581
623
  self._add_additional_models(additional_models)
582
624
 
583
- self._load_simtel_config_writer()
625
+ config_file_path = (
626
+ Path(config_file_path)
627
+ if config_file_path is not None
628
+ else self.get_config_file_path(label=label)
629
+ )
630
+
631
+ # Ensure the writer label matches the config file naming label.
632
+ self._load_simtel_config_writer(label=label)
584
633
  self.simtel_config_writer.write_telescope_config_file(
585
- config_file_path=self.config_file_path,
634
+ config_file_path=config_file_path,
586
635
  parameters=self.parameters,
587
636
  )
588
637
 
@@ -613,15 +662,16 @@ class ModelParameter:
613
662
  self._set_config_file_directory_and_name()
614
663
  return self._config_file_path
615
664
 
616
- def _load_simtel_config_writer(self):
665
+ def _load_simtel_config_writer(self, label=None):
617
666
  """Load the SimtelConfigWriter object."""
618
- if self.simtel_config_writer is None:
667
+ desired_label = self.label if label is None else label
668
+ if label is not None or self.simtel_config_writer is None:
619
669
  self.simtel_config_writer = SimtelConfigWriter(
620
670
  site=self.site,
621
671
  telescope_model_name=self.name,
622
672
  telescope_design_model=self.design_model,
623
673
  model_version=self.model_version,
624
- label=self.label,
674
+ label=desired_label,
625
675
  )
626
676
 
627
677
  def export_nsb_spectrum_to_telescope_altitude_correction_file(self, model_directory):