Skip to content

Commit f3bf93e

Browse files
committed
WIP: New application pctaddnoise
1 parent cc1c0ee commit f3bf93e

2 files changed

Lines changed: 110 additions & 93 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env python
2+
import argparse
3+
import json
4+
import sys
5+
import itk
6+
from itk import PCT as pct
7+
import numpy as np
8+
import numpy.lib.recfunctions as rfn
9+
import hepunits
10+
11+
12+
def build_parser():
13+
parser = pct.PCTArgumentParser(
14+
description="Add noise to ROOT data"
15+
)
16+
parser.add_argument(
17+
"-i",
18+
"--input",
19+
help="Root phase space file of particles",
20+
required=True,
21+
)
22+
parser.add_argument("-t","--tree", help="Root tree name", required=True)
23+
parser.add_argument("-o", "--output", help="Output file name", required=True)
24+
25+
parser.add_argument('--seed', help="Random number generator seed", type=int)
26+
parser.add_argument('--noise-energy', help="Standard deviation of the Gaussian noise on the energy", type=float)
27+
parser.add_argument('--noise-time', help="Standard deviation of the Gaussian noise on the time", type=float)
28+
29+
parser.add_argument(
30+
"--verbose", "-v", help="Verbose execution", default=False, action="store_true"
31+
)
32+
33+
tracker_uncertainty_group = parser.add_argument_group("Tracker uncertainty")
34+
tracker_uncertainty_group.add_argument("--material-budget", help="Material budget x/x0", type=float, default=5e-3)
35+
tracker_uncertainty_group.add_argument("--noise-position", help="Standard deviation of the tracker uncertainty (mm)", type=float)
36+
tracker_uncertainty_group.add_argument("--tracker-distance", help="Distance between trackers (cm)", type=float)
37+
38+
return parser
39+
40+
def get_sigma_sc(energy, x_over_x0, sp, dt):
41+
proton_mass_c2 = 938.272013 * hepunits.MeV
42+
betap = (energy + 2 * proton_mass_c2) * energy / (energy + proton_mass_c2)
43+
44+
# Equation (25) and (26) from [Krah et al, PMB, 2018]:
45+
T = np.zeros((2, 2))
46+
T[0, 1] = 1
47+
T[1, 0] = -1 / dt
48+
T[1, 1] = 1 / dt
49+
50+
sigma = (
51+
13.6 * hepunits.MeV / betap * np.sqrt(x_over_x0) * (1 + 0.038 * np.log(x_over_x0))
52+
)
53+
sigma_sc = np.zeros((energy.size, 2, 2))
54+
sigma_sc[:, 1, 1] = sigma**2
55+
return np.tile(sp**2 * T @ T.T, (energy.size, 1, 1)) + sigma_sc
56+
57+
def add_tracker_uncertainty(data, rng, material_budget, noise_position, tracker_distance):
58+
e = data["KineticEnergy"]
59+
sigma = get_sigma_sc(e, material_budget, noise_position * hepunits.mm, tracker_distance * hepunits.cm)
60+
w, q = np.linalg.eig(np.linalg.inv(sigma))
61+
xr = rng.standard_normal((e.size, 2, 2))
62+
W = np.zeros((e.size, 2, 2))
63+
W[:, 0, 0] = 1.0 / np.sqrt(w[:, 0])
64+
W[:, 1, 1] = 1.0 / np.sqrt(w[:, 1])
65+
dy_uncert = np.matmul(np.matmul(q, W), xr)
66+
data['Position_X'] += dy_uncert[:, 0, 0]
67+
data['Position_Y'] += dy_uncert[:, 0, 1]
68+
data['Direction_X'] += dy_uncert[:, 1, 0]
69+
data['Direction_Y'] += dy_uncert[:, 1, 1]
70+
71+
def add_gaussian_noise(data, branch, rng, noise):
72+
if noise is not None:
73+
try:
74+
data[branch] += rng.normal(scale=noise, size=len(data["KineticEnergy"]))
75+
except KeyError:
76+
print(f"Warning: cannot apply noise of {noise} on branch {branch} as the branch does not exist in the Root file! Skipping.", file=sys.stderr)
77+
78+
def process(args_info: argparse.Namespace):
79+
import uproot
80+
81+
rng = np.random.default_rng(args_info.seed)
82+
83+
if args_info.verbose:
84+
print("Reading input Root file")
85+
tree = uproot.open(args_info.input)[args_info.tree]
86+
data = tree.arrays(library="np")
87+
88+
if args_info.verbose:
89+
print("Applying noise…")
90+
91+
add_tracker_uncertainty(data, rng, args_info.material_budget, args_info.noise_position, args_info.tracker_distance)
92+
93+
add_gaussian_noise(data, "KineticEnergy", rng, args_info.noise_energy)
94+
add_gaussian_noise(data, "LocalTime", rng, args_info.noise_time)
95+
96+
if args_info.verbose:
97+
print("Writing output Root file…")
98+
with uproot.recreate(args_info.output) as output_file:
99+
output_file[args_info.tree] = data
100+
101+
102+
def main(argv=None):
103+
parser = build_parser()
104+
args_info = parser.parse_args(argv)
105+
process(args_info)
106+
107+
108+
if __name__ == "__main__":
109+
main()

applications/pctpairprotons/pctpairprotons.py

Lines changed: 1 addition & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#!/usr/bin/env python
22
import argparse
33
import json
4-
import sys
54
import itk
65
from itk import PCT as pct
76
import numpy as np
@@ -73,95 +72,8 @@ def build_parser():
7372
"--psout", help="Name of tree in output phase space", default="PhaseSpace"
7473
)
7574

76-
data_noise_group = parser.add_argument_group("Data noise")
77-
data_noise_group.add_argument('--noise-measurements', help="Standard deviation of the Gaussian noise on the measurements (energy or time)", type=float)
78-
data_noise_group.add_argument('--noise-position', help="Standard deviation of the Gaussian noise on the position", type=float)
79-
data_noise_group.add_argument('--tracker-distance', help="Distance between the two trackers of the upstream and downstream detectors, used to calculate noise on directions", type=float)
80-
data_noise_group.add_argument('--seed', help="Random number generator seed", type=int)
81-
8275
return parser
8376

84-
def add_noise(pairs, measurement_column, noise_measurements, noise_position, tracker_distance, seed):
85-
86-
rng = np.random.default_rng(seed)
87-
88-
if noise_measurements is not None:
89-
measurement_in = pairs[measurement_column + "_in"]
90-
measurement_out = pairs[measurement_column + "_out"]
91-
measurement_in += rng.normal(scale=noise_measurements, size=len(measurement_in))
92-
measurement_out += rng.normal(scale=noise_measurements, size=len(measurement_out))
93-
94-
if noise_position is not None:
95-
96-
u_in = pairs["u_in"]
97-
u_out = pairs["u_out"]
98-
v_in = pairs["v_in"]
99-
v_out = pairs["v_out"]
100-
101-
noise_u_in = u_in + rng.normal(scale=noise_position, size=len(u_in))
102-
noise_u_out = u_out + rng.normal(scale=noise_position, size=len(u_out))
103-
noise_v_in = v_in + rng.normal(scale=noise_position, size=len(v_in))
104-
noise_v_out = v_out + rng.normal(scale=noise_position, size=len(v_out))
105-
106-
if tracker_distance is None:
107-
print("Warning: noise on position was provided, but tracker distance is unspecified. No noise on directions will be applied.", file=sys.stderr)
108-
else:
109-
# Recover the point on the second tracker in each detector
110-
w_in = pairs["w_in"]
111-
w_out = pairs["w_out"]
112-
du_in = pairs["du_in"]
113-
du_out = pairs["du_out"]
114-
dv_in = pairs["dv_in"]
115-
dv_out = pairs["dv_out"]
116-
dw_in = pairs["dw_in"]
117-
dw_out = pairs["dw_out"]
118-
119-
w_in_2 = w_in - tracker_distance
120-
slope_in = (w_in_2 - w_in) / dw_in
121-
u_in_2 = u_in + slope_in * du_in
122-
v_in_2 = v_in + slope_in * dv_in
123-
124-
w_out_2 = w_out + tracker_distance
125-
slope_out = (w_out_2 - w_out) / dw_out
126-
u_out_2 = u_out + slope_out * du_out
127-
v_out_2 = v_out + slope_out * dv_out
128-
129-
# Add noise to the point on the second tracker
130-
noise_u_in_2 = u_in_2 + rng.normal(scale=noise_position, size=len(u_in_2))
131-
noise_v_in_2 = v_in_2 + rng.normal(scale=noise_position, size=len(v_in_2))
132-
noise_u_out_2 = u_out_2 + rng.normal(scale=noise_position, size=len(u_out_2))
133-
noise_v_out_2 = v_out_2 + rng.normal(scale=noise_position, size=len(v_out_2))
134-
135-
# Build a direction from these noisy points
136-
noise_du_in = noise_u_in - noise_u_in_2
137-
noise_dv_in = noise_v_in - noise_v_in_2
138-
noise_dw_in = tracker_distance
139-
noise_du_out = noise_u_out_2 - noise_u_out
140-
noise_dv_out = noise_v_out_2 - noise_v_out
141-
noise_dw_out = tracker_distance
142-
143-
norm_in = np.sqrt(noise_du_in**2 + noise_dv_in**2 + noise_dw_in**2)
144-
norm_out = np.sqrt(noise_du_out**2 + noise_dv_out**2 + noise_dw_out**2)
145-
146-
noise_du_in /= norm_in
147-
noise_dv_in /= norm_in
148-
noise_dw_in /= norm_in
149-
noise_du_out /= norm_out
150-
noise_dv_out /= norm_out
151-
noise_dw_out /= norm_out
152-
153-
pairs["du_in"] = noise_du_in
154-
pairs["du_out"] = noise_du_out
155-
pairs["dv_in"] = noise_dv_in
156-
pairs["dv_out"] = noise_dv_out
157-
pairs["dw_in"] = noise_dw_in
158-
pairs["dw_out"] = noise_dw_out
159-
160-
pairs["u_in"] = noise_u_in
161-
pairs["v_in"] = noise_v_in
162-
pairs["u_out"] = noise_u_out
163-
pairs["v_out"] = noise_v_out
164-
16577

16678
def process(args_info: argparse.Namespace):
16779
import uproot
@@ -176,7 +88,7 @@ def verbose(message):
17688
def verbose(message):
17789
pass
17890

179-
measurement_column = "PreGlobalTime" if args_info.store_time else "KineticEnergy"
91+
measurement_column = "LocalTime" if args_info.store_time else "KineticEnergy"
18092

18193
def load_tree_as_df(root_file, tree_name):
18294

@@ -274,10 +186,6 @@ def load_tree_as_df(root_file, tree_name):
274186
np.recarray.sort(pairs, order=["RunID", "EventID", "TrackID_in", "TrackID_out"])
275187
verbose("Merged input and output phase spaces.")
276188

277-
if args_info.noise_measurements is not None or args_info.noise_position is not None:
278-
verbose("Adding noise…")
279-
add_noise(pairs, measurement_column, args_info.noise_measurements, args_info.noise_position, args_info.tracker_distance, args_info.seed)
280-
281189
if args_info.fit is not None:
282190
verbose("Converting energy loss or TOF to WEPL…")
283191
with open(args_info.fit, encoding="utf-8") as f:

0 commit comments

Comments
 (0)