gammasimtools 0.19.0__py3-none-any.whl → 0.21.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.19.0.dist-info → gammasimtools-0.21.0.dist-info}/METADATA +1 -3
- {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/RECORD +54 -51
- {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/entry_points.txt +3 -3
- simtools/_version.py +2 -2
- simtools/applications/calculate_incident_angles.py +182 -0
- simtools/applications/db_add_simulation_model_from_repository_to_db.py +17 -14
- simtools/applications/db_add_value_from_json_to_db.py +6 -9
- simtools/applications/db_generate_compound_indexes.py +7 -3
- simtools/applications/db_get_file_from_db.py +11 -23
- simtools/applications/derive_psf_parameters.py +58 -39
- simtools/applications/derive_trigger_rates.py +91 -0
- simtools/applications/generate_corsika_histograms.py +7 -184
- simtools/applications/maintain_simulation_model_add_production.py +105 -0
- simtools/applications/plot_simtel_events.py +5 -189
- simtools/applications/print_version.py +8 -7
- simtools/applications/validate_file_using_schema.py +7 -4
- simtools/configuration/commandline_parser.py +17 -11
- simtools/corsika/corsika_histograms.py +81 -0
- simtools/data_model/validate_data.py +8 -3
- simtools/db/db_handler.py +122 -31
- simtools/db/db_model_upload.py +51 -30
- simtools/dependencies.py +10 -5
- simtools/layout/array_layout_utils.py +37 -5
- simtools/model/array_model.py +18 -1
- simtools/model/model_repository.py +118 -63
- simtools/model/site_model.py +25 -0
- simtools/production_configuration/derive_corsika_limits.py +9 -34
- simtools/ray_tracing/incident_angles.py +706 -0
- simtools/ray_tracing/psf_parameter_optimisation.py +999 -565
- simtools/schemas/model_parameter_and_data_schema.metaschema.yml +2 -2
- simtools/schemas/model_parameters/nsb_reference_spectrum.schema.yml +1 -1
- simtools/schemas/model_parameters/nsb_spectrum.schema.yml +22 -29
- simtools/schemas/model_parameters/stars.schema.yml +1 -1
- simtools/schemas/production_tables.schema.yml +5 -0
- simtools/simtel/simtel_config_writer.py +18 -20
- simtools/simtel/simtel_io_event_histograms.py +253 -516
- simtools/simtel/simtel_io_event_reader.py +51 -2
- simtools/simtel/simtel_io_event_writer.py +31 -11
- simtools/simtel/simtel_io_metadata.py +1 -1
- simtools/simtel/simtel_table_reader.py +3 -3
- simtools/simulator.py +1 -4
- simtools/telescope_trigger_rates.py +119 -0
- simtools/testing/log_inspector.py +13 -11
- simtools/utils/geometry.py +20 -0
- simtools/version.py +89 -0
- simtools/{corsika/corsika_histograms_visualize.py → visualization/plot_corsika_histograms.py} +109 -0
- simtools/visualization/plot_incident_angles.py +431 -0
- simtools/visualization/plot_psf.py +673 -0
- simtools/visualization/plot_simtel_event_histograms.py +376 -0
- simtools/visualization/{simtel_event_plots.py → plot_simtel_events.py} +284 -87
- simtools/visualization/visualize.py +1 -3
- simtools/applications/calculate_trigger_rate.py +0 -187
- simtools/applications/generate_sim_telarray_histograms.py +0 -196
- simtools/applications/maintain_simulation_model_add_production_table.py +0 -71
- simtools/simtel/simtel_io_histogram.py +0 -623
- simtools/simtel/simtel_io_histograms.py +0 -556
- {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/WHEEL +0 -0
- {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/licenses/LICENSE +0 -0
- {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Plot simtel event histograms filled with SimtelIOEventHistograms."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import numpy as np
|
|
7
|
+
from matplotlib.colors import LogNorm
|
|
8
|
+
|
|
9
|
+
from simtools.simtel.simtel_io_event_histograms import SimtelIOEventHistograms
|
|
10
|
+
|
|
11
|
+
_logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def plot(histograms, output_path=None, limits=None, rebin_factor=2, array_name=None):
|
|
15
|
+
"""
|
|
16
|
+
Plot simtel event histograms.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
histograms: SimtelIOEventHistograms
|
|
21
|
+
Instance containing the histograms to plot.
|
|
22
|
+
output_path: Path or str, optional
|
|
23
|
+
Directory to save plots. If None, plots will be displayed.
|
|
24
|
+
limits: dict, optional
|
|
25
|
+
Dictionary containing limits for plotting. Keys can include:
|
|
26
|
+
- "upper_radius_limit": Upper limit for core distance
|
|
27
|
+
- "lower_energy_limit": Lower limit for energy
|
|
28
|
+
- "viewcone_radius": Radius for the viewcone
|
|
29
|
+
rebin_factor: int, optional
|
|
30
|
+
Factor by which to reduce the number of bins in 2D histograms for re-binned plots.
|
|
31
|
+
Default is 2 (merge every 2 bins). Set to 0 or 1 to disable re-binning.
|
|
32
|
+
array_name: str, optional
|
|
33
|
+
Name of the telescope array configuration.
|
|
34
|
+
"""
|
|
35
|
+
_logger.info(f"Plotting histograms written to {output_path}")
|
|
36
|
+
|
|
37
|
+
plots = _generate_plot_configurations(histograms, limits)
|
|
38
|
+
_execute_plotting_loop(plots, output_path, rebin_factor, array_name)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_limits(name, limits):
|
|
42
|
+
"""
|
|
43
|
+
Extract limits from the provided dictionary for plotting.
|
|
44
|
+
|
|
45
|
+
Fine tuned to expected histograms to be plotted.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def _safe_value(limits, key):
|
|
49
|
+
val = limits.get(key)
|
|
50
|
+
return getattr(val, "value", None)
|
|
51
|
+
|
|
52
|
+
mapping = {
|
|
53
|
+
"energy": {"x": _safe_value(limits, "lower_energy_limit")},
|
|
54
|
+
"core_distance": {"x": _safe_value(limits, "upper_radius_limit")},
|
|
55
|
+
"angular_distance": {"x": _safe_value(limits, "viewcone_radius")},
|
|
56
|
+
"core_vs_energy": {
|
|
57
|
+
"x": _safe_value(limits, "upper_radius_limit"),
|
|
58
|
+
"y": _safe_value(limits, "lower_energy_limit"),
|
|
59
|
+
},
|
|
60
|
+
"angular_distance_vs_energy": {
|
|
61
|
+
"x": _safe_value(limits, "viewcone_radius"),
|
|
62
|
+
"y": _safe_value(limits, "lower_energy_limit"),
|
|
63
|
+
},
|
|
64
|
+
"x_core_shower_vs_y_core_shower": {"r": _safe_value(limits, "upper_radius_limit")},
|
|
65
|
+
}
|
|
66
|
+
return mapping.get(name)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _generate_plot_configurations(histograms, limits):
|
|
70
|
+
"""Generate plot configurations for all histogram types."""
|
|
71
|
+
hist_1d_params = {"color": "tab:green", "edgecolor": "tab:green", "lw": 1}
|
|
72
|
+
hist_2d_params = {"norm": "log", "cmap": "viridis", "show_contour": False}
|
|
73
|
+
hist_2d_normalized_params = {"norm": "linear", "cmap": "viridis", "show_contour": True}
|
|
74
|
+
plots = {}
|
|
75
|
+
for name, hist in histograms.items():
|
|
76
|
+
if hist["histogram"] is None:
|
|
77
|
+
continue
|
|
78
|
+
if hist["1d"]:
|
|
79
|
+
plots[name] = _create_1d_plot_config(
|
|
80
|
+
hist, name=name, plot_params=hist_1d_params, limits=limits
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
if "cumulative" in name or "efficiency" in name:
|
|
84
|
+
plot_params = hist_2d_normalized_params
|
|
85
|
+
else:
|
|
86
|
+
plot_params = hist_2d_params
|
|
87
|
+
|
|
88
|
+
plots[name] = _create_2d_plot_config(
|
|
89
|
+
hist, name=name, plot_params=plot_params, limits=limits
|
|
90
|
+
)
|
|
91
|
+
return plots
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _get_axis_title(axis_titles, axis):
|
|
95
|
+
"""Return axis title for given axis."""
|
|
96
|
+
if axis_titles is None:
|
|
97
|
+
return None
|
|
98
|
+
if axis == "x" and len(axis_titles) > 0:
|
|
99
|
+
return axis_titles[0]
|
|
100
|
+
if axis == "y" and len(axis_titles) > 1:
|
|
101
|
+
return axis_titles[1]
|
|
102
|
+
if axis == "z" and len(axis_titles) > 2:
|
|
103
|
+
return axis_titles[2]
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _create_1d_plot_config(histogram, name, plot_params, limits):
|
|
108
|
+
"""Create a 1D plot configuration."""
|
|
109
|
+
_logger.debug(f"Creating plot config for {name} with params: {plot_params}")
|
|
110
|
+
return {
|
|
111
|
+
"data": histogram["histogram"],
|
|
112
|
+
"bins": histogram["bin_edges"],
|
|
113
|
+
"plot_type": "histogram",
|
|
114
|
+
"plot_params": plot_params,
|
|
115
|
+
"labels": {
|
|
116
|
+
"x": _get_axis_title(histogram.get("axis_titles"), "x"),
|
|
117
|
+
"y": _get_axis_title(histogram.get("axis_titles"), "y"),
|
|
118
|
+
"title": f"{histogram['title']}: {name.replace('_', ' ')}",
|
|
119
|
+
},
|
|
120
|
+
"scales": histogram["plot_scales"],
|
|
121
|
+
"lines": _get_limits(name, limits) if limits else {},
|
|
122
|
+
"filename": name,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _create_2d_plot_config(histogram, name, plot_params, limits):
|
|
127
|
+
"""Create a 2D plot configuration."""
|
|
128
|
+
_logger.debug(f"Creating plot config for {name} with params: {plot_params}")
|
|
129
|
+
return {
|
|
130
|
+
"data": histogram["histogram"],
|
|
131
|
+
"bins": [histogram["bin_edges"][0], histogram["bin_edges"][1]],
|
|
132
|
+
"plot_type": "histogram2d",
|
|
133
|
+
"plot_params": plot_params,
|
|
134
|
+
"labels": {
|
|
135
|
+
"x": _get_axis_title(histogram.get("axis_titles"), "x"),
|
|
136
|
+
"y": _get_axis_title(histogram.get("axis_titles"), "y"),
|
|
137
|
+
"title": f"{histogram['title']}: {name.replace('_', ' ')}",
|
|
138
|
+
},
|
|
139
|
+
"lines": _get_limits(name, limits) if limits else {},
|
|
140
|
+
"scales": histogram["plot_scales"],
|
|
141
|
+
"colorbar_label": _get_axis_title(histogram.get("axis_titles"), "z"),
|
|
142
|
+
"filename": name,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _execute_plotting_loop(plots, output_path, rebin_factor, array_name):
|
|
147
|
+
"""Execute the main plotting loop for all plot configurations."""
|
|
148
|
+
for plot_key, plot_args in plots.items():
|
|
149
|
+
plot_filename = plot_args.pop("filename")
|
|
150
|
+
|
|
151
|
+
if plot_args.get("data") is None:
|
|
152
|
+
_logger.warning(f"Skipping plot {plot_key} - no data available")
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
if array_name and plot_args.get("labels", {}).get("title"):
|
|
156
|
+
plot_args["labels"]["title"] += f" ({array_name} array)"
|
|
157
|
+
|
|
158
|
+
filename = _build_plot_filename(plot_filename, array_name)
|
|
159
|
+
output_file = output_path / filename if output_path else None
|
|
160
|
+
result = _create_plot(**plot_args, output_file=output_file)
|
|
161
|
+
|
|
162
|
+
# Skip re-binned plot if main plot failed
|
|
163
|
+
if result is None:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
if _should_create_rebinned_plot(rebin_factor, plot_args, plot_key):
|
|
167
|
+
_create_rebinned_plot(plot_args, filename, output_path, rebin_factor)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _build_plot_filename(base_filename, array_name=None):
|
|
171
|
+
"""
|
|
172
|
+
Build the full plot filename with appropriate extensions.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
base_filename : str
|
|
177
|
+
The base filename without extension
|
|
178
|
+
array_name : str, optional
|
|
179
|
+
Name of the array to append to filename
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
str
|
|
184
|
+
Complete filename with extension
|
|
185
|
+
"""
|
|
186
|
+
return f"{base_filename}_{array_name}.png" if array_name else f"{base_filename}.png"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _should_create_rebinned_plot(rebin_factor, plot_args, plot_key):
|
|
190
|
+
"""
|
|
191
|
+
Check if a re-binned version of the plot should be created.
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
rebin_factor : int
|
|
196
|
+
Factor by which to rebin the energy axis
|
|
197
|
+
plot_args : dict
|
|
198
|
+
Plot arguments
|
|
199
|
+
plot_key : str
|
|
200
|
+
Key identifying the plot type
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
bool
|
|
205
|
+
True if a re-binned plot should be created, False otherwise
|
|
206
|
+
"""
|
|
207
|
+
return (
|
|
208
|
+
rebin_factor > 1
|
|
209
|
+
and plot_args["plot_type"] == "histogram2d"
|
|
210
|
+
and plot_key.endswith("_cumulative")
|
|
211
|
+
and plot_args.get("plot_params", {}).get("norm") == "linear"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _create_rebinned_plot(plot_args, filename, output_path, rebin_factor):
|
|
216
|
+
"""
|
|
217
|
+
Create a re-binned version of a 2D histogram plot.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
plot_args : dict
|
|
222
|
+
Plot arguments for the original plot
|
|
223
|
+
filename : str
|
|
224
|
+
Filename of the original plot
|
|
225
|
+
output_path : Path or None
|
|
226
|
+
Path to save the plot to, or None
|
|
227
|
+
rebin_factor : int
|
|
228
|
+
Factor by which to rebin the energy axis
|
|
229
|
+
"""
|
|
230
|
+
data = plot_args["data"]
|
|
231
|
+
bins = plot_args["bins"]
|
|
232
|
+
|
|
233
|
+
rebinned_data, rebinned_x_bins, rebinned_y_bins = SimtelIOEventHistograms.rebin_2d_histogram(
|
|
234
|
+
data, bins[0], bins[1], rebin_factor
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
rebinned_plot_args = plot_args.copy()
|
|
238
|
+
rebinned_plot_args["data"] = rebinned_data
|
|
239
|
+
rebinned_plot_args["bins"] = [rebinned_x_bins, rebinned_y_bins]
|
|
240
|
+
|
|
241
|
+
if rebinned_plot_args.get("labels", {}).get("title"):
|
|
242
|
+
rebinned_plot_args["labels"]["title"] += f" (Energy rebinned {rebin_factor}x)"
|
|
243
|
+
|
|
244
|
+
rebinned_filename = f"{filename.replace('.png', '')}_rebinned.png"
|
|
245
|
+
rebinned_output_file = output_path / rebinned_filename if output_path else None
|
|
246
|
+
_create_plot(**rebinned_plot_args, output_file=rebinned_output_file)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _create_plot(
|
|
250
|
+
data,
|
|
251
|
+
bins=None,
|
|
252
|
+
plot_type="histogram",
|
|
253
|
+
plot_params=None,
|
|
254
|
+
labels=None,
|
|
255
|
+
scales=None,
|
|
256
|
+
colorbar_label=None,
|
|
257
|
+
output_file=None,
|
|
258
|
+
lines=None,
|
|
259
|
+
):
|
|
260
|
+
"""Create and save a plot with the given parameters."""
|
|
261
|
+
plot_params = plot_params or {}
|
|
262
|
+
labels = labels or {}
|
|
263
|
+
scales = scales or {}
|
|
264
|
+
lines = lines or {}
|
|
265
|
+
|
|
266
|
+
if not _has_data(data):
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
fig, ax = plt.subplots(figsize=(8, 6))
|
|
270
|
+
_plot_data(ax, data, bins, plot_type, plot_params, colorbar_label)
|
|
271
|
+
_add_lines(ax, lines)
|
|
272
|
+
ax.set(
|
|
273
|
+
xlabel=labels.get("x", ""),
|
|
274
|
+
ylabel=labels.get("y", ""),
|
|
275
|
+
title=labels.get("title", ""),
|
|
276
|
+
xscale=scales.get("x", "linear"),
|
|
277
|
+
yscale=scales.get("y", "linear"),
|
|
278
|
+
)
|
|
279
|
+
if output_file:
|
|
280
|
+
_logger.info(f"Saving plot to {output_file}")
|
|
281
|
+
fig.savefig(output_file, dpi=300, bbox_inches="tight")
|
|
282
|
+
plt.close(fig)
|
|
283
|
+
else:
|
|
284
|
+
plt.tight_layout()
|
|
285
|
+
plt.show()
|
|
286
|
+
|
|
287
|
+
return fig
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _has_data(data):
|
|
291
|
+
"""Check that the data for plotting is not None or empty."""
|
|
292
|
+
if data is None or (isinstance(data, np.ndarray) and data.size == 0):
|
|
293
|
+
_logger.warning("No data available for plotting")
|
|
294
|
+
return False
|
|
295
|
+
return True
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _plot_data(ax, data, bins, plot_type, plot_params, colorbar_label):
|
|
299
|
+
"""Plot the data on the given axes."""
|
|
300
|
+
if plot_type == "histogram":
|
|
301
|
+
ax.bar(bins[:-1], data, width=np.diff(bins), **plot_params)
|
|
302
|
+
elif plot_type == "histogram2d":
|
|
303
|
+
pcm = _create_2d_histogram_plot(data, bins, plot_params)
|
|
304
|
+
plt.colorbar(pcm, label=colorbar_label)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _add_lines(ax, lines):
|
|
308
|
+
"""Add reference lines to the plot."""
|
|
309
|
+
if lines.get("x") is not None:
|
|
310
|
+
ax.axvline(lines["x"], color="r", linestyle="--", linewidth=0.5)
|
|
311
|
+
if lines.get("y") is not None:
|
|
312
|
+
ax.axhline(lines["y"], color="r", linestyle="--", linewidth=0.5)
|
|
313
|
+
if lines.get("r") is not None:
|
|
314
|
+
ax.add_artist(
|
|
315
|
+
plt.Circle((0, 0), lines["r"], color="r", fill=False, linestyle="--", linewidth=0.5)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _create_2d_histogram_plot(data, bins, plot_params):
|
|
320
|
+
"""
|
|
321
|
+
Create a 2D histogram plot with the given parameters.
|
|
322
|
+
|
|
323
|
+
Parameters
|
|
324
|
+
----------
|
|
325
|
+
data : np.ndarray
|
|
326
|
+
2D histogram data
|
|
327
|
+
bins : tuple of np.ndarray
|
|
328
|
+
Bin edges for x and y axes
|
|
329
|
+
plot_params : dict
|
|
330
|
+
Plot parameters including norm, cmap, and show_contour
|
|
331
|
+
|
|
332
|
+
Returns
|
|
333
|
+
-------
|
|
334
|
+
matplotlib.collections.QuadMesh
|
|
335
|
+
The created pcolormesh object for colorbar attachment
|
|
336
|
+
"""
|
|
337
|
+
if plot_params.get("norm") == "linear":
|
|
338
|
+
pcm = plt.pcolormesh(
|
|
339
|
+
bins[0],
|
|
340
|
+
bins[1],
|
|
341
|
+
data.T,
|
|
342
|
+
vmin=0,
|
|
343
|
+
vmax=1,
|
|
344
|
+
cmap=plot_params.get("cmap", "viridis"),
|
|
345
|
+
)
|
|
346
|
+
# Add contour line at value=1.0 for normalized histograms
|
|
347
|
+
if plot_params.get("show_contour", True):
|
|
348
|
+
x_centers = (bins[0][1:] + bins[0][:-1]) / 2
|
|
349
|
+
y_centers = (bins[1][1:] + bins[1][:-1]) / 2
|
|
350
|
+
x_mesh, y_mesh = np.meshgrid(x_centers, y_centers)
|
|
351
|
+
plt.contour(
|
|
352
|
+
x_mesh,
|
|
353
|
+
y_mesh,
|
|
354
|
+
data.T,
|
|
355
|
+
levels=[0.999999], # very close to 1 for floating point precision
|
|
356
|
+
colors=["tab:red"],
|
|
357
|
+
linestyles=["--"],
|
|
358
|
+
linewidths=[0.5],
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
# Handle empty or invalid data for logarithmic scaling
|
|
362
|
+
data_max = data.max()
|
|
363
|
+
if data_max <= 0:
|
|
364
|
+
_logger.warning("No positive data found for logarithmic scaling, using linear scale")
|
|
365
|
+
pcm = plt.pcolormesh(
|
|
366
|
+
bins[0], bins[1], data.T, vmin=0, vmax=max(1, data_max), cmap="viridis"
|
|
367
|
+
)
|
|
368
|
+
else:
|
|
369
|
+
# Ensure vmin is less than vmax for LogNorm
|
|
370
|
+
vmin = max(1, data[data > 0].min()) if np.any(data > 0) else 1
|
|
371
|
+
vmax = max(vmin + 1, data_max)
|
|
372
|
+
pcm = plt.pcolormesh(
|
|
373
|
+
bins[0], bins[1], data.T, norm=LogNorm(vmin=vmin, vmax=vmax), cmap="viridis"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
return pcm
|