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
@@ -1,7 +1,8 @@
1
1
  """Base class for running sim_telarray simulations."""
2
2
 
3
3
  import logging
4
- import os
4
+ import stat
5
+ import subprocess
5
6
  from pathlib import Path
6
7
 
7
8
  import simtools.utils.general as gen
@@ -96,7 +97,7 @@ class SimtelRunner:
96
97
 
97
98
  file.write('\necho "RUNTIME: $SECONDS"\n')
98
99
 
99
- os.system(f"chmod ug+x {script_file_path}")
100
+ script_file_path.chmod(script_file_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP)
100
101
  return script_file_path
101
102
 
102
103
  def run(self, test=False, input_file=None, run_number=None):
@@ -114,15 +115,17 @@ class SimtelRunner:
114
115
  """
115
116
  self._logger.debug("Running sim_telarray")
116
117
 
117
- command = self._make_run_command(run_number=run_number, input_file=input_file)
118
+ command, stdout_file, stderr_file = self._make_run_command(
119
+ run_number=run_number, input_file=input_file
120
+ )
118
121
 
119
122
  if test:
120
123
  self._logger.info(f"Running (test) with command: {command}")
121
- self._run_simtel_and_check_output(command)
124
+ self._run_simtel_and_check_output(command, stdout_file, stderr_file)
122
125
  else:
123
126
  self._logger.debug(f"Running ({self.runs_per_set}x) with command: {command}")
124
127
  for _ in range(self.runs_per_set):
125
- self._run_simtel_and_check_output(command)
128
+ self._run_simtel_and_check_output(command, stdout_file, stderr_file)
126
129
 
127
130
  self._check_run_result(run_number=run_number)
128
131
 
@@ -148,7 +151,7 @@ class SimtelRunner:
148
151
  self._logger.error(msg)
149
152
  raise SimtelExecutionError(msg)
150
153
 
151
- def _run_simtel_and_check_output(self, command):
154
+ def _run_simtel_and_check_output(self, command, stdout_file, stderr_file):
152
155
  """
153
156
  Run the sim_telarray command and check the exit code.
154
157
 
@@ -157,8 +160,24 @@ class SimtelRunner:
157
160
  SimtelExecutionError
158
161
  if run was not successful.
159
162
  """
160
- if os.system(command) != 0:
163
+ stdout_file = stdout_file if stdout_file else "/dev/null"
164
+ stderr_file = stderr_file if stderr_file else "/dev/null"
165
+ with (
166
+ open(f"{stdout_file}", "w", encoding="utf-8") as stdout,
167
+ open(f"{stderr_file}", "w", encoding="utf-8") as stderr,
168
+ ):
169
+ result = subprocess.run(
170
+ command,
171
+ shell=True,
172
+ text=True,
173
+ stdout=stdout,
174
+ stderr=stderr,
175
+ )
176
+
177
+ if result.returncode != 0:
178
+ self._logger.error(result.stderr)
161
179
  self._raise_simtel_error()
180
+ return result.returncode
162
181
 
163
182
  def _make_run_command(self, run_number=None, input_file=None):
164
183
  self._logger.debug(
@@ -167,7 +186,7 @@ class SimtelRunner:
167
186
  )
168
187
  input_file = input_file if input_file else "nofile"
169
188
  run_number = run_number if run_number else 1
170
- return f"{input_file}-{run_number}"
189
+ return f"{input_file}-{run_number}", None, None
171
190
 
172
191
  @staticmethod
173
192
  def get_config_option(par, value=None, weak_option=False):
@@ -3,7 +3,7 @@ $schema: http://json-schema.org/draft-06/schema#
3
3
  $ref: '#/definitions/SimtoolsIntegrationTestConfiguration'
4
4
  title: SimPipe integration test configuration metaschema
5
5
  description: YAML representation of integration test configuration metaschema
6
- version: 0.1.0
6
+ version: 0.2.0
7
7
  name: integration_tests_config.metaschema
8
8
  type: object
9
9
  additionalProperties: false
@@ -60,6 +60,20 @@ definitions:
60
60
  description: |
61
61
  "Path of output file tested to be generated by integration test."
62
62
  type: string
63
+ TEST_OUTPUT_FILES:
64
+ type: array
65
+ items:
66
+ type: object
67
+ properties:
68
+ PATH_DESCRIPTOR:
69
+ type: string
70
+ FILE:
71
+ type: string
72
+ EXPECTED_OUTPUT:
73
+ description: |
74
+ "Expected output of integration test."
75
+ type: object
76
+ required: ["PATH_DESCRIPTOR", "FILE"]
63
77
  FILE_TYPE:
64
78
  description: |
65
79
  "Expected file type of output file generated by integration test."
@@ -68,10 +82,6 @@ definitions:
68
82
  description: |
69
83
  "Reference file used for comparison."
70
84
  type: string
71
- EXPECTED_OUTPUT:
72
- description: |
73
- "Expected output of integration test."
74
- type: object
75
85
  TOLERANCE:
76
86
  description: "Allowed tolerance for floating point comparison."
77
87
  type: number
@@ -2,12 +2,99 @@
2
2
  $schema: http://json-schema.org/draft-06/schema#
3
3
  $ref: '#/definitions/SimtoolsModelParameters'
4
4
  title: SimPipe DB Model Parameter Metaschema
5
- description: YAML representation of db model parameter metaschema
6
- version: 0.1.0
5
+ description: |
6
+ YAML representation of db model parameter metaschema
7
+ (based on simulation model DB).
8
+ version: 0.2.0
7
9
  name: modelparameter.metaschema
8
10
  type: object
9
11
  additionalProperties: false
10
12
 
13
+ definitions:
14
+ SimtoolsModelParameters:
15
+ description: ""
16
+ type: object
17
+ properties:
18
+ _id:
19
+ type: string
20
+ description: "DB unique identifier."
21
+ entry_date:
22
+ type: string
23
+ description: "Value entry date."
24
+ file:
25
+ type: boolean
26
+ description: "This parameter is a file."
27
+ instrument:
28
+ type: string
29
+ description: "Associated instrument."
30
+ site:
31
+ type: string
32
+ description: "Associated CTAO site."
33
+ enum:
34
+ - North
35
+ - South
36
+ type:
37
+ type: string
38
+ description: "Data type"
39
+ enum:
40
+ - boolean
41
+ - dict
42
+ - double
43
+ - file
44
+ - float64
45
+ - int
46
+ - int64
47
+ - string
48
+ - uint
49
+ - uint32
50
+ - uint64
51
+ unit:
52
+ anyOf:
53
+ - type: string
54
+ - type: "null"
55
+ description: "Unit of the parameter."
56
+ value:
57
+ anyOf:
58
+ - type: boolean
59
+ - type: number
60
+ - type: string
61
+ - type: "null"
62
+ - type: array
63
+ description: "Value of the parameter."
64
+ parameter_version:
65
+ anyOf:
66
+ - type: string
67
+ description: "Parameter version."
68
+ schema_version:
69
+ anyOf:
70
+ - type: string
71
+ description: "Metaschema version."
72
+ unique_id:
73
+ anyOf:
74
+ - type: string
75
+ - type: "null"
76
+ description: "Unique ID of parameter definition."
77
+ required:
78
+ - file
79
+ - instrument
80
+ - parameter_version
81
+ - schema_version
82
+ - site
83
+ - type
84
+ - unit
85
+ - value
86
+ ...
87
+ ---
88
+ $schema: http://json-schema.org/draft-06/schema#
89
+ $ref: '#/definitions/SimtoolsModelParameters'
90
+ title: SimPipe DB Model Parameter Metaschema
91
+ description: |
92
+ YAML representation of db model parameter metaschema
93
+ (based on model_parameters DB).
94
+ version: 0.1.0
95
+ name: modelparameter.metaschema
96
+ type: object
97
+ additionalProperties: false
11
98
 
12
99
  definitions:
13
100
  SimtoolsModelParameters:
@@ -48,6 +135,7 @@ definitions:
48
135
  - boolean
49
136
  - double
50
137
  - int
138
+ - int64
51
139
  - string
52
140
  - uint
53
141
  - file
@@ -33,12 +33,42 @@ short_description: |-
33
33
  Effective focal length.
34
34
  Only to be used for image analysis, has no effect on the simulation.
35
35
  data:
36
- - type: double
36
+ - description: |-
37
+ Mean effective length for all directions of incidence.
38
+ type: double
37
39
  unit: cm
38
40
  default: 0.0
39
41
  allowed_range:
40
42
  min: 0.0
41
43
  max: 10000.0
44
+ - description: |-
45
+ Effective length for incidence directions in mirror/camera x-z plane
46
+ (if non-zero).
47
+ type: double
48
+ unit: cm
49
+ default: 0.0
50
+ allowed_range:
51
+ min: 0.0
52
+ max: 10000.0
53
+ - description: |-
54
+ Effective length for incidence directions in mirror/camera y-z plane
55
+ (if non-zero).
56
+ type: double
57
+ unit: cm
58
+ default: 0.0
59
+ allowed_range:
60
+ min: 0.0
61
+ max: 10000.0
62
+ - description: |-
63
+ Any displacement along x in the focal plane from asymmetric PSF behavior.
64
+ type: double
65
+ unit: cm
66
+ default: 0.0
67
+ - description: |-
68
+ Any displacement along y in the focal plane from asymmetric PSF behavior.
69
+ type: double
70
+ unit: cm
71
+ default: 0.0
42
72
  instrument:
43
73
  class: Structure
44
74
  type:
@@ -0,0 +1,410 @@
1
+ #!/usr/bin/python3
2
+ """Read tabular data in sim_telarray format and return as astropy table."""
3
+
4
+ import logging
5
+ import re
6
+
7
+ import astropy.units as u
8
+ from astropy.table import Table
9
+
10
+ from simtools.utils import general as gen
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def _data_columns(parameter_name, n_columns, n_dim):
16
+ """
17
+ Get column information for a given parameter.
18
+
19
+ Individual functions are adapted to the specific format of the sim_telarray tables.
20
+
21
+ Parameters
22
+ ----------
23
+ parameter_name: str
24
+ Model parameter name.
25
+ n_columns: int
26
+ Number of columns in the table.
27
+ n_dim: list
28
+ List of columns for n-dimensional tables defined by RPOL lines.
29
+
30
+ Returns
31
+ -------
32
+ list, str
33
+ List of columns for n-dimensional tables, description.
34
+ """
35
+ if parameter_name == "mirror_reflectivity":
36
+ return _data_columns_mirror_reflectivity(n_columns, n_dim)
37
+ if parameter_name in ("discriminator_pulse_shape", "fadc_pulse_shape"):
38
+ return _data_columns_pulse_shape(n_columns)
39
+ try:
40
+ return globals()[f"_data_columns_{parameter_name}"]()
41
+ except KeyError as exc:
42
+ raise ValueError(
43
+ f"Unsupported parameter for sim_telarray table reading: {parameter_name}"
44
+ ) from exc
45
+
46
+
47
+ def _data_columns_atmospheric_profile():
48
+ """Column representation for parameter atmospheric_profile."""
49
+ return (
50
+ [
51
+ {"name": "altitude", "description": "Altitude", "unit": "km"},
52
+ {"name": "density", "description": "Density", "unit": "g/cm^3"},
53
+ {"name": "thickness", "description": "Thickness", "unit": "g/cm^2"},
54
+ {
55
+ "name": "refractive_index",
56
+ "description": "Refractive index (n-1)",
57
+ "unit": None,
58
+ },
59
+ {
60
+ "name": "temperature",
61
+ "description": "Temperature",
62
+ "unit": "K",
63
+ },
64
+ {
65
+ "name": "pressure",
66
+ "description": "Pressure",
67
+ "unit": "mbar",
68
+ },
69
+ {
70
+ "name": "pw/w",
71
+ "description": "Partial pressure of water vapor",
72
+ "unit": None,
73
+ },
74
+ ],
75
+ "Atmospheric profile",
76
+ )
77
+
78
+
79
+ def _data_columns_pm_photoelectron_spectrum():
80
+ """Column description for parameter pm_photoelectron_spectrum."""
81
+ return (
82
+ [
83
+ {"name": "amplitude", "description": "Signal amplitude", "unit": None},
84
+ {
85
+ "name": "response",
86
+ "description": "response without afterpulsing component",
87
+ "unit": None,
88
+ },
89
+ {
90
+ "name": "response_with_ap",
91
+ "description": "response including afterpulsing component",
92
+ "unit": None,
93
+ },
94
+ ],
95
+ "Photoelectron spectrum",
96
+ )
97
+
98
+
99
+ def _data_columns_quantum_efficiency():
100
+ """Column description for parameter quantum_efficiency."""
101
+ return (
102
+ [
103
+ {"name": "wavelength", "description": "Wavelength", "unit": "nm"},
104
+ {
105
+ "name": "efficiency",
106
+ "description": "Quantum efficiency",
107
+ "unit": None,
108
+ },
109
+ {
110
+ "name": "efficiency_rms",
111
+ "description": "Quantum efficiency (standard deviation)",
112
+ "unit": None,
113
+ },
114
+ ],
115
+ "Quantum efficiency",
116
+ )
117
+
118
+
119
+ def _data_columns_camera_filter():
120
+ """Column description for parameter camera_filter."""
121
+ return (
122
+ [
123
+ {"name": "wavelength", "description": "Wavelength", "unit": "nm"},
124
+ {
125
+ "name": "transmission",
126
+ "description": "Average transmission",
127
+ "unit": None,
128
+ },
129
+ ],
130
+ "Camera window transmission",
131
+ )
132
+
133
+
134
+ def _data_columns_lightguide_efficiency_vs_wavelength():
135
+ """Column description for parameter lightguide_efficiency_vs_wavelength."""
136
+ return _data_columns_lightguide_efficiency_vs_incidence_angle()
137
+
138
+
139
+ def _data_columns_lightguide_efficiency_vs_incidence_angle():
140
+ """Column description for (parameter lightguide_efficiency_vs_incidence_angle."""
141
+ return (
142
+ [
143
+ {"name": "angle", "description": "Incidence angle", "unit": "deg"},
144
+ {
145
+ "name": "efficiency",
146
+ "description": "Light guide efficiency",
147
+ "unit": None,
148
+ },
149
+ ],
150
+ "Light guide efficiency vs incidence angle",
151
+ )
152
+
153
+
154
+ def _data_columns_mirror_reflectivity(n_columns, n_dim):
155
+ """Column description for parameter mirror_reflectivity."""
156
+ _columns = [
157
+ {"name": "wavelength", "description": "Wavelength", "unit": "nm"},
158
+ ]
159
+ if n_dim:
160
+ for angle in n_dim:
161
+ _columns.append(
162
+ {
163
+ "name": f"reflectivity_{angle}deg",
164
+ "description": f"Mirror reflectivity at {angle} deg",
165
+ "unit": None,
166
+ },
167
+ )
168
+ else:
169
+ _columns.append(
170
+ {
171
+ "name": "reflectivity",
172
+ "description": "Mirror reflectivity",
173
+ "unit": None,
174
+ },
175
+ )
176
+ if n_columns == 3:
177
+ _columns.append(
178
+ {
179
+ "name": "reflectivity_rms",
180
+ "description": "Mirror reflectivity (standard deviation)",
181
+ "unit": None,
182
+ },
183
+ )
184
+ if n_columns == 4:
185
+ _columns.append(
186
+ {
187
+ "name": "reflectivity_min",
188
+ "description": "Mirror reflectivity (min)",
189
+ "unit": None,
190
+ },
191
+ )
192
+ _columns.append(
193
+ {
194
+ "name": "reflectivity_max",
195
+ "description": "Mirror reflectivity (max)",
196
+ "unit": None,
197
+ },
198
+ )
199
+
200
+ return _columns, "Mirror reflectivity"
201
+
202
+
203
+ def _data_columns_pulse_shape(n_columns):
204
+ """Column description for parameters discriminator_pulse_shape, fadc_pulse_shape."""
205
+ _columns = [
206
+ {"name": "time", "description": "Time", "unit": "ns"},
207
+ {
208
+ "name": "amplitude",
209
+ "description": "Amplitude",
210
+ "unit": None,
211
+ },
212
+ ]
213
+ if n_columns == 3:
214
+ _columns.append(
215
+ {
216
+ "name": "amplitude (low gain)",
217
+ "description": "Amplitude (low gain)",
218
+ "unit": None,
219
+ },
220
+ )
221
+
222
+ return _columns, "Pulse shape"
223
+
224
+
225
+ def _data_columns_nsb_reference_spectrum():
226
+ """Column description for parameter nsb_reference_spectrum."""
227
+ return (
228
+ [
229
+ {"name": "wavelength", "description": "Wavelength", "unit": "nm"},
230
+ {
231
+ "name": "differential photon rate",
232
+ "description": "Differential photon rate",
233
+ "unit": "1.e9 / (nm s m^2 sr)",
234
+ },
235
+ ],
236
+ "NSB reference spectrum",
237
+ )
238
+
239
+
240
+ def read_simtel_table(parameter_name, file_path):
241
+ """
242
+ Read sim_telarray table file for a given parameter.
243
+
244
+ Parameters
245
+ ----------
246
+ parameter_name: str
247
+ Model parameter name.
248
+ file_path: Path
249
+ Name (full path) of the sim_telarray table file.
250
+
251
+ Returns
252
+ -------
253
+ Table
254
+ Astropy table.
255
+ """
256
+ if parameter_name == "atmospheric_transmission":
257
+ return _read_simtel_data_for_atmospheric_transmission(file_path)
258
+
259
+ rows, meta_from_simtel, n_columns, n_dim = _read_simtel_data(file_path)
260
+ columns_info, description = _data_columns(parameter_name, n_columns, n_dim)
261
+
262
+ rows = _adjust_columns_length(rows, len(columns_info))
263
+
264
+ metadata = {
265
+ "Name": parameter_name,
266
+ "File": str(file_path),
267
+ "Description:": description,
268
+ "Context_from_sim_telarray": meta_from_simtel,
269
+ }
270
+
271
+ table = Table(rows=rows, names=[col["name"] for col in columns_info])
272
+ for col, info in zip(table.colnames, columns_info):
273
+ table[col].unit = info.get("unit")
274
+ table[col].description = info.get("description")
275
+ table.meta.update(metadata)
276
+
277
+ return table
278
+
279
+
280
+ def _adjust_columns_length(rows, n_columns):
281
+ """
282
+ Adjust row lengths to match the specified column count.
283
+
284
+ - Truncate rows with extra columns beyond the specified count 'n_columns'.
285
+ - Pad shorter rows with zeros.
286
+ """
287
+ return [row[:n_columns] + [0.0] * max(0, n_columns - len(row)) for row in rows]
288
+
289
+
290
+ def _read_simtel_data(file_path):
291
+ """
292
+ Read data, comments, and (if available) axis definition from sim_telarray table.
293
+
294
+ Parameters
295
+ ----------
296
+ file_path: Path
297
+ Path to the sim_telarray table file.
298
+
299
+ Returns
300
+ -------
301
+ str, str, int, str
302
+ data, metadata (comments), number of columns (max value), n-dimensional axis description.
303
+ """
304
+ logger.debug(f"Reading sim_telarray table from {file_path}")
305
+ meta_lines = []
306
+ data_lines = []
307
+ n_dim_axis = None
308
+ r_pol_axis = None
309
+
310
+ lines = gen.read_file_encoded_in_utf_or_latin(file_path)
311
+
312
+ for line in lines:
313
+ stripped = line.strip()
314
+ if "@RPOL@" in stripped: # RPOL description for N-dimensional tables
315
+ match = re.search(r"#@RPOL@\[(\w+)=\]", stripped)
316
+ if match:
317
+ r_pol_axis = match.group(1)
318
+ elif r_pol_axis and r_pol_axis in stripped: # N-dimensional axis description
319
+ n_dim_axis = stripped.split("=")[1].split()
320
+ elif stripped.startswith("#"): # Metadata
321
+ meta_lines.append(stripped.lstrip("#").strip())
322
+ elif stripped: # Data
323
+ data_lines.append(stripped.split("%%%")[0].split("#")[0].strip()) # Remove comments
324
+
325
+ rows = [[float(part) for part in line.split()] for line in data_lines]
326
+ n_columns = max(len(row) for row in rows) if rows else 0
327
+
328
+ return rows, "\n".join(meta_lines), n_columns, n_dim_axis
329
+
330
+
331
+ def _read_simtel_data_for_atmospheric_transmission(file_path):
332
+ """
333
+ Read data and comments from sim_telarray table for atmospheric_transmission.
334
+
335
+ Parameters
336
+ ----------
337
+ file_path: Path
338
+ Path to the sim_telarray table file.
339
+
340
+ Returns
341
+ -------
342
+ astropy table
343
+ Table with atmospheric transmission.
344
+ """
345
+ lines = lines = gen.read_file_encoded_in_utf_or_latin(file_path)
346
+
347
+ observatory_level, height_bins = _read_header_line_for_atmospheric_transmission(
348
+ lines, file_path
349
+ )
350
+
351
+ wavelengths = []
352
+ heights = []
353
+ extinctions = []
354
+ meta_lines = []
355
+
356
+ for line in lines:
357
+ if line.startswith("#") or not line.strip():
358
+ meta_lines.append(line.lstrip("#").strip())
359
+ continue
360
+ parts = line.split()
361
+ try:
362
+ wl = float(parts[0])
363
+ for i, height in enumerate(height_bins):
364
+ extinction_value = float(parts[i + 1])
365
+ if extinction_value == 99999.0:
366
+ continue
367
+ wavelengths.append(wl)
368
+ heights.append(height)
369
+ extinctions.append(extinction_value)
370
+ except (ValueError, IndexError):
371
+ logger.debug(f"Skipping malformed line: {line.strip()}")
372
+
373
+ table = Table()
374
+ table["wavelength"] = wavelengths
375
+ table["altitude"] = heights
376
+ table["extinction"] = extinctions
377
+
378
+ table.meta.update(
379
+ {
380
+ "Name": "atmospheric_transmission",
381
+ "File": str(file_path),
382
+ "Description": "Atmospheric transmission",
383
+ "Context_from_sim_telarray": "\n".join(meta_lines),
384
+ "observatory_level": observatory_level,
385
+ }
386
+ )
387
+
388
+ return table
389
+
390
+
391
+ def _read_header_line_for_atmospheric_transmission(lines, file_path):
392
+ """Reader observatory level and height bins from header line for atmospheric transmission."""
393
+ header_line = None
394
+ observatory_level = None
395
+ for line in lines:
396
+ if "H2=" in line and "H1=" in line:
397
+ match_h2 = re.search(r"H2=\s*([\d.]+)", line)
398
+ if match_h2:
399
+ observatory_level = float(match_h2.group(1)) * u.km
400
+
401
+ if "H1=" in line:
402
+ header_line = line.split("H1=")[-1].strip()
403
+ break
404
+
405
+ if header_line is None:
406
+ raise ValueError(f"Header with 'H1=' not found file {file_path}")
407
+
408
+ height_bins = [float(x.replace(",", "")) for x in header_line.split()]
409
+
410
+ return observatory_level, height_bins