gammasimtools 0.24.0__py3-none-any.whl → 0.25.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.24.0.dist-info → gammasimtools-0.25.0.dist-info}/METADATA +1 -1
  2. {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/RECORD +58 -55
  3. {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/entry_points.txt +1 -0
  4. simtools/_version.py +2 -2
  5. simtools/application_control.py +50 -0
  6. simtools/applications/derive_psf_parameters.py +5 -0
  7. simtools/applications/derive_pulse_shape_parameters.py +195 -0
  8. simtools/applications/plot_array_layout.py +63 -1
  9. simtools/applications/simulate_flasher.py +3 -2
  10. simtools/applications/simulate_pedestals.py +1 -1
  11. simtools/applications/simulate_prod.py +8 -23
  12. simtools/applications/simulate_prod_htcondor_generator.py +7 -0
  13. simtools/applications/submit_array_layouts.py +5 -3
  14. simtools/applications/validate_file_using_schema.py +49 -123
  15. simtools/configuration/commandline_parser.py +8 -6
  16. simtools/corsika/corsika_config.py +197 -87
  17. simtools/data_model/model_data_writer.py +14 -2
  18. simtools/data_model/schema.py +112 -5
  19. simtools/data_model/validate_data.py +82 -48
  20. simtools/db/db_model_upload.py +2 -1
  21. simtools/db/mongo_db.py +133 -42
  22. simtools/dependencies.py +5 -9
  23. simtools/io/eventio_handler.py +128 -0
  24. simtools/job_execution/htcondor_script_generator.py +0 -2
  25. simtools/layout/array_layout_utils.py +1 -1
  26. simtools/model/array_model.py +36 -5
  27. simtools/model/model_parameter.py +0 -1
  28. simtools/model/model_repository.py +18 -5
  29. simtools/ray_tracing/psf_analysis.py +11 -8
  30. simtools/ray_tracing/psf_parameter_optimisation.py +822 -679
  31. simtools/reporting/docs_read_parameters.py +69 -9
  32. simtools/runners/corsika_runner.py +12 -3
  33. simtools/runners/corsika_simtel_runner.py +6 -0
  34. simtools/runners/runner_services.py +17 -7
  35. simtools/runners/simtel_runner.py +12 -54
  36. simtools/schemas/model_parameters/flasher_pulse_exp_decay.schema.yml +2 -0
  37. simtools/schemas/model_parameters/flasher_pulse_shape.schema.yml +50 -0
  38. simtools/schemas/model_parameters/flasher_pulse_width.schema.yml +2 -0
  39. simtools/schemas/simulation_models_info.schema.yml +2 -0
  40. simtools/simtel/pulse_shapes.py +268 -0
  41. simtools/simtel/simtel_config_writer.py +82 -1
  42. simtools/simtel/simtel_io_event_writer.py +2 -2
  43. simtools/simtel/simulator_array.py +58 -12
  44. simtools/simtel/simulator_light_emission.py +45 -8
  45. simtools/simulator.py +361 -347
  46. simtools/testing/assertions.py +62 -6
  47. simtools/testing/configuration.py +1 -1
  48. simtools/testing/log_inspector.py +4 -1
  49. simtools/testing/sim_telarray_metadata.py +1 -1
  50. simtools/testing/validate_output.py +44 -9
  51. simtools/utils/names.py +2 -4
  52. simtools/version.py +37 -0
  53. simtools/visualization/legend_handlers.py +14 -4
  54. simtools/visualization/plot_array_layout.py +229 -33
  55. simtools/visualization/plot_mirrors.py +837 -0
  56. simtools/simtel/simtel_io_file_info.py +0 -62
  57. {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/WHEEL +0 -0
  58. {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/licenses/LICENSE +0 -0
  59. {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/top_level.txt +0 -0
@@ -17,7 +17,7 @@ from simtools.io import ascii_handler, io_handler
17
17
  from simtools.model.telescope_model import TelescopeModel
18
18
  from simtools.utils import names
19
19
  from simtools.version import sort_versions
20
- from simtools.visualization import plot_pixels, plot_tables
20
+ from simtools.visualization import plot_mirrors, plot_pixels, plot_tables
21
21
 
22
22
  logger = logging.getLogger()
23
23
  MARKDOWN_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
@@ -72,6 +72,12 @@ class ReadParameters:
72
72
 
73
73
  if parameter == "camera_config_file":
74
74
  plot_names = self._plot_camera_config(parameter, parameter_version, input_file, outpath)
75
+ elif parameter in (
76
+ "mirror_list",
77
+ "primary_mirror_segmentation",
78
+ "secondary_mirror_segmentation",
79
+ ):
80
+ plot_names = self._plot_mirror_config(parameter, parameter_version, input_file, outpath)
75
81
  elif parameter_version:
76
82
  plot_names = self._plot_parameter_tables(
77
83
  parameter,
@@ -115,6 +121,39 @@ class ReadParameters:
115
121
 
116
122
  return plot_names
117
123
 
124
+ def _plot_mirror_config(self, parameter, parameter_version, input_file, outpath):
125
+ """Generate plots for mirror configuration files."""
126
+ if not parameter_version:
127
+ return []
128
+
129
+ plot_names = []
130
+ plot_name = input_file.stem.replace(".", "-")
131
+ plot_path = Path(f"{outpath}/{plot_name}").with_suffix(".png")
132
+
133
+ if not plot_path.exists():
134
+ plot_config = {
135
+ "parameter": parameter,
136
+ "telescope": self.array_element,
137
+ "parameter_version": parameter_version,
138
+ "site": self.site,
139
+ "model_version": self.model_version,
140
+ }
141
+
142
+ plot_mirrors.plot(
143
+ config=plot_config,
144
+ output_file=Path(f"{outpath}/{plot_name}"),
145
+ db_config=self.db_config,
146
+ )
147
+ plot_names.append(plot_name)
148
+ else:
149
+ logger.info(
150
+ "Mirror configuration file plot already exists: %s",
151
+ plot_path,
152
+ )
153
+ plot_names.append(plot_name)
154
+
155
+ return plot_names
156
+
118
157
  def _plot_parameter_tables(self, parameter, parameter_version, outpath):
119
158
  """Generate plots for parameter tables."""
120
159
  tel = self._get_telescope_identifier()
@@ -175,7 +214,7 @@ class ReadParameters:
175
214
  markdown_output_file = output_data_path / output_file_name
176
215
 
177
216
  if not markdown_output_file.exists():
178
- outpath = Path(io_handler.IOHandler().get_output_directory().parent / "_images")
217
+ outpath = io_handler.IOHandler().get_output_directory("_images")
179
218
  outpath.mkdir(parents=True, exist_ok=True)
180
219
 
181
220
  plot_names = self._generate_plots(parameter, parameter_version, input_file, outpath)
@@ -648,7 +687,7 @@ class ReadParameters:
648
687
 
649
688
  def _write_file_flag_section(self, file, parameter, comparison_data):
650
689
  """Write image/plot references when parameter entries include files."""
651
- outpath = Path(io_handler.IOHandler().get_output_directory().parent / "_images")
690
+ outpath = io_handler.IOHandler().get_output_directory("_images")
652
691
  latest_parameter_version = max(
653
692
  comparison_data.get(parameter),
654
693
  key=lambda x: tuple(map(int, x["parameter_version"].split("."))),
@@ -664,13 +703,25 @@ class ReadParameters:
664
703
 
665
704
  file.write("The latest parameter version is plotted below.\n\n")
666
705
 
667
- if parameter != "camera_config_file":
668
- plot_name = f"{parameter}_{latest_parameter_version}_{self.site}_{tel}"
669
- image_path = outpath / f"{plot_name}.png"
670
- file.write(f"![Parameter plot.]({image_path.as_posix()})")
706
+ if parameter in (
707
+ "camera_config_file",
708
+ "mirror_list",
709
+ "primary_mirror_segmentation",
710
+ "secondary_mirror_segmentation",
711
+ ):
712
+ self._write_file_based_plot(
713
+ file, parameter, comparison_data, latest_model_version, outpath
714
+ )
671
715
  return
672
716
 
673
- # camera_config_file: find latest value and convert markdown link to png filename
717
+ plot_name = f"{parameter}_{latest_parameter_version}_{self.site}_{tel}"
718
+ image_path = outpath / f"{plot_name}.png"
719
+ file.write(f"![Parameter plot.]({image_path.as_posix()})")
720
+
721
+ def _write_file_based_plot(
722
+ self, file, parameter, comparison_data, latest_model_version, outpath
723
+ ):
724
+ """Write plot reference for file-based parameters."""
674
725
  latest_value = None
675
726
  for item in comparison_data.get(parameter):
676
727
  if latest_model_version in item["model_version"].split(", "):
@@ -686,7 +737,16 @@ class ReadParameters:
686
737
 
687
738
  filename_png = Path(match.group(1)).with_suffix(".png").name
688
739
  image_path = outpath / filename_png
689
- file.write(f"![Camera configuration plot.]({image_path.as_posix()})")
740
+
741
+ plot_descriptions = {
742
+ "camera_config_file": "Camera configuration plot",
743
+ "mirror_list": "Mirror panel layout",
744
+ "primary_mirror_segmentation": "Primary mirror segmentation",
745
+ "secondary_mirror_segmentation": "Secondary mirror segmentation",
746
+ }
747
+
748
+ description = plot_descriptions.get(parameter, "Parameter plot")
749
+ file.write(f"![{description}.]({image_path.as_posix()})")
690
750
 
691
751
  def _write_array_layouts_section(self, file, layouts):
692
752
  """Write the array layouts section of the report."""
@@ -34,6 +34,8 @@ class CorsikaRunner:
34
34
  Use seeds based on run number and primary particle. If False, use sim_telarray seeds.
35
35
  use_multipipe: bool
36
36
  Use multipipe to run CORSIKA and sim_telarray.
37
+ curved_atmosphere_min_zenith_angle: Quantity
38
+ Minimum zenith angle for which to use the curved-atmosphere CORSIKA binary.
37
39
  """
38
40
 
39
41
  def __init__(
@@ -43,6 +45,7 @@ class CorsikaRunner:
43
45
  label=None,
44
46
  keep_seeds=False,
45
47
  use_multipipe=False,
48
+ curved_atmosphere_min_zenith_angle=None,
46
49
  ):
47
50
  """Initialize CorsikaRunner."""
48
51
  self._logger = logging.getLogger(__name__)
@@ -52,6 +55,7 @@ class CorsikaRunner:
52
55
  self.corsika_config = corsika_config
53
56
  self._keep_seeds = keep_seeds
54
57
  self._use_multipipe = use_multipipe
58
+ self.curved_atmosphere_min_zenith_angle = curved_atmosphere_min_zenith_angle
55
59
 
56
60
  self._simtel_path = Path(simtel_path)
57
61
  self.io_handler = io_handler.IOHandler()
@@ -63,7 +67,7 @@ class CorsikaRunner:
63
67
  self, run_number=None, extra_commands=None, input_file=None, use_pfp=True
64
68
  ):
65
69
  """
66
- Get the full path of the run script file for a given run number.
70
+ Prepare and write CORSIKA run script.
67
71
 
68
72
  Parameters
69
73
  ----------
@@ -183,7 +187,7 @@ class CorsikaRunner:
183
187
  """
184
188
  Get autoinputs command.
185
189
 
186
- corsika_autoinputs is a tool to generate random and user/host dependent
190
+ corsika_autoinputs is a tool to generate random seeds and user/host dependent
187
191
  parameters for CORSIKA configuration.
188
192
 
189
193
  Parameters
@@ -198,7 +202,12 @@ class CorsikaRunner:
198
202
  str
199
203
  autoinputs command.
200
204
  """
201
- corsika_bin_path = self._simtel_path.joinpath("corsika-run/corsika")
205
+ if self.corsika_config.use_curved_atmosphere:
206
+ corsika_bin_path = self._simtel_path.joinpath("corsika-run/corsika-curved")
207
+ self._logger.debug("Using curved-atmosphere CORSIKA binary.")
208
+ else:
209
+ corsika_bin_path = self._simtel_path.joinpath("corsika-run/corsika")
210
+ self._logger.debug("Using flat-atmosphere CORSIKA binary.")
202
211
 
203
212
  log_file = self.get_file_name(file_type="log", run_number=run_number)
204
213
  if self._use_multipipe:
@@ -45,6 +45,7 @@ class CorsikaSimtelRunner:
45
45
  sim_telarray_seeds=None,
46
46
  sequential=False,
47
47
  calibration_config=None,
48
+ curved_atmosphere_min_zenith_angle=None,
48
49
  ):
49
50
  self._logger = logging.getLogger(__name__)
50
51
  self.corsika_config = gen.ensure_iterable(corsika_config)
@@ -63,6 +64,7 @@ class CorsikaSimtelRunner:
63
64
  label=label,
64
65
  keep_seeds=keep_seeds,
65
66
  use_multipipe=use_multipipe,
67
+ curved_atmosphere_min_zenith_angle=curved_atmosphere_min_zenith_angle,
66
68
  )
67
69
  # The simulator array should be defined for every CORSIKA configuration
68
70
  # because it allows to define multiple sim_telarray instances
@@ -218,3 +220,7 @@ class CorsikaSimtelRunner:
218
220
  else self.simulator_array[model_version_index]
219
221
  )
220
222
  return runner.get_file_name(file_type=file_type, run_number=run_number, mode=mode)
223
+
224
+ def get_resources(self, run_number=None):
225
+ """Return computing resources used."""
226
+ return self.corsika_runner.get_resources(run_number)
@@ -28,7 +28,7 @@ class RunnerServices:
28
28
  self.corsika_config = corsika_config
29
29
  self.directory = {}
30
30
 
31
- def _get_info_for_file_name(self, run_number):
31
+ def _get_info_for_file_name(self, run_number, calibration_run_mode=None):
32
32
  """
33
33
  Return dictionary for building names for simulation output files.
34
34
 
@@ -36,6 +36,8 @@ class RunnerServices:
36
36
  ----------
37
37
  run_number : int
38
38
  Run number.
39
+ calibration_run_mode: str
40
+ Calibration run mode.
39
41
 
40
42
  Returns
41
43
  -------
@@ -43,9 +45,12 @@ class RunnerServices:
43
45
  Dictionary with the keys or building the file names for simulation output files.
44
46
  """
45
47
  _vc_high = self.corsika_config.get_config_parameter("VIEWCONE")[1]
46
- primary_name = self.corsika_config.primary
47
- if primary_name == "gamma" and _vc_high > 0:
48
- primary_name = "gamma_diffuse"
48
+ if calibration_run_mode is not None and calibration_run_mode != "":
49
+ primary_name = calibration_run_mode
50
+ else:
51
+ primary_name = self.corsika_config.primary
52
+ if primary_name == "gamma" and _vc_high > 0:
53
+ primary_name = "gamma_diffuse"
49
54
  return {
50
55
  "run_number": self.corsika_config.validate_run_number(run_number),
51
56
  "primary": primary_name,
@@ -119,7 +124,7 @@ class RunnerServices:
119
124
  _logger.debug(f"Checking if {run_sub_file} exists")
120
125
  return Path(run_sub_file).is_file()
121
126
 
122
- def _get_file_basename(self, run_number):
127
+ def _get_file_basename(self, run_number, calibration_run_mode):
123
128
  """
124
129
  Get the base name for the simulation files.
125
130
 
@@ -127,13 +132,15 @@ class RunnerServices:
127
132
  ----------
128
133
  run_number: int
129
134
  Run number.
135
+ calibration_run_mode: str
136
+ Calibration run mode.
130
137
 
131
138
  Returns
132
139
  -------
133
140
  str
134
141
  Base name for the simulation files.
135
142
  """
136
- info_for_file_name = self._get_info_for_file_name(run_number)
143
+ info_for_file_name = self._get_info_for_file_name(run_number, calibration_run_mode)
137
144
  file_label = f"_{info_for_file_name['label']}" if info_for_file_name.get("label") else ""
138
145
  zenith = self.corsika_config.get_config_parameter("THETAP")[0]
139
146
  azimuth = self.corsika_config.azimuth_angle
@@ -232,6 +239,7 @@ class RunnerServices:
232
239
  file_type,
233
240
  run_number=None,
234
241
  mode=None,
242
+ calibration_run_mode=None,
235
243
  _model_version_index=0,
236
244
  ): # pylint: disable=unused-argument
237
245
  """
@@ -245,6 +253,8 @@ class RunnerServices:
245
253
  Run number.
246
254
  mode: str
247
255
  out or err (optional, relevant only for sub_log).
256
+ calibration_run_mode: str
257
+ Calibration run mode.
248
258
  model_version_index: int
249
259
  Index of the model version.
250
260
  This is not used here, but in other implementations of this function is
@@ -261,7 +271,7 @@ class RunnerServices:
261
271
  ValueError
262
272
  If file_type is unknown.
263
273
  """
264
- file_name = self._get_file_basename(run_number)
274
+ file_name = self._get_file_basename(run_number, calibration_run_mode)
265
275
 
266
276
  if file_type in ["log", "histogram", "corsika_log"]:
267
277
  return self._get_log_file_path(file_type, file_name)
@@ -1,7 +1,6 @@
1
1
  """Base class for running sim_telarray simulations."""
2
2
 
3
3
  import logging
4
- import stat
5
4
  import subprocess
6
5
  from pathlib import Path
7
6
 
@@ -34,15 +33,25 @@ class SimtelRunner:
34
33
  CORSIKA configuration.
35
34
  use_multipipe: bool
36
35
  Use multipipe to run CORSIKA and sim_telarray.
36
+ calibration_run_mode: str
37
+ Calibration run mode.
37
38
  """
38
39
 
39
- def __init__(self, simtel_path, label=None, corsika_config=None, use_multipipe=False):
40
+ def __init__(
41
+ self,
42
+ simtel_path,
43
+ label=None,
44
+ corsika_config=None,
45
+ use_multipipe=False,
46
+ calibration_run_mode=None,
47
+ ):
40
48
  """Initialize SimtelRunner."""
41
49
  self._logger = logging.getLogger(__name__)
42
50
 
43
51
  self._simtel_path = Path(simtel_path)
44
52
  self.label = label
45
53
  self._base_directory = None
54
+ self.calibration_run_mode = calibration_run_mode
46
55
 
47
56
  self.runs_per_set = 1
48
57
 
@@ -51,56 +60,6 @@ class SimtelRunner:
51
60
  "corsika_sim_telarray" if use_multipipe else "sim_telarray"
52
61
  )
53
62
 
54
- def __repr__(self):
55
- """Return a string representation of the SimtelRunner object."""
56
- return f"SimtelRunner(label={self.label})\n"
57
-
58
- def prepare_run_script(self, test=False, input_file=None, run_number=None, extra_commands=None):
59
- """
60
- Build and return the full path of the bash run script containing the sim_telarray command.
61
-
62
- Parameters
63
- ----------
64
- test: bool
65
- Test flag for faster execution.
66
- input_file: str or Path
67
- Full path of the input CORSIKA file.
68
- run_number: int
69
- Run number.
70
- extra_commands: str
71
- Additional commands for running simulations given in config.yml.
72
-
73
- Returns
74
- -------
75
- Path
76
- Full path of the run script.
77
- """
78
- script_file_path = self.get_file_name(file_type="sub_script", run_number=run_number)
79
- self._logger.debug(f"Run bash script - {script_file_path}")
80
- self._logger.debug(f"Extra commands to be added to the run script {extra_commands}")
81
-
82
- command = self._make_run_command(run_number=run_number, input_file=input_file)
83
- with script_file_path.open("w", encoding="utf-8") as file:
84
- file.write("#!/usr/bin/env bash\n\n")
85
- file.write("set -e\n")
86
- file.write("set -o pipefail\n")
87
- file.write("\nSECONDS=0\n")
88
-
89
- if extra_commands is not None:
90
- file.write("# Writing extras\n")
91
- for line in extra_commands:
92
- file.write(f"{line}\n")
93
- file.write("# End of extras\n\n")
94
-
95
- n = 1 if test else self.runs_per_set
96
- for _ in range(n):
97
- file.write(f"{command}\n\n")
98
-
99
- file.write('\necho "RUNTIME: $SECONDS"\n')
100
-
101
- script_file_path.chmod(script_file_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP)
102
- return script_file_path
103
-
104
63
  def run(self, test=False, input_file=None, run_number=None):
105
64
  """
106
65
  Make run command and run sim_telarray.
@@ -148,8 +107,6 @@ class SimtelRunner:
148
107
  msg = gen.get_log_excerpt(self._log_file)
149
108
  else:
150
109
  msg = "Simtel log file does not exist."
151
-
152
- self._logger.error(msg)
153
110
  raise SimtelExecutionError(msg)
154
111
 
155
112
  def _run_simtel_and_check_output(self, command, stdout_file, stderr_file):
@@ -255,4 +212,5 @@ class SimtelRunner:
255
212
  run_number=run_number,
256
213
  mode=mode,
257
214
  _model_version_index=model_version_index,
215
+ calibration_run_mode=self.calibration_run_mode,
258
216
  )
@@ -1,6 +1,8 @@
1
1
  %YAML 1.2
2
2
  ---
3
3
  title: Schema for flasher_pulse_exp_decay model parameter
4
+ deprecated: true
5
+ deprecation_note: "Replaced by updated definition of flasher_pulse_shape"
4
6
  schema_version: 0.1.0
5
7
  meta_schema: simpipe-schema
6
8
  meta_schema_url: https://raw.githubusercontent.com/gammasim/simtools/main/src/simtools/schemas/model_parameter_and_data_schema.metaschema.yml
@@ -1,6 +1,56 @@
1
1
  %YAML 1.2
2
2
  ---
3
3
  title: Schema for flasher_pulse_shape model parameter
4
+ schema_version: 0.2.0
5
+ meta_schema: simpipe-schema
6
+ meta_schema_url: https://raw.githubusercontent.com/gammasim/simtools/main/src/simtools/schemas/model_parameter_and_data_schema.metaschema.yml
7
+ meta_schema_version: 0.1.0
8
+ name: flasher_pulse_shape
9
+ short_description: |-
10
+ Definition of flasher pulse shape.
11
+ description: |-
12
+ Definition of flasher pulse shape both shape and relevant
13
+ timing parameters.
14
+ data:
15
+ - type: string
16
+ description: Shape of the flasher pulse.
17
+ default: Gauss
18
+ allowed_values:
19
+ - Gauss
20
+ - TopHat
21
+ - Exponential
22
+ - Gauss-Exponential
23
+ - type: float64
24
+ description: |-
25
+ Characteristic width of the flasher pulse shape.
26
+ For 'TopHat', this is the pulse width, for 'Gauss' and 'Gauss-Exponential'
27
+ the rms width. Ignored if shape is 'Exponential'.
28
+ unit: ns
29
+ default: 0
30
+ allowed_range:
31
+ min: 0
32
+ - type: float64
33
+ description: |-
34
+ Exponential decay time of the flasher pulse shape.
35
+ Only relevant if shape is 'Exponential' or 'Gauss-Exponential'.
36
+ unit: ns
37
+ default: 0
38
+ allowed_range:
39
+ min: 0
40
+ instrument:
41
+ class: Calibration
42
+ activity:
43
+ setting:
44
+ - SetParameterFromExternal
45
+ validation:
46
+ - ValidateParameterByExpert
47
+ source:
48
+ - Initial instrument setup
49
+ simulation_software:
50
+ - name: simtools
51
+ version: ">0.24.0"
52
+ ---
53
+ title: Schema for flasher_pulse_shape model parameter
4
54
  schema_version: 0.1.0
5
55
  meta_schema: simpipe-schema
6
56
  meta_schema_url: https://raw.githubusercontent.com/gammasim/simtools/main/src/simtools/schemas/model_parameter_and_data_schema.metaschema.yml
@@ -1,6 +1,8 @@
1
1
  %YAML 1.2
2
2
  ---
3
3
  title: Schema for flasher_pulse_width model parameter
4
+ deprecated: true
5
+ deprecation_note: "Replaced by updated definition of flasher_pulse_shape"
4
6
  schema_version: 0.1.0
5
7
  meta_schema: simpipe-schema
6
8
  meta_schema_url: https://raw.githubusercontent.com/gammasim/simtools/main/src/simtools/schemas/model_parameter_and_data_schema.metaschema.yml
@@ -74,6 +74,8 @@ $defs:
74
74
  - type: string
75
75
  - type: array
76
76
  - type: "null"
77
+ model_parameter_schema_version:
78
+ $ref: 'common_definitions.schema.yml#/$defs/semantic_version_pattern'
77
79
  required:
78
80
  - version
79
81
  additionalProperties: false