gammasimtools 0.24.0__py3-none-any.whl → 0.25.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/METADATA +1 -1
- {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/RECORD +58 -55
- {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/entry_points.txt +1 -0
- simtools/_version.py +2 -2
- simtools/application_control.py +50 -0
- simtools/applications/derive_psf_parameters.py +5 -0
- simtools/applications/derive_pulse_shape_parameters.py +195 -0
- simtools/applications/plot_array_layout.py +63 -1
- simtools/applications/simulate_flasher.py +3 -2
- simtools/applications/simulate_pedestals.py +1 -1
- simtools/applications/simulate_prod.py +8 -23
- simtools/applications/simulate_prod_htcondor_generator.py +7 -0
- simtools/applications/submit_array_layouts.py +5 -3
- simtools/applications/validate_file_using_schema.py +49 -123
- simtools/configuration/commandline_parser.py +8 -6
- simtools/corsika/corsika_config.py +197 -87
- simtools/data_model/model_data_writer.py +14 -2
- simtools/data_model/schema.py +112 -5
- simtools/data_model/validate_data.py +82 -48
- simtools/db/db_model_upload.py +2 -1
- simtools/db/mongo_db.py +133 -42
- simtools/dependencies.py +5 -9
- simtools/io/eventio_handler.py +128 -0
- simtools/job_execution/htcondor_script_generator.py +0 -2
- simtools/layout/array_layout_utils.py +1 -1
- simtools/model/array_model.py +36 -5
- simtools/model/model_parameter.py +0 -1
- simtools/model/model_repository.py +18 -5
- simtools/ray_tracing/psf_analysis.py +11 -8
- simtools/ray_tracing/psf_parameter_optimisation.py +822 -679
- simtools/reporting/docs_read_parameters.py +69 -9
- simtools/runners/corsika_runner.py +12 -3
- simtools/runners/corsika_simtel_runner.py +6 -0
- simtools/runners/runner_services.py +17 -7
- simtools/runners/simtel_runner.py +12 -54
- simtools/schemas/model_parameters/flasher_pulse_exp_decay.schema.yml +2 -0
- simtools/schemas/model_parameters/flasher_pulse_shape.schema.yml +50 -0
- simtools/schemas/model_parameters/flasher_pulse_width.schema.yml +2 -0
- simtools/schemas/simulation_models_info.schema.yml +2 -0
- simtools/simtel/pulse_shapes.py +268 -0
- simtools/simtel/simtel_config_writer.py +82 -1
- simtools/simtel/simtel_io_event_writer.py +2 -2
- simtools/simtel/simulator_array.py +58 -12
- simtools/simtel/simulator_light_emission.py +45 -8
- simtools/simulator.py +361 -347
- simtools/testing/assertions.py +62 -6
- simtools/testing/configuration.py +1 -1
- simtools/testing/log_inspector.py +4 -1
- simtools/testing/sim_telarray_metadata.py +1 -1
- simtools/testing/validate_output.py +44 -9
- simtools/utils/names.py +2 -4
- simtools/version.py +37 -0
- simtools/visualization/legend_handlers.py +14 -4
- simtools/visualization/plot_array_layout.py +229 -33
- simtools/visualization/plot_mirrors.py +837 -0
- simtools/simtel/simtel_io_file_info.py +0 -62
- {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/WHEEL +0 -0
- {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/licenses/LICENSE +0 -0
- {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/top_level.txt +0 -0
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
"""Plot array elements for a layout."""
|
|
3
3
|
|
|
4
4
|
from collections import Counter
|
|
5
|
+
from typing import NamedTuple
|
|
5
6
|
|
|
6
7
|
import astropy.units as u
|
|
7
8
|
import matplotlib.patches as mpatches
|
|
8
9
|
import matplotlib.pyplot as plt
|
|
10
|
+
import numpy as np
|
|
9
11
|
from astropy.table import Column
|
|
10
12
|
from matplotlib.collections import PatchCollection
|
|
11
13
|
|
|
@@ -14,6 +16,21 @@ from simtools.utils import names
|
|
|
14
16
|
from simtools.visualization import legend_handlers as leg_h
|
|
15
17
|
|
|
16
18
|
|
|
19
|
+
class PlotBounds(NamedTuple):
|
|
20
|
+
"""Axis-aligned bounds for the layout in meters.
|
|
21
|
+
|
|
22
|
+
Attributes
|
|
23
|
+
----------
|
|
24
|
+
x_lim : tuple[float, float]
|
|
25
|
+
Min/max for x (meters).
|
|
26
|
+
y_lim : tuple[float, float]
|
|
27
|
+
Min/max for y (meters).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
x_lim: tuple[float, float]
|
|
31
|
+
y_lim: tuple[float, float]
|
|
32
|
+
|
|
33
|
+
|
|
17
34
|
def plot_array_layout(
|
|
18
35
|
telescopes,
|
|
19
36
|
show_tel_label=False,
|
|
@@ -23,6 +40,10 @@ def plot_array_layout(
|
|
|
23
40
|
grayed_out_elements=None,
|
|
24
41
|
highlighted_elements=None,
|
|
25
42
|
legend_location="best",
|
|
43
|
+
bounds_mode="exact",
|
|
44
|
+
padding=0.1,
|
|
45
|
+
x_lim=None,
|
|
46
|
+
y_lim=None,
|
|
26
47
|
):
|
|
27
48
|
"""
|
|
28
49
|
Plot telescope array layout.
|
|
@@ -50,10 +71,25 @@ def plot_array_layout(
|
|
|
50
71
|
-------
|
|
51
72
|
fig : Figure
|
|
52
73
|
Matplotlib figure object.
|
|
74
|
+
|
|
75
|
+
Other Parameters
|
|
76
|
+
----------------
|
|
77
|
+
bounds_mode : {"symmetric", "exact"}
|
|
78
|
+
Controls axis limits calculation. "symmetric" uses +-R where R is the padded
|
|
79
|
+
maximum extent (default), while "exact" uses individual x/y min/max bounds.
|
|
80
|
+
padding : float
|
|
81
|
+
Fractional padding applied around computed extents (used for both modes).
|
|
82
|
+
x_lim, y_lim : tuple(float, float), optional
|
|
83
|
+
Explicit axis limits in meters. If provided, these override axes_range and bounds_mode
|
|
84
|
+
for the respective axis. If only one is provided, the other axis is derived per mode.
|
|
53
85
|
"""
|
|
54
86
|
fig, ax = plt.subplots(1)
|
|
55
87
|
|
|
56
|
-
|
|
88
|
+
# If explicit limits are provided (one or both), filter patches accordingly
|
|
89
|
+
filter_x = x_lim
|
|
90
|
+
filter_y = y_lim
|
|
91
|
+
|
|
92
|
+
patches, plot_range, highlighted_patches, bounds = get_patches(
|
|
57
93
|
ax,
|
|
58
94
|
telescopes,
|
|
59
95
|
show_tel_label,
|
|
@@ -61,22 +97,136 @@ def plot_array_layout(
|
|
|
61
97
|
marker_scaling,
|
|
62
98
|
grayed_out_elements,
|
|
63
99
|
highlighted_elements,
|
|
100
|
+
filter_x_lim=filter_x,
|
|
101
|
+
filter_y_lim=filter_y,
|
|
64
102
|
)
|
|
65
103
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
104
|
+
plot_range, bounds = _get_patches_for_background_telescopes(
|
|
105
|
+
ax,
|
|
106
|
+
background_telescopes,
|
|
107
|
+
axes_range,
|
|
108
|
+
marker_scaling,
|
|
109
|
+
bounds_mode,
|
|
110
|
+
plot_range,
|
|
111
|
+
bounds,
|
|
112
|
+
filter_x_lim=filter_x,
|
|
113
|
+
filter_y_lim=filter_y,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if legend_location != "no_legend":
|
|
117
|
+
update_legend(ax, telescopes, grayed_out_elements, legend_location)
|
|
118
|
+
|
|
119
|
+
x_lim, y_lim = _get_axis_limits(
|
|
120
|
+
axes_range, bounds_mode, padding, plot_range, bounds, x_lim, y_lim
|
|
121
|
+
)
|
|
73
122
|
|
|
74
|
-
|
|
75
|
-
finalize_plot(ax, patches, "Easting [m]", "Northing [m]", plot_range, highlighted_patches)
|
|
123
|
+
finalize_plot(ax, patches, "Easting [m]", "Northing [m]", x_lim, y_lim, highlighted_patches)
|
|
76
124
|
|
|
77
125
|
return fig
|
|
78
126
|
|
|
79
127
|
|
|
128
|
+
def _get_axis_limits(
|
|
129
|
+
axes_range,
|
|
130
|
+
bounds_mode,
|
|
131
|
+
padding,
|
|
132
|
+
plot_range,
|
|
133
|
+
bounds,
|
|
134
|
+
x_lim_override=None,
|
|
135
|
+
y_lim_override=None,
|
|
136
|
+
):
|
|
137
|
+
"""Get axis limits based on mode and padding."""
|
|
138
|
+
|
|
139
|
+
def _derive_axis(axis: str) -> tuple[float, float]:
|
|
140
|
+
if bounds_mode == "exact":
|
|
141
|
+
if axis == "x":
|
|
142
|
+
span = bounds.x_lim[1] - bounds.x_lim[0]
|
|
143
|
+
pad = padding * span
|
|
144
|
+
return (bounds.x_lim[0] - pad, bounds.x_lim[1] + pad)
|
|
145
|
+
span = bounds.y_lim[1] - bounds.y_lim[0]
|
|
146
|
+
pad = padding * span
|
|
147
|
+
return (bounds.y_lim[0] - pad, bounds.y_lim[1] + pad)
|
|
148
|
+
# symmetric
|
|
149
|
+
sym = plot_range
|
|
150
|
+
padf = max(0.0, min(1.0, float(padding))) if padding is not None else 0.0
|
|
151
|
+
sym *= 1.0 + padf
|
|
152
|
+
return (-sym, sym)
|
|
153
|
+
|
|
154
|
+
# Highest priority: explicit overrides (per axis)
|
|
155
|
+
if x_lim_override is not None or y_lim_override is not None:
|
|
156
|
+
x_lim = x_lim_override if x_lim_override is not None else _derive_axis("x")
|
|
157
|
+
y_lim = y_lim_override if y_lim_override is not None else _derive_axis("y")
|
|
158
|
+
return x_lim, y_lim
|
|
159
|
+
|
|
160
|
+
if axes_range is not None:
|
|
161
|
+
return (-axes_range, axes_range), (-axes_range, axes_range)
|
|
162
|
+
# Derive both axes using selected mode
|
|
163
|
+
return _derive_axis("x"), _derive_axis("y")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _get_patches_for_background_telescopes(
|
|
167
|
+
ax,
|
|
168
|
+
background_telescopes,
|
|
169
|
+
axes_range,
|
|
170
|
+
marker_scaling,
|
|
171
|
+
bounds_mode,
|
|
172
|
+
plot_range,
|
|
173
|
+
bounds,
|
|
174
|
+
filter_x_lim=None,
|
|
175
|
+
filter_y_lim=None,
|
|
176
|
+
):
|
|
177
|
+
"""Get background telescope patches and update plot range/bounds."""
|
|
178
|
+
if background_telescopes is None:
|
|
179
|
+
return plot_range, bounds
|
|
180
|
+
|
|
181
|
+
bg_patches, bg_range, _, bg_bounds = get_patches(
|
|
182
|
+
ax,
|
|
183
|
+
background_telescopes,
|
|
184
|
+
False,
|
|
185
|
+
axes_range,
|
|
186
|
+
marker_scaling,
|
|
187
|
+
None,
|
|
188
|
+
None,
|
|
189
|
+
filter_x_lim=filter_x_lim,
|
|
190
|
+
filter_y_lim=filter_y_lim,
|
|
191
|
+
)
|
|
192
|
+
ax.add_collection(PatchCollection(bg_patches, match_original=True, alpha=0.1))
|
|
193
|
+
if axes_range is None:
|
|
194
|
+
if bounds_mode == "symmetric":
|
|
195
|
+
plot_range = max(plot_range, bg_range)
|
|
196
|
+
else:
|
|
197
|
+
bounds = PlotBounds(
|
|
198
|
+
x_lim=(
|
|
199
|
+
min(bounds.x_lim[0], bg_bounds.x_lim[0]),
|
|
200
|
+
max(bounds.x_lim[1], bg_bounds.x_lim[1]),
|
|
201
|
+
),
|
|
202
|
+
y_lim=(
|
|
203
|
+
min(bounds.y_lim[0], bg_bounds.y_lim[0]),
|
|
204
|
+
max(bounds.y_lim[1], bg_bounds.y_lim[1]),
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
return plot_range, bounds
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _apply_limits_filter(telescopes, pos_x, pos_y, filter_x_lim, filter_y_lim):
|
|
211
|
+
"""Filter telescope table and positions by optional axis limits."""
|
|
212
|
+
if filter_x_lim is None and filter_y_lim is None:
|
|
213
|
+
return telescopes, pos_x, pos_y
|
|
214
|
+
|
|
215
|
+
px = np.asarray(pos_x.to_value(u.m))
|
|
216
|
+
py = np.asarray(pos_y.to_value(u.m))
|
|
217
|
+
mask = np.ones(px.shape, dtype=bool)
|
|
218
|
+
if filter_x_lim is not None:
|
|
219
|
+
mask &= (px >= float(filter_x_lim[0])) & (px <= float(filter_x_lim[1]))
|
|
220
|
+
if filter_y_lim is not None:
|
|
221
|
+
mask &= (py >= float(filter_y_lim[0])) & (py <= float(filter_y_lim[1]))
|
|
222
|
+
|
|
223
|
+
if mask.size and mask.any():
|
|
224
|
+
return telescopes[mask], pos_x[mask], pos_y[mask]
|
|
225
|
+
|
|
226
|
+
# No telescopes within limits
|
|
227
|
+
return telescopes[:0], pos_x[:0], pos_y[:0]
|
|
228
|
+
|
|
229
|
+
|
|
80
230
|
def get_patches(
|
|
81
231
|
ax,
|
|
82
232
|
telescopes,
|
|
@@ -85,6 +235,8 @@ def get_patches(
|
|
|
85
235
|
marker_scaling,
|
|
86
236
|
grayed_out_elements=None,
|
|
87
237
|
highlighted_elements=None,
|
|
238
|
+
filter_x_lim=None,
|
|
239
|
+
filter_y_lim=None,
|
|
88
240
|
):
|
|
89
241
|
"""
|
|
90
242
|
Get plot patches and axis range.
|
|
@@ -111,38 +263,61 @@ def get_patches(
|
|
|
111
263
|
patches : list
|
|
112
264
|
List of telescope patches.
|
|
113
265
|
axes_range : float
|
|
114
|
-
Calculated or input axis range.
|
|
266
|
+
Calculated or input symmetric axis range (meters).
|
|
115
267
|
highlighted_patches : list
|
|
116
268
|
List of highlighted telescope patches.
|
|
269
|
+
bounds : PlotBounds
|
|
270
|
+
Min/max for x and y in meters.
|
|
117
271
|
"""
|
|
118
272
|
pos_x, pos_y = get_positions(telescopes)
|
|
119
|
-
|
|
120
|
-
|
|
273
|
+
tel_table, pos_x, pos_y = _apply_limits_filter(
|
|
274
|
+
telescopes, pos_x, pos_y, filter_x_lim, filter_y_lim
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
tel_table["pos_x_rotated"] = Column(pos_x)
|
|
278
|
+
tel_table["pos_y_rotated"] = Column(pos_y)
|
|
121
279
|
|
|
122
280
|
patches, radii, highlighted_patches = create_patches(
|
|
123
|
-
|
|
281
|
+
tel_table, marker_scaling, show_tel_label, ax, grayed_out_elements, highlighted_elements
|
|
124
282
|
)
|
|
125
283
|
|
|
284
|
+
if len(radii) == 0:
|
|
285
|
+
r = 0.0
|
|
286
|
+
else:
|
|
287
|
+
radii_q = u.Quantity(radii)
|
|
288
|
+
r = float(np.nanmax(radii_q).to_value(u.m))
|
|
289
|
+
|
|
290
|
+
if len(pos_x) == 0:
|
|
291
|
+
bounds = PlotBounds(x_lim=(0.0, 0.0), y_lim=(0.0, 0.0))
|
|
292
|
+
if axes_range:
|
|
293
|
+
return patches, axes_range, highlighted_patches, bounds
|
|
294
|
+
return patches, 0.0, highlighted_patches, bounds
|
|
295
|
+
|
|
296
|
+
x_min = float(np.nanmin(pos_x).to_value(u.m)) - r
|
|
297
|
+
x_max = float(np.nanmax(pos_x).to_value(u.m)) + r
|
|
298
|
+
y_min = float(np.nanmin(pos_y).to_value(u.m)) - r
|
|
299
|
+
y_max = float(np.nanmax(pos_y).to_value(u.m)) + r
|
|
300
|
+
bounds = PlotBounds(x_lim=(x_min, x_max), y_lim=(y_min, y_max))
|
|
301
|
+
|
|
126
302
|
if axes_range:
|
|
127
|
-
return patches, axes_range, highlighted_patches
|
|
303
|
+
return patches, axes_range, highlighted_patches, bounds
|
|
128
304
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
max_y = max(abs(pos_y.min().value), abs(pos_y.max().value)) + r
|
|
305
|
+
max_x = max(abs(x_min), abs(x_max))
|
|
306
|
+
max_y = max(abs(y_min), abs(y_max))
|
|
132
307
|
updated_axes_range = max(max_x, max_y) * 1.1
|
|
133
308
|
|
|
134
|
-
return patches, updated_axes_range, highlighted_patches
|
|
309
|
+
return patches, updated_axes_range, highlighted_patches, bounds
|
|
135
310
|
|
|
136
311
|
|
|
137
312
|
@u.quantity_input(x=u.m, y=u.m, radius=u.m)
|
|
138
|
-
def get_telescope_patch(
|
|
313
|
+
def get_telescope_patch(tel_type, x, y, radius, is_grayed_out=False):
|
|
139
314
|
"""
|
|
140
315
|
Create patch for a telescope.
|
|
141
316
|
|
|
142
317
|
Parameters
|
|
143
318
|
----------
|
|
144
|
-
|
|
145
|
-
Telescope
|
|
319
|
+
tel_type: str
|
|
320
|
+
Telescope type.
|
|
146
321
|
x : Quantity
|
|
147
322
|
X position.
|
|
148
323
|
y : Quantity
|
|
@@ -157,24 +332,34 @@ def get_telescope_patch(name, x, y, radius, is_grayed_out=False):
|
|
|
157
332
|
patch : Patch
|
|
158
333
|
Circle or rectangle patch.
|
|
159
334
|
"""
|
|
160
|
-
|
|
335
|
+
config = leg_h.get_telescope_config(tel_type)
|
|
161
336
|
x, y, r = x.to(u.m), y.to(u.m), radius.to(u.m)
|
|
162
337
|
|
|
163
|
-
color = "gray" if is_grayed_out else
|
|
338
|
+
color = "gray" if is_grayed_out else config["color"]
|
|
339
|
+
fill_flag = True if is_grayed_out else bool(config.get("filled", True))
|
|
164
340
|
|
|
165
|
-
if
|
|
341
|
+
if config.get("shape", "circle") == "square":
|
|
166
342
|
return mpatches.Rectangle(
|
|
167
343
|
((x - r / 2).value, (y - r / 2).value),
|
|
168
344
|
width=r.value,
|
|
169
345
|
height=r.value,
|
|
170
|
-
fill=
|
|
346
|
+
fill=fill_flag,
|
|
347
|
+
color=color,
|
|
348
|
+
)
|
|
349
|
+
if config.get("shape") == "hexagon":
|
|
350
|
+
return mpatches.RegularPolygon(
|
|
351
|
+
(x.value, y.value),
|
|
352
|
+
numVertices=6,
|
|
353
|
+
radius=r.value * np.sqrt(3) / 2,
|
|
354
|
+
orientation=np.pi / 6,
|
|
355
|
+
fill=fill_flag,
|
|
171
356
|
color=color,
|
|
172
357
|
)
|
|
173
358
|
|
|
174
359
|
return mpatches.Circle(
|
|
175
360
|
(x.value, y.value),
|
|
176
361
|
radius=r.value,
|
|
177
|
-
fill=
|
|
362
|
+
fill=fill_flag,
|
|
178
363
|
alpha=0.5 if is_grayed_out else 1.0,
|
|
179
364
|
color=color,
|
|
180
365
|
)
|
|
@@ -243,7 +428,10 @@ def create_patches(
|
|
|
243
428
|
name = get_telescope_name(tel)
|
|
244
429
|
radius = get_sphere_radius(tel)
|
|
245
430
|
radii.append(radius)
|
|
246
|
-
|
|
431
|
+
try:
|
|
432
|
+
tel_type = names.get_array_element_type_from_name(name)
|
|
433
|
+
except ValueError:
|
|
434
|
+
tel_type = None
|
|
247
435
|
|
|
248
436
|
is_grayed_out = name in grayed_out_set
|
|
249
437
|
is_highlighted = name in highlighted_set
|
|
@@ -275,7 +463,7 @@ def create_patches(
|
|
|
275
463
|
name,
|
|
276
464
|
ha="center",
|
|
277
465
|
va="bottom",
|
|
278
|
-
fontsize=fontsize,
|
|
466
|
+
fontsize=fontsize * 0.8,
|
|
279
467
|
)
|
|
280
468
|
|
|
281
469
|
return patches, radii, highlighted_patches
|
|
@@ -344,7 +532,15 @@ def update_legend(ax, telescopes, grayed_out_elements=None, legend_location="bes
|
|
|
344
532
|
ax.legend(objs, labels, handler_map=handler_map, prop={"size": 11}, loc=legend_location)
|
|
345
533
|
|
|
346
534
|
|
|
347
|
-
def finalize_plot(
|
|
535
|
+
def finalize_plot(
|
|
536
|
+
ax,
|
|
537
|
+
patches,
|
|
538
|
+
x_title,
|
|
539
|
+
y_title,
|
|
540
|
+
x_lim=None,
|
|
541
|
+
y_lim=None,
|
|
542
|
+
highlighted_patches=None,
|
|
543
|
+
):
|
|
348
544
|
"""Finalize plot appearance and limits."""
|
|
349
545
|
ax.add_collection(PatchCollection(patches, match_original=True))
|
|
350
546
|
|
|
@@ -354,7 +550,7 @@ def finalize_plot(ax, patches, x_title, y_title, axes_range, highlighted_patches
|
|
|
354
550
|
ax.set(xlabel=x_title, ylabel=y_title)
|
|
355
551
|
ax.tick_params(labelsize=8)
|
|
356
552
|
ax.axis("square")
|
|
357
|
-
if
|
|
358
|
-
ax.set_xlim(
|
|
359
|
-
ax.set_ylim(
|
|
553
|
+
if x_lim is not None and y_lim is not None:
|
|
554
|
+
ax.set_xlim(*x_lim)
|
|
555
|
+
ax.set_ylim(*y_lim)
|
|
360
556
|
plt.tight_layout()
|