gammasimtools 0.23.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 (90) hide show
  1. {gammasimtools-0.23.0.dist-info → gammasimtools-0.25.0.dist-info}/METADATA +1 -1
  2. {gammasimtools-0.23.0.dist-info → gammasimtools-0.25.0.dist-info}/RECORD +89 -85
  3. {gammasimtools-0.23.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 +54 -4
  6. simtools/applications/convert_geo_coordinates_of_array_elements.py +1 -1
  7. simtools/applications/db_add_file_to_db.py +2 -2
  8. simtools/applications/db_add_simulation_model_from_repository_to_db.py +1 -1
  9. simtools/applications/db_add_value_from_json_to_db.py +2 -2
  10. simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +1 -1
  11. simtools/applications/db_generate_compound_indexes.py +1 -1
  12. simtools/applications/db_get_array_layouts_from_db.py +2 -2
  13. simtools/applications/db_get_file_from_db.py +1 -1
  14. simtools/applications/db_get_parameter_from_db.py +1 -1
  15. simtools/applications/db_inspect_databases.py +4 -2
  16. simtools/applications/db_upload_model_repository.py +1 -1
  17. simtools/applications/derive_ctao_array_layouts.py +1 -1
  18. simtools/applications/derive_psf_parameters.py +5 -0
  19. simtools/applications/derive_pulse_shape_parameters.py +195 -0
  20. simtools/applications/generate_array_config.py +1 -1
  21. simtools/applications/maintain_simulation_model_add_production.py +11 -21
  22. simtools/applications/plot_array_layout.py +63 -1
  23. simtools/applications/production_generate_grid.py +1 -1
  24. simtools/applications/simulate_flasher.py +3 -2
  25. simtools/applications/simulate_pedestals.py +1 -1
  26. simtools/applications/simulate_prod.py +8 -23
  27. simtools/applications/simulate_prod_htcondor_generator.py +7 -0
  28. simtools/applications/submit_array_layouts.py +7 -5
  29. simtools/applications/validate_camera_fov.py +1 -1
  30. simtools/applications/validate_cumulative_psf.py +2 -2
  31. simtools/applications/validate_file_using_schema.py +49 -123
  32. simtools/applications/validate_optics.py +1 -1
  33. simtools/configuration/commandline_parser.py +15 -15
  34. simtools/configuration/configurator.py +1 -1
  35. simtools/corsika/corsika_config.py +199 -91
  36. simtools/data_model/model_data_writer.py +15 -3
  37. simtools/data_model/schema.py +145 -36
  38. simtools/data_model/validate_data.py +82 -48
  39. simtools/db/db_handler.py +61 -294
  40. simtools/db/db_model_upload.py +3 -2
  41. simtools/db/mongo_db.py +626 -0
  42. simtools/dependencies.py +38 -17
  43. simtools/io/eventio_handler.py +128 -0
  44. simtools/job_execution/htcondor_script_generator.py +0 -2
  45. simtools/layout/array_layout.py +7 -7
  46. simtools/layout/array_layout_utils.py +4 -4
  47. simtools/model/array_model.py +72 -72
  48. simtools/model/calibration_model.py +12 -9
  49. simtools/model/model_parameter.py +196 -160
  50. simtools/model/model_repository.py +176 -39
  51. simtools/model/model_utils.py +3 -3
  52. simtools/model/site_model.py +59 -27
  53. simtools/model/telescope_model.py +21 -13
  54. simtools/ray_tracing/mirror_panel_psf.py +4 -4
  55. simtools/ray_tracing/psf_analysis.py +11 -8
  56. simtools/ray_tracing/psf_parameter_optimisation.py +823 -680
  57. simtools/reporting/docs_auto_report_generator.py +1 -1
  58. simtools/reporting/docs_read_parameters.py +72 -11
  59. simtools/runners/corsika_runner.py +12 -3
  60. simtools/runners/corsika_simtel_runner.py +6 -0
  61. simtools/runners/runner_services.py +17 -7
  62. simtools/runners/simtel_runner.py +12 -54
  63. simtools/schemas/model_parameters/flasher_pulse_exp_decay.schema.yml +2 -0
  64. simtools/schemas/model_parameters/flasher_pulse_shape.schema.yml +50 -0
  65. simtools/schemas/model_parameters/flasher_pulse_width.schema.yml +2 -0
  66. simtools/schemas/simulation_models_info.schema.yml +4 -1
  67. simtools/simtel/pulse_shapes.py +268 -0
  68. simtools/simtel/simtel_config_writer.py +179 -21
  69. simtools/simtel/simtel_io_event_writer.py +2 -2
  70. simtools/simtel/simulator_array.py +58 -12
  71. simtools/simtel/simulator_light_emission.py +45 -8
  72. simtools/simulator.py +361 -346
  73. simtools/testing/assertions.py +110 -10
  74. simtools/testing/configuration.py +1 -1
  75. simtools/testing/log_inspector.py +4 -1
  76. simtools/testing/sim_telarray_metadata.py +1 -1
  77. simtools/testing/validate_output.py +46 -15
  78. simtools/utils/names.py +2 -4
  79. simtools/utils/value_conversion.py +10 -5
  80. simtools/version.py +61 -0
  81. simtools/visualization/legend_handlers.py +14 -4
  82. simtools/visualization/plot_array_layout.py +229 -33
  83. simtools/visualization/plot_mirrors.py +837 -0
  84. simtools/visualization/plot_pixels.py +1 -1
  85. simtools/visualization/plot_psf.py +1 -1
  86. simtools/visualization/plot_tables.py +1 -1
  87. simtools/simtel/simtel_io_file_info.py +0 -62
  88. {gammasimtools-0.23.0.dist-info → gammasimtools-0.25.0.dist-info}/WHEEL +0 -0
  89. {gammasimtools-0.23.0.dist-info → gammasimtools-0.25.0.dist-info}/licenses/LICENSE +0 -0
  90. {gammasimtools-0.23.0.dist-info → gammasimtools-0.25.0.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,17 @@
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
 
8
10
  import numpy as np
9
11
  import yaml
10
12
 
13
+ from simtools.simtel.simtel_io_metadata import read_sim_telarray_metadata
14
+
11
15
  _logger = logging.getLogger(__name__)
12
16
 
13
17
 
@@ -124,7 +128,35 @@ def assert_expected_output(file, expected_output):
124
128
  return True
125
129
 
126
130
 
127
- def check_output_from_sim_telarray(file, expected_output):
131
+ def assert_expected_simtel_metadata(file, expected_metadata):
132
+ """
133
+ Assert that expected metadata is present in the sim_telarray file.
134
+
135
+ Parameters
136
+ ----------
137
+ file: Path
138
+ Path to the sim_telarray file.
139
+ expected_metadata: dict
140
+ Expected metadata values.
141
+
142
+ """
143
+ global_meta, telescope_meta = read_sim_telarray_metadata(file)
144
+
145
+ for key, value in expected_metadata.items():
146
+ if key not in global_meta and key not in telescope_meta:
147
+ _logger.error(f"Metadata key {key} not found in sim_telarray file {file}")
148
+ return False
149
+ if key in global_meta and global_meta[key] != value:
150
+ _logger.error(
151
+ f"Metadata key {key} has value {global_meta[key]} instead of expected {value}"
152
+ )
153
+ return False
154
+ _logger.debug(f"Metadata key {key} matches expected value {value}")
155
+
156
+ return True
157
+
158
+
159
+ def check_output_from_sim_telarray(file, file_test):
128
160
  """
129
161
  Check that the sim_telarray simulation result is reasonable and matches the expected output.
130
162
 
@@ -132,20 +164,88 @@ def check_output_from_sim_telarray(file, expected_output):
132
164
  ----------
133
165
  file: Path
134
166
  Path to the sim_telarray file.
135
- expected_output: dict
136
- Expected output values.
167
+ file_test: dict
168
+ File test description including expected output and metadata.
137
169
 
138
170
  Raises
139
171
  ------
140
172
  ValueError
141
173
  If the file is not a zstd compressed file.
142
174
  """
143
- if file.suffix != ".zst":
144
- raise ValueError(
145
- f"Expected output file {file} is not a zstd compressed file "
146
- f"(i.e., a sim_telarray file)."
175
+ if "expected_output" not in file_test and "expected_simtel_metadata" not in file_test:
176
+ _logger.debug(f"No expected output or metadata provided, skipping checks {file_test}")
177
+ return True
178
+
179
+ assert_output = assert_metadata = True
180
+
181
+ if "expected_output" in file_test:
182
+ assert_output = assert_expected_output(
183
+ file=file, expected_output=file_test["expected_output"]
147
184
  )
148
185
 
149
- return assert_n_showers_and_energy_range(file=file) and assert_expected_output(
150
- file=file, expected_output=expected_output
151
- )
186
+ if "expected_simtel_metadata" in file_test:
187
+ assert_metadata = assert_expected_simtel_metadata(
188
+ file=file, expected_metadata=file_test["expected_simtel_metadata"]
189
+ )
190
+
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,11 +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
- if "expected_output" in file_test:
121
- assert assertions.check_output_from_sim_telarray(
122
- output_file_path,
123
- file_test["expected_output"],
124
- )
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)
125
144
 
126
145
 
127
146
  def _validate_model_parameter_json_file(config, model_parameter_validation, db_config):
@@ -139,7 +158,7 @@ def _validate_model_parameter_json_file(config, model_parameter_validation, db_c
139
158
 
140
159
  """
141
160
  _logger.info(f"Checking model parameter json file: {model_parameter_validation}")
142
- db = db_handler.DatabaseHandler(mongo_db_config=db_config)
161
+ db = db_handler.DatabaseHandler(db_config=db_config)
143
162
 
144
163
  reference_parameter_name = model_parameter_validation.get("reference_parameter_name")
145
164
 
@@ -346,7 +365,7 @@ def _validate_simtel_cfg_files(config, simtel_cfg_file):
346
365
  f"Comparing simtel cfg files: {reference_file} and {test_file} "
347
366
  f"for model version {config['configuration']['model_version']}"
348
367
  )
349
- return _compare_simtel_cfg_files(reference_file, test_file)
368
+ assert _compare_simtel_cfg_files(reference_file, test_file)
350
369
 
351
370
 
352
371
  def _compare_simtel_cfg_files(reference_file, test_file):
@@ -354,8 +373,8 @@ def _compare_simtel_cfg_files(reference_file, test_file):
354
373
  Compare two sim_telarray configuration files.
355
374
 
356
375
  Line-by-line string comparison. Requires similar sequence of
357
- parameters in the files. Ignore lines containing 'config_release'
358
- (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).
359
378
 
360
379
  Parameters
361
380
  ----------
@@ -374,16 +393,28 @@ def _compare_simtel_cfg_files(reference_file, test_file):
374
393
  reference_cfg = [line.rstrip() for line in f1 if line.strip()]
375
394
  test_cfg = [line.rstrip() for line in f2 if line.strip()]
376
395
 
377
- 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):
378
410
  _logger.error(
379
- f"Line counts differ: {reference_file} ({len(reference_cfg)} lines), "
380
- 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)."
381
414
  )
382
415
  return False
383
416
 
384
- for ref_line, test_line in zip(reference_cfg, test_cfg):
385
- if any(ignore in ref_line for ignore in ("config_release", "Label")):
386
- continue
417
+ for ref_line, test_line in zip(reference_cfg_filtered, test_cfg_filtered):
387
418
  if ref_line != test_line:
388
419
  _logger.error(
389
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"
@@ -170,16 +170,21 @@ def get_value_as_quantity(value, unit):
170
170
 
171
171
  Raises
172
172
  ------
173
- u.UnitConversionError
173
+ ValueError
174
174
  If the value cannot be converted to the given unit.
175
175
  """
176
176
  if isinstance(value, u.Quantity):
177
177
  try:
178
178
  return value.to(unit)
179
- except u.UnitConversionError:
180
- _logger.error(f"Cannot convert {value.unit} to {unit}.")
181
- raise
182
- return value * unit
179
+ except u.UnitConversionError as exc:
180
+ raise ValueError(f"Cannot convert {value} with unit {value.unit} to {unit}.") from exc
181
+ elif not isinstance(value, int | float):
182
+ return value
183
+
184
+ if unit is None or unit == "null":
185
+ return value * u.dimensionless_unscaled
186
+
187
+ return value * u.Unit(unit)
183
188
 
184
189
 
185
190
  def _unit_as_string(unit):
simtools/version.py CHANGED
@@ -4,6 +4,9 @@
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
+
9
+ from packaging.specifiers import SpecifierSet
7
10
  from packaging.version import InvalidVersion, Version
8
11
 
9
12
  MAJOR_MINOR_PATCH = "major.minor.patch"
@@ -189,3 +192,61 @@ def compare_versions(version_string_1, version_string_2, level=MAJOR_MINOR_PATCH
189
192
  raise ValueError(f"Unknown level: {level}")
190
193
 
191
194
  return (ver1 > ver2) - (ver1 < ver2)
195
+
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
+
232
+ def check_version_constraint(version_string, constraint):
233
+ """
234
+ Check if a version satisfies a constraint.
235
+
236
+ Parameters
237
+ ----------
238
+ version_string : str
239
+ The version string to check (e.g., "6.0.2").
240
+ constraint : str
241
+ The version constraint to check against (e.g., ">=6.0.0").
242
+
243
+ Returns
244
+ -------
245
+ bool
246
+ True if the version satisfies the constraint, False otherwise.
247
+ """
248
+ spec = SpecifierSet(constraint.strip(), prereleases=True)
249
+ ver = Version(version_string)
250
+ if ver in spec:
251
+ return True
252
+ return False
@@ -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])