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
@@ -1,7 +1,9 @@
1
1
  """Functions asserting certain conditions are met (used e.g., in integration tests)."""
2
2
 
3
+ import gzip
3
4
  import json
4
5
  import logging
6
+ import tarfile
5
7
  from collections import defaultdict
6
8
  from pathlib import Path
7
9
 
@@ -174,12 +176,6 @@ def check_output_from_sim_telarray(file, file_test):
174
176
  _logger.debug(f"No expected output or metadata provided, skipping checks {file_test}")
175
177
  return True
176
178
 
177
- if file.suffix != ".zst":
178
- raise ValueError(
179
- f"Expected output file {file} is not a zstd compressed file "
180
- f"(i.e., a sim_telarray file)."
181
- )
182
-
183
179
  assert_output = assert_metadata = True
184
180
 
185
181
  if "expected_output" in file_test:
@@ -193,3 +189,63 @@ def check_output_from_sim_telarray(file, file_test):
193
189
  )
194
190
 
195
191
  return assert_n_showers_and_energy_range(file=file) and assert_output and assert_metadata
192
+
193
+
194
+ def _find_patterns(text, patterns):
195
+ """Find patterns in text."""
196
+ return {p for p in patterns if p in text}
197
+
198
+
199
+ def _read_log(member, tar):
200
+ """Read and decode a gzipped log file from a tar archive."""
201
+ with tar.extractfile(member) as gz, gzip.open(gz, "rb") as f:
202
+ return f.read().decode("utf-8", "ignore")
203
+
204
+
205
+ def check_simulation_logs(tar_file, file_test):
206
+ """
207
+ Check log files of CORSIKA and sim_telarray for expected output.
208
+
209
+ Parameters
210
+ ----------
211
+ tar_file: Path
212
+ Path to a log file tar package.
213
+ file_test: dict
214
+ File test description including expected log output.
215
+
216
+ Raises
217
+ ------
218
+ ValueError
219
+ If the file is not a tar file.
220
+ """
221
+ expected_log = file_test.get("expected_log_output", {})
222
+ wanted = expected_log.get("pattern", [])
223
+ forbidden = expected_log.get("forbidden_pattern", [])
224
+
225
+ if not (wanted or forbidden):
226
+ _logger.debug(f"No expected log output provided, skipping checks {file_test}")
227
+ return True
228
+
229
+ if not tarfile.is_tarfile(tar_file):
230
+ raise ValueError(f"File {tar_file} is not a tar file.")
231
+
232
+ found, bad = set(), set()
233
+ with tarfile.open(tar_file, "r:*") as tar:
234
+ for member in tar.getmembers():
235
+ if not member.name.endswith(".log.gz"):
236
+ continue
237
+ _logger.info(f"Scanning {member.name}")
238
+ text = _read_log(member, tar)
239
+ found |= _find_patterns(text, wanted)
240
+ bad |= _find_patterns(text, forbidden)
241
+
242
+ if bad:
243
+ _logger.error(f"Forbidden patterns found: {list(bad)}")
244
+ return False
245
+ missing = [p for p in wanted if p not in found]
246
+ if missing:
247
+ _logger.error(f"Missing expected patterns: {missing}")
248
+ return False
249
+
250
+ _logger.debug(f"All expected patterns found: {wanted}")
251
+ return True
@@ -204,7 +204,7 @@ def _prepare_test_options(config, output_path, model_version=None):
204
204
  if model_version and "model_version" in config:
205
205
  config.update({"model_version": model_version})
206
206
 
207
- for key in ["output_path", "data_directory", "pack_for_grid_register"]:
207
+ for key in ["output_path", "pack_for_grid_register"]:
208
208
  if key in config:
209
209
  config[key] = str(Path(output_path).joinpath(config[key]))
210
210
 
@@ -15,7 +15,10 @@ ERROR_PATTERNS = [
15
15
  re.compile(r"segmentation fault", re.IGNORECASE),
16
16
  ]
17
17
 
18
- IGNORE_PATTERNS = [re.compile(r"Falling back to 'utf-8' with errors='ignore'", re.IGNORECASE)]
18
+ IGNORE_PATTERNS = [
19
+ re.compile(r"Falling back to 'utf-8' with errors='ignore'", re.IGNORECASE),
20
+ re.compile(r"Failed to get user name[^\n]*setting it to UNKNOWN_USER", re.IGNORECASE),
21
+ ]
19
22
 
20
23
 
21
24
  def inspect(log_text):
@@ -4,9 +4,9 @@ import logging
4
4
 
5
5
  import numpy as np
6
6
 
7
+ from simtools.io.eventio_handler import get_corsika_run_number
7
8
  from simtools.simtel.simtel_config_reader import SimtelConfigReader
8
9
  from simtools.simtel.simtel_config_writer import sim_telarray_random_seeds
9
- from simtools.simtel.simtel_io_file_info import get_corsika_run_number
10
10
  from simtools.simtel.simtel_io_metadata import (
11
11
  get_sim_telarray_telescope_id,
12
12
  read_sim_telarray_metadata,
@@ -13,6 +13,22 @@ from simtools.testing import assertions
13
13
 
14
14
  _logger = logging.getLogger(__name__)
15
15
 
16
+ # Keys to ignore when comparing sim_telarray configuration files
17
+ # (e.g., version numbers, system dependent parameters, CORSIKA options)
18
+ cfg_ignore_keys = [
19
+ "config_release",
20
+ "Label",
21
+ "simtools_version",
22
+ "simtools_model_production_version",
23
+ "simtools_build_opt",
24
+ "simtools_extra_def",
25
+ "simtools_hadronic_model",
26
+ "simtools_avx_flag",
27
+ "simtools_corsika_version",
28
+ "simtools_corsika_opt_patch_version",
29
+ "simtools_bernlohr_version",
30
+ ]
31
+
16
32
 
17
33
  def validate_application_output(
18
34
  config, from_command_line=None, from_config_file=None, db_config=None
@@ -39,6 +55,10 @@ def validate_application_output(
39
55
 
40
56
  for integration_test in config["integration_tests"]:
41
57
  _logger.info(f"Testing application output: {integration_test}")
58
+ _logger.debug(
59
+ f"Model version from command line: {from_command_line}, "
60
+ f"from config file: {from_config_file}"
61
+ )
42
62
 
43
63
  if from_command_line == from_config_file:
44
64
  _validate_output_files(config, integration_test, db_config)
@@ -117,7 +137,10 @@ def _validate_output_path_and_file(config, integration_file_tests):
117
137
  except AssertionError as exc:
118
138
  raise AssertionError(f"Output file {output_file_path} does not exist. ") from exc
119
139
 
120
- assert assertions.check_output_from_sim_telarray(output_file_path, file_test)
140
+ if output_file_path.name.endswith(".simtel.zst"):
141
+ assert assertions.check_output_from_sim_telarray(output_file_path, file_test)
142
+ elif output_file_path.name.endswith(".log_hist.tar.gz"):
143
+ assert assertions.check_simulation_logs(output_file_path, file_test)
121
144
 
122
145
 
123
146
  def _validate_model_parameter_json_file(config, model_parameter_validation, db_config):
@@ -350,8 +373,8 @@ def _compare_simtel_cfg_files(reference_file, test_file):
350
373
  Compare two sim_telarray configuration files.
351
374
 
352
375
  Line-by-line string comparison. Requires similar sequence of
353
- parameters in the files. Ignore lines containing 'config_release'
354
- (as it contains the simtools package version).
376
+ parameters in the files. Ignore lines listed in cfg_ignore_keys
377
+ (e.g., simtools package versions or hadronic interaction model strings).
355
378
 
356
379
  Parameters
357
380
  ----------
@@ -370,16 +393,28 @@ def _compare_simtel_cfg_files(reference_file, test_file):
370
393
  reference_cfg = [line.rstrip() for line in f1 if line.strip()]
371
394
  test_cfg = [line.rstrip() for line in f2 if line.strip()]
372
395
 
373
- if len(reference_cfg) != len(test_cfg):
396
+ def filter_ignored(cfg_lines, file_label):
397
+ filtered = []
398
+ for line in cfg_lines:
399
+ ignored_key = next((ignore for ignore in cfg_ignore_keys if ignore in line), None)
400
+ if ignored_key:
401
+ _logger.debug(f"Ignoring line in {file_label} due to key '{ignored_key}': {line}")
402
+ continue
403
+ filtered.append(line)
404
+ return filtered
405
+
406
+ reference_cfg_filtered = filter_ignored(reference_cfg, "reference file")
407
+ test_cfg_filtered = filter_ignored(test_cfg, "test file")
408
+
409
+ if len(reference_cfg_filtered) != len(test_cfg_filtered):
374
410
  _logger.error(
375
- f"Line counts differ: {reference_file} ({len(reference_cfg)} lines), "
376
- f"{test_file} ({len(test_cfg)} lines)."
411
+ f"Line counts differ after filtering: {reference_file} "
412
+ f"({len(reference_cfg_filtered)} lines), "
413
+ f"{test_file} ({len(test_cfg_filtered)} lines)."
377
414
  )
378
415
  return False
379
416
 
380
- for ref_line, test_line in zip(reference_cfg, test_cfg):
381
- if any(ignore in ref_line for ignore in ("config_release", "Label", "simtools_version")):
382
- continue
417
+ for ref_line, test_line in zip(reference_cfg_filtered, test_cfg_filtered):
383
418
  if ref_line != test_line:
384
419
  _logger.error(
385
420
  f"Configuration files {reference_file} and {test_file} do not match: "
simtools/utils/names.py CHANGED
@@ -516,6 +516,8 @@ def get_site_from_array_element_name(array_element_name):
516
516
  Site name(s).
517
517
  """
518
518
  try: # e.g. instrument is 'North' as given for the site parameters
519
+ if array_element_name.startswith("OBS"):
520
+ return validate_site_name(array_element_name.split("-")[1])
519
521
  return validate_site_name(array_element_name)
520
522
  except ValueError: # e.g. instrument is 'LSTN' as given for the array element types
521
523
  return array_elements()[get_array_element_type_from_name(array_element_name)]["site"]
@@ -630,7 +632,6 @@ def get_simulation_software_name_from_parameter_name(
630
632
 
631
633
  def simtel_config_file_name(
632
634
  site,
633
- model_version,
634
635
  array_name=None,
635
636
  telescope_model_name=None,
636
637
  label=None,
@@ -645,8 +646,6 @@ def simtel_config_file_name(
645
646
  South or North.
646
647
  telescope_model_name: str
647
648
  LST-1, MST-FlashCam, ...
648
- model_version: str
649
- Version of the model.
650
649
  label: str
651
650
  Instance label.
652
651
  extra_label: str
@@ -661,7 +660,6 @@ def simtel_config_file_name(
661
660
  name += f"-{array_name}" if array_name is not None else ""
662
661
  name += f"-{site}"
663
662
  name += f"-{telescope_model_name}" if telescope_model_name is not None else ""
664
- name += f"-{model_version}"
665
663
  name += f"_{label}" if label is not None else ""
666
664
  name += f"_{extra_label}" if extra_label is not None else ""
667
665
  name += ".cfg"
simtools/version.py CHANGED
@@ -4,6 +4,8 @@
4
4
  # which is adapted from https://github.com/astropy/astropy/blob/master/astropy/version.py
5
5
  # see https://github.com/astropy/astropy/pull/10774 for a discussion on why this needed.
6
6
 
7
+ import re
8
+
7
9
  from packaging.specifiers import SpecifierSet
8
10
  from packaging.version import InvalidVersion, Version
9
11
 
@@ -192,6 +194,41 @@ def compare_versions(version_string_1, version_string_2, level=MAJOR_MINOR_PATCH
192
194
  return (ver1 > ver2) - (ver1 < ver2)
193
195
 
194
196
 
197
+ def is_valid_semantic_version(version_string, strict=True):
198
+ """
199
+ Check if a string is a valid semantic version.
200
+
201
+ Parameters
202
+ ----------
203
+ version_string : str
204
+ The version string to validate (e.g., "6.0.2", "1.0.0-alpha").
205
+ strict : bool, optional
206
+ If True, use PEP 440 validation (packaging.version.Version).
207
+ If False, use SemVer 2.0.0 regex pattern (allows more flexible pre-release identifiers).
208
+
209
+ Returns
210
+ -------
211
+ bool
212
+ True if the version string is valid, False otherwise.
213
+ """
214
+ if not version_string:
215
+ return False
216
+
217
+ if strict:
218
+ try:
219
+ Version(version_string)
220
+ return True
221
+ except InvalidVersion:
222
+ return False
223
+ else:
224
+ semver_regex = (
225
+ r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)" # major.minor.patch
226
+ r"(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?" # pre-release
227
+ r"(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$" # build metadata
228
+ )
229
+ return bool(re.match(semver_regex, version_string))
230
+
231
+
195
232
  def check_version_constraint(version_string, constraint):
196
233
  """
197
234
  Check if a version satisfies a constraint.
@@ -11,12 +11,20 @@ Radii are relative to a reference radius (REFERENCE_RADIUS).
11
11
  """
12
12
  TELESCOPE_CONFIG = {
13
13
  "LST": {"color": "darkorange", "radius": 12.5, "shape": "circle", "filled": False},
14
- "MST": {"color": "dodgerblue", "radius": 9.15, "shape": "circle", "filled": True},
15
- "SCT": {"color": "black", "radius": 7.15, "shape": "square", "filled": True},
16
- "SST": {"color": "darkgreen", "radius": 3.0, "shape": "circle", "filled": True},
14
+ "MST": {"color": "dodgerblue", "radius": 9.15, "shape": "circle", "filled": False},
15
+ "SCT": {"color": "black", "radius": 7.15, "shape": "square", "filled": False},
16
+ "SST": {"color": "darkgreen", "radius": 3.0, "shape": "circle", "filled": False},
17
17
  "HESS": {"color": "grey", "radius": 6.0, "shape": "hexagon", "filled": True},
18
18
  "MAGIC": {"color": "grey", "radius": 8.5, "shape": "hexagon", "filled": True},
19
19
  "VERITAS": {"color": "grey", "radius": 6.0, "shape": "hexagon", "filled": True},
20
+ "CEI": {"color": "purple", "radius": 2.0, "shape": "hexagon", "filled": True},
21
+ "RLD": {"color": "brown", "radius": 2.0, "shape": "hexagon", "filled": True},
22
+ "STP": {"color": "olive", "radius": 2.0, "shape": "hexagon", "filled": True},
23
+ "MSP": {"color": "teal", "radius": 2.0, "shape": "hexagon", "filled": True},
24
+ "ILL": {"color": "red", "radius": 2.0, "shape": "hexagon", "filled": False},
25
+ "WST": {"color": "maroon", "radius": 2.0, "shape": "hexagon", "filled": True},
26
+ "ASC": {"color": "cyan", "radius": 2.0, "shape": "hexagon", "filled": True},
27
+ "DUS": {"color": "magenta", "radius": 2.0, "shape": "hexagon", "filled": True},
20
28
  }
21
29
 
22
30
  REFERENCE_RADIUS = 12.5
@@ -30,7 +38,7 @@ def get_telescope_config(telescope_type):
30
38
 
31
39
  Parameters
32
40
  ----------
33
- telescope_type : str
41
+ telescope_type : str, None
34
42
  The type of the telescope (e.g., "LSTN", "MSTS").
35
43
 
36
44
  Returns
@@ -38,6 +46,8 @@ def get_telescope_config(telescope_type):
38
46
  dict
39
47
  The configuration dictionary for the telescope type.
40
48
  """
49
+ if telescope_type is None:
50
+ return {"color": "blue", "radius": 2.0, "shape": "hexagon", "filled": True}
41
51
  config = TELESCOPE_CONFIG.get(telescope_type)
42
52
  if not config and len(telescope_type) >= 3:
43
53
  config = TELESCOPE_CONFIG.get(telescope_type[:3])