forked from jmw464/KalmanML
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhelices.py
275 lines (231 loc) · 10.7 KB
/
helices.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
import numpy as np
import utils
import scipy.optimize as opt
import scipy.stats as stats
import matplotlib.pyplot as plt
class Helix:
# The 3 points to which the helix is fitted
points = None
# After aligning the B field with the z direction,
# this is the center of the circle interpolated to the points projected to the xy-plane
center = None
# Radius of the circle interpolated to the points projected to the xy-plane
radius = None
# Phase of the parameterization
phi = None
# Angular frequency of the parameterization
omega = None
# Rotation matrix to go to the coordinate system where B is pointed in the z-direction
Rot = None
# Inverse of Rot, equal to transpose of Rot_inv
Rot_inv = None
# The point in points to start stepping from, either given or automatically chosen as the furthest one from the origin.
start_point = None
def __init__(self):
pass
def solve(self, p1, p2, p3, B):
"""
Solves a helix from three space points given the local direction of the B field
---
p1, p2, p3 : float(3) : space points in cartesian coordinates
B : float(3) : direction of B field, magnitude unimportant
---
success : bool : Whether the helix was successfully solved
"""
self.points = np.array([p1, p2, p3])
dists = [np.sum(p**2) for p in self.points]
self.start_point = self.points[np.argmax(dists)]
def define_circle(p1, p2, p3):
"""
Copy pasted from https://stackoverflow.com/questions/28910718/give-3-points-and-a-plot-circle
"""
temp = p2[0] * p2[0] + p2[1] * p2[1]
bc = (p1[0] * p1[0] + p1[1] * p1[1] - temp) / 2
cd = (temp - p3[0] * p3[0] - p3[1] * p3[1]) / 2
det = (p1[0] - p2[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p2[1])
# assert abs(det) > 1.0e-6, "Points in a straight line"
if abs(det) < 1.0e-6:
raise ValueError
# Center of circle
cx = (bc*(p2[1] - p3[1]) - cd*(p1[1] - p2[1])) / det
cy = ((p1[0] - p2[0]) * cd - (p2[0] - p3[0]) * bc) / det
radius = np.sqrt((cx - p1[0])**2 + (cy - p1[1])**2)
return np.array([cx, cy, 0]), radius
def rotation_matrix(a, b=np.array([0, 0, 1])):
if (a == b).all():
return np.identity(3)
v = np.cross(a, b)
s = np.sqrt(np.sum(v**2))
c = np.dot(a, b)
I = np.identity(3)
v_x = np.array([
[0 , -v[2], v[1] ],
[v[2] , 0 , -v[0]],
[-v[1], v[0] , 0 ]
])
return I + v_x + ((1 - c)/s**2) * (v_x @ v_x)
Bnorm = B / np.sqrt(np.sum(B**2))
self.Rot = rotation_matrix(Bnorm)
self.Rot_inv = np.transpose(self.Rot)
p1r = self.Rot @ p1
p2r = self.Rot @ p2
p3r = self.Rot @ p3
self.center, self.radius = define_circle(p1r[:2], p2r[:2], p3r[:2])
p1r -= self.center
p2r -= self.center
p3r -= self.center
# Assume all points differ by no more than one complete arc
rotated_points = np.array([p1r, p2r, p3r])
rotated_points = rotated_points[np.argsort(rotated_points[:,-1])]
angles = np.empty(rotated_points.shape[0])
for i, p in enumerate(rotated_points):
angles[i] = utils.arctan(p[1], p[0])
pos_angles = np.copy(angles)
for i in [1, 2]:
while pos_angles[i] < pos_angles[i-1]:
pos_angles[i] += 2*np.pi
neg_angles = np.copy(angles)
for i in [1, 2]:
while neg_angles[i] > neg_angles[i-1]:
neg_angles[i] -= 2*np.pi
angles = pos_angles if abs(pos_angles[1] - pos_angles[0]) < abs(neg_angles[1] - neg_angles[0]) else neg_angles
try:
result = stats.linregress(rotated_points[:,-1], angles)
except ValueError:
print("Linear regression error:", [p1, p2, p3])
print(rotated_points[:,-1])
return False
self.omega, self.phi = result.slope, result.intercept
return True
def curve(self, t):
"""
Parametric equation for the helix
---
t : float : Parametric variable
---
pos : float(3) : Spacial position of the helix in cartesian coordinates
"""
x = self.radius * np.cos(self.omega * t + self.phi) + self.center[0]
y = self.radius * np.sin(self.omega * t + self.phi) + self.center[1]
z = t
return self.Rot_inv @ np.array([x, y, z])
def stepper(self, stepsize):
"""
Generator function that yields spaces points on helix
---
stepsize : float : Spacial stepsize between consecutive points on helix to be yielded
---
point : float(3) : Next point on the helix
t : float(3) : Time of next point. NB: This is not a physical time, only a parameterization
"""
THRESHOLD_RADIUS = 1000 # Radius at which we assume the particle will make at most 1 precession.
speed = np.sqrt(1 + (self.radius * self.omega)**2)
delta_t = stepsize / speed
start_rot = self.Rot @ self.start_point
assert np.sum(self.start_point**2) == np.sum(start_rot**2)
start_r = np.sqrt(np.sum(start_rot**2))
if start_rot[2] < start_r and self.radius > THRESHOLD_RADIUS:
start_phi = utils.arctan(start_rot[1], start_rot[0])
t = (start_phi - self.phi) / self.omega
else:
t = start_rot[2]
direction = np.int32(np.sum(self.curve(t)**2) < np.sum(self.curve(t + delta_t)**2))
direction = 2 * direction - 1
while True:
yield self.curve(t), t
t += direction * delta_t
def newton_intersection(helix, module, start_t):
"""
Finds the intersection of a helix and a module using Newton's method
---
helix : Helix : helix object
module : pd.Dataframe : module specifications loaded from standard detector file
start_t : float : initial estimation parametric variable, i.e. helix.curve(start_t) is near the module
---
pos : float(3) : position of the intersection
"""
module_center = np.array([module.cx, module.cy, module.cz])
module_rotation = np.array([
[module.rot_xu, module.rot_xv, module.rot_xw],
[module.rot_yu, module.rot_yv, module.rot_yw],
[module.rot_zu, module.rot_zv, module.rot_zw]
])
def curve_mf(t):
pos_xyz = helix.curve(t)
pos_uvw = np.transpose(module_rotation) @ (pos_xyz - module_center)
return pos_uvw
try:
intersection_time = opt.newton(lambda t: curve_mf(t)[2], start_t)
except(RuntimeError):
return None
# Check if intersection is within module boundaries
u, v, w = curve_mf(intersection_time)
if not utils.check_module_boundary(u, v, module):
return None
return helix.curve(intersection_time)
def find_helix_intersection(helix, geometry, stepsize):
"""
Find intersection between a helix and the next layer
---
helix : Helix : helix object
geometry : pd.Dataframe : dataframe with entries being all modules in the detector
stepsize : float : step size with which to go along the helix before applying Newton's method, in mm.
---
intersection : float(3) : spacial position of intersection
intersection_vol : int : volume id for intersection layer
intersection_lay : int : layer id for intersection layer
"""
# Boundaries of the detector
R_BOUND = 1050
Z_BOUND = 3000
stepper = helix.stepper(stepsize)
init_pos, _ = next(stepper)
init_vol, init_lay, _ = geometry.nearest_layer(init_pos)
intersection = None
intersection_vol = None
intersection_lay = None
while intersection is None:
curr_pos, curr_time = next(stepper)
if abs(curr_pos[2]) > Z_BOUND or np.sqrt(curr_pos[0]**2 + curr_pos[1]**2) > R_BOUND:
break
curr_vol, curr_lay, curr_dist = geometry.nearest_layer(curr_pos)
if curr_vol is None:
continue
different_layer = curr_vol != init_vol or curr_lay != init_lay
near_next_layer = different_layer and curr_dist < 2*stepsize
if near_next_layer:
near_modules = geometry.nearest_modules(curr_vol, curr_lay, curr_pos)
for i in range(near_modules.shape[0]):
module = near_modules.iloc[i]
intersection = newton_intersection(helix, module, curr_time)
if intersection is not None:
intersection_vol, intersection_lay, _ = geometry.nearest_layer(intersection)
assert intersection_vol == curr_vol and intersection_lay == curr_lay
break
return intersection, intersection_vol, intersection_lay
def get_next_hits(points, stepsize, radius, hit_locator, geometry):
"""
Find all plausible next hits given last three hits
---
points : float(3,3) : last three points of track candidate
stepsize : float : step size in mm with which to step on helix
radius : float : search radius in mm around helix intersection
hit_locator : utils.HitLocator : hit locating object
geometry : utils.Geometry : detector geometry object
---
hits : float(10,n) : next hits
intersection : float(3,3) : helix intersection where hits are found
"""
helix = Helix()
avg_pos = np.mean(points, axis=0)
solve_success = helix.solve(points[0], points[1], points[2], geometry.bmap.get(avg_pos))
if not solve_success:
print("Helix solver broken")
return None, None
intersection, inter_vol, inter_lay = find_helix_intersection(helix, geometry, stepsize)
if intersection is None:
return None, None
inter_phi = utils.arctan(intersection[1], intersection[0])
inter_t = intersection[2] if inter_vol in geometry.BARRELS else np.sqrt(intersection[0]**2 + intersection[1]**2)
projection = (inter_phi, inter_t)
return hit_locator.get_hits_around(inter_vol, inter_lay, projection, radius), intersection