Skip to content

Commit d99a62d

Browse files
authored
Merge pull request #59 from ATTPC/gwm17_dev
PolarArbitrary and doc updates
2 parents acc46fe + f2799a4 commit d99a62d

File tree

5 files changed

+164
-25
lines changed

5 files changed

+164
-25
lines changed

docs/user_guide/detector/index.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ includes effects pointed out by Adam Anthony in his [thesis](https://ezproxy.msu
1313
The detector system code simulates an event by going through a series of steps to
1414
ultimately produce its point cloud. For each event, the detector system:
1515

16-
1. Generate the trajectory of the each nucleus in the exit channel of the event by solving its equation of motion in the AT-TPC with a fine grained-timestep
16+
1. Generate the trajectory of the each nucleus in the exit channel (or the nuclei specified by the user) of the event by solving its equation of motion in the AT-TPC with a fine grained-timestep
1717
2. Determines how many electrons are created at each point of each nucleus' trajectory
1818
3. Converts the z coordinate of each point in each trajectory to a GET electronics sampling-frequency based Time Bucket using the electron drift velocity
1919
3. Transports each point of the nucleus' trajectory to the pad plane, applying diffusion if requested, to identify the sensing pad for each timestep
@@ -141,6 +141,17 @@ step reaction a(b,c)d a=0, b=1, c=2, d=3. In a two step, a(b,c)d->e+f, e=4, f=5,
141141
on for more complex scenarios. These labels are particularly useful for evaluating the
142142
performance of machine learning methods like clustering in downstream analyses.
143143

144+
## Which nuclei get simulated in the detector simulation
145+
146+
By default, the detector simulation considers all nuclei in the exit channel. That is,
147+
in a two step reaction a(b,c)d->e+f, the nuclei c, e, f are included in the detector
148+
simulation and used to generate a point cloud. However, this is not always desireable.
149+
In some usecases, only on particle is interesting and all others simply generate extra
150+
data. As such, the `run_simulation` function contains an optional keyword argument
151+
`indices` which can be used to specify the indices of the nuclei to include in the
152+
detector simulation. Indices follow the same convention as the kinematics simulation,
153+
i.e. for our earlier example two step reaction a=0, b=1, c=2, d=3, e=4, and f=5.
154+
144155
## Why Point clouds
145156

146157
It is worth elaborating why the detector system outputs point clouds instead of raw

docs/user_guide/getting_started.md

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Getting Started
22

3-
Once attpc_engine is installed it's time to setup our simulation project. Below is an outline of what we'd expect your project structure to look like
3+
Once attpc_engine is installed it's time to setup our simulation project. Below is an
4+
outline of what we'd expect your project structure to look like
45

56
```txt
67
|---my_sim
@@ -13,13 +14,18 @@ Once attpc_engine is installed it's time to setup our simulation project. Below
1314
| | |---detector
1415
```
1516

16-
You have a folder (in this case called `my_sim`) with two Python files (`generate_kinematics.py` and `apply_detector.py`) a virtual environment with attpc_engine installed (`.venv`) and an output directory with a folder for kinematics and detector effects. Note that you may want the output to be stored on a remote disk or other location as simulation files can be big at times (> 1 GB).
17+
You have a folder (in this case called `my_sim`) with two Python files
18+
(`generate_kinematics.py` and `apply_detector.py`) a virtual environment with
19+
attpc_engine installed (`.venv`) and an output directory with a folder for kinematics
20+
and detector effects. Note that you may want the output to be stored on a remote disk
21+
or other location as simulation files can be big at times (> 1 GB).
1722

1823
Now lets talk about generating and sampling a kinematics phase space!
1924

2025
## Sampling Kinematics
2126

22-
Below is an example script for running a kinematics sample, which in our example project we would write in `my_sim/generate_kinematics.py`
27+
Below is an example script for running a kinematics sample, which in our example
28+
project we would write in `my_sim/generate_kinematics.py`
2329

2430
```python
2531
from attpc_engine.kinematics import (
@@ -70,10 +76,13 @@ if __name__ == "__main__":
7076
main()
7177
```
7278

73-
First we import all of our pieces from the attpc_engine library and its dependencies. We also import the Python standard library Path object to handle our file paths.
79+
First we import all of our pieces from the attpc_engine library and its dependencies.
80+
We also import the Python standard library Path object to handle our file paths.
7481

75-
We then start to define our kinematics configuration. First we define the output path to be an HDF5 file in our output directory.
76-
We also load a spyral-utils [gas target](https://attpc.github.io/spyral-utils/api/nuclear/target) from a file path.
82+
We then start to define our kinematics configuration. First we define the output path
83+
to be an HDF5 file in our output directory.
84+
We also load a spyral-utils [gas target](https://attpc.github.io/spyral-utils/api/nuclear/target)
85+
from a file path.
7786

7887
```python
7988
target = load_target(target_path, nuclear_map)
@@ -90,7 +99,21 @@ nevents = 10000
9099
beam_energy = 184.131 # MeV
91100
```
92101

93-
Now were ready to define our kinematics Pipeline. The first argument of the Pipeline is a list of steps, where each step is either a Reaction or Deacy. The first element of the list must *always* be a Reaction, and all subsequent steps are *always* Decays. The residual of the previous step is *always* the parent of the next step. The Pipeline will attempt to validate this information for you. We also must define a list of ExcitationDistribution objects. These describe the state in the residual that is populated by each Reaction/Decay. There is exactly *one* distribution per step. There are two types of predefined ExcitationDistributions (ExcitationGaussian and ExcitationUniform), but others can be implemented by implementing the ExcitationDistribution protocol. Similarly, we use PolarUniform to define a uniform polar angle distribution from 0 degrees to 180 degrees in the center of mass frame (technically, this is from -1 to 1 in cos(polar)). We also define our KinematicsTargetMaterial, which includes our gas target, as well as the allowed space within the target for our reaction vertex (range in z in meters and standard deviation of cylindrical ρ in meters).
102+
Now were ready to define our kinematics Pipeline. The first argument of the Pipeline
103+
is a list of steps, where each step is either a Reaction or Deacy. The first element
104+
of the list must *always* be a Reaction, and all subsequent steps are *always* Decays.
105+
The residual of the previous step is *always* the parent of the next step. The
106+
Pipeline will attempt to validate this information for you. We also must define a list
107+
of ExcitationDistribution objects. These describe the state in the residual that is
108+
populated by each Reaction/Decay. There is exactly *one* distribution per step. There
109+
are two types of predefined ExcitationDistributions (ExcitationGaussian and
110+
ExcitationUniform), but others can be implemented by implementing the
111+
ExcitationDistribution protocol. Similarly, we use PolarUniform to define a uniform
112+
polar angle distribution from 0 degrees to 180 degrees in the center of mass frame
113+
(technically, this is from -1 to 1 in cos(polar)). We also define our
114+
KinematicsTargetMaterial, which includes our gas target, as well as the allowed space
115+
within the target for our reaction vertex (range in z in meters and standard deviation
116+
of cylindrical ρ in meters).
94117

95118
```python
96119
pipeline = KinematicsPipeline(
@@ -126,11 +149,13 @@ if __name__ == "__main__":
126149
main()
127150
```
128151

129-
That's it! This script will then sample 10000 events from the kinematic phase space of ${}^{16}C(d,d')$ and write the data out to an HDF5 file in our output directory.
152+
That's it! This script will then sample 10000 events from the kinematic phase space of
153+
${}^{16}C(d,d')$ and write the data out to an HDF5 file in our output directory.
130154

131155
## Applying the Detector
132156

133-
Below is an example script for running a kinematics sample, which in our example project we would write in `my_sim/apply_detector.py`
157+
Below is an example script for running a kinematics sample, which in our example
158+
project we would write in `my_sim/apply_detector.py`
134159

135160
```python
136161
from attpc_engine.detector import (
@@ -193,7 +218,10 @@ if __name__ == "__main__":
193218
main()
194219
```
195220

196-
Just like in the kinematics script, we start off by importing a whole bunch of code. Next we define our kinematics input (which is the output of the kinematics script) and an output path. Note that for the output path we simply specify a directory; this is because our writer will handle breaking up the output data into reasonably sized files.
221+
Just like in the kinematics script, we start off by importing a whole bunch of code.
222+
Next we define our kinematics input (which is the output of the kinematics script) and
223+
an output path. Note that for the output path we simply specify a directory; this is
224+
because our writer will handle breaking up the output data into reasonably sized files.
197225

198226
```python
199227
input_path = Path("output/kinematics/c16dd_d2_300Torr_184MeV.h5")
@@ -209,7 +237,8 @@ if not isinstance(gas, GasTarget):
209237
raise Exception(f"Could not load target data from {target_path}!")
210238
```
211239

212-
and finally we begin to define the detector specific configuration, which is ultimately stored in a Config object.
240+
and finally we begin to define the detector specific configuration, which is ultimately
241+
stored in a Config object.
213242

214243
```python
215244
detector = DetectorParams(
@@ -237,13 +266,21 @@ pads = PadParams()
237266
config = Config(detector, electronics, pads)
238267
```
239268

240-
Note that by not passing any arguments to `PadParams` we are using the default pad description that is bundled with the package. See the [detector](./detector/index.md) guide for more details. For the output, we create a SpyralWriter object. This will take in the simulation data and convert it to match the format expected by the Spyral analysis. Note the final argument of the Writer; this is the maximum size of an individual file in events (here we've specified 5,000 events). The writer will then split our output up into many files, which will help later when trying to analyze the data with a framework like Spyral.
269+
Note that by not passing any arguments to `PadParams` we are using the default pad
270+
description that is bundled with the package. See the [detector](./detector/index.md)
271+
guide for more details. For the output, we create a SpyralWriter object. This will take
272+
in the simulation data and convert it to match the format expected by the Spyral
273+
analysis. Note the final argument of the Writer; this is the maximum size of an
274+
individual file in events (here we've specified 5,000 events). The writer will then
275+
split our output up into many files, which will help later when trying to analyze the
276+
data with a framework like Spyral.
241277

242278
```python
243279
writer = SpyralWriter(output_path, config, 5_000)
244280
```
245281

246-
Then, just like in the kinematics script we set up a main function and set it to be run when the script is processed
282+
Then, just like in the kinematics script we set up a main function and set it to be
283+
run when the script is processed
247284

248285
```python
249286
def main():
@@ -257,7 +294,25 @@ if __name__ == "__main__":
257294
main()
258295
```
259296

260-
And just like that, we can now take our kinematic samples and apply detector effects!
297+
And just like that, we can now take our kinematic samples and apply detector effects!
298+
Note that by default the simulation will apply detector effects and generate a point cloud
299+
containing *every single final product* from the kinematics simulation. If you only want
300+
to generate a point cloud containing specific particles you can utilize the `indices`
301+
argument of `run_simulation`:
302+
303+
```python
304+
def main():
305+
run_simulation(
306+
config,
307+
input_path,
308+
writer,
309+
indices=[2],
310+
)
311+
```
312+
313+
The `indices` argument is a list of particle indices to be included in the simulation.
314+
The indicies follow the same convention as the kinematics simulation: i.e. for a reaction
315+
a(b,c)d a=0, b=1, c=2, d=3. If you have a decay a(b,c)d->e+f e=4, f=5, and so on.
261316

262317
## More Details
263318

src/attpc_engine/detector/simulator.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def simulate(
5656
mass_numbers: np.ndarray,
5757
config: Config,
5858
rng: Generator,
59-
indicies: list[int],
59+
indices: list[int],
6060
) -> tuple[np.ndarray, np.ndarray]:
6161
"""Apply detector simulation to a kinematics event
6262
@@ -78,7 +78,7 @@ def simulate(
7878
The detector simulation parameters
7979
rng: numpy.random.Generator
8080
A random number generator
81-
indicies: list[int]
81+
indices: list[int]
8282
The indicies in the list of nuclei which should be simulated.
8383
Typically this would be all final products of the reaction
8484
@@ -93,7 +93,7 @@ def simulate(
9393
points = Dict.empty(
9494
key_type=types.int64, value_type=types.Tuple(types=[types.int64, types.int64])
9595
)
96-
for idx in indicies:
96+
for idx in indices:
9797
if proton_numbers[idx] == 0:
9898
continue
9999
nucleus = nuclear_map.get_data(proton_numbers[idx], mass_numbers[idx])
@@ -119,7 +119,7 @@ def run_simulation(
119119
config: Config,
120120
input_path: Path,
121121
writer: SimulationWriter,
122-
indicies: list[int] | None = None,
122+
indices: list[int] | None = None,
123123
):
124124
"""Run the detector simulation
125125
@@ -134,7 +134,7 @@ def run_simulation(
134134
Path to HDF5 file containing kinematics
135135
writer: SimulationWriter
136136
An object which implements the SimulationWriter Protocol
137-
indicies: list[int] | None
137+
indices: list[int] | None
138138
List of which nuclei to include in the detector simulation. Nuclei are
139139
specified by index of which they occur in the kinematic arrays. For example,
140140
in a simple one step reaction, a(b,c)d 0=a, 1=b, 2=c, 3=d. For two step
@@ -150,8 +150,8 @@ def run_simulation(
150150

151151
# Decide which nuclei to sim, either by user input or all reaction final products
152152
nuclei_to_sim = None
153-
if indicies is not None:
154-
nuclei_to_sim = indicies
153+
if indices is not None:
154+
nuclei_to_sim = indices
155155
else:
156156
# default nuclei to sim, all final outgoing particles
157157
nuclei_to_sim = [idx for idx in range(2, len(proton_numbers), 2)]

src/attpc_engine/kinematics/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
ExcitationBreitWigner,
1414
)
1515

16-
from .angle import PolarDistribution, PolarUniform
16+
from .angle import PolarDistribution, PolarUniform, PolarArbitrary
1717

1818
from .reaction import Reaction, Decay
1919

@@ -26,6 +26,7 @@
2626
"ExcitationUniform",
2727
"ExcitationBreitWigner",
2828
"PolarDistribution",
29+
"PolarArbitrary",
2930
"PolarUniform",
3031
"Reaction",
3132
"Decay",

src/attpc_engine/kinematics/angle.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def sample(self, rng: Generator) -> float: # type: ignore
2626
2727
Returns
2828
-------
29-
float:
29+
float
3030
The sampled angle in radians
3131
"""
3232
pass
@@ -74,7 +74,79 @@ def sample(self, rng: Generator) -> float:
7474
7575
Returns
7676
-------
77-
float:
77+
float
7878
The sampled angle in radians
7979
"""
8080
return np.arccos(rng.uniform(self.cos_angle_min, self.cos_angle_max))
81+
82+
83+
class PolarArbitrary:
84+
"""An arbitrary, finite-precision polar angular distribution in the CM frame
85+
86+
Define an arbitrary probability distribution for the polar angle using an
87+
array of uniformly-spaced angles and their probabilities, as well as the spacing
88+
of the angle values. We then sample from the distribution and smear the output angle
89+
within the spacing.
90+
91+
Note: If your distribution is defined using *any* of the pre-defined distributions,
92+
you should favor using those over PolarArbitrary. Sampling of arbitrary functions
93+
comes with a runtime performance penalty. That is: do not use this to sample from a
94+
uniform distribution.
95+
96+
Parameters
97+
----------
98+
angles: numpy.ndarray
99+
The array of *lower* angle values. Each angle corresponds to a bin covering
100+
angle + bin_width. Angles should be in units of radians.
101+
probabilities: numpy.ndarray
102+
The probability of a given angle bin. Should sum to 1.0
103+
angle_bin_width: float
104+
The width of the angle bins in radians
105+
106+
Attributes
107+
----------
108+
angle_width: float
109+
The angle bin width in radians
110+
probs: numpy.ndarray
111+
The probability of a given angle bin
112+
angles: numpy.ndarray
113+
The array of *lower* angle values. Each angle corresponds to a bin covering
114+
angle + bin_width. Angles should be in units of radians.
115+
116+
Methods
117+
-------
118+
sample(rng)
119+
Sample the distribution, returning a polar angle in radians
120+
"""
121+
122+
def __init__(
123+
self,
124+
angles: np.ndarray,
125+
probabilities: np.ndarray,
126+
angle_bin_width: float,
127+
):
128+
if np.sum(probabilities) > 1.0:
129+
raise ValueError(
130+
f"The sum of the probabilities passed to PolarArbitrary should be 1.0. Yours sum to {np.sum(probabilities)}"
131+
)
132+
self.angle_width = angle_bin_width
133+
self.probs = probabilities
134+
self.angles = angles
135+
136+
def sample(self, rng: Generator) -> float:
137+
"""Sample the distribution, returning a polar angle in radians
138+
139+
Parameters
140+
----------
141+
rng: numpy.random.Generator
142+
The random number generator
143+
144+
Returns
145+
-------
146+
float
147+
The sampled angle in radians
148+
"""
149+
return (
150+
rng.choice(self.angles, p=self.probs)
151+
+ rng.uniform(0.0, 1.0) * self.angle_width
152+
)

0 commit comments

Comments
 (0)