77from scipy .spatial .distance import cdist
88
99from spine .utils .globals import (
10- TRACK_SHP , SHAPE_LABELS , PID_LABELS , PID_MASSES , PID_TO_PDG )
10+ SHOWR_SHP , TRACK_SHP , SHAPE_LABELS , PID_LABELS , PID_MASSES , PID_TO_PDG )
1111from spine .utils .decorators import inherit_docstring
1212
1313from spine .data .particle import Particle
@@ -212,19 +212,28 @@ class RecoParticle(ParticleBase, RecoBase):
212212 (M) List of indexes of PPN points associated with this particle
213213 ppn_points : np.ndarray
214214 (M, 3) List of PPN points tagged to this particle
215- vertex_distance: float
215+ vertex_distance : float
216216 Set-to-point distance between all particle points and the parent
217- interaction vertex. (untis of cm)
218- shower_split_angle: float
219- Estimate of the opening angle of the shower. If particle is not a
220- shower, then this is set to -1. (units of degrees)
217+ interaction vertex position in cm
218+ start_dedx : float
219+ dE/dx around a user-defined neighborhood of the start point in MeV/cm
220+ start_straightness : float
221+ Explained variance ratio of the beginning of the particle
222+ directional_spread : float
223+ Estimate of the angular spread of the particle (cosine spread)
224+ axial_spread : float
225+ Pearson correlation coefficient of the axial profile of the particle
226+ w.r.t. to the distance from its start point
221227 """
222228 pid_scores : np .ndarray = None
223229 primary_scores : np .ndarray = None
224230 ppn_ids : np .ndarray = None
225231 ppn_points : np .ndarray = None
226232 vertex_distance : float = - 1.
227- shower_split_angle : float = - 1.
233+ start_dedx : float = - 1.
234+ start_straightness : float = - 1.
235+ directional_spread : float = - 1.
236+ axial_spread : float = - np .inf
228237
229238 # Fixed-length attributes
230239 _fixed_length_attrs = (
@@ -265,19 +274,34 @@ def __str__(self):
265274 def merge (self , other ):
266275 """Merge another particle instance into this one.
267276
268- This method can only merge two track objects with well defined start
269- and end points.
277+ The merging strategy differs depending on the the particle shapes
278+ merged together. There are two categories:
279+ - Track + track
280+ - The start/end points are produced by finding the combination of points
281+ which are farthest away from each other (one from each constituent)
282+ - The primary scores/primary status match that of the constituent
283+ particle with the highest primary score
284+ - The PID scores/PID value match that of the constituent particle with
285+ the highest primary score
286+ - Shower + Track
287+ - The track is always merged into the shower, not the other way around
288+ - The start point of the shower is updated to be the track end point
289+ further away from the current shower start point
290+ - The primary scores/primary status match that of the constituent
291+ particle with the highest primary score
292+ - The PID scores/PID value is kept unchanged (that of the shower)
270293
271294 Parameters
272295 ----------
273296 other : RecoParticle
274297 Other reconstructed particle to merge into this one
275298 """
276- # Check that both particles being merged are tracks
277- assert self .shape == TRACK_SHP and other .shape == TRACK_SHP , (
278- "Can only merge two track particles." )
299+ # Check that the particles being merged fit one of two categories
300+ assert (self .shape in (SHOWR_SHP , TRACK_SHP ) and
301+ other .shape == TRACK_SHP ), (
302+ "Can only merge two track particles or a track into a shower." )
279303
280- # Check that neither particle has yet been matches
304+ # Check that neither particle has yet been matched
281305 assert not self .is_matched and not other .is_matched , (
282306 "Cannot merge particles that already have matches." )
283307
@@ -287,27 +311,45 @@ def merge(self, other):
287311 setattr (self , attr , val )
288312
289313 # Select end points and end directions appropriately
290- points_i = np .vstack ([self .start_point , self .end_point ])
291- points_j = np .vstack ([other .start_point , other .end_point ])
292- dirs_i = np .vstack ([self .start_dir , self .end_dir ])
293- dirs_j = np .vstack ([other .start_dir , other .end_dir ])
314+ if self .shape == TRACK_SHP :
315+ # If two tracks, pick points furthest apart
316+ points_i = np .vstack ([self .start_point , self .end_point ])
317+ points_j = np .vstack ([other .start_point , other .end_point ])
318+ dirs_i = np .vstack ([self .start_dir , self .end_dir ])
319+ dirs_j = np .vstack ([other .start_dir , other .end_dir ])
320+
321+ dists = cdist (points_i , points_j )
322+ max_index = np .argmax (dists )
323+ max_i , max_j = max_index // 2 , max_index % 2
324+
325+ self .start_point = points_i [max_i ]
326+ self .end_point = points_j [max_j ]
327+ self .start_dir = dirs_i [max_i ]
328+ self .end_dir = dirs_j [max_j ]
294329
295- dists = cdist (points_i , points_j )
296- max_index = np .argmax (dists )
297- max_i , max_j = max_index // 2 , max_index % 2
330+ else :
331+ # If a shower and a track, pick track point furthest from shower
332+ points_i = self .start_point .reshape (- 1 , 3 )
333+ points_j = np .vstack ([other .start_point , other .end_point ])
334+ dirs_j = np .vstack ([other .start_dir , other .end_dir ])
335+
336+ dists = cdist (points_i , points_j )
337+ max_j = np .argmax (dists )
298338
299- self .start_point = points_i [max_i ]
300- self .end_point = points_j [max_j ]
301- self .start_dir = dirs_i [max_i ]
302- self .end_dir = dirs_j [max_j ]
339+ self .start_point = points_j [max_j ]
340+ self .start_dir = dirs_j [max_j ]
303341
304- # If one of the two particles is a primary, the new one is
342+ # Match primary/PID to the most primary particle
305343 if other .primary_scores [- 1 ] > self .primary_scores [- 1 ]:
306344 self .primary_scores = other .primary_scores
345+ self .is_primary = other .is_primary
346+ if self .shape == TRACK_SHP :
347+ self .pid_scores = other .pid_scores
348+ self .pid = other .pid
307349
308- # For PID, pick the most confident prediction (could be better...)
309- if np . max ( other .pid_scores ) > np . max ( self . pid_scores ) :
310- self .pid_scores = other .pid_scores
350+ # If the calorimetric KEs have been computed, can safely sum
351+ if other .calo_ke > 0. :
352+ self .calo_ke + = other .calo_ke
311353
312354 @property
313355 def mass (self ):
@@ -387,12 +429,12 @@ def momentum(self, momentum):
387429 def reco_ke (self ):
388430 """Alias for `ke`, to match nomenclature in truth."""
389431 return self .ke
390-
432+
391433 @property
392434 def reco_momentum (self ):
393435 """Alias for `momentum`, to match nomenclature in truth."""
394436 return self .momentum
395-
437+
396438 @property
397439 def reco_length (self ):
398440 """Alias for `length`, to match nomenclature in truth."""
@@ -402,7 +444,7 @@ def reco_length(self):
402444 def reco_start_dir (self ):
403445 """Alias for `start_dir`, to match nomenclature in truth."""
404446 return self .start_dir
405-
447+
406448 @property
407449 def reco_end_dir (self ):
408450 """Alias for `end_dir`, to match nomenclature in truth."""
0 commit comments