gammasimtools 0.8.2__py3-none-any.whl → 0.9.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 (65) hide show
  1. {gammasimtools-0.8.2.dist-info → gammasimtools-0.9.0.dist-info}/METADATA +3 -3
  2. {gammasimtools-0.8.2.dist-info → gammasimtools-0.9.0.dist-info}/RECORD +64 -59
  3. {gammasimtools-0.8.2.dist-info → gammasimtools-0.9.0.dist-info}/WHEEL +1 -1
  4. {gammasimtools-0.8.2.dist-info → gammasimtools-0.9.0.dist-info}/entry_points.txt +2 -0
  5. simtools/_version.py +2 -2
  6. simtools/applications/convert_all_model_parameters_from_simtel.py +1 -1
  7. simtools/applications/convert_geo_coordinates_of_array_elements.py +8 -9
  8. simtools/applications/convert_model_parameter_from_simtel.py +1 -1
  9. simtools/applications/db_add_model_parameters_from_repository_to_db.py +2 -10
  10. simtools/applications/db_add_value_from_json_to_db.py +1 -9
  11. simtools/applications/db_get_array_layouts_from_db.py +3 -1
  12. simtools/applications/db_get_parameter_from_db.py +1 -1
  13. simtools/applications/derive_mirror_rnda.py +10 -1
  14. simtools/applications/derive_psf_parameters.py +1 -1
  15. simtools/applications/generate_array_config.py +1 -5
  16. simtools/applications/generate_regular_arrays.py +9 -6
  17. simtools/applications/plot_array_layout.py +3 -1
  18. simtools/applications/plot_tabular_data.py +84 -0
  19. simtools/applications/production_scale_events.py +1 -2
  20. simtools/applications/simulate_light_emission.py +2 -2
  21. simtools/applications/simulate_prod.py +24 -59
  22. simtools/applications/simulate_prod_htcondor_generator.py +95 -0
  23. simtools/applications/submit_data_from_external.py +1 -1
  24. simtools/applications/validate_camera_efficiency.py +1 -1
  25. simtools/applications/validate_camera_fov.py +3 -7
  26. simtools/applications/validate_cumulative_psf.py +3 -7
  27. simtools/applications/validate_file_using_schema.py +31 -21
  28. simtools/applications/validate_optics.py +3 -4
  29. simtools/camera_efficiency.py +1 -4
  30. simtools/configuration/commandline_parser.py +7 -13
  31. simtools/configuration/configurator.py +6 -19
  32. simtools/data_model/metadata_collector.py +18 -0
  33. simtools/data_model/metadata_model.py +18 -5
  34. simtools/data_model/model_data_writer.py +1 -1
  35. simtools/data_model/validate_data.py +67 -10
  36. simtools/db/db_handler.py +92 -315
  37. simtools/io_operations/legacy_data_handler.py +61 -0
  38. simtools/job_execution/htcondor_script_generator.py +133 -0
  39. simtools/job_execution/job_manager.py +77 -50
  40. simtools/model/camera.py +4 -2
  41. simtools/model/model_parameter.py +40 -10
  42. simtools/model/site_model.py +1 -1
  43. simtools/ray_tracing/mirror_panel_psf.py +47 -27
  44. simtools/runners/corsika_runner.py +14 -3
  45. simtools/runners/runner_services.py +3 -3
  46. simtools/runners/simtel_runner.py +27 -8
  47. simtools/schemas/integration_tests_config.metaschema.yml +15 -5
  48. simtools/schemas/model_parameter.metaschema.yml +90 -2
  49. simtools/schemas/model_parameters/effective_focal_length.schema.yml +31 -1
  50. simtools/simtel/simtel_table_reader.py +410 -0
  51. simtools/simtel/simulator_camera_efficiency.py +6 -4
  52. simtools/simtel/simulator_light_emission.py +2 -2
  53. simtools/simtel/simulator_ray_tracing.py +1 -2
  54. simtools/simulator.py +80 -33
  55. simtools/testing/configuration.py +12 -8
  56. simtools/testing/helpers.py +5 -5
  57. simtools/testing/validate_output.py +26 -26
  58. simtools/utils/general.py +50 -3
  59. simtools/utils/names.py +2 -2
  60. simtools/utils/value_conversion.py +9 -1
  61. simtools/visualization/plot_tables.py +106 -0
  62. simtools/visualization/visualize.py +43 -5
  63. simtools/db/db_from_repo_handler.py +0 -106
  64. {gammasimtools-0.8.2.dist-info → gammasimtools-0.9.0.dist-info}/LICENSE +0 -0
  65. {gammasimtools-0.8.2.dist-info → gammasimtools-0.9.0.dist-info}/top_level.txt +0 -0
@@ -154,10 +154,12 @@ class SimulatorCameraEfficiency(SimtelRunner):
154
154
  command += " 300" # Xmax
155
155
  command += f" {self._telescope_model.get_parameter_value('atmospheric_profile')}"
156
156
  command += f" {self.zenith_angle}"
157
- command += f" 2>{self._file_log}"
158
- command += f" >{self._file_simtel}"
159
157
 
160
- return f"cd {self._simtel_path.joinpath('sim_telarray')} && {command}"
158
+ return (
159
+ f"cd {self._simtel_path.joinpath('sim_telarray')} && {command}",
160
+ self._file_simtel,
161
+ self._file_log,
162
+ )
161
163
 
162
164
  def _check_run_result(self, run_number=None): # pylint: disable=unused-argument
163
165
  """Check run results.
@@ -169,7 +171,7 @@ class SimulatorCameraEfficiency(SimtelRunner):
169
171
  """
170
172
  # Checking run
171
173
  if not self._file_simtel.exists():
172
- msg = "Camera efficiency simulation results file does not exist"
174
+ msg = f"Camera efficiency simulation results file does not exist ({self._file_simtel})."
173
175
  self._logger.error(msg)
174
176
  raise RuntimeError(msg)
175
177
 
@@ -1,7 +1,7 @@
1
1
  """Simulation using the light emission package for calibration."""
2
2
 
3
3
  import logging
4
- import os
4
+ import stat
5
5
  from pathlib import Path
6
6
 
7
7
  import astropy.units as u
@@ -469,5 +469,5 @@ class SimulatorLightEmission(SimtelRunner):
469
469
  file.write(f"{command_plot}\n\n")
470
470
  file.write("# End\n\n")
471
471
 
472
- os.system(f"chmod ug+x {_script_file}")
472
+ _script_file.chmod(_script_file.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP)
473
473
  return _script_file
@@ -191,9 +191,8 @@ class SimulatorRayTracing(SimtelRunner):
191
191
  command += super().get_config_option("mirror_align_random_distance", "0.")
192
192
  command += super().get_config_option("mirror_align_random_vertical", "0.,28.,0.,0.")
193
193
  command += " " + str(self._corsika_file)
194
- command += f" 2>&1 > {self._log_file} 2>&1"
195
194
 
196
- return command
195
+ return command, self._log_file, self._log_file
197
196
 
198
197
  def _check_run_result(self, run_number=None): # pylint: disable=unused-argument
199
198
  """
simtools/simulator.py CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  import logging
4
4
  import re
5
+ import shutil
6
+ import tarfile
5
7
  from collections import defaultdict
6
8
  from pathlib import Path
7
9
 
@@ -68,7 +70,7 @@ class Simulator:
68
70
  self.runs = self._initialize_run_list()
69
71
  self._results = defaultdict(list)
70
72
  self._test = self.args_dict.get("test", False)
71
- self._submit_engine = self.args_dict.get("submit_engine", "local")
73
+ self.submit_engine = self.args_dict.get("submit_engine", "local")
72
74
  self._submit_options = self.args_dict.get("submit_options", None)
73
75
  self._extra_commands = extra_commands
74
76
 
@@ -265,7 +267,7 @@ class Simulator:
265
267
  input_file_list: str or list of str
266
268
  Single file or list of files of shower simulations.
267
269
  """
268
- self._logger.info(f"Submission command: {self._submit_engine}")
270
+ self._logger.info(f"Submission command: {self.submit_engine}")
269
271
 
270
272
  runs_and_files_to_submit = self._get_runs_and_files_to_submit(
271
273
  input_file_list=input_file_list
@@ -281,7 +283,7 @@ class Simulator:
281
283
  )
282
284
 
283
285
  job_manager = JobManager(
284
- submit_engine=self._submit_engine,
286
+ submit_engine=self.submit_engine,
285
287
  submit_options=self._submit_options,
286
288
  test=self._test,
287
289
  )
@@ -389,50 +391,46 @@ class Simulator:
389
391
  run number
390
392
 
391
393
  """
392
- self._results["output"].append(
393
- str(self._simulation_runner.get_file_name(file_type="output", run_number=run_number))
394
+ keys = ["output", "sub_out", "log", "input", "hist", "corsika_log"]
395
+ defaults = {key: None for key in keys}
396
+ results = {key: defaults[key] for key in keys}
397
+ results["output"] = str(
398
+ self._simulation_runner.get_file_name(file_type="output", run_number=run_number)
394
399
  )
395
- self._results["sub_out"].append(
396
- str(
397
- self._simulation_runner.get_file_name(
398
- file_type="sub_log", run_number=run_number, mode="out"
399
- )
400
+ results["sub_out"] = str(
401
+ self._simulation_runner.get_file_name(
402
+ file_type="sub_log", mode="out", run_number=run_number
400
403
  )
401
404
  )
402
405
 
403
- if self.simulation_software in ["simtel", "corsika_simtel"]:
404
- self._results["log"].append(
405
- str(
406
- self._simulation_runner.get_file_name(
407
- simulation_software="simtel", file_type="log", run_number=run_number
408
- )
406
+ if "simtel" in self.simulation_software:
407
+ results["log"] = str(
408
+ self._simulation_runner.get_file_name(
409
+ file_type="log", simulation_software="simtel", run_number=run_number
409
410
  )
410
411
  )
411
- self._results["input"].append(str(file))
412
- self._results["hist"].append(
413
- str(
414
- self._simulation_runner.get_file_name(
415
- simulation_software="simtel", file_type="histogram", run_number=run_number
416
- )
412
+ results["input"] = str(file)
413
+ results["hist"] = str(
414
+ self._simulation_runner.get_file_name(
415
+ file_type="histogram", simulation_software="simtel", run_number=run_number
417
416
  )
418
417
  )
419
- else:
420
- self._results["corsika_autoinputs_log"].append(
421
- str(
422
- self._simulation_runner.get_file_name(
423
- simulation_software="corsika", file_type="log", run_number=run_number
424
- )
418
+
419
+ if "corsika" in self.simulation_software:
420
+ results["corsika_log"] = str(
421
+ self._simulation_runner.get_file_name(
422
+ file_type="corsika_log", simulation_software="corsika", run_number=run_number
425
423
  )
426
424
  )
427
- self._results["input"].append(None)
428
- self._results["hist"].append(None)
429
- self._results["log"].append(None)
425
+
426
+ for key in keys:
427
+ self._results[key].append(results[key])
430
428
 
431
429
  def get_file_list(self, file_type="output"):
432
430
  """
433
431
  Get list of files generated by simulations.
434
432
 
435
- Options are "input", "output", "hist", "log".
433
+ Options are "input", "output", "hist", "log", "corsika_log".
436
434
  Not all file types are available for all simulation types.
437
435
  Returns an empty list for an unknown file type.
438
436
 
@@ -548,7 +546,7 @@ class Simulator:
548
546
 
549
547
  def save_file_lists(self):
550
548
  """Save files lists for output and log files."""
551
- for file_type in ["output", "log", "hist"]:
549
+ for file_type in ["output", "log", "corsika_log", "hist"]:
552
550
  file_name = self.io_handler.get_output_directory(label=self.label).joinpath(
553
551
  f"{file_type}_files.txt"
554
552
  )
@@ -560,3 +558,52 @@ class Simulator:
560
558
  f.write(f"{line}\n")
561
559
  else:
562
560
  self._logger.debug(f"No files to save for {file_type} files.")
561
+
562
+ def pack_for_register(self, directory_for_grid_upload=None):
563
+ """
564
+ Pack simulation output files for registering on the grid.
565
+
566
+ Parameters
567
+ ----------
568
+ directory_for_grid_upload: str
569
+ Directory for the tarball with output files.
570
+
571
+ """
572
+ self._logger.info(
573
+ f"Packing the output files for registering on the grid ({directory_for_grid_upload})"
574
+ )
575
+ output_files = self.get_file_list(file_type="output")
576
+ log_files = self.get_file_list(file_type="log")
577
+ corsika_log_files = self.get_file_list(file_type="corsika_log")
578
+ histogram_files = self.get_file_list(file_type="hist")
579
+ tar_file_name = Path(log_files[0]).name.replace("log.gz", "log_hist.tar.gz")
580
+ directory_for_grid_upload = (
581
+ Path(directory_for_grid_upload)
582
+ if directory_for_grid_upload
583
+ else self.io_handler.get_output_directory(label=self.label).joinpath(
584
+ "directory_for_grid_upload"
585
+ )
586
+ )
587
+ directory_for_grid_upload.mkdir(parents=True, exist_ok=True)
588
+
589
+ tar_file_name = directory_for_grid_upload.joinpath(tar_file_name)
590
+
591
+ with tarfile.open(tar_file_name, "w:gz") as tar:
592
+ files_to_tar = (
593
+ (log_files[:1] if log_files else [])
594
+ + (histogram_files[:1] if histogram_files else [])
595
+ + (corsika_log_files[:1] if corsika_log_files else [])
596
+ )
597
+ for file_to_tar in files_to_tar:
598
+ tar.add(file_to_tar, arcname=Path(file_to_tar).name)
599
+
600
+ for file_to_move in [*output_files]:
601
+ source_file = Path(file_to_move)
602
+ destination_file = directory_for_grid_upload / source_file.name
603
+ if destination_file.exists():
604
+ self._logger.warning(f"Overwriting existing file: {destination_file}")
605
+ # Note that this will overwrite previous files which exist in the directory
606
+ # It should be fine for normal production since each run is on a separate node
607
+ # so no files are expected there.
608
+ shutil.move(source_file, destination_file)
609
+ self._logger.info(f"Output files for the grid placed in {directory_for_grid_upload!s}")
@@ -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,25 @@
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
9
  if "camera-efficiency" in config["APPLICATION"]:
11
10
  if not _new_testeff_version():
12
- pytest.skip(
11
+ return (
13
12
  "Any applications calling the old version of testeff are skipped "
14
13
  "due to a limitation of the old testeff not allowing to specify "
15
14
  "the include directory. Please update your sim_telarray tarball."
16
15
  )
17
16
  full_test_name = f"{config['APPLICATION']}_{config['TEST_NAME']}"
18
17
  if "simtools-validate-camera-efficiency_SSTS" == full_test_name:
19
- pytest.skip(
18
+ return (
20
19
  "The test simtools-validate-camera-efficiency_SSTS is skipped "
21
20
  "since the fake SST mirrors are not yet implemented (#1155)"
22
21
  )
22
+ return None
23
23
 
24
24
 
25
25
  def _new_testeff_version():
@@ -28,7 +28,7 @@ def _new_testeff_version():
28
28
 
29
29
  This test checks if the new version is used.
30
30
  """
31
- testeff_path = os.path.join(os.getenv("SIMTOOLS_SIMTEL_PATH"), "sim_telarray/testeff.c")
31
+ testeff_path = Path(os.getenv("SIMTOOLS_SIMTEL_PATH")) / "sim_telarray/testeff.c"
32
32
  try:
33
33
  with open(testeff_path, encoding="utf-8") as file:
34
34
  file_content = file.read()
@@ -55,8 +55,13 @@ def validate_application_output(config):
55
55
  if "REFERENCE_OUTPUT_FILE" in integration_test:
56
56
  _validate_reference_output_file(config, integration_test)
57
57
 
58
+ if "TEST_OUTPUT_FILES" in integration_test:
59
+ _validate_output_path_and_file(config, integration_test["TEST_OUTPUT_FILES"])
58
60
  if "OUTPUT_FILE" in integration_test:
59
- _validate_output_path_and_file(config, integration_test)
61
+ _validate_output_path_and_file(
62
+ config,
63
+ [{"PATH_DESCRIPTOR": "OUTPUT_PATH", "FILE": integration_test["OUTPUT_FILE"]}],
64
+ )
60
65
 
61
66
  if "FILE_TYPE" in integration_test:
62
67
  assert assertions.assert_file_type(
@@ -79,31 +84,25 @@ def _validate_reference_output_file(config, integration_test):
79
84
  )
80
85
 
81
86
 
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)
87
+ def _validate_output_path_and_file(config, integration_file_tests):
88
+ """Check if output paths and files exist."""
89
+ for file_test in integration_file_tests:
90
+ try:
91
+ output_path = config["CONFIGURATION"][file_test["PATH_DESCRIPTOR"]]
92
+ except KeyError as exc:
93
+ raise KeyError(
94
+ f"Path {file_test['PATH_DESCRIPTOR']} not found in integration test configuration."
95
+ ) from exc
96
+
97
+ output_file_path = Path(output_path) / file_test["FILE"]
98
+ _logger.info(f"Checking path: {output_file_path}")
99
+ assert output_file_path.exists()
100
+
101
+ if "EXPECTED_OUTPUT" in file_test:
102
+ assert assertions.check_output_from_sim_telarray(
103
+ output_file_path,
104
+ file_test["EXPECTED_OUTPUT"],
105
+ )
107
106
 
108
107
 
109
108
  def compare_files(file1, file2, tolerance=1.0e-5, test_columns=None):
@@ -235,6 +234,7 @@ def compare_ecsv_files(file1, file2, tolerance=1.0e-5, test_columns=None):
235
234
 
236
235
  if np.issubdtype(table1_masked[col_name].dtype, np.floating):
237
236
  if not np.allclose(table1_masked[col_name], table2_masked[col_name], rtol=tolerance):
237
+ _logger.warning(f"Column {col_name} outside of relative tolerance {tolerance}")
238
238
  return False
239
239
 
240
240
  return True
simtools/utils/general.py CHANGED
@@ -164,6 +164,9 @@ def collect_data_from_file(file_name):
164
164
  return yaml.safe_load(file)
165
165
  except yaml.constructor.ConstructorError:
166
166
  return _load_yaml_using_astropy(file)
167
+ except yaml.composer.ComposerError:
168
+ file.seek(0)
169
+ return list(yaml.safe_load_all(file))
167
170
 
168
171
 
169
172
  def collect_kwargs(label, in_kwargs):
@@ -333,9 +336,10 @@ def program_is_executable(program):
333
336
  Follows https://stackoverflow.com/questions/377017/
334
337
 
335
338
  """
339
+ program = Path(program)
336
340
 
337
341
  def is_exe(fpath):
338
- return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
342
+ return fpath.is_file() and os.access(fpath, os.X_OK)
339
343
 
340
344
  fpath, _ = os.path.split(program)
341
345
  if fpath:
@@ -344,7 +348,7 @@ def program_is_executable(program):
344
348
  else:
345
349
  try:
346
350
  for path in os.environ["PATH"].split(os.pathsep):
347
- exe_file = os.path.join(path, program)
351
+ exe_file = Path(path) / program
348
352
  if is_exe(exe_file):
349
353
  return exe_file
350
354
  except KeyError:
@@ -442,7 +446,7 @@ def get_file_age(file_path):
442
446
  if not Path(file_path).is_file():
443
447
  raise FileNotFoundError(f"'{file_path}' does not exist or is not a file.")
444
448
 
445
- file_stats = os.stat(file_path)
449
+ file_stats = Path(file_path).stat()
446
450
  modification_time = file_stats.st_mtime
447
451
  current_time = time.time()
448
452
 
@@ -785,3 +789,46 @@ def read_file_encoded_in_utf_or_latin(file_name):
785
789
  raise UnicodeDecodeError("Unable to decode file using UTF-8 or Latin-1.") from exc
786
790
 
787
791
  return lines
792
+
793
+
794
+ def get_structure_array_from_table(table, column_names):
795
+ """
796
+ Get a structured array from an astropy table for a selected list of columns.
797
+
798
+ Parameters
799
+ ----------
800
+ table: astropy.table.Table
801
+ Table to be converted.
802
+ column_names: list
803
+ List of column names to be included in the structured array.
804
+
805
+ Returns
806
+ -------
807
+ numpy.ndarray
808
+ Structured array containing the table data.
809
+ """
810
+ return np.array(
811
+ list(zip(*[np.array(table[col]) for col in column_names])),
812
+ dtype=[(col, np.array(table[col]).dtype) for col in column_names],
813
+ )
814
+
815
+
816
+ def convert_keys_in_dict_to_lowercase(data):
817
+ """
818
+ Recursively convert all dictionary keys to lowercase.
819
+
820
+ Parameters
821
+ ----------
822
+ data: dict
823
+ Dictionary to be converted.
824
+
825
+ Returns
826
+ -------
827
+ dict
828
+ Dictionary with all keys converted to lowercase.
829
+ """
830
+ if isinstance(data, dict):
831
+ return {k.lower(): convert_keys_in_dict_to_lowercase(v) for k, v in data.items()}
832
+ if isinstance(data, list):
833
+ return [convert_keys_in_dict_to_lowercase(i) for i in data]
834
+ return data
simtools/utils/names.py CHANGED
@@ -1,10 +1,10 @@
1
1
  """Validation of names."""
2
2
 
3
- import glob
4
3
  import logging
5
4
  import re
6
5
  from functools import cache
7
6
  from importlib.resources import files
7
+ from pathlib import Path
8
8
 
9
9
  import yaml
10
10
 
@@ -59,7 +59,7 @@ def site_names():
59
59
  @cache
60
60
  def load_model_parameters(class_key_list):
61
61
  model_parameters = {}
62
- schema_files = glob.glob(str(files("simtools") / "schemas/model_parameters") + "/*.yml")
62
+ schema_files = list(Path(files("simtools") / "schemas/model_parameters").rglob("*.yml"))
63
63
  for schema_file in schema_files:
64
64
  with open(schema_file, encoding="utf-8") as f:
65
65
  data = yaml.safe_load(f)
@@ -84,7 +84,7 @@ def get_value_unit_type(value, unit_str=None):
84
84
  pass
85
85
  base_unit = unit_str
86
86
 
87
- return base_value, base_unit, base_type
87
+ return base_value, _unit_as_string(base_unit), base_type
88
88
 
89
89
 
90
90
  def split_value_and_unit(value):
@@ -174,3 +174,11 @@ def get_value_as_quantity(value, unit):
174
174
  _logger.error(f"Cannot convert {value.unit} to {unit}.")
175
175
  raise
176
176
  return value * unit
177
+
178
+
179
+ def _unit_as_string(unit):
180
+ """Return the string representation of a unit. Collapse if it is a list of identical items."""
181
+ if not isinstance(unit, list):
182
+ unit = [unit]
183
+ unit = [str(element) if element is not None else None for element in unit]
184
+ return unit[0] if len(set(unit)) == 1 else unit
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/python3
2
+ """Plot tabular data."""
3
+
4
+ import numpy as np
5
+
6
+ import simtools.utils.general as gen
7
+ from simtools.io_operations import legacy_data_handler
8
+ from simtools.model.site_model import SiteModel
9
+ from simtools.model.telescope_model import TelescopeModel
10
+ from simtools.visualization import visualize
11
+
12
+
13
+ def plot(config, output_file, db_config=None):
14
+ """
15
+ Plot tabular data from data or from model parameter files.
16
+
17
+ Parameters
18
+ ----------
19
+ config: dict
20
+ Configuration dictionary for plotting.
21
+ output_file: str
22
+ Output file.
23
+ """
24
+ data = read_table_data(config, db_config)
25
+
26
+ fig = visualize.plot_1d(
27
+ data,
28
+ **config,
29
+ )
30
+ visualize.save_figure(fig, output_file)
31
+
32
+
33
+ def read_table_data(config, db_config):
34
+ """
35
+ Read table data from file or parameter database.
36
+
37
+ Parameters
38
+ ----------
39
+ config: dict
40
+ Configuration dictionary for plotting.
41
+
42
+ Returns
43
+ -------
44
+ Dict
45
+ Dict with table data (astropy tables).
46
+ """
47
+ data = {}
48
+
49
+ for _config in config["tables"]:
50
+ if "parameter" in _config:
51
+ table = _read_table_from_model_database(_config, db_config)
52
+ elif "file_name" in _config:
53
+ table = legacy_data_handler.read_legacy_data_as_table(
54
+ _config["file_name"], _config["type"]
55
+ )
56
+ else:
57
+ raise ValueError("No table data defined in configuration.")
58
+ if _config.get("normalize_y"):
59
+ table[_config["column_y"]] = (
60
+ table[_config["column_y"]] / table[_config["column_y"]].max()
61
+ )
62
+ if _config.get("select_values"):
63
+ table = _select_values_from_table(
64
+ table,
65
+ _config["select_values"]["column_name"],
66
+ _config["select_values"]["value"],
67
+ )
68
+ data[_config["label"]] = gen.get_structure_array_from_table(
69
+ table, [_config["column_x"], _config["column_y"]]
70
+ )
71
+ return data
72
+
73
+
74
+ def _read_table_from_model_database(table_config, db_config):
75
+ """
76
+ Read table data from model parameter database.
77
+
78
+ Parameters
79
+ ----------
80
+ table_config: dict
81
+ Configuration dictionary for table data.
82
+
83
+ Returns
84
+ -------
85
+ Table
86
+ Astropy table.
87
+ """
88
+ if "telescope" in table_config:
89
+ model = TelescopeModel(
90
+ site=table_config["site"],
91
+ telescope_name=table_config["telescope"],
92
+ model_version=table_config["model_version"],
93
+ mongo_db_config=db_config,
94
+ )
95
+ else:
96
+ model = SiteModel(
97
+ site=table_config["site"],
98
+ model_version=table_config["model_version"],
99
+ mongo_db_config=db_config,
100
+ )
101
+ return model.get_model_file_as_table(table_config["parameter"])
102
+
103
+
104
+ def _select_values_from_table(table, column_name, value):
105
+ """Return a table with only the rows where column_name == value."""
106
+ return table[np.isclose(table[column_name], value)]