gammasimtools 0.24.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.
Files changed (138) hide show
  1. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/METADATA +2 -1
  2. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/RECORD +134 -130
  3. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/entry_points.txt +3 -1
  4. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/licenses/LICENSE +1 -1
  5. simtools/_version.py +2 -2
  6. simtools/application_control.py +78 -0
  7. simtools/applications/calculate_incident_angles.py +0 -2
  8. simtools/applications/convert_geo_coordinates_of_array_elements.py +1 -2
  9. simtools/applications/db_add_file_to_db.py +1 -1
  10. simtools/applications/db_add_simulation_model_from_repository_to_db.py +1 -1
  11. simtools/applications/db_add_value_from_json_to_db.py +1 -1
  12. simtools/applications/db_generate_compound_indexes.py +1 -1
  13. simtools/applications/db_get_array_layouts_from_db.py +2 -6
  14. simtools/applications/db_get_file_from_db.py +1 -1
  15. simtools/applications/db_get_parameter_from_db.py +1 -1
  16. simtools/applications/db_inspect_databases.py +1 -1
  17. simtools/applications/db_upload_model_repository.py +1 -1
  18. simtools/applications/derive_ctao_array_layouts.py +1 -2
  19. simtools/applications/derive_mirror_rnda.py +1 -3
  20. simtools/applications/derive_psf_parameters.py +5 -1
  21. simtools/applications/derive_pulse_shape_parameters.py +194 -0
  22. simtools/applications/derive_trigger_rates.py +1 -1
  23. simtools/applications/docs_produce_array_element_report.py +2 -8
  24. simtools/applications/docs_produce_calibration_reports.py +1 -3
  25. simtools/applications/docs_produce_model_parameter_reports.py +0 -2
  26. simtools/applications/docs_produce_simulation_configuration_report.py +1 -3
  27. simtools/applications/generate_array_config.py +0 -1
  28. simtools/applications/generate_corsika_histograms.py +48 -235
  29. simtools/applications/generate_regular_arrays.py +5 -35
  30. simtools/applications/generate_simtel_event_data.py +2 -2
  31. simtools/applications/maintain_simulation_model_add_production.py +2 -2
  32. simtools/applications/maintain_simulation_model_write_array_element_positions.py +87 -0
  33. simtools/applications/plot_array_layout.py +64 -108
  34. simtools/applications/plot_simulated_event_distributions.py +57 -0
  35. simtools/applications/plot_tabular_data.py +0 -1
  36. simtools/applications/plot_tabular_data_for_model_parameter.py +1 -6
  37. simtools/applications/production_derive_corsika_limits.py +1 -1
  38. simtools/applications/production_generate_grid.py +0 -1
  39. simtools/applications/run_application.py +1 -1
  40. simtools/applications/simulate_flasher.py +3 -4
  41. simtools/applications/simulate_illuminator.py +0 -1
  42. simtools/applications/simulate_pedestals.py +2 -6
  43. simtools/applications/simulate_prod.py +9 -28
  44. simtools/applications/simulate_prod_htcondor_generator.py +8 -1
  45. simtools/applications/submit_array_layouts.py +7 -7
  46. simtools/applications/submit_model_parameter_from_external.py +1 -3
  47. simtools/applications/validate_camera_efficiency.py +0 -1
  48. simtools/applications/validate_camera_fov.py +0 -1
  49. simtools/applications/validate_cumulative_psf.py +0 -2
  50. simtools/applications/validate_file_using_schema.py +49 -123
  51. simtools/applications/validate_optics.py +0 -13
  52. simtools/camera/camera_efficiency.py +1 -6
  53. simtools/camera/single_photon_electron_spectrum.py +2 -1
  54. simtools/configuration/commandline_parser.py +43 -8
  55. simtools/configuration/configurator.py +6 -11
  56. simtools/corsika/corsika_config.py +204 -99
  57. simtools/corsika/corsika_histograms.py +411 -1735
  58. simtools/corsika/primary_particle.py +1 -1
  59. simtools/data_model/metadata_collector.py +5 -2
  60. simtools/data_model/metadata_model.py +0 -4
  61. simtools/data_model/model_data_writer.py +27 -17
  62. simtools/data_model/schema.py +112 -5
  63. simtools/data_model/validate_data.py +80 -48
  64. simtools/db/db_handler.py +19 -8
  65. simtools/db/db_model_upload.py +2 -1
  66. simtools/db/mongo_db.py +133 -42
  67. simtools/dependencies.py +83 -44
  68. simtools/io/ascii_handler.py +4 -2
  69. simtools/io/table_handler.py +1 -1
  70. simtools/job_execution/htcondor_script_generator.py +0 -2
  71. simtools/layout/array_layout.py +4 -12
  72. simtools/layout/array_layout_utils.py +227 -58
  73. simtools/model/array_model.py +37 -18
  74. simtools/model/calibration_model.py +0 -4
  75. simtools/model/legacy_model_parameter.py +134 -0
  76. simtools/model/model_parameter.py +24 -14
  77. simtools/model/model_repository.py +18 -5
  78. simtools/model/model_utils.py +1 -6
  79. simtools/model/site_model.py +0 -4
  80. simtools/model/telescope_model.py +6 -11
  81. simtools/production_configuration/derive_corsika_limits.py +6 -11
  82. simtools/production_configuration/interpolation_handler.py +16 -16
  83. simtools/ray_tracing/incident_angles.py +5 -11
  84. simtools/ray_tracing/mirror_panel_psf.py +3 -7
  85. simtools/ray_tracing/psf_analysis.py +29 -27
  86. simtools/ray_tracing/psf_parameter_optimisation.py +822 -680
  87. simtools/ray_tracing/ray_tracing.py +6 -15
  88. simtools/reporting/docs_auto_report_generator.py +8 -13
  89. simtools/reporting/docs_read_parameters.py +70 -16
  90. simtools/runners/corsika_runner.py +15 -10
  91. simtools/runners/corsika_simtel_runner.py +9 -8
  92. simtools/runners/runner_services.py +17 -7
  93. simtools/runners/simtel_runner.py +11 -58
  94. simtools/runners/simtools_runner.py +2 -4
  95. simtools/schemas/model_parameters/flasher_pulse_exp_decay.schema.yml +2 -0
  96. simtools/schemas/model_parameters/flasher_pulse_shape.schema.yml +50 -0
  97. simtools/schemas/model_parameters/flasher_pulse_width.schema.yml +2 -0
  98. simtools/schemas/simulation_models_info.schema.yml +2 -0
  99. simtools/settings.py +154 -0
  100. simtools/sim_events/file_info.py +128 -0
  101. simtools/{simtel/simtel_io_event_histograms.py → sim_events/histograms.py} +25 -15
  102. simtools/{simtel/simtel_io_event_reader.py → sim_events/reader.py} +20 -17
  103. simtools/{simtel/simtel_io_event_writer.py → sim_events/writer.py} +84 -25
  104. simtools/simtel/pulse_shapes.py +273 -0
  105. simtools/simtel/simtel_config_writer.py +146 -22
  106. simtools/simtel/simtel_table_reader.py +6 -4
  107. simtools/simtel/simulator_array.py +62 -23
  108. simtools/simtel/simulator_camera_efficiency.py +4 -6
  109. simtools/simtel/simulator_light_emission.py +101 -19
  110. simtools/simtel/simulator_ray_tracing.py +4 -10
  111. simtools/simulator.py +360 -353
  112. simtools/telescope_trigger_rates.py +3 -4
  113. simtools/testing/assertions.py +115 -8
  114. simtools/testing/configuration.py +2 -3
  115. simtools/testing/helpers.py +2 -3
  116. simtools/testing/log_inspector.py +5 -1
  117. simtools/testing/sim_telarray_metadata.py +1 -1
  118. simtools/testing/validate_output.py +69 -23
  119. simtools/utils/general.py +37 -0
  120. simtools/utils/geometry.py +0 -77
  121. simtools/utils/names.py +7 -9
  122. simtools/version.py +37 -0
  123. simtools/visualization/legend_handlers.py +21 -10
  124. simtools/visualization/plot_array_layout.py +312 -41
  125. simtools/visualization/plot_corsika_histograms.py +143 -605
  126. simtools/visualization/plot_mirrors.py +834 -0
  127. simtools/visualization/plot_pixels.py +2 -4
  128. simtools/visualization/plot_psf.py +0 -1
  129. simtools/visualization/plot_simtel_event_histograms.py +4 -4
  130. simtools/visualization/plot_simtel_events.py +6 -11
  131. simtools/visualization/plot_tables.py +8 -19
  132. simtools/visualization/visualize.py +22 -2
  133. simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +0 -160
  134. simtools/applications/print_version.py +0 -53
  135. simtools/io/hdf5_handler.py +0 -139
  136. simtools/simtel/simtel_io_file_info.py +0 -62
  137. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/WHEEL +0 -0
  138. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,29 @@
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
- Define properties of different telescope types for visualization purposes.
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
- "MST": {"color": "dodgerblue", "radius": 9.15, "shape": "circle", "filled": True},
15
- "SCT": {"color": "black", "radius": 7.15, "shape": "square", "filled": True},
16
- "SST": {"color": "darkgreen", "radius": 3.0, "shape": "circle", "filled": True},
13
+ "MST": {"color": "dodgerblue", "radius": 9.15, "shape": "circle", "filled": False},
14
+ "SCT": {"color": "black", "radius": 7.15, "shape": "square", "filled": False},
15
+ "SST": {"color": "darkgreen", "radius": 3.0, "shape": "circle", "filled": False},
17
16
  "HESS": {"color": "grey", "radius": 6.0, "shape": "hexagon", "filled": True},
18
17
  "MAGIC": {"color": "grey", "radius": 8.5, "shape": "hexagon", "filled": True},
19
18
  "VERITAS": {"color": "grey", "radius": 6.0, "shape": "hexagon", "filled": True},
19
+ "CEI": {"color": "purple", "radius": 2.0, "shape": "hexagon", "filled": True},
20
+ "RLD": {"color": "brown", "radius": 2.0, "shape": "hexagon", "filled": True},
21
+ "STP": {"color": "olive", "radius": 2.0, "shape": "hexagon", "filled": True},
22
+ "MSP": {"color": "teal", "radius": 2.0, "shape": "hexagon", "filled": True},
23
+ "ILL": {"color": "red", "radius": 2.0, "shape": "hexagon", "filled": False},
24
+ "WST": {"color": "maroon", "radius": 2.0, "shape": "hexagon", "filled": True},
25
+ "ASC": {"color": "cyan", "radius": 2.0, "shape": "hexagon", "filled": True},
26
+ "DUS": {"color": "magenta", "radius": 2.0, "shape": "hexagon", "filled": True},
20
27
  }
21
28
 
22
29
  REFERENCE_RADIUS = 12.5
@@ -30,7 +37,7 @@ def get_telescope_config(telescope_type):
30
37
 
31
38
  Parameters
32
39
  ----------
33
- telescope_type : str
40
+ telescope_type : str, None
34
41
  The type of the telescope (e.g., "LSTN", "MSTS").
35
42
 
36
43
  Returns
@@ -38,10 +45,12 @@ def get_telescope_config(telescope_type):
38
45
  dict
39
46
  The configuration dictionary for the telescope type.
40
47
  """
48
+ if telescope_type is None:
49
+ return {"color": "blue", "radius": 2.0, "shape": "hexagon", "filled": True}
41
50
  config = TELESCOPE_CONFIG.get(telescope_type)
42
51
  if not config and len(telescope_type) >= 3:
43
52
  config = TELESCOPE_CONFIG.get(telescope_type[:3])
44
- return config
53
+ return config.copy() if config else None
45
54
 
46
55
 
47
56
  def calculate_center(handlebox, width_factor=3, height_factor=3):
@@ -262,6 +271,8 @@ class BaseLegendHandler:
262
271
  x0, y0 = calculate_center(handlebox)
263
272
  radius = handlebox.height
264
273
  patch = self._create_hexagon(handlebox, x0, y0, radius)
274
+ else:
275
+ raise ValueError(f"Unknown shape: {shape}")
265
276
 
266
277
  handlebox.add_artist(patch)
267
278
  return patch
@@ -2,16 +2,91 @@
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
8
+ import matplotlib as mpl
7
9
  import matplotlib.patches as mpatches
8
10
  import matplotlib.pyplot as plt
11
+ import numpy as np
12
+ from adjustText import adjust_text
9
13
  from astropy.table import Column
10
14
  from matplotlib.collections import PatchCollection
11
15
 
12
16
  from simtools.utils import geometry as transf
13
17
  from simtools.utils import names
14
18
  from simtools.visualization import legend_handlers as leg_h
19
+ from simtools.visualization import visualize
20
+
21
+
22
+ class PlotBounds(NamedTuple):
23
+ """Axis-aligned bounds for the layout in meters.
24
+
25
+ Attributes
26
+ ----------
27
+ x_lim : tuple[float, float]
28
+ Min/max for x (meters).
29
+ y_lim : tuple[float, float]
30
+ Min/max for y (meters).
31
+ """
32
+
33
+ x_lim: tuple[float, float]
34
+ y_lim: tuple[float, float]
35
+
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()
15
90
 
16
91
 
17
92
  def plot_array_layout(
@@ -23,6 +98,10 @@ def plot_array_layout(
23
98
  grayed_out_elements=None,
24
99
  highlighted_elements=None,
25
100
  legend_location="best",
101
+ bounds_mode="exact",
102
+ padding=0.1,
103
+ x_lim=None,
104
+ y_lim=None,
26
105
  ):
27
106
  """
28
107
  Plot telescope array layout.
@@ -50,10 +129,25 @@ def plot_array_layout(
50
129
  -------
51
130
  fig : Figure
52
131
  Matplotlib figure object.
132
+
133
+ Other Parameters
134
+ ----------------
135
+ bounds_mode : {"symmetric", "exact"}
136
+ Controls axis limits calculation. "symmetric" uses +-R where R is the padded
137
+ maximum extent (default), while "exact" uses individual x/y min/max bounds.
138
+ padding : float
139
+ Fractional padding applied around computed extents (used for both modes).
140
+ x_lim, y_lim : tuple(float, float), optional
141
+ Explicit axis limits in meters. If provided, these override axes_range and bounds_mode
142
+ for the respective axis. If only one is provided, the other axis is derived per mode.
53
143
  """
54
144
  fig, ax = plt.subplots(1)
55
145
 
56
- patches, plot_range, highlighted_patches = get_patches(
146
+ # If explicit limits are provided (one or both), filter patches accordingly
147
+ filter_x = x_lim
148
+ filter_y = y_lim
149
+
150
+ patches, plot_range, highlighted_patches, bounds, text_objects = get_patches(
57
151
  ax,
58
152
  telescopes,
59
153
  show_tel_label,
@@ -61,20 +155,145 @@ def plot_array_layout(
61
155
  marker_scaling,
62
156
  grayed_out_elements,
63
157
  highlighted_elements,
158
+ filter_x_lim=filter_x,
159
+ filter_y_lim=filter_y,
160
+ )
161
+
162
+ plot_range, bounds = _get_patches_for_background_telescopes(
163
+ ax,
164
+ background_telescopes,
165
+ axes_range,
166
+ marker_scaling,
167
+ bounds_mode,
168
+ plot_range,
169
+ bounds,
170
+ filter_x_lim=filter_x,
171
+ filter_y_lim=filter_y,
172
+ )
173
+
174
+ if legend_location != "no_legend":
175
+ update_legend(ax, telescopes, grayed_out_elements, legend_location)
176
+
177
+ x_lim, y_lim = _get_axis_limits(
178
+ axes_range, bounds_mode, padding, plot_range, bounds, x_lim, y_lim
64
179
  )
65
180
 
66
- if background_telescopes is not None:
67
- bg_patches, bg_range, _ = get_patches(
68
- ax, background_telescopes, False, axes_range, marker_scaling
181
+ finalize_plot(ax, patches, "Easting [m]", "Northing [m]", x_lim, y_lim, highlighted_patches)
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,
69
192
  )
70
- ax.add_collection(PatchCollection(bg_patches, match_original=True, alpha=0.1))
71
- if axes_range is None:
193
+
194
+ return fig
195
+
196
+
197
+ def _get_axis_limits(
198
+ axes_range,
199
+ bounds_mode,
200
+ padding,
201
+ plot_range,
202
+ bounds,
203
+ x_lim_override=None,
204
+ y_lim_override=None,
205
+ ):
206
+ """Get axis limits based on mode and padding."""
207
+
208
+ def _derive_axis(axis: str) -> tuple[float, float]:
209
+ if bounds_mode == "exact":
210
+ if axis == "x":
211
+ span = bounds.x_lim[1] - bounds.x_lim[0]
212
+ pad = padding * span
213
+ return (bounds.x_lim[0] - pad, bounds.x_lim[1] + pad)
214
+ span = bounds.y_lim[1] - bounds.y_lim[0]
215
+ pad = padding * span
216
+ return (bounds.y_lim[0] - pad, bounds.y_lim[1] + pad)
217
+ # symmetric
218
+ sym = plot_range
219
+ padf = max(0.0, min(1.0, float(padding))) if padding is not None else 0.0
220
+ sym *= 1.0 + padf
221
+ return (-sym, sym)
222
+
223
+ # Highest priority: explicit overrides (per axis)
224
+ if x_lim_override is not None or y_lim_override is not None:
225
+ x_lim = x_lim_override if x_lim_override is not None else _derive_axis("x")
226
+ y_lim = y_lim_override if y_lim_override is not None else _derive_axis("y")
227
+ return x_lim, y_lim
228
+
229
+ if axes_range is not None:
230
+ return (-axes_range, axes_range), (-axes_range, axes_range)
231
+ # Derive both axes using selected mode
232
+ return _derive_axis("x"), _derive_axis("y")
233
+
234
+
235
+ def _get_patches_for_background_telescopes(
236
+ ax,
237
+ background_telescopes,
238
+ axes_range,
239
+ marker_scaling,
240
+ bounds_mode,
241
+ plot_range,
242
+ bounds,
243
+ filter_x_lim=None,
244
+ filter_y_lim=None,
245
+ ):
246
+ """Get background telescope patches and update plot range/bounds."""
247
+ if background_telescopes is None:
248
+ return plot_range, bounds
249
+
250
+ bg_patches, bg_range, _, bg_bounds, _ = get_patches(
251
+ ax,
252
+ background_telescopes,
253
+ False,
254
+ axes_range,
255
+ marker_scaling,
256
+ None,
257
+ None,
258
+ filter_x_lim=filter_x_lim,
259
+ filter_y_lim=filter_y_lim,
260
+ )
261
+ ax.add_collection(PatchCollection(bg_patches, match_original=True, alpha=0.1))
262
+ if axes_range is None:
263
+ if bounds_mode == "symmetric":
72
264
  plot_range = max(plot_range, bg_range)
265
+ else:
266
+ bounds = PlotBounds(
267
+ x_lim=(
268
+ min(bounds.x_lim[0], bg_bounds.x_lim[0]),
269
+ max(bounds.x_lim[1], bg_bounds.x_lim[1]),
270
+ ),
271
+ y_lim=(
272
+ min(bounds.y_lim[0], bg_bounds.y_lim[0]),
273
+ max(bounds.y_lim[1], bg_bounds.y_lim[1]),
274
+ ),
275
+ )
276
+ return plot_range, bounds
73
277
 
74
- update_legend(ax, telescopes, grayed_out_elements, legend_location)
75
- finalize_plot(ax, patches, "Easting [m]", "Northing [m]", plot_range, highlighted_patches)
76
278
 
77
- return fig
279
+ def _apply_limits_filter(telescopes, pos_x, pos_y, filter_x_lim, filter_y_lim):
280
+ """Filter telescope table and positions by optional axis limits."""
281
+ if filter_x_lim is None and filter_y_lim is None:
282
+ return telescopes, pos_x, pos_y
283
+
284
+ px = np.asarray(pos_x.to_value(u.m))
285
+ py = np.asarray(pos_y.to_value(u.m))
286
+ mask = np.ones(px.shape, dtype=bool)
287
+ if filter_x_lim is not None:
288
+ mask &= (px >= float(filter_x_lim[0])) & (px <= float(filter_x_lim[1]))
289
+ if filter_y_lim is not None:
290
+ mask &= (py >= float(filter_y_lim[0])) & (py <= float(filter_y_lim[1]))
291
+
292
+ if mask.size and mask.any():
293
+ return telescopes[mask], pos_x[mask], pos_y[mask]
294
+
295
+ # No telescopes within limits
296
+ return telescopes[:0], pos_x[:0], pos_y[:0]
78
297
 
79
298
 
80
299
  def get_patches(
@@ -85,6 +304,8 @@ def get_patches(
85
304
  marker_scaling,
86
305
  grayed_out_elements=None,
87
306
  highlighted_elements=None,
307
+ filter_x_lim=None,
308
+ filter_y_lim=None,
88
309
  ):
89
310
  """
90
311
  Get plot patches and axis range.
@@ -111,38 +332,63 @@ def get_patches(
111
332
  patches : list
112
333
  List of telescope patches.
113
334
  axes_range : float
114
- Calculated or input axis range.
335
+ Calculated or input symmetric axis range (meters).
115
336
  highlighted_patches : list
116
337
  List of highlighted telescope patches.
338
+ bounds : PlotBounds
339
+ Min/max for x and y in meters.
340
+ text_objects : list
341
+ List of text objects for labels.
117
342
  """
118
343
  pos_x, pos_y = get_positions(telescopes)
119
- telescopes["pos_x_rotated"] = Column(pos_x)
120
- telescopes["pos_y_rotated"] = Column(pos_y)
344
+ tel_table, pos_x, pos_y = _apply_limits_filter(
345
+ telescopes, pos_x, pos_y, filter_x_lim, filter_y_lim
346
+ )
347
+
348
+ tel_table["pos_x_rotated"] = Column(pos_x)
349
+ tel_table["pos_y_rotated"] = Column(pos_y)
121
350
 
122
- patches, radii, highlighted_patches = create_patches(
123
- telescopes, marker_scaling, show_tel_label, ax, grayed_out_elements, highlighted_elements
351
+ patches, radii, highlighted_patches, text_objects = create_patches(
352
+ tel_table, marker_scaling, show_tel_label, ax, grayed_out_elements, highlighted_elements
124
353
  )
125
354
 
355
+ if len(radii) == 0:
356
+ r = 0.0
357
+ else:
358
+ radii_q = u.Quantity(radii)
359
+ r = float(np.nanmax(radii_q).to_value(u.m))
360
+
361
+ if len(pos_x) == 0:
362
+ bounds = PlotBounds(x_lim=(0.0, 0.0), y_lim=(0.0, 0.0))
363
+ if axes_range:
364
+ return patches, axes_range, highlighted_patches, bounds, text_objects
365
+ return patches, 0.0, highlighted_patches, bounds, text_objects
366
+
367
+ x_min = float(np.nanmin(pos_x).to_value(u.m)) - r
368
+ x_max = float(np.nanmax(pos_x).to_value(u.m)) + r
369
+ y_min = float(np.nanmin(pos_y).to_value(u.m)) - r
370
+ y_max = float(np.nanmax(pos_y).to_value(u.m)) + r
371
+ bounds = PlotBounds(x_lim=(x_min, x_max), y_lim=(y_min, y_max))
372
+
126
373
  if axes_range:
127
- return patches, axes_range, highlighted_patches
374
+ return patches, axes_range, highlighted_patches, bounds, text_objects
128
375
 
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
376
+ max_x = max(abs(x_min), abs(x_max))
377
+ max_y = max(abs(y_min), abs(y_max))
132
378
  updated_axes_range = max(max_x, max_y) * 1.1
133
379
 
134
- return patches, updated_axes_range, highlighted_patches
380
+ return patches, updated_axes_range, highlighted_patches, bounds, text_objects
135
381
 
136
382
 
137
383
  @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):
384
+ def get_telescope_patch(tel_type, x, y, radius, is_grayed_out=False):
139
385
  """
140
386
  Create patch for a telescope.
141
387
 
142
388
  Parameters
143
389
  ----------
144
- name : str
145
- Telescope name.
390
+ tel_type: str
391
+ Telescope type.
146
392
  x : Quantity
147
393
  X position.
148
394
  y : Quantity
@@ -157,24 +403,34 @@ def get_telescope_patch(name, x, y, radius, is_grayed_out=False):
157
403
  patch : Patch
158
404
  Circle or rectangle patch.
159
405
  """
160
- tel_type = names.get_array_element_type_from_name(name)
406
+ config = leg_h.get_telescope_config(tel_type)
161
407
  x, y, r = x.to(u.m), y.to(u.m), radius.to(u.m)
162
408
 
163
- color = "gray" if is_grayed_out else leg_h.get_telescope_config(tel_type)["color"]
409
+ color = "gray" if is_grayed_out else config["color"]
410
+ fill_flag = True if is_grayed_out else bool(config.get("filled", True))
164
411
 
165
- if tel_type == "SCTS":
412
+ if config.get("shape", "circle") == "square":
166
413
  return mpatches.Rectangle(
167
414
  ((x - r / 2).value, (y - r / 2).value),
168
415
  width=r.value,
169
416
  height=r.value,
170
- fill=is_grayed_out,
417
+ fill=fill_flag,
418
+ color=color,
419
+ )
420
+ if config.get("shape") == "hexagon":
421
+ return mpatches.RegularPolygon(
422
+ (x.value, y.value),
423
+ numVertices=6,
424
+ radius=r.value * np.sqrt(3) / 2,
425
+ orientation=np.pi / 6,
426
+ fill=fill_flag,
171
427
  color=color,
172
428
  )
173
429
 
174
430
  return mpatches.Circle(
175
431
  (x.value, y.value),
176
432
  radius=r.value,
177
- fill=is_grayed_out or tel_type.startswith("MST"),
433
+ fill=fill_flag,
178
434
  alpha=0.5 if is_grayed_out else 1.0,
179
435
  color=color,
180
436
  )
@@ -232,8 +488,10 @@ def create_patches(
232
488
  Telescope radii.
233
489
  highlighted_patches : list
234
490
  List of highlighted telescope patches.
491
+ text_objects : list
492
+ List of text objects for labels.
235
493
  """
236
- patches, radii, highlighted_patches = [], [], []
494
+ patches, radii, highlighted_patches, text_objects = [], [], [], []
237
495
  fontsize, scale_factor = (4, 2) if len(telescopes) > 30 else (8, 1)
238
496
 
239
497
  grayed_out_set = set(grayed_out_elements) if grayed_out_elements else set()
@@ -243,7 +501,10 @@ def create_patches(
243
501
  name = get_telescope_name(tel)
244
502
  radius = get_sphere_radius(tel)
245
503
  radii.append(radius)
246
- tel_type = names.get_array_element_type_from_name(name)
504
+ try:
505
+ tel_type = names.get_array_element_type_from_name(name)
506
+ except ValueError:
507
+ tel_type = None
247
508
 
248
509
  is_grayed_out = name in grayed_out_set
249
510
  is_highlighted = name in highlighted_set
@@ -269,16 +530,18 @@ def create_patches(
269
530
  highlighted_patches.append(highlight_patch)
270
531
 
271
532
  if show_label:
272
- ax.text(
273
- tel["pos_x_rotated"].value,
274
- tel["pos_y_rotated"].value + scale_factor * radius.value,
275
- name,
276
- ha="center",
277
- va="bottom",
278
- fontsize=fontsize,
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
+ )
279
542
  )
280
543
 
281
- return patches, radii, highlighted_patches
544
+ return patches, radii, highlighted_patches, text_objects
282
545
 
283
546
 
284
547
  def get_telescope_name(tel):
@@ -344,7 +607,15 @@ def update_legend(ax, telescopes, grayed_out_elements=None, legend_location="bes
344
607
  ax.legend(objs, labels, handler_map=handler_map, prop={"size": 11}, loc=legend_location)
345
608
 
346
609
 
347
- def finalize_plot(ax, patches, x_title, y_title, axes_range, highlighted_patches=None):
610
+ def finalize_plot(
611
+ ax,
612
+ patches,
613
+ x_title,
614
+ y_title,
615
+ x_lim=None,
616
+ y_lim=None,
617
+ highlighted_patches=None,
618
+ ):
348
619
  """Finalize plot appearance and limits."""
349
620
  ax.add_collection(PatchCollection(patches, match_original=True))
350
621
 
@@ -354,7 +625,7 @@ def finalize_plot(ax, patches, x_title, y_title, axes_range, highlighted_patches
354
625
  ax.set(xlabel=x_title, ylabel=y_title)
355
626
  ax.tick_params(labelsize=8)
356
627
  ax.axis("square")
357
- if axes_range:
358
- ax.set_xlim(-axes_range, axes_range)
359
- ax.set_ylim(-axes_range, axes_range)
628
+ if x_lim is not None and y_lim is not None:
629
+ ax.set_xlim(*x_lim)
630
+ ax.set_ylim(*y_lim)
360
631
  plt.tight_layout()