gammasimtools 0.8.2__py3-none-any.whl → 0.10.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 (122) hide show
  1. {gammasimtools-0.8.2.dist-info → gammasimtools-0.10.0.dist-info}/METADATA +4 -4
  2. {gammasimtools-0.8.2.dist-info → gammasimtools-0.10.0.dist-info}/RECORD +119 -105
  3. {gammasimtools-0.8.2.dist-info → gammasimtools-0.10.0.dist-info}/WHEEL +1 -1
  4. {gammasimtools-0.8.2.dist-info → gammasimtools-0.10.0.dist-info}/entry_points.txt +4 -1
  5. simtools/_version.py +2 -2
  6. simtools/applications/calculate_trigger_rate.py +15 -38
  7. simtools/applications/convert_all_model_parameters_from_simtel.py +9 -28
  8. simtools/applications/convert_geo_coordinates_of_array_elements.py +54 -53
  9. simtools/applications/convert_model_parameter_from_simtel.py +2 -2
  10. simtools/applications/db_add_file_to_db.py +1 -2
  11. simtools/applications/db_add_simulation_model_from_repository_to_db.py +110 -0
  12. simtools/applications/db_add_value_from_json_to_db.py +2 -11
  13. simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +6 -6
  14. simtools/applications/db_get_array_layouts_from_db.py +3 -1
  15. simtools/applications/db_get_file_from_db.py +11 -12
  16. simtools/applications/db_get_parameter_from_db.py +44 -32
  17. simtools/applications/derive_mirror_rnda.py +10 -1
  18. simtools/applications/derive_photon_electron_spectrum.py +99 -0
  19. simtools/applications/derive_psf_parameters.py +1 -1
  20. simtools/applications/generate_array_config.py +18 -22
  21. simtools/applications/generate_regular_arrays.py +24 -21
  22. simtools/applications/generate_simtel_array_histograms.py +11 -48
  23. simtools/applications/plot_array_layout.py +3 -1
  24. simtools/applications/plot_tabular_data.py +84 -0
  25. simtools/applications/production_generate_simulation_config.py +25 -7
  26. simtools/applications/production_scale_events.py +3 -4
  27. simtools/applications/simulate_light_emission.py +2 -2
  28. simtools/applications/simulate_prod.py +25 -60
  29. simtools/applications/simulate_prod_htcondor_generator.py +95 -0
  30. simtools/applications/submit_data_from_external.py +12 -4
  31. simtools/applications/submit_model_parameter_from_external.py +8 -6
  32. simtools/applications/validate_camera_efficiency.py +3 -3
  33. simtools/applications/validate_camera_fov.py +3 -7
  34. simtools/applications/validate_cumulative_psf.py +3 -7
  35. simtools/applications/validate_file_using_schema.py +38 -24
  36. simtools/applications/validate_optics.py +3 -4
  37. simtools/{camera_efficiency.py → camera/camera_efficiency.py} +1 -4
  38. simtools/camera/single_photon_electron_spectrum.py +168 -0
  39. simtools/configuration/commandline_parser.py +14 -13
  40. simtools/configuration/configurator.py +6 -19
  41. simtools/constants.py +10 -3
  42. simtools/corsika/corsika_config.py +8 -7
  43. simtools/corsika/corsika_histograms.py +1 -1
  44. simtools/data_model/data_reader.py +0 -3
  45. simtools/data_model/metadata_collector.py +21 -4
  46. simtools/data_model/metadata_model.py +8 -111
  47. simtools/data_model/model_data_writer.py +18 -64
  48. simtools/data_model/schema.py +213 -0
  49. simtools/data_model/validate_data.py +73 -51
  50. simtools/db/db_handler.py +395 -790
  51. simtools/db/db_model_upload.py +139 -0
  52. simtools/io_operations/hdf5_handler.py +54 -24
  53. simtools/io_operations/legacy_data_handler.py +61 -0
  54. simtools/job_execution/htcondor_script_generator.py +133 -0
  55. simtools/job_execution/job_manager.py +77 -50
  56. simtools/layout/array_layout.py +33 -28
  57. simtools/model/array_model.py +13 -7
  58. simtools/model/camera.py +4 -2
  59. simtools/model/model_parameter.py +61 -63
  60. simtools/model/site_model.py +3 -3
  61. simtools/production_configuration/calculate_statistical_errors_grid_point.py +119 -144
  62. simtools/production_configuration/event_scaler.py +7 -17
  63. simtools/production_configuration/generate_simulation_config.py +5 -32
  64. simtools/production_configuration/interpolation_handler.py +8 -11
  65. simtools/ray_tracing/mirror_panel_psf.py +47 -27
  66. simtools/runners/corsika_runner.py +14 -3
  67. simtools/runners/corsika_simtel_runner.py +3 -1
  68. simtools/runners/runner_services.py +3 -3
  69. simtools/runners/simtel_runner.py +27 -8
  70. simtools/schemas/input/MST_mirror_2f_measurements.schema.yml +39 -0
  71. simtools/schemas/input/single_pe_spectrum.schema.yml +38 -0
  72. simtools/schemas/integration_tests_config.metaschema.yml +23 -3
  73. simtools/schemas/model_parameter.metaschema.yml +95 -2
  74. simtools/schemas/model_parameter_and_data_schema.metaschema.yml +2 -0
  75. simtools/schemas/model_parameters/array_element_position_utm.schema.yml +1 -1
  76. simtools/schemas/model_parameters/array_window.schema.yml +37 -0
  77. simtools/schemas/model_parameters/asum_clipping.schema.yml +0 -4
  78. simtools/schemas/model_parameters/channels_per_chip.schema.yml +1 -1
  79. simtools/schemas/model_parameters/corsika_iact_io_buffer.schema.yml +2 -2
  80. simtools/schemas/model_parameters/dsum_clipping.schema.yml +0 -2
  81. simtools/schemas/model_parameters/dsum_ignore_below.schema.yml +0 -2
  82. simtools/schemas/model_parameters/dsum_offset.schema.yml +0 -2
  83. simtools/schemas/model_parameters/dsum_pedsub.schema.yml +0 -2
  84. simtools/schemas/model_parameters/dsum_pre_clipping.schema.yml +0 -2
  85. simtools/schemas/model_parameters/dsum_prescale.schema.yml +0 -2
  86. simtools/schemas/model_parameters/dsum_presum_max.schema.yml +0 -2
  87. simtools/schemas/model_parameters/dsum_presum_shift.schema.yml +0 -2
  88. simtools/schemas/model_parameters/dsum_shaping.schema.yml +0 -2
  89. simtools/schemas/model_parameters/dsum_shaping_renormalize.schema.yml +0 -2
  90. simtools/schemas/model_parameters/dsum_threshold.schema.yml +0 -2
  91. simtools/schemas/model_parameters/dsum_zero_clip.schema.yml +0 -2
  92. simtools/schemas/model_parameters/effective_focal_length.schema.yml +31 -1
  93. simtools/schemas/model_parameters/fadc_compensate_pedestal.schema.yml +1 -1
  94. simtools/schemas/model_parameters/fadc_lg_compensate_pedestal.schema.yml +1 -1
  95. simtools/schemas/model_parameters/fadc_noise.schema.yml +3 -3
  96. simtools/schemas/model_parameters/fake_mirror_list.schema.yml +33 -0
  97. simtools/schemas/model_parameters/laser_photons.schema.yml +2 -2
  98. simtools/schemas/model_parameters/secondary_mirror_degraded_reflection.schema.yml +1 -1
  99. simtools/schemas/production_configuration_metrics.schema.yml +68 -0
  100. simtools/schemas/production_tables.schema.yml +41 -0
  101. simtools/simtel/simtel_config_writer.py +5 -6
  102. simtools/simtel/simtel_io_histogram.py +32 -67
  103. simtools/simtel/simtel_io_histograms.py +15 -30
  104. simtools/simtel/simtel_table_reader.py +410 -0
  105. simtools/simtel/simulator_array.py +2 -1
  106. simtools/simtel/simulator_camera_efficiency.py +11 -4
  107. simtools/simtel/simulator_light_emission.py +5 -3
  108. simtools/simtel/simulator_ray_tracing.py +2 -2
  109. simtools/simulator.py +80 -33
  110. simtools/testing/configuration.py +12 -8
  111. simtools/testing/helpers.py +9 -16
  112. simtools/testing/validate_output.py +152 -68
  113. simtools/utils/general.py +149 -12
  114. simtools/utils/names.py +25 -21
  115. simtools/utils/value_conversion.py +9 -1
  116. simtools/visualization/plot_tables.py +106 -0
  117. simtools/visualization/visualize.py +43 -5
  118. simtools/applications/db_add_model_parameters_from_repository_to_db.py +0 -184
  119. simtools/db/db_array_elements.py +0 -130
  120. simtools/db/db_from_repo_handler.py +0 -106
  121. {gammasimtools-0.8.2.dist-info → gammasimtools-0.10.0.dist-info}/LICENSE +0 -0
  122. {gammasimtools-0.8.2.dist-info → gammasimtools-0.10.0.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,6 @@
3
3
  import logging
4
4
  from pathlib import Path
5
5
 
6
- import pytest
7
6
  import yaml
8
7
 
9
8
  import simtools.utils.general as gen
@@ -11,6 +10,10 @@ import simtools.utils.general as gen
11
10
  _logger = logging.getLogger(__name__)
12
11
 
13
12
 
13
+ class VersionError(Exception):
14
+ """Raise if model version requested is not supported."""
15
+
16
+
14
17
  def get_list_of_test_configurations(config_files):
15
18
  """
16
19
  Return list of test configuration dictionaries or test names.
@@ -121,7 +124,7 @@ def _skip_test_for_model_version(config, model_version_requested):
121
124
  return
122
125
  model_version_config = config["CONFIGURATION"]["MODEL_VERSION"]
123
126
  if model_version_requested != model_version_config:
124
- pytest.skip(
127
+ raise VersionError(
125
128
  f"Model version requested {model_version_requested} not supported for this test"
126
129
  )
127
130
 
@@ -158,13 +161,14 @@ def _prepare_test_options(config, output_path, model_version=None):
158
161
 
159
162
  tmp_config_file = output_path / "tmp_config.yml"
160
163
  config_file_model_version = config.get("MODEL_VERSION")
161
- if model_version is not None and "MODEL_VERSION" in config:
164
+ if model_version and "MODEL_VERSION" in config:
162
165
  config.update({"MODEL_VERSION": model_version})
163
- if "OUTPUT_PATH" in config:
164
- config.update({"OUTPUT_PATH": str(Path(output_path).joinpath(config["OUTPUT_PATH"]))})
165
- config.update({"USE_PLAIN_OUTPUT_PATH": True})
166
- if "DATA_DIRECTORY" in config:
167
- config.update({"DATA_DIRECTORY": str(Path(output_path).joinpath(config["DATA_DIRECTORY"]))})
166
+
167
+ for key in ["OUTPUT_PATH", "DATA_DIRECTORY", "PACK_FOR_GRID_REGISTER"]:
168
+ if key in config:
169
+ config[key] = str(Path(output_path).joinpath(config[key]))
170
+ if key == "OUTPUT_PATH":
171
+ config["USE_PLAIN_OUTPUT_PATH"] = True
168
172
 
169
173
  _logger.info(f"Writing config file: {tmp_config_file}")
170
174
  with open(tmp_config_file, "w", encoding="utf-8") as file:
@@ -1,25 +1,18 @@
1
1
  """Helper functions for integration testing."""
2
2
 
3
3
  import os
4
-
5
- import pytest
4
+ from pathlib import Path
6
5
 
7
6
 
8
7
  def skip_camera_efficiency(config):
9
8
  """Skip camera efficiency tests if the old version of testeff is used."""
10
- if "camera-efficiency" in config["APPLICATION"]:
11
- if not _new_testeff_version():
12
- pytest.skip(
13
- "Any applications calling the old version of testeff are skipped "
14
- "due to a limitation of the old testeff not allowing to specify "
15
- "the include directory. Please update your sim_telarray tarball."
16
- )
17
- full_test_name = f"{config['APPLICATION']}_{config['TEST_NAME']}"
18
- if "simtools-validate-camera-efficiency_SSTS" == full_test_name:
19
- pytest.skip(
20
- "The test simtools-validate-camera-efficiency_SSTS is skipped "
21
- "since the fake SST mirrors are not yet implemented (#1155)"
22
- )
9
+ if "camera-efficiency" in config["APPLICATION"] and not _new_testeff_version():
10
+ return (
11
+ "Any applications calling the old version of testeff are skipped "
12
+ "due to a limitation of the old testeff not allowing to specify "
13
+ "the include directory. Please update your sim_telarray tarball."
14
+ )
15
+ return None
23
16
 
24
17
 
25
18
  def _new_testeff_version():
@@ -28,7 +21,7 @@ def _new_testeff_version():
28
21
 
29
22
  This test checks if the new version is used.
30
23
  """
31
- testeff_path = os.path.join(os.getenv("SIMTOOLS_SIMTEL_PATH"), "sim_telarray/testeff.c")
24
+ testeff_path = Path(os.getenv("SIMTOOLS_SIMTEL_PATH")) / "sim_telarray/testeff.c"
32
25
  try:
33
26
  with open(testeff_path, encoding="utf-8") as file:
34
27
  file_content = file.read()
@@ -12,39 +12,22 @@ from simtools.testing import assertions
12
12
  _logger = logging.getLogger(__name__)
13
13
 
14
14
 
15
- def validate_all_tests(config, request, config_file_model_version):
16
- """
17
- Validate test output for all integration tests.
18
-
19
- Parameters
20
- ----------
21
- config: dict
22
- Integration test configuration dictionary.
23
- request: request
24
- Request object.
25
- config_file_model_version: str
26
- Model version from the configuration file.
27
-
28
- """
29
- if request.config.getoption("--model_version") is None:
30
- validate_application_output(config)
31
- elif config_file_model_version is not None:
32
- _from_command_line = request.config.getoption("--model_version")
33
- _from_config_file = config_file_model_version
34
- if _from_command_line == _from_config_file:
35
- validate_application_output(config)
36
-
37
-
38
- def validate_application_output(config):
15
+ def validate_application_output(config, from_command_line=None, from_config_file=None):
39
16
  """
40
17
  Validate application output against expected output.
41
18
 
42
19
  Expected output is defined in configuration file.
20
+ Some tests run only if the model version from the command line
21
+ equals the model version from the configuration file.
43
22
 
44
23
  Parameters
45
24
  ----------
46
25
  config: dict
47
26
  dictionary with the configuration for the application test.
27
+ from_command_line: str
28
+ Model version from the command line.
29
+ from_config_file: str
30
+ Model version from the configuration file.
48
31
 
49
32
  """
50
33
  if "INTEGRATION_TESTS" not in config:
@@ -52,19 +35,37 @@ def validate_application_output(config):
52
35
 
53
36
  for integration_test in config["INTEGRATION_TESTS"]:
54
37
  _logger.info(f"Testing application output: {integration_test}")
55
- if "REFERENCE_OUTPUT_FILE" in integration_test:
56
- _validate_reference_output_file(config, integration_test)
57
-
58
- if "OUTPUT_FILE" in integration_test:
59
- _validate_output_path_and_file(config, integration_test)
60
-
61
- if "FILE_TYPE" in integration_test:
62
- assert assertions.assert_file_type(
63
- integration_test["FILE_TYPE"],
64
- Path(config["CONFIGURATION"]["OUTPUT_PATH"]).joinpath(
65
- config["CONFIGURATION"]["OUTPUT_FILE"]
66
- ),
67
- )
38
+
39
+ if from_command_line == from_config_file:
40
+ if "REFERENCE_OUTPUT_FILE" in integration_test:
41
+ _validate_reference_output_file(config, integration_test)
42
+
43
+ if "TEST_OUTPUT_FILES" in integration_test:
44
+ _validate_output_path_and_file(config, integration_test["TEST_OUTPUT_FILES"])
45
+
46
+ if "OUTPUT_FILE" in integration_test:
47
+ _validate_output_path_and_file(
48
+ config,
49
+ [{"PATH_DESCRIPTOR": "OUTPUT_PATH", "FILE": integration_test["OUTPUT_FILE"]}],
50
+ )
51
+
52
+ if "FILE_TYPE" in integration_test:
53
+ assert assertions.assert_file_type(
54
+ integration_test["FILE_TYPE"],
55
+ Path(config["CONFIGURATION"]["OUTPUT_PATH"]).joinpath(
56
+ config["CONFIGURATION"]["OUTPUT_FILE"]
57
+ ),
58
+ )
59
+ _test_simtel_cfg_files(config, integration_test, from_command_line, from_config_file)
60
+
61
+
62
+ def _test_simtel_cfg_files(config, integration_test, from_command_line, from_config_file):
63
+ """Test simtel cfg files."""
64
+ test_simtel_cfg_file = integration_test.get("TEST_SIMTEL_CFG_FILES", {}).get(
65
+ from_command_line or from_config_file
66
+ )
67
+ if test_simtel_cfg_file:
68
+ _validate_simtel_cfg_files(config, test_simtel_cfg_file)
68
69
 
69
70
 
70
71
  def _validate_reference_output_file(config, integration_test):
@@ -79,31 +80,25 @@ def _validate_reference_output_file(config, integration_test):
79
80
  )
80
81
 
81
82
 
82
- def _validate_output_path_and_file(config, integration_test):
83
- """Check if output path and file exist."""
84
- _logger.info(f"PATH {config['CONFIGURATION']['OUTPUT_PATH']}")
85
- _logger.info(f"File {integration_test['OUTPUT_FILE']}")
86
-
87
- data_path = config["CONFIGURATION"].get(
88
- "DATA_DIRECTORY", config["CONFIGURATION"]["OUTPUT_PATH"]
89
- )
90
- output_file_path = Path(data_path) / integration_test["OUTPUT_FILE"]
91
-
92
- _logger.info(f"Checking path: {output_file_path}")
93
- assert output_file_path.exists()
94
-
95
- expected_output = [
96
- d["EXPECTED_OUTPUT"] for d in config["INTEGRATION_TESTS"] if "EXPECTED_OUTPUT" in d
97
- ]
98
- if expected_output and "log_hist" not in integration_test["OUTPUT_FILE"]:
99
- # Get the expected output from the configuration file
100
- expected_output = expected_output[0]
101
- _logger.info(
102
- f"Checking the output of {integration_test['OUTPUT_FILE']} "
103
- "complies with the expected output: "
104
- f"{expected_output}"
105
- )
106
- assert assertions.check_output_from_sim_telarray(output_file_path, expected_output)
83
+ def _validate_output_path_and_file(config, integration_file_tests):
84
+ """Check if output paths and files exist."""
85
+ for file_test in integration_file_tests:
86
+ try:
87
+ output_path = config["CONFIGURATION"][file_test["PATH_DESCRIPTOR"]]
88
+ except KeyError as exc:
89
+ raise KeyError(
90
+ f"Path {file_test['PATH_DESCRIPTOR']} not found in integration test configuration."
91
+ ) from exc
92
+
93
+ output_file_path = Path(output_path) / file_test["FILE"]
94
+ _logger.info(f"Checking path: {output_file_path}")
95
+ assert output_file_path.exists()
96
+
97
+ if "EXPECTED_OUTPUT" in file_test:
98
+ assert assertions.check_output_from_sim_telarray(
99
+ output_file_path,
100
+ file_test["EXPECTED_OUTPUT"],
101
+ )
107
102
 
108
103
 
109
104
  def compare_files(file1, file2, tolerance=1.0e-5, test_columns=None):
@@ -129,12 +124,13 @@ def compare_files(file1, file2, tolerance=1.0e-5, test_columns=None):
129
124
  """
130
125
  _file1_suffix = Path(file1).suffix
131
126
  _file2_suffix = Path(file2).suffix
127
+ _logger.info("Comparing files: %s and %s", file1, file2)
132
128
  if _file1_suffix != _file2_suffix:
133
129
  raise ValueError(f"File suffixes do not match: {file1} and {file2}")
134
130
  if _file1_suffix == ".ecsv":
135
131
  return compare_ecsv_files(file1, file2, tolerance, test_columns)
136
132
  if _file1_suffix in (".json", ".yaml", ".yml"):
137
- return compare_json_or_yaml_files(file1, file2)
133
+ return compare_json_or_yaml_files(file1, file2, tolerance)
138
134
 
139
135
  _logger.warning(f"Unknown file type for files: {file1} and {file2}")
140
136
  return False
@@ -169,11 +165,35 @@ def compare_json_or_yaml_files(file1, file2, tolerance=1.0e-2):
169
165
  if data1 == data2:
170
166
  return True
171
167
 
172
- if "value" in data1 and isinstance(data1["value"], str):
173
- value_list_1 = gen.convert_string_to_list(data1.pop("value"))
174
- value_list_2 = gen.convert_string_to_list(data2.pop("value"))
175
- return np.allclose(value_list_1, value_list_2, rtol=tolerance)
176
- return data1 == data2
168
+ if data1.keys() != data2.keys():
169
+ _logger.error(f"Keys do not match: {data1.keys()} and {data2.keys()}")
170
+ return False
171
+ _comparison = all(
172
+ (
173
+ _compare_value_from_parameter_dict(data1[k], data2[k], tolerance)
174
+ if k == "value"
175
+ else data1[k] == data2[k]
176
+ )
177
+ for k in data1
178
+ )
179
+ if not _comparison:
180
+ _logger.error(f"Values do not match: {data1} and {data2} (tolerance: {tolerance})")
181
+ return _comparison
182
+
183
+
184
+ def _compare_value_from_parameter_dict(data1, data2, tolerance):
185
+ """Compare value fields given in different formats."""
186
+
187
+ def _as_list(value):
188
+ if isinstance(value, str):
189
+ return gen.convert_string_to_list(value)
190
+ if isinstance(value, list | np.ndarray):
191
+ return value
192
+ return [value]
193
+
194
+ _logger.info(f"Comparing values: {data1} and {data2} (tolerance: {tolerance})")
195
+
196
+ return np.allclose(_as_list(data1), _as_list(data2), rtol=tolerance)
177
197
 
178
198
 
179
199
  def compare_ecsv_files(file1, file2, tolerance=1.0e-5, test_columns=None):
@@ -235,6 +255,70 @@ def compare_ecsv_files(file1, file2, tolerance=1.0e-5, test_columns=None):
235
255
 
236
256
  if np.issubdtype(table1_masked[col_name].dtype, np.floating):
237
257
  if not np.allclose(table1_masked[col_name], table2_masked[col_name], rtol=tolerance):
258
+ _logger.warning(f"Column {col_name} outside of relative tolerance {tolerance}")
238
259
  return False
239
260
 
240
261
  return True
262
+
263
+
264
+ def _validate_simtel_cfg_files(config, simtel_cfg_file):
265
+ """
266
+ Check sim_telarray configuration files and compare with reference file.
267
+
268
+ Note the finetuned naming of configuration files by simtools.
269
+
270
+ """
271
+ reference_file = Path(simtel_cfg_file)
272
+ test_file = Path(config["CONFIGURATION"]["OUTPUT_PATH"]) / reference_file.name.replace(
273
+ "_test", f"_{config['CONFIGURATION']['LABEL']}"
274
+ )
275
+ _logger.info(
276
+ f"Comparing simtel cfg files: {reference_file} and {test_file} "
277
+ f"for model version {config['CONFIGURATION']['MODEL_VERSION']}"
278
+ )
279
+ return _compare_simtel_cfg_files(reference_file, test_file)
280
+
281
+
282
+ def _compare_simtel_cfg_files(reference_file, test_file):
283
+ """
284
+ Compare two sim_telarray configuration files.
285
+
286
+ Line-by-line string comparison. Requires similar sequence of
287
+ parameters in the files. Ignore lines containing 'config_release'
288
+ (as it contains the simtools package version).
289
+
290
+ Parameters
291
+ ----------
292
+ reference_file: Path
293
+ Reference sim_telarray configuration file.
294
+ test_file: Path
295
+ Test sim_telarray configuration file.
296
+
297
+ Returns
298
+ -------
299
+ bool
300
+ True if the files are equal.
301
+
302
+ """
303
+ with open(reference_file, encoding="utf-8") as f1, open(test_file, encoding="utf-8") as f2:
304
+ reference_cfg = [line.rstrip() for line in f1 if line.strip()]
305
+ test_cfg = [line.rstrip() for line in f2 if line.strip()]
306
+
307
+ if len(reference_cfg) != len(test_cfg):
308
+ _logger.error(
309
+ f"Line counts differ: {reference_file} ({len(reference_cfg)} lines), "
310
+ f"{test_file} ({len(test_cfg)} lines)."
311
+ )
312
+ return False
313
+
314
+ for ref_line, test_line in zip(reference_cfg, test_cfg):
315
+ if any(ignore in ref_line for ignore in ("config_release", "Label")):
316
+ continue
317
+ if ref_line != test_line:
318
+ _logger.error(
319
+ f"Configuration files {reference_file} and {test_file} do not match: "
320
+ f"'{ref_line}' and '{test_line}'"
321
+ )
322
+ return False
323
+
324
+ return True
simtools/utils/general.py CHANGED
@@ -17,6 +17,7 @@ import yaml
17
17
  __all__ = [
18
18
  "InvalidConfigDataError",
19
19
  "change_dict_keys_case",
20
+ "clear_default_sim_telarray_cfg_directories",
20
21
  "collect_data_from_file",
21
22
  "collect_final_lines",
22
23
  "collect_kwargs",
@@ -81,6 +82,28 @@ def is_url(url):
81
82
  return False
82
83
 
83
84
 
85
+ def url_exists(url):
86
+ """
87
+ Check if a URL exists.
88
+
89
+ Parameters
90
+ ----------
91
+ url: str
92
+ URL to be checked.
93
+
94
+ Returns
95
+ -------
96
+ bool
97
+ True if URL exists.
98
+ """
99
+ try:
100
+ with urllib.request.urlopen(url, timeout=5) as response:
101
+ return response.status == 200
102
+ except (urllib.error.URLError, AttributeError) as e:
103
+ _logger.error(f"URL {url} does not exist: {e}")
104
+ return False
105
+
106
+
84
107
  def collect_data_from_http(url):
85
108
  """
86
109
  Download yaml or json file from url and return it contents as dict.
@@ -135,7 +158,7 @@ def collect_data_from_http(url):
135
158
  return data
136
159
 
137
160
 
138
- def collect_data_from_file(file_name):
161
+ def collect_data_from_file(file_name, yaml_document=None):
139
162
  """
140
163
  Collect data from file based on its extension.
141
164
 
@@ -143,6 +166,8 @@ def collect_data_from_file(file_name):
143
166
  ----------
144
167
  file_name: str
145
168
  Name of the yaml/json/ascii file.
169
+ yaml_document: None, int
170
+ Return list of yaml documents or a single document (for yaml files with several documents).
146
171
 
147
172
  Returns
148
173
  -------
@@ -152,18 +177,34 @@ def collect_data_from_file(file_name):
152
177
  if is_url(file_name):
153
178
  return collect_data_from_http(file_name)
154
179
 
180
+ suffix = Path(file_name).suffix.lower()
155
181
  with open(file_name, encoding="utf-8") as file:
156
- if Path(file_name).suffix.lower() == ".json":
182
+ if suffix == ".json":
157
183
  return json.load(file)
184
+ if suffix == ".list":
185
+ return [line.strip() for line in file.readlines()]
186
+ if suffix in [".yml", ".yaml"]:
187
+ return _collect_data_from_yaml_file(file, file_name, yaml_document)
188
+ return None
158
189
 
159
- if Path(file_name).suffix.lower() == ".list":
160
- lines = file.readlines()
161
- return [line.strip() for line in lines]
162
190
 
163
- try:
164
- return yaml.safe_load(file)
165
- except yaml.constructor.ConstructorError:
166
- return _load_yaml_using_astropy(file)
191
+ def _collect_data_from_yaml_file(file, file_name, yaml_document):
192
+ """Collect data from a yaml file."""
193
+ try:
194
+ return yaml.safe_load(file)
195
+ except yaml.constructor.ConstructorError:
196
+ return _load_yaml_using_astropy(file)
197
+ except yaml.composer.ComposerError:
198
+ pass
199
+ file.seek(0)
200
+ if yaml_document is None:
201
+ return list(yaml.safe_load_all(file))
202
+ try:
203
+ return list(yaml.safe_load_all(file))[yaml_document]
204
+ except IndexError as exc:
205
+ raise InvalidConfigDataError(
206
+ f"YAML file {file_name} does not contain {yaml_document} documents."
207
+ ) from exc
167
208
 
168
209
 
169
210
  def collect_kwargs(label, in_kwargs):
@@ -333,9 +374,10 @@ def program_is_executable(program):
333
374
  Follows https://stackoverflow.com/questions/377017/
334
375
 
335
376
  """
377
+ program = Path(program)
336
378
 
337
379
  def is_exe(fpath):
338
- return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
380
+ return fpath.is_file() and os.access(fpath, os.X_OK)
339
381
 
340
382
  fpath, _ = os.path.split(program)
341
383
  if fpath:
@@ -344,7 +386,7 @@ def program_is_executable(program):
344
386
  else:
345
387
  try:
346
388
  for path in os.environ["PATH"].split(os.pathsep):
347
- exe_file = os.path.join(path, program)
389
+ exe_file = Path(path) / program
348
390
  if is_exe(exe_file):
349
391
  return exe_file
350
392
  except KeyError:
@@ -442,7 +484,7 @@ def get_file_age(file_path):
442
484
  if not Path(file_path).is_file():
443
485
  raise FileNotFoundError(f"'{file_path}' does not exist or is not a file.")
444
486
 
445
- file_stats = os.stat(file_path)
487
+ file_stats = Path(file_path).stat()
446
488
  modification_time = file_stats.st_mtime
447
489
  current_time = time.time()
448
490
 
@@ -785,3 +827,98 @@ def read_file_encoded_in_utf_or_latin(file_name):
785
827
  raise UnicodeDecodeError("Unable to decode file using UTF-8 or Latin-1.") from exc
786
828
 
787
829
  return lines
830
+
831
+
832
+ def get_structure_array_from_table(table, column_names):
833
+ """
834
+ Get a structured array from an astropy table for a selected list of columns.
835
+
836
+ Parameters
837
+ ----------
838
+ table: astropy.table.Table
839
+ Table to be converted.
840
+ column_names: list
841
+ List of column names to be included in the structured array.
842
+
843
+ Returns
844
+ -------
845
+ numpy.ndarray
846
+ Structured array containing the table data.
847
+ """
848
+ return np.array(
849
+ list(zip(*[np.array(table[col]) for col in column_names])),
850
+ dtype=[(col, np.array(table[col]).dtype) for col in column_names],
851
+ )
852
+
853
+
854
+ def convert_keys_in_dict_to_lowercase(data):
855
+ """
856
+ Recursively convert all dictionary keys to lowercase.
857
+
858
+ Parameters
859
+ ----------
860
+ data: dict
861
+ Dictionary to be converted.
862
+
863
+ Returns
864
+ -------
865
+ dict
866
+ Dictionary with all keys converted to lowercase.
867
+ """
868
+ if isinstance(data, dict):
869
+ return {k.lower(): convert_keys_in_dict_to_lowercase(v) for k, v in data.items()}
870
+ if isinstance(data, list):
871
+ return [convert_keys_in_dict_to_lowercase(i) for i in data]
872
+ return data
873
+
874
+
875
+ def clear_default_sim_telarray_cfg_directories(command):
876
+ """Prefix the command to clear default sim_telarray configuration directories.
877
+
878
+ Parameters
879
+ ----------
880
+ command: str
881
+ Command to be prefixed.
882
+
883
+ Returns
884
+ -------
885
+ str
886
+ Prefixed command.
887
+
888
+ """
889
+ return f"SIM_TELARRAY_CONFIG_PATH='' {command}"
890
+
891
+
892
+ def get_list_of_files_from_command_line(file_names, suffix_list):
893
+ """
894
+ Get a list of files from the command line.
895
+
896
+ Files can be given as a list of file names or as a text file containing the list of files.
897
+ The list of suffixes restrict the files types to be returned. Note that a file list must
898
+ have a different suffix than those in the suffix list.
899
+
900
+ Parameters
901
+ ----------
902
+ file_names: list
903
+ List of file names to be checked.
904
+ suffix_list: list
905
+ List of suffixes to be checked.
906
+
907
+ Returns
908
+ -------
909
+ list
910
+ List of files with the given suffixes.
911
+ """
912
+ _files = []
913
+ for one_file in file_names:
914
+ path = Path(one_file)
915
+ try:
916
+ if path.suffix in suffix_list:
917
+ _files.append(one_file)
918
+ elif len(file_names) == 1:
919
+ with open(one_file, encoding="utf-8") as file:
920
+ _files.extend(line.strip() for line in file)
921
+ except FileNotFoundError as exc:
922
+ _logger.error(f"{one_file} is not a file.")
923
+ raise FileNotFoundError from exc
924
+ return _files