11from .parameters import Config
2- from .solver import (
3- equation_of_motion ,
4- stop_condition ,
5- forward_z_bound_condition ,
6- backward_z_bound_condition ,
7- rho_bound_condition ,
8- )
9- from .transporter import transport_track
2+ from .solver import generate_point_cloud
103from .writer import SimulationWriter
114from .constants import NUM_TB
125from .. import nuclear_map
169import numpy as np
1710import h5py as h5
1811from tqdm import trange
19- from scipy .integrate import solve_ivp
2012from numpy .random import default_rng , Generator
2113from pathlib import Path
2214from numba .typed import Dict
2315from numba .core import types
2416from numba import njit
2517
2618
27- # Time steps to solve ODE at. Each step is 1e-10 s
28- TIME_STEPS = np .linspace (0 , 10e-7 , 10001 )
29-
30-
31- class SimEvent :
32- """A simulated event from the kinematics pipeline.
33-
34- Parameters
35- ----------
36- kinematics: numpy.ndarray
37- Nx4 array of four vectors of all N nuclei in the simulated event. From first
38- to last column are px, py, pz, E.
39- vertex: numpy.ndarray
40- The reaction vertex position in meters. Given as a 3-array formated
41- [x,y,z].
42- proton_numbers: numpy.ndarray
43- Nx1 array of proton numbers of all nuclei from reaction and decays in the pipeline.
44- mass_numbers: numpy.ndarray
45- Nx1 array of mass numbers of all nuclei from reaction and decasy in the pipeline.
46-
47- Attributes
48- ----------
49- nuclei: list[SimParticle]
50- Instance of SimParticle for each nuclei in the exit channel of the pipeline.
51-
52- Methods
53- -------
54- digitize(config)
55- Transform the kinematics into point clouds
56- """
57-
58- def __init__ (
59- self ,
60- kinematics : np .ndarray ,
61- vertex : np .ndarray ,
62- proton_numbers : np .ndarray ,
63- mass_numbers : np .ndarray ,
64- ):
65- self .nuclei : list [SimParticle ] = []
66-
67- # Only simulate the nuclei in the exit channel
68- # Pattern is:
69- # skip target, projectile, take ejectile
70- # then skip every other. Append last nucleus as well
71- total_nuclei = len (kinematics ) - 1
72- for idx in range (2 , total_nuclei , 2 ):
73- if proton_numbers [idx ] == 0 :
74- continue
75- self .nuclei .append (
76- SimParticle (
77- kinematics [idx ], vertex , proton_numbers [idx ], mass_numbers [idx ]
78- )
79- )
80- if proton_numbers [- 1 ] != 0 :
81- self .nuclei .append (
82- SimParticle (
83- kinematics [- 1 ], vertex , proton_numbers [- 1 ], mass_numbers [- 1 ]
84- )
85- )
86-
87- def digitize (self , config : Config , rng : Generator ) -> np .ndarray :
88- """Transform the kinematics into point clouds
89-
90- Digitizes the simulated event, converting the number of
91- electrons detected on each pad to a signal in ADC units.
92- The hardware ID of each pad is included to make the complete
93- AT-TPC trace.
94-
95- Parameters
96- ----------
97- config: Config
98- The simulation configuration
99-
100- Returns
101- -------
102- point_array: numpy.ndarray
103- An Nx3 array representing the point cloud. Each row is a point, with elements
104- [pad id, time bucket, electrons]
105- """
106- # Sum points from all particles
107- points = Dict .empty (key_type = types .int64 , value_type = types .int64 )
108- for nuc in self .nuclei :
109- nuc .generate_point_cloud (config , rng , points )
110-
111- # Convert to numpy array of [pad, tb, e], now combined over pad/tb combos
112- point_array = dict_to_points (points )
113-
114- # Wiggle point TBs over interval [0.0, 1.0). This simulates effect of converting
115- # the (in principle) int TBs to floats.
116- point_array [:, 1 ] += rng .uniform (low = 0.0 , high = 1.0 , size = len (point_array ))
117-
118- # Remove points outside legal bounds in time. TODO check if this is needed
119- point_array = point_array [
120- np .logical_and (0 <= point_array [:, 1 ], point_array [:, 1 ] < NUM_TB )
121- ]
122-
123- return point_array
124-
125-
12619@njit
12720def dict_to_points (points : NumbaTypedDict [int , int ]) -> np .ndarray :
12821 """
@@ -149,205 +42,52 @@ def dict_to_points(points: NumbaTypedDict[int, int]) -> np.ndarray:
14942 return point_array
15043
15144
152- class SimParticle :
153- """
154- A nucleus in a simulated event
155-
156- Parameters
157- ----------
158- data: numpy.ndarray
159- Simulated four vector of nucleus from kinematics.
160- vertex: numpy.ndarray
161- The reaction vertex position in meters. Given as a 3-array formated
162- [x,y,z].
163- proton_number: int
164- Number of protons in nucleus
165- mass_number: int
166- Number of nucleons in nucleus
167-
168- Attributes
169- ----------
170- data: numpy.ndarray
171- Simulated four vector of nucleus from kinematics.
172- nucleus: spyral_utils.nuclear.NucleusData
173- Data of the simulated nucleus
174- vertex: numpy.ndarray
175- The reaction vertex position in meters. Given as a 3-array formated
176- [x,y,z].
177-
178- Methods
179- ----------
180- generate_track()
181- Solves EoM for this nucleus in the AT-TPC
182- generate_electrons()
183- Finds the number of electrons produced at each point of the
184- simulated track
185- generate_point_cloud()
186- Makes the AT-TPC point cloud of this nucleus' track
187- """
188-
189- def __init__ (
190- self ,
191- data : np .ndarray ,
192- vertex : np .ndarray ,
193- proton_number : int ,
194- mass_number : int ,
195- ):
196- self .data = data
197- self .nucleus = nuclear_map .get_data (proton_number , mass_number )
198- self .vertex = vertex
199-
200- def generate_track (self , config : Config ) -> np .ndarray :
201- """Solves EoM for this nucleus in the AT-TPC
202-
203- Solution is evaluated over a fixed time interval.
204-
205- Parameters
206- ----------
207- config: Config
208- The simulation configuration
209-
210- Returns
211- -------
212- numpy.ndarray
213- Nx6 array where each row is a solution to one of the ODEs evaluated at
214- the Nth time step.
215- """
216- # Find initial state of nucleus. (x, y, z, px, py, pz)
217- initial_state : np .ndarray = np .zeros (6 )
218- initial_state [:3 ] = self .vertex # m
219- initial_state [3 :] = self .data [:3 ] / self .nucleus .mass # unitless (gamma * beta)
220-
221- # Set ODE stop conditions. See SciPy solve_ivp docs
222- stop_condition .terminal = True
223- stop_condition .direction = - 1.0
224- forward_z_bound_condition .terminal = True
225- forward_z_bound_condition .direction = 1.0
226- backward_z_bound_condition .terminal = True
227- backward_z_bound_condition .direction = - 1.0
228- rho_bound_condition .terminal = True
229- rho_bound_condition .direction = 1.0
230-
231- track = solve_ivp (
232- equation_of_motion ,
233- (0.0 , 1.0 ), # Set upper time limit to 1 sec. This should never be reached
234- initial_state ,
235- method = "Radau" ,
236- events = [
237- stop_condition ,
238- forward_z_bound_condition ,
239- backward_z_bound_condition ,
240- rho_bound_condition ,
241- ],
242- t_eval = TIME_STEPS ,
243- args = (
244- config .det_params .bfield * - 1.0 ,
245- config .det_params .efield * - 1.0 ,
246- config .det_params .gas_target ,
247- self .nucleus ,
248- ),
249- )
250-
251- return track .y .T # Return transpose to easily index by row
252-
253- def generate_electrons (
254- self , config : Config , track : np .ndarray , rng : Generator
255- ) -> np .ndarray :
256- """Find the number of electrons made at each point of the nucleus' track.
257-
258- Parameters
259- ----------
260- config: Config
261- The simulation configuration
262- track: numpy.ndarray
263- Nx6 array where each row is a solution to one of the ODEs evaluated at
264- the Nth time step.
265- rng: numpy.random.Generator
266- numpy random number generator
267-
268- Returns
269- -------
270- numpy.ndarray
271- Returns an array of the number of electrons made at each
272- point in the trajectory
273- """
274- # Find energy of nucleus at each point of its track
275- gv = np .linalg .norm (track [:, 3 :], axis = 1 )
276- beta = np .sqrt (gv ** 2.0 / (1.0 + gv ** 2.0 ))
277- gamma = gv / beta
278- energy = self .nucleus .mass * (gamma - 1.0 ) # MeV
279-
280- # Find number of electrons created at each point of its track
281- electrons = np .zeros_like (energy )
282- electrons [1 :] = abs (np .diff (energy )) # Magnitude of energy lost
283- electrons *= (
284- 1.0e6 / config .det_params .w_value
285- ) # Convert to eV to match units of W-value
286-
287- # Adjust number of electrons by Fano factor, can only have integer amount of electrons
288- electrons = np .array (
289- [
290- rng .normal (point , np .sqrt (config .det_params .fano_factor * point ))
291- for point in electrons
292- ],
293- dtype = np .int64 ,
294- )
295- return electrons
296-
297- def generate_point_cloud (
298- self , config : Config , rng : Generator , points : NumbaTypedDict
299- ):
300- """Create the point cloud
301-
302- Finds the pads hit by the electrons transported from each point
303- of the nucleus' trajectory to the pad plane.
304-
305- Parameters
306- ----------
307- config: Config
308- The simulation configuration
309- rng: numpy.random.Generator
310- numpy random number generator
311- points: numba.typed.Dict[int, int]
312- A dictionary mapping a unique pad,tb key to the number of electrons.
313- """
314- # Generate nucleus' track and calculate the electrons made at each point
315- track = self .generate_track (config )
316- electrons = self .generate_electrons (config , track , rng )
317-
318- # Remove points in trajectory that create less than 1 electron
319- mask = electrons >= 1
320- track = track [mask ]
321- electrons = electrons [mask ]
45+ def simulate (
46+ momenta : np .ndarray ,
47+ vertex : np .ndarray ,
48+ proton_numbers : np .ndarray ,
49+ mass_numbers : np .ndarray ,
50+ config : Config ,
51+ rng : Generator ,
52+ indicies : list [int ] | None ,
53+ ) -> np .ndarray :
54+ nuclei_to_sim = None
55+ if indicies is not None :
56+ nuclei_to_sim = indicies
57+ else :
58+ # default nuclei to sim, all final outgoing particles
59+ nuclei_to_sim = [idx for idx in range (2 , len (proton_numbers ), 2 )]
60+ nuclei_to_sim .append (len (proton_numbers ) - 1 ) # add the last
61+
62+ points = Dict .empty (key_type = types .int64 , value_type = types .int64 )
63+
64+ for idx in nuclei_to_sim :
65+ if proton_numbers [idx ] == 0 :
66+ continue
67+ nucleus = nuclear_map .get_data (proton_numbers [idx ], mass_numbers [idx ])
68+ momentum = momenta [idx ]
69+ generate_point_cloud (momentum , vertex , nucleus , config , rng , points )
32270
323- # Apply gain factor from micropattern gas detectors
324- electrons *= config . det_params . mpgd_gain
71+ # Convert to numpy array of [pad, tb, e], now combined over pad/tb combos
72+ point_array = dict_to_points ( points )
32573
326- # Convert z position of trajectory to exact time buckets
327- dv = config .drift_velocity
328- track [:, 2 ] = (
329- config .det_params .length - track [:, 2 ]
330- ) / dv + config .elec_params .micromegas_edge
74+ # Wiggle point TBs over interval [0.0, 1.0). This simulates effect of converting
75+ # the (in principle) int TBs to floats.
76+ point_array [:, 1 ] += rng .uniform (low = 0.0 , high = 1.0 , size = len (point_array ))
33177
332- if config .pad_grid_edges is None or config .pad_grid is None :
333- raise ValueError ("Pad grid is not loaded at SimParticle.generate_hits!" )
78+ # Remove points outside legal bounds in time. TODO check if this is needed
79+ point_array = point_array [
80+ np .logical_and (0 <= point_array [:, 1 ], point_array [:, 1 ] < NUM_TB )
81+ ]
33482
335- transport_track (
336- config .pad_grid ,
337- config .pad_grid_edges ,
338- config .det_params .diffusion ,
339- config .det_params .efield ,
340- config .drift_velocity ,
341- track ,
342- electrons ,
343- points ,
344- )
83+ return point_array
34584
34685
34786def run_simulation (
34887 config : Config ,
34988 input_path : Path ,
35089 writer : SimulationWriter ,
90+ indicies : list [int ] | None = None ,
35191):
35292 """Run the simulation
35393
@@ -398,7 +138,7 @@ def run_simulation(
398138 dataset : h5 .Dataset = input_data_group [f"chunk_{ chunk } " ][ # type: ignore
399139 f"event_{ event_number } "
400140 ] # type: ignore
401- sim = SimEvent (
141+ cloud = simulate (
402142 dataset [:].copy (), # type: ignore
403143 np .array (
404144 [
@@ -409,9 +149,11 @@ def run_simulation(
409149 ),
410150 proton_numbers , # type: ignore
411151 mass_numbers , # type: ignore
152+ config ,
153+ rng ,
154+ indicies ,
412155 )
413156
414- cloud = sim .digitize (config , rng )
415157 if len (cloud ) == 0 :
416158 continue
417159
0 commit comments