gammasimtools 0.25.0__py3-none-any.whl → 0.26.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.
- {gammasimtools-0.25.0.dist-info → gammasimtools-0.26.0.dist-info}/METADATA +2 -1
- {gammasimtools-0.25.0.dist-info → gammasimtools-0.26.0.dist-info}/RECORD +122 -121
- {gammasimtools-0.25.0.dist-info → gammasimtools-0.26.0.dist-info}/entry_points.txt +2 -1
- {gammasimtools-0.25.0.dist-info → gammasimtools-0.26.0.dist-info}/licenses/LICENSE +1 -1
- simtools/_version.py +2 -2
- simtools/application_control.py +35 -7
- simtools/applications/calculate_incident_angles.py +0 -2
- simtools/applications/convert_geo_coordinates_of_array_elements.py +1 -2
- simtools/applications/db_add_file_to_db.py +1 -1
- simtools/applications/db_add_simulation_model_from_repository_to_db.py +1 -1
- simtools/applications/db_add_value_from_json_to_db.py +1 -1
- simtools/applications/db_generate_compound_indexes.py +1 -1
- simtools/applications/db_get_array_layouts_from_db.py +2 -6
- simtools/applications/db_get_file_from_db.py +1 -1
- simtools/applications/db_get_parameter_from_db.py +1 -1
- simtools/applications/db_inspect_databases.py +1 -1
- simtools/applications/db_upload_model_repository.py +1 -1
- simtools/applications/derive_ctao_array_layouts.py +1 -2
- simtools/applications/derive_mirror_rnda.py +1 -3
- simtools/applications/derive_psf_parameters.py +0 -1
- simtools/applications/derive_pulse_shape_parameters.py +0 -1
- simtools/applications/derive_trigger_rates.py +1 -1
- simtools/applications/docs_produce_array_element_report.py +2 -8
- simtools/applications/docs_produce_calibration_reports.py +1 -3
- simtools/applications/docs_produce_model_parameter_reports.py +0 -2
- simtools/applications/docs_produce_simulation_configuration_report.py +1 -3
- simtools/applications/generate_array_config.py +0 -1
- simtools/applications/generate_corsika_histograms.py +48 -235
- simtools/applications/generate_regular_arrays.py +5 -35
- simtools/applications/generate_simtel_event_data.py +2 -2
- simtools/applications/maintain_simulation_model_add_production.py +2 -2
- simtools/applications/maintain_simulation_model_write_array_element_positions.py +87 -0
- simtools/applications/plot_array_layout.py +5 -111
- simtools/applications/plot_simulated_event_distributions.py +57 -0
- simtools/applications/plot_tabular_data.py +0 -1
- simtools/applications/plot_tabular_data_for_model_parameter.py +1 -6
- simtools/applications/production_derive_corsika_limits.py +1 -1
- simtools/applications/production_generate_grid.py +0 -1
- simtools/applications/run_application.py +1 -1
- simtools/applications/simulate_flasher.py +0 -2
- simtools/applications/simulate_illuminator.py +0 -1
- simtools/applications/simulate_pedestals.py +1 -5
- simtools/applications/simulate_prod.py +1 -5
- simtools/applications/simulate_prod_htcondor_generator.py +1 -1
- simtools/applications/submit_array_layouts.py +2 -4
- simtools/applications/submit_model_parameter_from_external.py +1 -3
- simtools/applications/validate_camera_efficiency.py +0 -1
- simtools/applications/validate_camera_fov.py +0 -1
- simtools/applications/validate_cumulative_psf.py +0 -2
- simtools/applications/validate_optics.py +0 -13
- simtools/camera/camera_efficiency.py +1 -6
- simtools/camera/single_photon_electron_spectrum.py +2 -1
- simtools/configuration/commandline_parser.py +35 -2
- simtools/configuration/configurator.py +6 -11
- simtools/corsika/corsika_config.py +16 -21
- simtools/corsika/corsika_histograms.py +411 -1735
- simtools/corsika/primary_particle.py +1 -1
- simtools/data_model/metadata_collector.py +5 -2
- simtools/data_model/metadata_model.py +0 -4
- simtools/data_model/model_data_writer.py +13 -15
- simtools/data_model/validate_data.py +1 -3
- simtools/db/db_handler.py +19 -8
- simtools/dependencies.py +81 -38
- simtools/io/ascii_handler.py +4 -2
- simtools/io/table_handler.py +1 -1
- simtools/layout/array_layout.py +4 -12
- simtools/layout/array_layout_utils.py +226 -57
- simtools/model/array_model.py +1 -13
- simtools/model/calibration_model.py +0 -4
- simtools/model/legacy_model_parameter.py +134 -0
- simtools/model/model_parameter.py +24 -13
- simtools/model/model_utils.py +1 -6
- simtools/model/site_model.py +0 -4
- simtools/model/telescope_model.py +6 -11
- simtools/production_configuration/derive_corsika_limits.py +6 -11
- simtools/production_configuration/interpolation_handler.py +16 -16
- simtools/ray_tracing/incident_angles.py +5 -11
- simtools/ray_tracing/mirror_panel_psf.py +3 -7
- simtools/ray_tracing/psf_analysis.py +18 -19
- simtools/ray_tracing/psf_parameter_optimisation.py +0 -1
- simtools/ray_tracing/ray_tracing.py +6 -15
- simtools/reporting/docs_auto_report_generator.py +8 -13
- simtools/reporting/docs_read_parameters.py +2 -8
- simtools/runners/corsika_runner.py +5 -9
- simtools/runners/corsika_simtel_runner.py +3 -8
- simtools/runners/simtel_runner.py +0 -5
- simtools/runners/simtools_runner.py +2 -4
- simtools/settings.py +154 -0
- simtools/{io/eventio_handler.py → sim_events/file_info.py} +3 -3
- simtools/{simtel/simtel_io_event_histograms.py → sim_events/histograms.py} +25 -15
- simtools/{simtel/simtel_io_event_reader.py → sim_events/reader.py} +20 -17
- simtools/{simtel/simtel_io_event_writer.py → sim_events/writer.py} +84 -25
- simtools/simtel/pulse_shapes.py +7 -2
- simtools/simtel/simtel_config_writer.py +79 -36
- simtools/simtel/simtel_table_reader.py +6 -4
- simtools/simtel/simulator_array.py +4 -11
- simtools/simtel/simulator_camera_efficiency.py +4 -6
- simtools/simtel/simulator_light_emission.py +69 -24
- simtools/simtel/simulator_ray_tracing.py +4 -10
- simtools/simulator.py +7 -14
- simtools/telescope_trigger_rates.py +3 -4
- simtools/testing/assertions.py +84 -33
- simtools/testing/configuration.py +1 -2
- simtools/testing/helpers.py +2 -3
- simtools/testing/log_inspector.py +1 -0
- simtools/testing/sim_telarray_metadata.py +1 -1
- simtools/testing/validate_output.py +34 -23
- simtools/utils/general.py +37 -0
- simtools/utils/geometry.py +0 -77
- simtools/utils/names.py +5 -5
- simtools/visualization/legend_handlers.py +7 -6
- simtools/visualization/plot_array_layout.py +91 -16
- simtools/visualization/plot_corsika_histograms.py +143 -605
- simtools/visualization/plot_mirrors.py +1 -4
- simtools/visualization/plot_pixels.py +2 -4
- simtools/visualization/plot_psf.py +0 -1
- simtools/visualization/plot_simtel_event_histograms.py +4 -4
- simtools/visualization/plot_simtel_events.py +6 -11
- simtools/visualization/plot_tables.py +8 -19
- simtools/visualization/visualize.py +22 -2
- simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +0 -160
- simtools/applications/print_version.py +0 -53
- simtools/io/hdf5_handler.py +0 -139
- {gammasimtools-0.25.0.dist-info → gammasimtools-0.26.0.dist-info}/WHEEL +0 -0
- {gammasimtools-0.25.0.dist-info → gammasimtools-0.26.0.dist-info}/top_level.txt +0 -0
|
@@ -13,26 +13,36 @@ from simtools.testing import assertions
|
|
|
13
13
|
|
|
14
14
|
_logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
|
+
|
|
17
|
+
def _versions_match(from_command_line, from_config_file):
|
|
18
|
+
"""Return True if validations should run for the given versions.
|
|
19
|
+
|
|
20
|
+
Behavior:
|
|
21
|
+
- If no version is provided from the command line, run validations.
|
|
22
|
+
- If a filter is provided from the command line, run only when it matches
|
|
23
|
+
the version(s) from the config file.
|
|
24
|
+
"""
|
|
25
|
+
if from_command_line is None:
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
# Normalize to collections for comparison
|
|
29
|
+
cmd_versions = from_command_line if isinstance(from_command_line, list) else [from_command_line]
|
|
30
|
+
cfg_versions = from_config_file if isinstance(from_config_file, list) else [from_config_file]
|
|
31
|
+
|
|
32
|
+
# Consider a match if any overlap exists
|
|
33
|
+
return any(cv in cmd_versions for cv in cfg_versions)
|
|
34
|
+
|
|
35
|
+
|
|
16
36
|
# Keys to ignore when comparing sim_telarray configuration files
|
|
17
37
|
# (e.g., version numbers, system dependent parameters, CORSIKA options)
|
|
18
38
|
cfg_ignore_keys = [
|
|
19
39
|
"config_release",
|
|
20
40
|
"Label",
|
|
21
|
-
"
|
|
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",
|
|
41
|
+
"simtools_", # ignore all simtools_ keys - version/build info dependence
|
|
30
42
|
]
|
|
31
43
|
|
|
32
44
|
|
|
33
|
-
def validate_application_output(
|
|
34
|
-
config, from_command_line=None, from_config_file=None, db_config=None
|
|
35
|
-
):
|
|
45
|
+
def validate_application_output(config, from_command_line=None, from_config_file=None):
|
|
36
46
|
"""
|
|
37
47
|
Validate application output against expected output.
|
|
38
48
|
|
|
@@ -60,8 +70,8 @@ def validate_application_output(
|
|
|
60
70
|
f"from config file: {from_config_file}"
|
|
61
71
|
)
|
|
62
72
|
|
|
63
|
-
if from_command_line
|
|
64
|
-
_validate_output_files(config, integration_test
|
|
73
|
+
if _versions_match(from_command_line, from_config_file):
|
|
74
|
+
_validate_output_files(config, integration_test)
|
|
65
75
|
|
|
66
76
|
if "file_type" in integration_test:
|
|
67
77
|
assert assertions.assert_file_type(
|
|
@@ -73,7 +83,7 @@ def validate_application_output(
|
|
|
73
83
|
_test_simtel_cfg_files(config, integration_test, from_command_line, from_config_file)
|
|
74
84
|
|
|
75
85
|
|
|
76
|
-
def _validate_output_files(config, integration_test
|
|
86
|
+
def _validate_output_files(config, integration_test):
|
|
77
87
|
"""Validate output files."""
|
|
78
88
|
if "reference_output_file" in integration_test:
|
|
79
89
|
_validate_reference_output_file(config, integration_test)
|
|
@@ -85,11 +95,7 @@ def _validate_output_files(config, integration_test, db_config):
|
|
|
85
95
|
[{"path_descriptor": "output_path", "file": integration_test["output_file"]}],
|
|
86
96
|
)
|
|
87
97
|
if "model_parameter_validation" in integration_test:
|
|
88
|
-
_validate_model_parameter_json_file(
|
|
89
|
-
config,
|
|
90
|
-
integration_test["model_parameter_validation"],
|
|
91
|
-
db_config,
|
|
92
|
-
)
|
|
98
|
+
_validate_model_parameter_json_file(config, integration_test["model_parameter_validation"])
|
|
93
99
|
|
|
94
100
|
|
|
95
101
|
def _test_simtel_cfg_files(config, integration_test, from_command_line, from_config_file):
|
|
@@ -135,15 +141,20 @@ def _validate_output_path_and_file(config, integration_file_tests):
|
|
|
135
141
|
try:
|
|
136
142
|
assert output_file_path.exists()
|
|
137
143
|
except AssertionError as exc:
|
|
138
|
-
raise AssertionError(
|
|
144
|
+
raise AssertionError(
|
|
145
|
+
f"Output file {output_file_path} does not exist. "
|
|
146
|
+
f"Directory contents: {list(output_file_path.parent.iterdir())}"
|
|
147
|
+
) from exc
|
|
139
148
|
|
|
140
149
|
if output_file_path.name.endswith(".simtel.zst"):
|
|
141
150
|
assert assertions.check_output_from_sim_telarray(output_file_path, file_test)
|
|
142
151
|
elif output_file_path.name.endswith(".log_hist.tar.gz"):
|
|
143
152
|
assert assertions.check_simulation_logs(output_file_path, file_test)
|
|
153
|
+
elif output_file_path.suffix == ".log":
|
|
154
|
+
assert assertions.check_plain_log(output_file_path, file_test)
|
|
144
155
|
|
|
145
156
|
|
|
146
|
-
def _validate_model_parameter_json_file(config, model_parameter_validation
|
|
157
|
+
def _validate_model_parameter_json_file(config, model_parameter_validation):
|
|
147
158
|
"""
|
|
148
159
|
Validate model parameter json file and compare it with a reference parameter from the database.
|
|
149
160
|
|
|
@@ -158,7 +169,7 @@ def _validate_model_parameter_json_file(config, model_parameter_validation, db_c
|
|
|
158
169
|
|
|
159
170
|
"""
|
|
160
171
|
_logger.info(f"Checking model parameter json file: {model_parameter_validation}")
|
|
161
|
-
db = db_handler.DatabaseHandler(
|
|
172
|
+
db = db_handler.DatabaseHandler()
|
|
162
173
|
|
|
163
174
|
reference_parameter_name = model_parameter_validation.get("reference_parameter_name")
|
|
164
175
|
|
simtools/utils/general.py
CHANGED
|
@@ -11,6 +11,7 @@ import urllib.request
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from urllib.parse import urlparse
|
|
13
13
|
|
|
14
|
+
import dotenv
|
|
14
15
|
import numpy as np
|
|
15
16
|
|
|
16
17
|
_logger = logging.getLogger(__name__)
|
|
@@ -850,3 +851,39 @@ def get_list_of_files_from_command_line(file_names, suffix_list):
|
|
|
850
851
|
def now_date_time_in_isoformat():
|
|
851
852
|
"""Return date and time in isoformat and second accuracy."""
|
|
852
853
|
return datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds")
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def load_environment_variables(env_file=".env", env_list=None):
|
|
857
|
+
"""
|
|
858
|
+
Load environment variables (from a .env file or directly from the environment).
|
|
859
|
+
|
|
860
|
+
Allow to read a specific list of variables or all variables from the .env file.
|
|
861
|
+
|
|
862
|
+
Parameters
|
|
863
|
+
----------
|
|
864
|
+
env_file: str
|
|
865
|
+
Path to the .env file.
|
|
866
|
+
env_list: list, optional
|
|
867
|
+
List of environment variables to be read. If None, all variables are read.
|
|
868
|
+
|
|
869
|
+
Returns
|
|
870
|
+
-------
|
|
871
|
+
dict
|
|
872
|
+
Dictionary mapping environment variable names (lowercase, without the
|
|
873
|
+
``SIMTOOLS_`` prefix) to their cleaned string values.
|
|
874
|
+
"""
|
|
875
|
+
dotenv.load_dotenv(env_file or None)
|
|
876
|
+
keys = (
|
|
877
|
+
list(dotenv.dotenv_values(env_file).keys())
|
|
878
|
+
if env_list is None
|
|
879
|
+
else [f"SIMTOOLS_{s.upper()}" for s in env_list]
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
env_values = {}
|
|
883
|
+
for key in keys:
|
|
884
|
+
env_value = os.environ.get(key)
|
|
885
|
+
if env_value is None:
|
|
886
|
+
continue
|
|
887
|
+
cleaned_value = env_value.split("#")[0].strip().replace('"', "").replace("'", "")
|
|
888
|
+
env_values[key.removeprefix("SIMTOOLS_").lower()] = cleaned_value
|
|
889
|
+
return env_values
|
simtools/utils/geometry.py
CHANGED
|
@@ -1,88 +1,11 @@
|
|
|
1
1
|
"""A collection of functions related to geometrical transformations."""
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
3
|
import math
|
|
5
4
|
|
|
6
5
|
import astropy.units as u
|
|
7
6
|
import numpy as np
|
|
8
7
|
from astropy.units import UnitsError
|
|
9
8
|
|
|
10
|
-
_logger = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def convert_2d_to_radial_distr(hist_2d, xaxis, yaxis, bins=50, max_dist=1000):
|
|
14
|
-
"""
|
|
15
|
-
Convert a 2d histogram of positions, e.g. photon positions on the ground, to a 1D distribution.
|
|
16
|
-
|
|
17
|
-
Parameters
|
|
18
|
-
----------
|
|
19
|
-
hist_2d: numpy.ndarray
|
|
20
|
-
The histogram counts.
|
|
21
|
-
xaxis: numpy.array
|
|
22
|
-
The values of the x axis (histogram bin edges) on the ground.
|
|
23
|
-
yaxis: numpy.array
|
|
24
|
-
The values of the y axis (histogram bin edges) on the ground.
|
|
25
|
-
bins: float
|
|
26
|
-
Number of bins in distance.
|
|
27
|
-
max_dist: float
|
|
28
|
-
Maximum distance to consider in the 1D histogram, usually in meters.
|
|
29
|
-
|
|
30
|
-
Returns
|
|
31
|
-
-------
|
|
32
|
-
np.array
|
|
33
|
-
The values of the 1D histogram with size = int(max_dist/bin_size).
|
|
34
|
-
np.array
|
|
35
|
-
The bin edges of the 1D histogram with size = int(max_dist/bin_size) + 1.
|
|
36
|
-
|
|
37
|
-
"""
|
|
38
|
-
# Check if the histogram will make sense
|
|
39
|
-
bins_step = 2 * max_dist / bins # in the 2d array, the positive and negative direction count.
|
|
40
|
-
for axis in [xaxis, yaxis]:
|
|
41
|
-
if (bins_step < np.diff(axis)).any():
|
|
42
|
-
msg = (
|
|
43
|
-
f"The histogram with number of bins {bins} and maximum distance of {max_dist} "
|
|
44
|
-
f"resulted in a bin size smaller than the original array. Please adjust those "
|
|
45
|
-
f"parameters to increase the bin size and avoid nan in the histogram values."
|
|
46
|
-
)
|
|
47
|
-
_logger.warning(msg)
|
|
48
|
-
break
|
|
49
|
-
|
|
50
|
-
grid_2d_x, grid_2d_y = np.meshgrid(xaxis[:-1], yaxis[:-1]) # [:-1], since xaxis and yaxis are
|
|
51
|
-
# the hist bin_edges (n + 1).
|
|
52
|
-
# radial_distance_map maps the distance to the center from each element in a square matrix.
|
|
53
|
-
radial_distance_map = np.sqrt(grid_2d_x**2 + grid_2d_y**2)
|
|
54
|
-
# The sorting and unravel_index give us the two indices for the position of the sorted element
|
|
55
|
-
# in the original 2d matrix
|
|
56
|
-
sorted_indices = np.unravel_index(
|
|
57
|
-
np.argsort(radial_distance_map, axis=None), np.shape(radial_distance_map)
|
|
58
|
-
)
|
|
59
|
-
x_indices_sorted, y_indices_sorted = sorted_indices[0], sorted_indices[1]
|
|
60
|
-
|
|
61
|
-
# We construct a 1D array with the histogram counts sorted according to the distance to the
|
|
62
|
-
# center.
|
|
63
|
-
hist_sorted = np.array(
|
|
64
|
-
[hist_2d[i_x, i_y] for i_x, i_y in zip(x_indices_sorted, y_indices_sorted)]
|
|
65
|
-
)
|
|
66
|
-
distance_sorted = np.sort(radial_distance_map, axis=None)
|
|
67
|
-
|
|
68
|
-
# For larger distances, we have more elements in a slice 'dr' in radius, hence, we need to
|
|
69
|
-
# account for it using weights below.
|
|
70
|
-
|
|
71
|
-
weights, radial_bin_edges = np.histogram(distance_sorted, bins=bins, range=(0, max_dist))
|
|
72
|
-
histogram_1d = np.empty_like(weights, dtype=float)
|
|
73
|
-
|
|
74
|
-
for i_radial, _ in enumerate(radial_bin_edges[:-1]):
|
|
75
|
-
# Here we sum all the events within a radial interval 'dr' and then divide by the number of
|
|
76
|
-
# bins that fit this interval.
|
|
77
|
-
indices_to_sum = (distance_sorted >= radial_bin_edges[i_radial]) * (
|
|
78
|
-
distance_sorted < radial_bin_edges[i_radial + 1]
|
|
79
|
-
)
|
|
80
|
-
if weights[i_radial] != 0:
|
|
81
|
-
histogram_1d[i_radial] = np.sum(hist_sorted[indices_to_sum]) / weights[i_radial]
|
|
82
|
-
else:
|
|
83
|
-
histogram_1d[i_radial] = 0
|
|
84
|
-
return histogram_1d, radial_bin_edges
|
|
85
|
-
|
|
86
9
|
|
|
87
10
|
@u.quantity_input(rotation_angle_phi=u.rad, rotation_angle_theta=u.rad)
|
|
88
11
|
def rotate(x, y, rotation_around_z_axis, rotation_around_y_axis=0):
|
simtools/utils/names.py
CHANGED
|
@@ -189,13 +189,13 @@ def model_parameters(class_key_list=None):
|
|
|
189
189
|
dict
|
|
190
190
|
Model parameters definitions.
|
|
191
191
|
"""
|
|
192
|
-
_parameters = {}
|
|
193
192
|
if class_key_list is None:
|
|
194
193
|
return _load_model_parameters()
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
return {
|
|
195
|
+
key: value
|
|
196
|
+
for key, value in _load_model_parameters().items()
|
|
197
|
+
if value.get("instrument", {}).get("class", "") in class_key_list
|
|
198
|
+
}
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
def site_parameters():
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
"""Helper functions for legend handlers used for plotting."""
|
|
2
2
|
|
|
3
|
+
# pylint: disable=too-few-public-methods
|
|
4
|
+
|
|
3
5
|
import matplotlib.colors as mcolors
|
|
4
6
|
import matplotlib.patches as mpatches
|
|
5
7
|
import numpy as np
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Radii are relative to a reference radius (REFERENCE_RADIUS).
|
|
11
|
-
"""
|
|
9
|
+
# Define properties of different telescope types for visualization purposes.
|
|
10
|
+
# Radii are relative to a reference radius (REFERENCE_RADIUS).
|
|
12
11
|
TELESCOPE_CONFIG = {
|
|
13
12
|
"LST": {"color": "darkorange", "radius": 12.5, "shape": "circle", "filled": False},
|
|
14
13
|
"MST": {"color": "dodgerblue", "radius": 9.15, "shape": "circle", "filled": False},
|
|
@@ -51,7 +50,7 @@ def get_telescope_config(telescope_type):
|
|
|
51
50
|
config = TELESCOPE_CONFIG.get(telescope_type)
|
|
52
51
|
if not config and len(telescope_type) >= 3:
|
|
53
52
|
config = TELESCOPE_CONFIG.get(telescope_type[:3])
|
|
54
|
-
return config
|
|
53
|
+
return config.copy() if config else None
|
|
55
54
|
|
|
56
55
|
|
|
57
56
|
def calculate_center(handlebox, width_factor=3, height_factor=3):
|
|
@@ -272,6 +271,8 @@ class BaseLegendHandler:
|
|
|
272
271
|
x0, y0 = calculate_center(handlebox)
|
|
273
272
|
radius = handlebox.height
|
|
274
273
|
patch = self._create_hexagon(handlebox, x0, y0, radius)
|
|
274
|
+
else:
|
|
275
|
+
raise ValueError(f"Unknown shape: {shape}")
|
|
275
276
|
|
|
276
277
|
handlebox.add_artist(patch)
|
|
277
278
|
return patch
|
|
@@ -5,15 +5,18 @@ from collections import Counter
|
|
|
5
5
|
from typing import NamedTuple
|
|
6
6
|
|
|
7
7
|
import astropy.units as u
|
|
8
|
+
import matplotlib as mpl
|
|
8
9
|
import matplotlib.patches as mpatches
|
|
9
10
|
import matplotlib.pyplot as plt
|
|
10
11
|
import numpy as np
|
|
12
|
+
from adjustText import adjust_text
|
|
11
13
|
from astropy.table import Column
|
|
12
14
|
from matplotlib.collections import PatchCollection
|
|
13
15
|
|
|
14
16
|
from simtools.utils import geometry as transf
|
|
15
17
|
from simtools.utils import names
|
|
16
18
|
from simtools.visualization import legend_handlers as leg_h
|
|
19
|
+
from simtools.visualization import visualize
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
class PlotBounds(NamedTuple):
|
|
@@ -31,6 +34,61 @@ class PlotBounds(NamedTuple):
|
|
|
31
34
|
y_lim: tuple[float, float]
|
|
32
35
|
|
|
33
36
|
|
|
37
|
+
def plot_array_layouts(args_dict, output_path, layouts, background_layout=None):
|
|
38
|
+
"""
|
|
39
|
+
Plot multiple array layouts.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
args_dict : dict
|
|
44
|
+
Application arguments.
|
|
45
|
+
output_path : Path
|
|
46
|
+
Output path for figures.
|
|
47
|
+
layouts : dict
|
|
48
|
+
Dictionary of layout name to telescope table.
|
|
49
|
+
background_layout : Table or None
|
|
50
|
+
Optional background telescope table.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
figs : dict
|
|
55
|
+
Dictionary of layout name to matplotlib figure object.
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
mpl.use("Agg")
|
|
59
|
+
for layout in layouts:
|
|
60
|
+
fig_out = plot_array_layout(
|
|
61
|
+
telescopes=layout["array_elements"],
|
|
62
|
+
show_tel_label=args_dict["show_labels"],
|
|
63
|
+
axes_range=args_dict["axes_range"],
|
|
64
|
+
marker_scaling=args_dict["marker_scaling"],
|
|
65
|
+
background_telescopes=background_layout,
|
|
66
|
+
grayed_out_elements=args_dict["grayed_out_array_elements"],
|
|
67
|
+
highlighted_elements=args_dict["highlighted_array_elements"],
|
|
68
|
+
legend_location=args_dict["legend_location"],
|
|
69
|
+
bounds_mode=args_dict["bounds"],
|
|
70
|
+
padding=args_dict["padding"],
|
|
71
|
+
x_lim=tuple(args_dict["x_lim"]) if args_dict["x_lim"] else None,
|
|
72
|
+
y_lim=tuple(args_dict["y_lim"]) if args_dict["y_lim"] else None,
|
|
73
|
+
)
|
|
74
|
+
site_string = ""
|
|
75
|
+
if layout.get("site") is not None:
|
|
76
|
+
site_string = f"_{layout['site']}"
|
|
77
|
+
elif args_dict["site"] is not None:
|
|
78
|
+
site_string = f"_{args_dict['site']}"
|
|
79
|
+
coordinate_system_string = (
|
|
80
|
+
f"_{args_dict['coordinate_system']}"
|
|
81
|
+
if args_dict["coordinate_system"] not in layout["name"]
|
|
82
|
+
else ""
|
|
83
|
+
)
|
|
84
|
+
plot_file_name = args_dict["figure_name"] or (
|
|
85
|
+
f"array_layout_{layout['name']}{site_string}{coordinate_system_string}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
visualize.save_figure(fig_out, output_path / plot_file_name, dpi=400)
|
|
89
|
+
plt.close()
|
|
90
|
+
|
|
91
|
+
|
|
34
92
|
def plot_array_layout(
|
|
35
93
|
telescopes,
|
|
36
94
|
show_tel_label=False,
|
|
@@ -89,7 +147,7 @@ def plot_array_layout(
|
|
|
89
147
|
filter_x = x_lim
|
|
90
148
|
filter_y = y_lim
|
|
91
149
|
|
|
92
|
-
patches, plot_range, highlighted_patches, bounds = get_patches(
|
|
150
|
+
patches, plot_range, highlighted_patches, bounds, text_objects = get_patches(
|
|
93
151
|
ax,
|
|
94
152
|
telescopes,
|
|
95
153
|
show_tel_label,
|
|
@@ -122,6 +180,17 @@ def plot_array_layout(
|
|
|
122
180
|
|
|
123
181
|
finalize_plot(ax, patches, "Easting [m]", "Northing [m]", x_lim, y_lim, highlighted_patches)
|
|
124
182
|
|
|
183
|
+
if text_objects:
|
|
184
|
+
adjust_text(
|
|
185
|
+
text_objects,
|
|
186
|
+
ax=ax,
|
|
187
|
+
arrowprops={"arrowstyle": "->", "color": "grey", "alpha": 0.8, "lw": 0.8, "ls": "--"},
|
|
188
|
+
expand=(2.0, 2.0),
|
|
189
|
+
prevent_crossings=True,
|
|
190
|
+
min_arrow_len=8,
|
|
191
|
+
ensure_inside_axes=True,
|
|
192
|
+
)
|
|
193
|
+
|
|
125
194
|
return fig
|
|
126
195
|
|
|
127
196
|
|
|
@@ -178,7 +247,7 @@ def _get_patches_for_background_telescopes(
|
|
|
178
247
|
if background_telescopes is None:
|
|
179
248
|
return plot_range, bounds
|
|
180
249
|
|
|
181
|
-
bg_patches, bg_range, _, bg_bounds = get_patches(
|
|
250
|
+
bg_patches, bg_range, _, bg_bounds, _ = get_patches(
|
|
182
251
|
ax,
|
|
183
252
|
background_telescopes,
|
|
184
253
|
False,
|
|
@@ -268,6 +337,8 @@ def get_patches(
|
|
|
268
337
|
List of highlighted telescope patches.
|
|
269
338
|
bounds : PlotBounds
|
|
270
339
|
Min/max for x and y in meters.
|
|
340
|
+
text_objects : list
|
|
341
|
+
List of text objects for labels.
|
|
271
342
|
"""
|
|
272
343
|
pos_x, pos_y = get_positions(telescopes)
|
|
273
344
|
tel_table, pos_x, pos_y = _apply_limits_filter(
|
|
@@ -277,7 +348,7 @@ def get_patches(
|
|
|
277
348
|
tel_table["pos_x_rotated"] = Column(pos_x)
|
|
278
349
|
tel_table["pos_y_rotated"] = Column(pos_y)
|
|
279
350
|
|
|
280
|
-
patches, radii, highlighted_patches = create_patches(
|
|
351
|
+
patches, radii, highlighted_patches, text_objects = create_patches(
|
|
281
352
|
tel_table, marker_scaling, show_tel_label, ax, grayed_out_elements, highlighted_elements
|
|
282
353
|
)
|
|
283
354
|
|
|
@@ -290,8 +361,8 @@ def get_patches(
|
|
|
290
361
|
if len(pos_x) == 0:
|
|
291
362
|
bounds = PlotBounds(x_lim=(0.0, 0.0), y_lim=(0.0, 0.0))
|
|
292
363
|
if axes_range:
|
|
293
|
-
return patches, axes_range, highlighted_patches, bounds
|
|
294
|
-
return patches, 0.0, highlighted_patches, bounds
|
|
364
|
+
return patches, axes_range, highlighted_patches, bounds, text_objects
|
|
365
|
+
return patches, 0.0, highlighted_patches, bounds, text_objects
|
|
295
366
|
|
|
296
367
|
x_min = float(np.nanmin(pos_x).to_value(u.m)) - r
|
|
297
368
|
x_max = float(np.nanmax(pos_x).to_value(u.m)) + r
|
|
@@ -300,13 +371,13 @@ def get_patches(
|
|
|
300
371
|
bounds = PlotBounds(x_lim=(x_min, x_max), y_lim=(y_min, y_max))
|
|
301
372
|
|
|
302
373
|
if axes_range:
|
|
303
|
-
return patches, axes_range, highlighted_patches, bounds
|
|
374
|
+
return patches, axes_range, highlighted_patches, bounds, text_objects
|
|
304
375
|
|
|
305
376
|
max_x = max(abs(x_min), abs(x_max))
|
|
306
377
|
max_y = max(abs(y_min), abs(y_max))
|
|
307
378
|
updated_axes_range = max(max_x, max_y) * 1.1
|
|
308
379
|
|
|
309
|
-
return patches, updated_axes_range, highlighted_patches, bounds
|
|
380
|
+
return patches, updated_axes_range, highlighted_patches, bounds, text_objects
|
|
310
381
|
|
|
311
382
|
|
|
312
383
|
@u.quantity_input(x=u.m, y=u.m, radius=u.m)
|
|
@@ -417,8 +488,10 @@ def create_patches(
|
|
|
417
488
|
Telescope radii.
|
|
418
489
|
highlighted_patches : list
|
|
419
490
|
List of highlighted telescope patches.
|
|
491
|
+
text_objects : list
|
|
492
|
+
List of text objects for labels.
|
|
420
493
|
"""
|
|
421
|
-
patches, radii, highlighted_patches = [], [], []
|
|
494
|
+
patches, radii, highlighted_patches, text_objects = [], [], [], []
|
|
422
495
|
fontsize, scale_factor = (4, 2) if len(telescopes) > 30 else (8, 1)
|
|
423
496
|
|
|
424
497
|
grayed_out_set = set(grayed_out_elements) if grayed_out_elements else set()
|
|
@@ -457,16 +530,18 @@ def create_patches(
|
|
|
457
530
|
highlighted_patches.append(highlight_patch)
|
|
458
531
|
|
|
459
532
|
if show_label:
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
533
|
+
text_objects.append(
|
|
534
|
+
ax.text(
|
|
535
|
+
tel["pos_x_rotated"].value,
|
|
536
|
+
tel["pos_y_rotated"].value + scale_factor * radius.value,
|
|
537
|
+
name,
|
|
538
|
+
ha="center",
|
|
539
|
+
va="center",
|
|
540
|
+
fontsize=fontsize * 0.8,
|
|
541
|
+
)
|
|
467
542
|
)
|
|
468
543
|
|
|
469
|
-
return patches, radii, highlighted_patches
|
|
544
|
+
return patches, radii, highlighted_patches, text_objects
|
|
470
545
|
|
|
471
546
|
|
|
472
547
|
def get_telescope_name(tel):
|