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.
Files changed (59) hide show
  1. {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/METADATA +1 -1
  2. {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/RECORD +58 -55
  3. {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/entry_points.txt +1 -0
  4. simtools/_version.py +2 -2
  5. simtools/application_control.py +50 -0
  6. simtools/applications/derive_psf_parameters.py +5 -0
  7. simtools/applications/derive_pulse_shape_parameters.py +195 -0
  8. simtools/applications/plot_array_layout.py +63 -1
  9. simtools/applications/simulate_flasher.py +3 -2
  10. simtools/applications/simulate_pedestals.py +1 -1
  11. simtools/applications/simulate_prod.py +8 -23
  12. simtools/applications/simulate_prod_htcondor_generator.py +7 -0
  13. simtools/applications/submit_array_layouts.py +5 -3
  14. simtools/applications/validate_file_using_schema.py +49 -123
  15. simtools/configuration/commandline_parser.py +8 -6
  16. simtools/corsika/corsika_config.py +197 -87
  17. simtools/data_model/model_data_writer.py +14 -2
  18. simtools/data_model/schema.py +112 -5
  19. simtools/data_model/validate_data.py +82 -48
  20. simtools/db/db_model_upload.py +2 -1
  21. simtools/db/mongo_db.py +133 -42
  22. simtools/dependencies.py +5 -9
  23. simtools/io/eventio_handler.py +128 -0
  24. simtools/job_execution/htcondor_script_generator.py +0 -2
  25. simtools/layout/array_layout_utils.py +1 -1
  26. simtools/model/array_model.py +36 -5
  27. simtools/model/model_parameter.py +0 -1
  28. simtools/model/model_repository.py +18 -5
  29. simtools/ray_tracing/psf_analysis.py +11 -8
  30. simtools/ray_tracing/psf_parameter_optimisation.py +822 -679
  31. simtools/reporting/docs_read_parameters.py +69 -9
  32. simtools/runners/corsika_runner.py +12 -3
  33. simtools/runners/corsika_simtel_runner.py +6 -0
  34. simtools/runners/runner_services.py +17 -7
  35. simtools/runners/simtel_runner.py +12 -54
  36. simtools/schemas/model_parameters/flasher_pulse_exp_decay.schema.yml +2 -0
  37. simtools/schemas/model_parameters/flasher_pulse_shape.schema.yml +50 -0
  38. simtools/schemas/model_parameters/flasher_pulse_width.schema.yml +2 -0
  39. simtools/schemas/simulation_models_info.schema.yml +2 -0
  40. simtools/simtel/pulse_shapes.py +268 -0
  41. simtools/simtel/simtel_config_writer.py +82 -1
  42. simtools/simtel/simtel_io_event_writer.py +2 -2
  43. simtools/simtel/simulator_array.py +58 -12
  44. simtools/simtel/simulator_light_emission.py +45 -8
  45. simtools/simulator.py +361 -347
  46. simtools/testing/assertions.py +62 -6
  47. simtools/testing/configuration.py +1 -1
  48. simtools/testing/log_inspector.py +4 -1
  49. simtools/testing/sim_telarray_metadata.py +1 -1
  50. simtools/testing/validate_output.py +44 -9
  51. simtools/utils/names.py +2 -4
  52. simtools/version.py +37 -0
  53. simtools/visualization/legend_handlers.py +14 -4
  54. simtools/visualization/plot_array_layout.py +229 -33
  55. simtools/visualization/plot_mirrors.py +837 -0
  56. simtools/simtel/simtel_io_file_info.py +0 -62
  57. {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/WHEEL +0 -0
  58. {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/licenses/LICENSE +0 -0
  59. {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
- patches, plot_range, highlighted_patches = get_patches(
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
- if background_telescopes is not None:
67
- bg_patches, bg_range, _ = get_patches(
68
- ax, background_telescopes, False, axes_range, marker_scaling
69
- )
70
- ax.add_collection(PatchCollection(bg_patches, match_original=True, alpha=0.1))
71
- if axes_range is None:
72
- plot_range = max(plot_range, bg_range)
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
- update_legend(ax, telescopes, grayed_out_elements, legend_location)
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
- telescopes["pos_x_rotated"] = Column(pos_x)
120
- telescopes["pos_y_rotated"] = Column(pos_y)
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
- telescopes, marker_scaling, show_tel_label, ax, grayed_out_elements, highlighted_elements
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
- r = max(radii).value
130
- max_x = max(abs(pos_x.min().value), abs(pos_x.max().value)) + r
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(name, x, y, radius, is_grayed_out=False):
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
- name : str
145
- Telescope name.
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
- tel_type = names.get_array_element_type_from_name(name)
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 leg_h.get_telescope_config(tel_type)["color"]
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 tel_type == "SCTS":
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=is_grayed_out,
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=is_grayed_out or tel_type.startswith("MST"),
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
- tel_type = names.get_array_element_type_from_name(name)
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(ax, patches, x_title, y_title, axes_range, highlighted_patches=None):
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 axes_range:
358
- ax.set_xlim(-axes_range, axes_range)
359
- ax.set_ylim(-axes_range, axes_range)
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()