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,4 +1,4 @@
1
- """Generate a reduced dataset from sim_telarray output files using astropy tables."""
1
+ """Generate a reduced dataset from simulation files (CORSIKA/sim_telarray) using astropy tables."""
2
2
 
3
3
  import logging
4
4
  from dataclasses import dataclass
@@ -6,7 +6,7 @@ from dataclasses import dataclass
6
6
  import astropy.units as u
7
7
  import numpy as np
8
8
  from astropy.table import Table
9
- from eventio import EventIOFile
9
+ from eventio import EventIOFile, iact
10
10
  from eventio.simtel import (
11
11
  ArrayEvent,
12
12
  MCEvent,
@@ -17,7 +17,10 @@ from eventio.simtel import (
17
17
  )
18
18
 
19
19
  from simtools.corsika.primary_particle import PrimaryParticle
20
- from simtools.simtel.simtel_io_file_info import get_corsika_run_header
20
+ from simtools.sim_events.file_info import (
21
+ get_combined_eventio_run_header,
22
+ get_corsika_run_and_event_headers,
23
+ )
21
24
  from simtools.simtel.simtel_io_metadata import (
22
25
  get_sim_telarray_telescope_id_to_telescope_name_mapping,
23
26
  read_sim_telarray_metadata,
@@ -68,11 +71,11 @@ class TableSchemas:
68
71
  }
69
72
 
70
73
 
71
- class SimtelIOEventDataWriter:
74
+ class EventDataWriter:
72
75
  """
73
- Process sim_telarray events and write tables to file.
76
+ Process simulation events (CORSIKA/sim_telarray) and write tables to file.
74
77
 
75
- Extracts essential information from sim_telarray output files:
78
+ Extracts essential information from simulation files, including:
76
79
 
77
80
  - Shower parameters (energy, core location, direction)
78
81
  - Trigger patterns
@@ -108,7 +111,7 @@ class SimtelIOEventDataWriter:
108
111
  Returns
109
112
  -------
110
113
  list
111
- List of astropy tables containing processed data.
114
+ List of tables containing processed data.
112
115
  """
113
116
  for i, file in enumerate(self.input_files[: self.max_files]):
114
117
  self._logger.info(f"Processing file {i + 1}/{self.max_files}: {file}")
@@ -117,13 +120,15 @@ class SimtelIOEventDataWriter:
117
120
  return self.create_tables()
118
121
 
119
122
  def create_tables(self):
120
- """Create astropy tables from collected data."""
123
+ """Create tables from collected data."""
121
124
  tables = []
122
125
  for data, schema, name in [
123
126
  (self.shower_data, TableSchemas.shower_schema, "SHOWERS"),
124
127
  (self.trigger_data, TableSchemas.trigger_schema, "TRIGGERS"),
125
128
  (self.file_info, TableSchemas.file_info_schema, "FILE_INFO"),
126
129
  ]:
130
+ if len(data) == 0:
131
+ continue
127
132
  table = Table(rows=data, names=schema.keys())
128
133
  table.meta["EXTNAME"] = name
129
134
  self._add_units_to_table(table, schema)
@@ -149,40 +154,69 @@ class SimtelIOEventDataWriter:
149
154
  self._process_mc_event(eventio_object)
150
155
  elif isinstance(eventio_object, ArrayEvent):
151
156
  self._process_array_event(eventio_object, file_id)
157
+ elif isinstance(eventio_object, iact.EventHeader):
158
+ self._process_mc_shower_from_iact(eventio_object, file_id)
152
159
 
153
160
  def _process_mc_run_header(self, eventio_object):
154
- """Process MC run header and update data lists."""
161
+ """Process MC run header (sim_telarray file)."""
155
162
  mc_head = eventio_object.parse()
156
163
  self.n_use = mc_head["n_use"] # reuse factor n_use needed to extend the values below
157
164
  self._logger.info(f"Shower reuse factor: {self.n_use} (viewcone: {mc_head['viewcone']})")
158
165
 
159
166
  def _process_file_info(self, file_id, file):
160
167
  """Process file information and append to file info list."""
161
- run_info = get_corsika_run_header(file)
162
- self.telescope_id_to_name = get_sim_telarray_telescope_id_to_telescope_name_mapping(file)
163
- particle = PrimaryParticle(
164
- particle_id_type="eventio_id", particle_id=run_info.get("primary_id", 1)
165
- )
168
+ run_info = get_combined_eventio_run_header(file)
169
+ if run_info: # sim_telarray file
170
+ self.telescope_id_to_name = get_sim_telarray_telescope_id_to_telescope_name_mapping(
171
+ file
172
+ )
173
+ corsika7_id = PrimaryParticle(
174
+ particle_id_type="eventio_id",
175
+ particle_id=run_info.get("primary_id", 1),
176
+ ).corsika7_id
177
+ nsb = self.get_nsb_level_from_sim_telarray_metadata(file)
178
+
179
+ e_min, e_max = run_info["E_range"]
180
+ view_cone_min, view_cone_max = run_info["viewcone"]
181
+ core_min, core_max = run_info["core_range"]
182
+ azimuth, el = np.degrees(run_info["direction"])
183
+ zenith = 90.0 - el
184
+ else: # CORSIKA IACT file
185
+ run_header, event_header = get_corsika_run_and_event_headers(file)
186
+ corsika7_id = int(event_header["particle_id"])
187
+ e_min = event_header["energy_min"]
188
+ e_max = event_header["energy_max"]
189
+ zenith = np.degrees(event_header["zenith"])
190
+ # Rotate to geographic north
191
+ azimuth = np.degrees(
192
+ event_header["azimuth"] - event_header["angle_array_x_magnetic_north"]
193
+ )
194
+ view_cone_min = event_header["viewcone_inner_angle"]
195
+ view_cone_max = event_header["viewcone_outer_angle"]
196
+ core_min = 0.0
197
+ core_max = run_header["x_scatter"] / 1.0e2 # cm to m
198
+ nsb = 0.0
199
+
166
200
  self.file_info.append(
167
201
  {
168
202
  "file_name": str(file),
169
203
  "file_id": file_id,
170
- "particle_id": particle.corsika7_id,
171
- "energy_min": run_info["E_range"][0],
172
- "energy_max": run_info["E_range"][1],
173
- "viewcone_min": run_info["viewcone"][0],
174
- "viewcone_max": run_info["viewcone"][1],
175
- "core_scatter_min": run_info["core_range"][0],
176
- "core_scatter_max": run_info["core_range"][1],
177
- "zenith": 90.0 - np.degrees(run_info["direction"][1]),
178
- "azimuth": np.degrees(run_info["direction"][0]),
179
- "nsb_level": self.get_nsb_level_from_sim_telarray_metadata(file),
204
+ "particle_id": corsika7_id,
205
+ "energy_min": e_min,
206
+ "energy_max": e_max,
207
+ "viewcone_min": view_cone_min,
208
+ "viewcone_max": view_cone_max,
209
+ "core_scatter_min": core_min,
210
+ "core_scatter_max": core_max,
211
+ "zenith": zenith,
212
+ "azimuth": azimuth,
213
+ "nsb_level": nsb,
180
214
  }
181
215
  )
182
216
 
183
217
  def _process_mc_shower(self, eventio_object, file_id):
184
218
  """
185
- Process MC shower and update shower event list.
219
+ Process MC shower from sim_telarray file and update shower event list.
186
220
 
187
221
  Duplicated entries 'self.n_use' times to match the number simulated events with
188
222
  different core positions.
@@ -204,6 +238,31 @@ class SimtelIOEventDataWriter:
204
238
  for _ in range(self.n_use)
205
239
  )
206
240
 
241
+ def _process_mc_shower_from_iact(self, eventio_object, file_id):
242
+ """
243
+ Process MC shower from IACT file and update shower event list.
244
+
245
+ Duplicated entries 'self.n_use' times to match the number simulated events with
246
+ different core positions.
247
+ """
248
+ shower_header = eventio_object.parse()
249
+ self.n_use = int(shower_header["n_reuse"])
250
+
251
+ self.shower_data.extend(
252
+ {
253
+ "shower_id": shower_header["event_number"],
254
+ "event_id": shower_header["event_number"] * 100 + i,
255
+ "file_id": file_id,
256
+ "simulated_energy": shower_header["total_energy"],
257
+ "x_core": shower_header["reuse_x"][i] / 1.0e2,
258
+ "y_core": shower_header["reuse_y"][i] / 1.0e2,
259
+ "shower_azimuth": np.degrees(shower_header["azimuth"]),
260
+ "shower_altitude": 90.0 - np.degrees(shower_header["zenith"]),
261
+ "area_weight": 1.0,
262
+ }
263
+ for i in range(self.n_use)
264
+ )
265
+
207
266
  def _process_mc_event(self, eventio_object):
208
267
  """
209
268
  Process MC event and update shower event list.
@@ -0,0 +1,273 @@
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(float(tau), 1e-9)
98
+ t_arr = np.asarray(t, dtype=float)
99
+ expo = -t_arr / tau
100
+ expo = np.minimum(expo, 0.0)
101
+ with np.errstate(over="ignore", under="ignore", invalid="ignore"):
102
+ e = np.exp(expo)
103
+ return np.where(t_arr >= 0, e, 0.0)
104
+
105
+
106
+ def generate_gauss_expconv_pulse(
107
+ sigma_ns,
108
+ tau_ns,
109
+ dt_ns=0.1,
110
+ t_start_ns=-10,
111
+ t_stop_ns=25,
112
+ center_on_peak=False,
113
+ ):
114
+ """Generate a Gaussian convolved with a causal exponential.
115
+
116
+ Parameters
117
+ ----------
118
+ sigma_ns : float
119
+ Gaussian standard deviation (ns).
120
+ tau_ns : float
121
+ Exponential decay constant (ns).
122
+ dt_ns : float, optional
123
+ Time sampling step (ns). Default is 0.1 ns.
124
+ t_start_ns : float
125
+ Together with ``t_stop_ns``, defines the explicit start of the time grid
126
+ for pulse generation (ns).
127
+ t_stop_ns : float
128
+ Together with ``t_start_ns``, defines the explicit end of the time grid
129
+ for pulse generation (ns).
130
+ center_on_peak : bool, optional
131
+ If True, shift the returned time array so the pulse maximum is at t=0.
132
+ Default is False.
133
+
134
+ Returns
135
+ -------
136
+ tuple[numpy.ndarray, numpy.ndarray]
137
+ Tuple ``(t, y)`` with time samples in ns and normalized pulse amplitude (peak 1).
138
+ """
139
+ left = float(t_start_ns)
140
+ right = float(t_stop_ns)
141
+ t = np.arange(left, right, dt_ns, dtype=float)
142
+ g = _gaussian(t, sigma_ns)
143
+ e = _exp_decay(t, tau_ns)
144
+ y = fftconvolve(g, e, mode="same")
145
+ if y.max() > 0:
146
+ y = y / y.max()
147
+ if center_on_peak:
148
+ i_max = int(np.argmax(y))
149
+ t = t - float(t[i_max])
150
+ return t, y
151
+
152
+
153
+ def solve_sigma_tau_from_rise_fall(
154
+ rise_width_ns,
155
+ fall_width_ns,
156
+ dt_ns=0.1,
157
+ rise_range=(0.1, 0.9),
158
+ fall_range=(0.9, 0.1),
159
+ t_start_ns=-10,
160
+ t_stop_ns=25,
161
+ ):
162
+ """Solve (sigma, tau) so convolved pulse matches target rise/fall widths.
163
+
164
+ Parameters
165
+ ----------
166
+ rise_width_ns : float
167
+ Desired width on the rising edge in ns between rise_range=(low, high) fractions.
168
+ fall_width_ns : float
169
+ Desired width on the falling edge in ns between fall_range=(high, low) fractions.
170
+ dt_ns : float
171
+ Time step for internal pulse sampling in ns.
172
+ rise_range : tuple[float, float]
173
+ Fractional amplitudes (low, high) for the rising width, defaults to (0.1, 0.9).
174
+ fall_range : tuple[float, float]
175
+ Fractional amplitudes (high, low) for the falling width, defaults to (0.9, 0.1).
176
+ t_start_ns : float
177
+ Optional start time (ns) for the internal sampling window. If provided together with
178
+ ``t_stop_ns``, overrides the default window.
179
+ t_stop_ns : float
180
+ Optional stop time (ns) for the internal sampling window. If provided together with
181
+ ``t_start_ns``, overrides the default window.
182
+
183
+ Returns
184
+ -------
185
+ tuple[float, float]
186
+ Tuple ``(sigma_ns, tau_ns)`` giving the Gaussian sigma and exponential tau in ns.
187
+ """
188
+ t = np.arange(float(t_start_ns), float(t_stop_ns) + dt_ns, dt_ns, dtype=float)
189
+
190
+ def pulse(sigma, tau):
191
+ g = _gaussian(t, sigma)
192
+ e = _exp_decay(t, tau)
193
+ y = fftconvolve(g, e, mode="same")
194
+ return y / y.max() if y.max() > 0 else y
195
+
196
+ rise_lo, rise_hi = rise_range
197
+ fall_hi, fall_lo = fall_range
198
+
199
+ def residuals(x):
200
+ sigma, tau = x
201
+ y = pulse(sigma, tau)
202
+ r = _rise_width(t, y, y_low=rise_lo, y_high=rise_hi) - rise_width_ns
203
+ f = _fall_width(t, y, y_high=fall_hi, y_low=fall_lo) - fall_width_ns
204
+ return [r, f]
205
+
206
+ res = least_squares(residuals, x0=[0.3, 10.0], bounds=(1e-6, 500))
207
+ sigma, tau = float(res.x[0]), float(res.x[1])
208
+ return sigma, tau
209
+
210
+
211
+ def generate_pulse_from_rise_fall_times(
212
+ rise_width_ns,
213
+ fall_width_ns,
214
+ dt_ns=0.1,
215
+ rise_range=(0.1, 0.9),
216
+ fall_range=(0.9, 0.1),
217
+ t_start_ns=-10,
218
+ t_stop_ns=25,
219
+ center_on_peak=False,
220
+ ):
221
+ """Generate pulse from rise/fall time specifications.
222
+
223
+ Parameters
224
+ ----------
225
+ rise_width_ns : float
226
+ Target rise time (ns) between the fractional levels defined by ``rise_range``.
227
+ Defaults correspond to 10-90% rise time.
228
+ fall_width_ns : float
229
+ Target fall time (ns) between the fractional levels defined by ``fall_range``.
230
+ Defaults correspond to 90-10% fall time.
231
+ dt_ns : float, optional
232
+ Time sampling step (ns). Default is 0.1 ns.
233
+ rise_range : tuple[float, float], optional
234
+ Fractional amplitudes (low, high) for rise-time definition. Default (0.1, 0.9).
235
+ fall_range : tuple[float, float], optional
236
+ Fractional amplitudes (high, low) for fall-time definition. Default (0.9, 0.1).
237
+ t_start_ns : float, optional
238
+ Start time (ns) for the internal solver sampling window and output grid. Default -10.
239
+ t_stop_ns : float, optional
240
+ Stop time (ns) for the internal solver sampling window and output grid. Default 25.
241
+ center_on_peak : bool, optional
242
+ If True, shift the returned time array so the pulse maximum is at t=0.
243
+ Default is False.
244
+
245
+ Returns
246
+ -------
247
+ tuple[numpy.ndarray, numpy.ndarray]
248
+ Tuple ``(t, y)`` with time samples in ns and normalized pulse amplitude (peak 1).
249
+
250
+ Notes
251
+ -----
252
+ The model is a Gaussian convolved with an exponential. The parameters (sigma, tau)
253
+ are solved via least-squares such that the resulting pulse matches the requested rise and
254
+ fall times measured on monotonic segments relative to the peak.
255
+ """
256
+ sigma, tau = solve_sigma_tau_from_rise_fall(
257
+ rise_width_ns,
258
+ fall_width_ns,
259
+ dt_ns=dt_ns,
260
+ rise_range=rise_range,
261
+ fall_range=fall_range,
262
+ t_start_ns=t_start_ns,
263
+ t_stop_ns=t_stop_ns,
264
+ )
265
+
266
+ return generate_gauss_expconv_pulse(
267
+ sigma,
268
+ tau,
269
+ dt_ns=dt_ns,
270
+ t_start_ns=t_start_ns,
271
+ t_stop_ns=t_stop_ns,
272
+ center_on_peak=center_on_peak,
273
+ )