gammasimtools 0.23.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.23.0.dist-info → gammasimtools-0.25.0.dist-info}/METADATA +1 -1
- {gammasimtools-0.23.0.dist-info → gammasimtools-0.25.0.dist-info}/RECORD +89 -85
- {gammasimtools-0.23.0.dist-info → gammasimtools-0.25.0.dist-info}/entry_points.txt +1 -0
- simtools/_version.py +2 -2
- simtools/application_control.py +54 -4
- simtools/applications/convert_geo_coordinates_of_array_elements.py +1 -1
- simtools/applications/db_add_file_to_db.py +2 -2
- simtools/applications/db_add_simulation_model_from_repository_to_db.py +1 -1
- simtools/applications/db_add_value_from_json_to_db.py +2 -2
- simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +1 -1
- simtools/applications/db_generate_compound_indexes.py +1 -1
- simtools/applications/db_get_array_layouts_from_db.py +2 -2
- 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 +4 -2
- simtools/applications/db_upload_model_repository.py +1 -1
- simtools/applications/derive_ctao_array_layouts.py +1 -1
- simtools/applications/derive_psf_parameters.py +5 -0
- simtools/applications/derive_pulse_shape_parameters.py +195 -0
- simtools/applications/generate_array_config.py +1 -1
- simtools/applications/maintain_simulation_model_add_production.py +11 -21
- simtools/applications/plot_array_layout.py +63 -1
- simtools/applications/production_generate_grid.py +1 -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 +7 -5
- simtools/applications/validate_camera_fov.py +1 -1
- simtools/applications/validate_cumulative_psf.py +2 -2
- simtools/applications/validate_file_using_schema.py +49 -123
- simtools/applications/validate_optics.py +1 -1
- simtools/configuration/commandline_parser.py +15 -15
- simtools/configuration/configurator.py +1 -1
- simtools/corsika/corsika_config.py +199 -91
- simtools/data_model/model_data_writer.py +15 -3
- simtools/data_model/schema.py +145 -36
- simtools/data_model/validate_data.py +82 -48
- simtools/db/db_handler.py +61 -294
- simtools/db/db_model_upload.py +3 -2
- simtools/db/mongo_db.py +626 -0
- simtools/dependencies.py +38 -17
- simtools/io/eventio_handler.py +128 -0
- simtools/job_execution/htcondor_script_generator.py +0 -2
- simtools/layout/array_layout.py +7 -7
- simtools/layout/array_layout_utils.py +4 -4
- simtools/model/array_model.py +72 -72
- simtools/model/calibration_model.py +12 -9
- simtools/model/model_parameter.py +196 -160
- simtools/model/model_repository.py +176 -39
- simtools/model/model_utils.py +3 -3
- simtools/model/site_model.py +59 -27
- simtools/model/telescope_model.py +21 -13
- simtools/ray_tracing/mirror_panel_psf.py +4 -4
- simtools/ray_tracing/psf_analysis.py +11 -8
- simtools/ray_tracing/psf_parameter_optimisation.py +823 -680
- simtools/reporting/docs_auto_report_generator.py +1 -1
- simtools/reporting/docs_read_parameters.py +72 -11
- 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 +4 -1
- simtools/simtel/pulse_shapes.py +268 -0
- simtools/simtel/simtel_config_writer.py +179 -21
- 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 -346
- simtools/testing/assertions.py +110 -10
- 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 +46 -15
- simtools/utils/names.py +2 -4
- simtools/utils/value_conversion.py +10 -5
- simtools/version.py +61 -0
- simtools/visualization/legend_handlers.py +14 -4
- simtools/visualization/plot_array_layout.py +229 -33
- simtools/visualization/plot_mirrors.py +837 -0
- simtools/visualization/plot_pixels.py +1 -1
- simtools/visualization/plot_psf.py +1 -1
- simtools/visualization/plot_tables.py +1 -1
- simtools/simtel/simtel_io_file_info.py +0 -62
- {gammasimtools-0.23.0.dist-info → gammasimtools-0.25.0.dist-info}/WHEEL +0 -0
- {gammasimtools-0.23.0.dist-info → gammasimtools-0.25.0.dist-info}/licenses/LICENSE +0 -0
- {gammasimtools-0.23.0.dist-info → gammasimtools-0.25.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Pulse shape computations for light emission simulations for flasher."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy.optimize import least_squares
|
|
5
|
+
from scipy.signal import fftconvolve
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _rise_width(t, y, y_low=0.1, y_high=0.9):
|
|
9
|
+
"""Measure rise width between fractional amplitudes.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
t : array-like
|
|
14
|
+
Time samples in ns.
|
|
15
|
+
y : array-like
|
|
16
|
+
Pulse amplitude samples (normalized or arbitrary units).
|
|
17
|
+
y_low : float, optional
|
|
18
|
+
Lower fractional amplitude (0..1) on the rising edge. Default is 0.1.
|
|
19
|
+
y_high : float, optional
|
|
20
|
+
Upper fractional amplitude (0..1) on the rising edge. Default is 0.9.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
float
|
|
25
|
+
Width in ns between ``y_low`` and ``y_high`` on the rising edge.
|
|
26
|
+
"""
|
|
27
|
+
i_peak = int(np.argmax(y))
|
|
28
|
+
tr = t[: i_peak + 1]
|
|
29
|
+
yr = y[: i_peak + 1]
|
|
30
|
+
t_low = np.interp(y_low, yr, tr)
|
|
31
|
+
t_high = np.interp(y_high, yr, tr)
|
|
32
|
+
return t_high - t_low
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _fall_width(t, y, y_high=0.9, y_low=0.1):
|
|
36
|
+
"""Measure fall width between fractional amplitudes.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
t : array-like
|
|
41
|
+
Time samples in ns.
|
|
42
|
+
y : array-like
|
|
43
|
+
Pulse amplitude samples (normalized or arbitrary units).
|
|
44
|
+
y_high : float, optional
|
|
45
|
+
Higher fractional amplitude (0..1) on the falling edge. Default is 0.9.
|
|
46
|
+
y_low : float, optional
|
|
47
|
+
Lower fractional amplitude (0..1) on the falling edge. Default is 0.1.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
float
|
|
52
|
+
Width in ns between ``y_high`` and ``y_low`` on the falling edge.
|
|
53
|
+
"""
|
|
54
|
+
i_peak = int(np.argmax(y))
|
|
55
|
+
tf = t[i_peak:]
|
|
56
|
+
yf = y[i_peak:]
|
|
57
|
+
t_rev = tf[::-1]
|
|
58
|
+
y_rev = yf[::-1]
|
|
59
|
+
t_hi = np.interp(y_high, y_rev, t_rev)
|
|
60
|
+
t_lo = np.interp(y_low, y_rev, t_rev)
|
|
61
|
+
return t_lo - t_hi
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _gaussian(t, sigma):
|
|
65
|
+
"""Gaussian pulse shape.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
t : array-like
|
|
70
|
+
Time samples in ns.
|
|
71
|
+
sigma : float
|
|
72
|
+
Gaussian standard deviation in ns.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
numpy.ndarray
|
|
77
|
+
Gaussian values at ``t`` (unitless), with a small safeguard for ``sigma``.
|
|
78
|
+
"""
|
|
79
|
+
return np.exp(-0.5 * (t / max(sigma, 1e-9)) ** 2)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _exp_decay(t, tau):
|
|
83
|
+
"""Causal exponential decay shape.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
t : array-like
|
|
88
|
+
Time samples in ns.
|
|
89
|
+
tau : float
|
|
90
|
+
Exponential decay constant in ns.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
numpy.ndarray
|
|
95
|
+
Exponential values at ``t`` (unitless), zero for ``t < 0``.
|
|
96
|
+
"""
|
|
97
|
+
tau = max(tau, 1e-9)
|
|
98
|
+
return np.where(t >= 0, np.exp(-t / tau), 0.0)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def generate_gauss_expconv_pulse(
|
|
102
|
+
sigma_ns,
|
|
103
|
+
tau_ns,
|
|
104
|
+
dt_ns=0.1,
|
|
105
|
+
t_start_ns=-10,
|
|
106
|
+
t_stop_ns=25,
|
|
107
|
+
center_on_peak=False,
|
|
108
|
+
):
|
|
109
|
+
"""Generate a Gaussian convolved with a causal exponential.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
sigma_ns : float
|
|
114
|
+
Gaussian standard deviation (ns).
|
|
115
|
+
tau_ns : float
|
|
116
|
+
Exponential decay constant (ns).
|
|
117
|
+
dt_ns : float, optional
|
|
118
|
+
Time sampling step (ns). Default is 0.1 ns.
|
|
119
|
+
t_start_ns : float
|
|
120
|
+
Together with ``t_stop_ns``, defines the explicit start of the time grid
|
|
121
|
+
for pulse generation (ns).
|
|
122
|
+
t_stop_ns : float
|
|
123
|
+
Together with ``t_start_ns``, defines the explicit end of the time grid
|
|
124
|
+
for pulse generation (ns).
|
|
125
|
+
center_on_peak : bool, optional
|
|
126
|
+
If True, shift the returned time array so the pulse maximum is at t=0.
|
|
127
|
+
Default is False.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
tuple[numpy.ndarray, numpy.ndarray]
|
|
132
|
+
Tuple ``(t, y)`` with time samples in ns and normalized pulse amplitude (peak 1).
|
|
133
|
+
"""
|
|
134
|
+
left = float(t_start_ns)
|
|
135
|
+
right = float(t_stop_ns)
|
|
136
|
+
t = np.arange(left, right, dt_ns, dtype=float)
|
|
137
|
+
g = _gaussian(t, sigma_ns)
|
|
138
|
+
e = _exp_decay(t, tau_ns)
|
|
139
|
+
y = fftconvolve(g, e, mode="same")
|
|
140
|
+
if y.max() > 0:
|
|
141
|
+
y = y / y.max()
|
|
142
|
+
if center_on_peak:
|
|
143
|
+
i_max = int(np.argmax(y))
|
|
144
|
+
t = t - float(t[i_max])
|
|
145
|
+
return t, y
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def solve_sigma_tau_from_rise_fall(
|
|
149
|
+
rise_width_ns,
|
|
150
|
+
fall_width_ns,
|
|
151
|
+
dt_ns=0.1,
|
|
152
|
+
rise_range=(0.1, 0.9),
|
|
153
|
+
fall_range=(0.9, 0.1),
|
|
154
|
+
t_start_ns=-10,
|
|
155
|
+
t_stop_ns=25,
|
|
156
|
+
):
|
|
157
|
+
"""Solve (sigma, tau) so convolved pulse matches target rise/fall widths.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
rise_width_ns : float
|
|
162
|
+
Desired width on the rising edge in ns between rise_range=(low, high) fractions.
|
|
163
|
+
fall_width_ns : float
|
|
164
|
+
Desired width on the falling edge in ns between fall_range=(high, low) fractions.
|
|
165
|
+
dt_ns : float
|
|
166
|
+
Time step for internal pulse sampling in ns.
|
|
167
|
+
rise_range : tuple[float, float]
|
|
168
|
+
Fractional amplitudes (low, high) for the rising width, defaults to (0.1, 0.9).
|
|
169
|
+
fall_range : tuple[float, float]
|
|
170
|
+
Fractional amplitudes (high, low) for the falling width, defaults to (0.9, 0.1).
|
|
171
|
+
t_start_ns : float
|
|
172
|
+
Optional start time (ns) for the internal sampling window. If provided together with
|
|
173
|
+
``t_stop_ns``, overrides the default window.
|
|
174
|
+
t_stop_ns : float
|
|
175
|
+
Optional stop time (ns) for the internal sampling window. If provided together with
|
|
176
|
+
``t_start_ns``, overrides the default window.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
tuple[float, float]
|
|
181
|
+
Tuple ``(sigma_ns, tau_ns)`` giving the Gaussian sigma and exponential tau in ns.
|
|
182
|
+
"""
|
|
183
|
+
t = np.arange(float(t_start_ns), float(t_stop_ns) + dt_ns, dt_ns, dtype=float)
|
|
184
|
+
|
|
185
|
+
def pulse(sigma, tau):
|
|
186
|
+
g = _gaussian(t, sigma)
|
|
187
|
+
e = _exp_decay(t, tau)
|
|
188
|
+
y = fftconvolve(g, e, mode="same")
|
|
189
|
+
return y / y.max() if y.max() > 0 else y
|
|
190
|
+
|
|
191
|
+
rise_lo, rise_hi = rise_range
|
|
192
|
+
fall_hi, fall_lo = fall_range
|
|
193
|
+
|
|
194
|
+
def residuals(x):
|
|
195
|
+
sigma, tau = x
|
|
196
|
+
y = pulse(sigma, tau)
|
|
197
|
+
r = _rise_width(t, y, y_low=rise_lo, y_high=rise_hi) - rise_width_ns
|
|
198
|
+
f = _fall_width(t, y, y_high=fall_hi, y_low=fall_lo) - fall_width_ns
|
|
199
|
+
return [r, f]
|
|
200
|
+
|
|
201
|
+
res = least_squares(residuals, x0=[0.3, 10.0], bounds=(1e-6, 500))
|
|
202
|
+
sigma, tau = float(res.x[0]), float(res.x[1])
|
|
203
|
+
return sigma, tau
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def generate_pulse_from_rise_fall_times(
|
|
207
|
+
rise_width_ns,
|
|
208
|
+
fall_width_ns,
|
|
209
|
+
dt_ns=0.1,
|
|
210
|
+
rise_range=(0.1, 0.9),
|
|
211
|
+
fall_range=(0.9, 0.1),
|
|
212
|
+
t_start_ns=-10,
|
|
213
|
+
t_stop_ns=25,
|
|
214
|
+
center_on_peak=False,
|
|
215
|
+
):
|
|
216
|
+
"""Generate pulse from rise/fall time specifications.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
rise_width_ns : float
|
|
221
|
+
Target rise time (ns) between the fractional levels defined by ``rise_range``.
|
|
222
|
+
Defaults correspond to 10-90% rise time.
|
|
223
|
+
fall_width_ns : float
|
|
224
|
+
Target fall time (ns) between the fractional levels defined by ``fall_range``.
|
|
225
|
+
Defaults correspond to 90-10% fall time.
|
|
226
|
+
dt_ns : float, optional
|
|
227
|
+
Time sampling step (ns). Default is 0.1 ns.
|
|
228
|
+
rise_range : tuple[float, float], optional
|
|
229
|
+
Fractional amplitudes (low, high) for rise-time definition. Default (0.1, 0.9).
|
|
230
|
+
fall_range : tuple[float, float], optional
|
|
231
|
+
Fractional amplitudes (high, low) for fall-time definition. Default (0.9, 0.1).
|
|
232
|
+
t_start_ns : float, optional
|
|
233
|
+
Start time (ns) for the internal solver sampling window and output grid. Default -10.
|
|
234
|
+
t_stop_ns : float, optional
|
|
235
|
+
Stop time (ns) for the internal solver sampling window and output grid. Default 25.
|
|
236
|
+
center_on_peak : bool, optional
|
|
237
|
+
If True, shift the returned time array so the pulse maximum is at t=0.
|
|
238
|
+
Default is False.
|
|
239
|
+
|
|
240
|
+
Returns
|
|
241
|
+
-------
|
|
242
|
+
tuple[numpy.ndarray, numpy.ndarray]
|
|
243
|
+
Tuple ``(t, y)`` with time samples in ns and normalized pulse amplitude (peak 1).
|
|
244
|
+
|
|
245
|
+
Notes
|
|
246
|
+
-----
|
|
247
|
+
The model is a Gaussian convolved with an exponential. The parameters (sigma, tau)
|
|
248
|
+
are solved via least-squares such that the resulting pulse matches the requested rise and
|
|
249
|
+
fall times measured on monotonic segments relative to the peak.
|
|
250
|
+
"""
|
|
251
|
+
sigma, tau = solve_sigma_tau_from_rise_fall(
|
|
252
|
+
rise_width_ns,
|
|
253
|
+
fall_width_ns,
|
|
254
|
+
dt_ns=dt_ns,
|
|
255
|
+
rise_range=rise_range,
|
|
256
|
+
fall_range=fall_range,
|
|
257
|
+
t_start_ns=t_start_ns,
|
|
258
|
+
t_stop_ns=t_stop_ns,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return generate_gauss_expconv_pulse(
|
|
262
|
+
sigma,
|
|
263
|
+
tau,
|
|
264
|
+
dt_ns=dt_ns,
|
|
265
|
+
t_start_ns=t_start_ns,
|
|
266
|
+
t_stop_ns=t_stop_ns,
|
|
267
|
+
center_on_peak=center_on_peak,
|
|
268
|
+
)
|
|
@@ -11,8 +11,11 @@ import numpy as np
|
|
|
11
11
|
import simtools.utils.general as gen
|
|
12
12
|
import simtools.version
|
|
13
13
|
from simtools.io import ascii_handler
|
|
14
|
+
from simtools.simtel.pulse_shapes import generate_pulse_from_rise_fall_times
|
|
14
15
|
from simtools.utils import names
|
|
15
16
|
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
16
19
|
|
|
17
20
|
def sim_telarray_random_seeds(seed, number):
|
|
18
21
|
"""
|
|
@@ -121,6 +124,84 @@ class SimtelConfigWriter:
|
|
|
121
124
|
):
|
|
122
125
|
file.write(f"{meta}\n")
|
|
123
126
|
|
|
127
|
+
@staticmethod
|
|
128
|
+
def write_lightpulse_table_gauss_expconv(
|
|
129
|
+
file_path,
|
|
130
|
+
width_ns=None,
|
|
131
|
+
exp_decay_ns=None,
|
|
132
|
+
dt_ns=0.1,
|
|
133
|
+
rise_range=(0.1, 0.9),
|
|
134
|
+
fall_range=(0.9, 0.1),
|
|
135
|
+
fadc_sum_bins=None,
|
|
136
|
+
time_margin_ns=10.0,
|
|
137
|
+
):
|
|
138
|
+
"""Write a pulse table for a Gaussian convolved with a causal exponential.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
file_path : str or pathlib.Path
|
|
143
|
+
Destination path of the ASCII pulse table to write. Parent directory must exist.
|
|
144
|
+
width_ns : float
|
|
145
|
+
Target rise time in ns between the fractional levels defined by ``rise_range``.
|
|
146
|
+
Defaults correspond to 10-90% rise time.
|
|
147
|
+
exp_decay_ns : float
|
|
148
|
+
Target fall time in ns between the fractional levels defined by ``fall_range``.
|
|
149
|
+
Defaults correspond to 90-10% fall time.
|
|
150
|
+
dt_ns : float, optional
|
|
151
|
+
Time sampling step in ns for the generated pulse table. Default is 0.1.
|
|
152
|
+
rise_range : tuple[float, float], optional
|
|
153
|
+
Fractional amplitude bounds (low, high) for rise-time definition. Default (0.1, 0.9).
|
|
154
|
+
fall_range : tuple[float, float], optional
|
|
155
|
+
Fractional amplitude bounds (high, low) for fall-time definition. Default (0.9, 0.1).
|
|
156
|
+
fadc_sum_bins : int
|
|
157
|
+
Length of the FADC integration window (treated as ns here) used to derive
|
|
158
|
+
the internal time sampling window of the solver as [-(margin), bins + margin].
|
|
159
|
+
time_margin_ns : float, optional
|
|
160
|
+
Margin in ns to add to both ends of the FADC window when ``fadc_sum_bins`` is given.
|
|
161
|
+
Default is 5.0 ns.
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
pathlib.Path
|
|
166
|
+
The path to the created pulse table file.
|
|
167
|
+
|
|
168
|
+
Notes
|
|
169
|
+
-----
|
|
170
|
+
The underlying model is a Gaussian convolved with a causal exponential. The model
|
|
171
|
+
parameters (sigma, tau) are solved such that the normalized pulse matches the requested
|
|
172
|
+
rise and fall times. The pulse is normalized to a peak amplitude of 1.
|
|
173
|
+
"""
|
|
174
|
+
if width_ns is None or exp_decay_ns is None:
|
|
175
|
+
raise ValueError("width_ns (rise 10-90) and exp_decay_ns (fall 90-10) are required")
|
|
176
|
+
logger.info(
|
|
177
|
+
f"Generating lightpulse table with rise10-90={width_ns} ns, "
|
|
178
|
+
f"fall90-10={exp_decay_ns} ns, dt={dt_ns} ns"
|
|
179
|
+
)
|
|
180
|
+
width = float(fadc_sum_bins)
|
|
181
|
+
t_start_ns = -abs(time_margin_ns + width)
|
|
182
|
+
t_stop_ns = +abs(time_margin_ns + width)
|
|
183
|
+
t, y = generate_pulse_from_rise_fall_times(
|
|
184
|
+
width_ns,
|
|
185
|
+
exp_decay_ns,
|
|
186
|
+
dt_ns=dt_ns,
|
|
187
|
+
rise_range=rise_range,
|
|
188
|
+
fall_range=fall_range,
|
|
189
|
+
t_start_ns=t_start_ns,
|
|
190
|
+
t_stop_ns=t_stop_ns,
|
|
191
|
+
center_on_peak=True,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return SimtelConfigWriter._write_ascii_pulse_table(file_path, t, y)
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def _write_ascii_pulse_table(file_path, t, y):
|
|
198
|
+
"""Write two-column ASCII pulse table."""
|
|
199
|
+
with open(file_path, "w", encoding="utf-8") as fh:
|
|
200
|
+
fh.write("# time[ns] amplitude\n")
|
|
201
|
+
for ti, yi in zip(t, y):
|
|
202
|
+
fh.write(f"{ti:.6f} {yi:.8f}\n")
|
|
203
|
+
return Path(file_path)
|
|
204
|
+
|
|
124
205
|
def _get_parameters_for_sim_telarray(self, parameters, config_file_path):
|
|
125
206
|
"""
|
|
126
207
|
Convert parameter dictionary to sim_telarray configuration file format.
|
|
@@ -221,7 +302,7 @@ class SimtelConfigWriter:
|
|
|
221
302
|
value = "none" if value is None else value # simtel requires 'none'
|
|
222
303
|
if isinstance(value, bool):
|
|
223
304
|
value = 1 if value else 0
|
|
224
|
-
elif isinstance(value,
|
|
305
|
+
elif isinstance(value, list | np.ndarray):
|
|
225
306
|
value = gen.convert_list_to_string(value, shorten_list=True)
|
|
226
307
|
return value
|
|
227
308
|
|
|
@@ -528,7 +609,7 @@ class SimtelConfigWriter:
|
|
|
528
609
|
"""
|
|
529
610
|
file.write(self.TAB + "% Site parameters\n")
|
|
530
611
|
for par, value in site_parameters.items():
|
|
531
|
-
simtel_name,
|
|
612
|
+
simtel_name, simtel_value = self._convert_model_parameters_to_simtel_format(
|
|
532
613
|
names.get_simulation_software_name_from_parameter_name(
|
|
533
614
|
par, software_name="sim_telarray"
|
|
534
615
|
),
|
|
@@ -537,7 +618,7 @@ class SimtelConfigWriter:
|
|
|
537
618
|
telescope_model,
|
|
538
619
|
)
|
|
539
620
|
if simtel_name is not None:
|
|
540
|
-
file.write(f"{self.TAB}{simtel_name} = {
|
|
621
|
+
file.write(f"{self.TAB}{simtel_name} = {simtel_value}\n")
|
|
541
622
|
for meta in self._get_sim_telarray_metadata(
|
|
542
623
|
"site", site_parameters, None, additional_metadata
|
|
543
624
|
):
|
|
@@ -592,34 +673,111 @@ class SimtelConfigWriter:
|
|
|
592
673
|
telescope_model: dict of TelescopeModel
|
|
593
674
|
Telescope models.
|
|
594
675
|
"""
|
|
676
|
+
trigger_per_telescope_type = self._group_telescopes_by_type(telescope_model)
|
|
677
|
+
hardstereo_lines, non_hardstereo_groups, all_non_hardstereo_tels, multiplicity = (
|
|
678
|
+
self._process_telescope_triggers(array_triggers, trigger_per_telescope_type)
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
array_triggers_file = "array_triggers.dat"
|
|
682
|
+
with open(model_path / array_triggers_file, "w", encoding="utf-8") as file:
|
|
683
|
+
file.write("# Array trigger definition\n")
|
|
684
|
+
self._write_trigger_lines(
|
|
685
|
+
file, hardstereo_lines, non_hardstereo_groups, all_non_hardstereo_tels, multiplicity
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
return array_triggers_file
|
|
689
|
+
|
|
690
|
+
def _group_telescopes_by_type(self, telescope_model):
|
|
691
|
+
"""Group telescopes by their type."""
|
|
595
692
|
trigger_per_telescope_type = {}
|
|
596
693
|
for count, tel_name in enumerate(telescope_model.keys()):
|
|
597
694
|
telescope_type = names.get_array_element_type_from_name(tel_name)
|
|
598
695
|
trigger_per_telescope_type.setdefault(telescope_type, []).append(count + 1)
|
|
696
|
+
return trigger_per_telescope_type
|
|
697
|
+
|
|
698
|
+
def _process_telescope_triggers(self, array_triggers, trigger_per_telescope_type):
|
|
699
|
+
"""Process telescope triggers and group them by hardstereo and parameters."""
|
|
700
|
+
hardstereo_lines = []
|
|
701
|
+
non_hardstereo_groups = {}
|
|
702
|
+
all_non_hardstereo_tels = []
|
|
703
|
+
multiplicity = None
|
|
599
704
|
|
|
600
|
-
trigger_lines = {}
|
|
601
705
|
for tel_type, tel_list in trigger_per_telescope_type.items():
|
|
602
706
|
trigger_dict = self._get_array_triggers_for_telescope_type(
|
|
603
707
|
array_triggers, tel_type, len(tel_list)
|
|
604
708
|
)
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
width = trigger_dict["width"]["value"] * u.Unit(trigger_dict["width"]["unit"]).to("ns")
|
|
608
|
-
trigger_lines[tel_type] += f" width {width}"
|
|
609
|
-
if trigger_dict.get("hard_stereo", {}).get("value"):
|
|
610
|
-
trigger_lines[tel_type] += " hardstereo"
|
|
611
|
-
if all(trigger_dict["min_separation"][key] is not None for key in ["value", "unit"]):
|
|
612
|
-
min_sep = trigger_dict["min_separation"]["value"] * u.Unit(
|
|
613
|
-
trigger_dict["min_separation"]["unit"]
|
|
614
|
-
).to("m")
|
|
615
|
-
trigger_lines[tel_type] += f" minsep {min_sep}"
|
|
709
|
+
width, minsep = self._extract_trigger_parameters(trigger_dict)
|
|
710
|
+
multiplicity = trigger_dict["multiplicity"]["value"] # Store for later use
|
|
616
711
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
712
|
+
if trigger_dict.get("hard_stereo", {}).get("value"):
|
|
713
|
+
line = self._build_trigger_line(
|
|
714
|
+
trigger_dict, tel_list, width, minsep, hardstereo=True
|
|
715
|
+
)
|
|
716
|
+
hardstereo_lines.append(line)
|
|
717
|
+
else:
|
|
718
|
+
key = (width, minsep)
|
|
719
|
+
non_hardstereo_groups.setdefault(key, []).extend(tel_list)
|
|
720
|
+
all_non_hardstereo_tels.extend(tel_list)
|
|
721
|
+
|
|
722
|
+
return hardstereo_lines, non_hardstereo_groups, all_non_hardstereo_tels, multiplicity
|
|
723
|
+
|
|
724
|
+
def _extract_trigger_parameters(self, trigger_dict):
|
|
725
|
+
"""Extract width and min_separation parameters from trigger dictionary."""
|
|
726
|
+
width = trigger_dict["width"]["value"] * u.Unit(trigger_dict["width"]["unit"]).to("ns")
|
|
727
|
+
minsep = None
|
|
728
|
+
if all(trigger_dict["min_separation"][key] is not None for key in ["value", "unit"]):
|
|
729
|
+
minsep = trigger_dict["min_separation"]["value"] * u.Unit(
|
|
730
|
+
trigger_dict["min_separation"]["unit"]
|
|
731
|
+
).to("m")
|
|
732
|
+
return width, minsep
|
|
733
|
+
|
|
734
|
+
def _build_trigger_line(self, trigger_dict, tel_list, width, minsep, hardstereo=False):
|
|
735
|
+
"""Build a trigger line string."""
|
|
736
|
+
line = f"Trigger {trigger_dict['multiplicity']['value']} of "
|
|
737
|
+
line += ", ".join(map(str, tel_list))
|
|
738
|
+
line += f" width {width}"
|
|
739
|
+
if hardstereo:
|
|
740
|
+
line += " hardstereo"
|
|
741
|
+
if minsep is not None:
|
|
742
|
+
line += f" minsep {minsep}"
|
|
743
|
+
return line
|
|
744
|
+
|
|
745
|
+
def _write_trigger_lines(
|
|
746
|
+
self, file, hardstereo_lines, non_hardstereo_groups, all_non_hardstereo_tels, multiplicity
|
|
747
|
+
):
|
|
748
|
+
"""Write all trigger lines to file."""
|
|
749
|
+
# Write hardstereo lines first
|
|
750
|
+
for line in hardstereo_lines:
|
|
751
|
+
file.write(f"{line}\n")
|
|
752
|
+
|
|
753
|
+
# Write individual non-hardstereo groups if they have different parameters
|
|
754
|
+
if len(non_hardstereo_groups) > 1:
|
|
755
|
+
for (width, minsep), tel_list in non_hardstereo_groups.items():
|
|
756
|
+
line = f"Trigger {multiplicity} of "
|
|
757
|
+
line += ", ".join(map(str, tel_list))
|
|
758
|
+
line += f" width {width}"
|
|
759
|
+
if minsep is not None:
|
|
760
|
+
line += f" minsep {minsep}"
|
|
761
|
+
file.write(f"{line}\n")
|
|
762
|
+
|
|
763
|
+
# Write combined line with all non-hardstereo telescopes using shortest values
|
|
764
|
+
if all_non_hardstereo_tels:
|
|
765
|
+
min_width = min(width for width, minsep in non_hardstereo_groups.keys())
|
|
766
|
+
min_minsep = self._get_minimum_minsep(non_hardstereo_groups)
|
|
767
|
+
|
|
768
|
+
combined_line = f"Trigger {multiplicity} of "
|
|
769
|
+
combined_line += ", ".join(map(str, sorted(all_non_hardstereo_tels)))
|
|
770
|
+
combined_line += f" width {min_width}"
|
|
771
|
+
if min_minsep is not None:
|
|
772
|
+
combined_line += f" minsep {min_minsep}"
|
|
773
|
+
file.write(f"{combined_line}\n")
|
|
774
|
+
|
|
775
|
+
def _get_minimum_minsep(self, non_hardstereo_groups):
|
|
776
|
+
"""Get minimum min_separation value from groups."""
|
|
777
|
+
minsep_values = [
|
|
778
|
+
minsep for width, minsep in non_hardstereo_groups.keys() if minsep is not None
|
|
779
|
+
]
|
|
780
|
+
return min(minsep_values) if minsep_values else None
|
|
623
781
|
|
|
624
782
|
def _get_array_triggers_for_telescope_type(
|
|
625
783
|
self, array_triggers, telescope_type, num_telescopes_of_type
|
|
@@ -17,7 +17,7 @@ from eventio.simtel import (
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
from simtools.corsika.primary_particle import PrimaryParticle
|
|
20
|
-
from simtools.
|
|
20
|
+
from simtools.io.eventio_handler import get_combined_corsika_run_header
|
|
21
21
|
from simtools.simtel.simtel_io_metadata import (
|
|
22
22
|
get_sim_telarray_telescope_id_to_telescope_name_mapping,
|
|
23
23
|
read_sim_telarray_metadata,
|
|
@@ -158,7 +158,7 @@ class SimtelIOEventDataWriter:
|
|
|
158
158
|
|
|
159
159
|
def _process_file_info(self, file_id, file):
|
|
160
160
|
"""Process file information and append to file info list."""
|
|
161
|
-
run_info =
|
|
161
|
+
run_info = get_combined_corsika_run_header(file)
|
|
162
162
|
self.telescope_id_to_name = get_sim_telarray_telescope_id_to_telescope_name_mapping(file)
|
|
163
163
|
particle = PrimaryParticle(
|
|
164
164
|
particle_id_type="eventio_id", particle_id=run_info.get("primary_id", 1)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Simulation runner for array simulations."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import stat
|
|
4
5
|
|
|
5
6
|
from simtools.io import io_handler
|
|
6
7
|
from simtools.runners.simtel_runner import InvalidOutputFileError, SimtelRunner
|
|
@@ -42,6 +43,7 @@ class SimulatorArray(SimtelRunner):
|
|
|
42
43
|
simtel_path=simtel_path,
|
|
43
44
|
corsika_config=corsika_config,
|
|
44
45
|
use_multipipe=use_multipipe,
|
|
46
|
+
calibration_run_mode=calibration_config.get("run_mode") if calibration_config else None,
|
|
45
47
|
)
|
|
46
48
|
|
|
47
49
|
self.sim_telarray_seeds = sim_telarray_seeds
|
|
@@ -50,6 +52,52 @@ class SimulatorArray(SimtelRunner):
|
|
|
50
52
|
self.io_handler = io_handler.IOHandler()
|
|
51
53
|
self._log_file = None
|
|
52
54
|
|
|
55
|
+
def prepare_run_script(self, test=False, input_file=None, run_number=None, extra_commands=None):
|
|
56
|
+
"""
|
|
57
|
+
Build and return the full path of the bash run script containing the sim_telarray command.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
test: bool
|
|
62
|
+
Test flag for faster execution.
|
|
63
|
+
input_file: str or Path
|
|
64
|
+
Full path of the input CORSIKA file.
|
|
65
|
+
run_number: int
|
|
66
|
+
Run number.
|
|
67
|
+
extra_commands: list[str]
|
|
68
|
+
Additional commands for running simulations given in config.yml.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
Path
|
|
73
|
+
Full path of the run script.
|
|
74
|
+
"""
|
|
75
|
+
script_file_path = self.get_file_name(file_type="sub_script", run_number=run_number)
|
|
76
|
+
self._logger.debug(f"Run bash script - {script_file_path}")
|
|
77
|
+
self._logger.debug(f"Extra commands to be added to the run script {extra_commands}")
|
|
78
|
+
|
|
79
|
+
command = self.make_run_command(run_number=run_number, input_file=input_file)
|
|
80
|
+
with script_file_path.open("w", encoding="utf-8") as file:
|
|
81
|
+
file.write("#!/usr/bin/env bash\n\n")
|
|
82
|
+
file.write("set -e\n")
|
|
83
|
+
file.write("set -o pipefail\n")
|
|
84
|
+
file.write("\nSECONDS=0\n")
|
|
85
|
+
|
|
86
|
+
if extra_commands:
|
|
87
|
+
file.write("# Writing extras\n")
|
|
88
|
+
for line in extra_commands:
|
|
89
|
+
file.write(f"{line}\n")
|
|
90
|
+
file.write("# End of extras\n\n")
|
|
91
|
+
|
|
92
|
+
n = 1 if test else self.runs_per_set
|
|
93
|
+
for _ in range(n):
|
|
94
|
+
file.write(f"{command}\n\n")
|
|
95
|
+
|
|
96
|
+
file.write('\necho "RUNTIME: $SECONDS"\n')
|
|
97
|
+
|
|
98
|
+
script_file_path.chmod(script_file_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP)
|
|
99
|
+
return script_file_path
|
|
100
|
+
|
|
53
101
|
def make_run_command(self, run_number=None, input_file=None, weak_pointing=None):
|
|
54
102
|
"""
|
|
55
103
|
Build and return the command to run sim_telarray.
|
|
@@ -111,12 +159,12 @@ class SimulatorArray(SimtelRunner):
|
|
|
111
159
|
command += super().get_config_option(key, cfg[key])
|
|
112
160
|
|
|
113
161
|
run_mode = cfg.get("run_mode")
|
|
114
|
-
if run_mode in ("pedestals", "
|
|
162
|
+
if run_mode in ("pedestals", "pedestals_nsb_only"):
|
|
115
163
|
n_events = cfg.get("number_of_pedestal_events", cfg["number_of_events"])
|
|
116
164
|
command += super().get_config_option("pedestal_events", n_events)
|
|
117
|
-
if run_mode == "
|
|
118
|
-
command += self.
|
|
119
|
-
if run_mode == "
|
|
165
|
+
if run_mode == "pedestals_nsb_only":
|
|
166
|
+
command += self._pedestals_nsb_only_command()
|
|
167
|
+
if run_mode == "pedestals_dark":
|
|
120
168
|
n_events = cfg.get("number_of_dark_events", cfg["number_of_events"])
|
|
121
169
|
command += super().get_config_option("dark_events", n_events)
|
|
122
170
|
if run_mode == "direct_injection":
|
|
@@ -155,7 +203,7 @@ class SimulatorArray(SimtelRunner):
|
|
|
155
203
|
|
|
156
204
|
return command
|
|
157
205
|
|
|
158
|
-
def
|
|
206
|
+
def _pedestals_nsb_only_command(self):
|
|
159
207
|
"""
|
|
160
208
|
Generate the command to run sim_telarray for nsb-only pedestal simulations.
|
|
161
209
|
|
|
@@ -193,7 +241,7 @@ class SimulatorArray(SimtelRunner):
|
|
|
193
241
|
|
|
194
242
|
def _check_run_result(self, run_number=None):
|
|
195
243
|
"""
|
|
196
|
-
Check if
|
|
244
|
+
Check if sim_telarray output file exists.
|
|
197
245
|
|
|
198
246
|
Parameters
|
|
199
247
|
----------
|
|
@@ -203,18 +251,16 @@ class SimulatorArray(SimtelRunner):
|
|
|
203
251
|
Returns
|
|
204
252
|
-------
|
|
205
253
|
bool
|
|
206
|
-
True if
|
|
254
|
+
True if sim_telarray output file exists.
|
|
207
255
|
|
|
208
256
|
Raises
|
|
209
257
|
------
|
|
210
258
|
InvalidOutputFileError
|
|
211
|
-
If
|
|
259
|
+
If sim_telarray output file does not exist.
|
|
212
260
|
"""
|
|
213
261
|
output_file = self.get_file_name(file_type="simtel_output", run_number=run_number)
|
|
214
262
|
if not output_file.exists():
|
|
215
|
-
|
|
216
|
-
self._logger.error(msg)
|
|
217
|
-
raise InvalidOutputFileError(msg)
|
|
263
|
+
raise InvalidOutputFileError(f"sim_telarray output file {output_file} does not exist.")
|
|
218
264
|
self._logger.debug(f"sim_telarray output file {output_file} exists.")
|
|
219
265
|
return True
|
|
220
266
|
|
|
@@ -223,7 +269,7 @@ class SimulatorArray(SimtelRunner):
|
|
|
223
269
|
"""
|
|
224
270
|
Get the power law index for sim_telarray.
|
|
225
271
|
|
|
226
|
-
Events will be
|
|
272
|
+
Events will be filled in histograms in sim_telarray with a weight according to
|
|
227
273
|
the difference between this exponent and the one used for the shower simulations.
|
|
228
274
|
|
|
229
275
|
Parameters
|