Skip to content

Commit d019ed3

Browse files
committed
Refactor of simulator, solver. Add option for selective sim.
1 parent 709d59c commit d019ed3

3 files changed

Lines changed: 202 additions & 300 deletions

File tree

src/attpc_engine/detector/simulator.py

Lines changed: 41 additions & 299 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
from .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
103
from .writer import SimulationWriter
114
from .constants import NUM_TB
125
from .. import nuclear_map
@@ -16,113 +9,13 @@
169
import numpy as np
1710
import h5py as h5
1811
from tqdm import trange
19-
from scipy.integrate import solve_ivp
2012
from numpy.random import default_rng, Generator
2113
from pathlib import Path
2214
from numba.typed import Dict
2315
from numba.core import types
2416
from 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
12720
def 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

34786
def 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

Comments
 (0)