Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
43b191d
Single particle dedx diagnosis
Jan 14, 2025
51c4085
single particle reco all points
yeonjaej Jan 14, 2025
8646e14
removed ghost point for dedx calc
yeonjaej Jan 16, 2025
ce67fe4
Dedx Diagnosis
Jan 17, 2025
624039c
Clear pycache
Jan 18, 2025
5c4d398
Update version.py
francois-drielsma Jan 16, 2025
4d76c9e
Single particle dedx diagnosis
Jan 14, 2025
1cd4bd4
now returns number of voxels too
yeonjaej Jan 21, 2025
26f5717
Merge remote-tracking branch 'origin/develop' into dedx
yeonjaej Jan 21, 2025
b73ac87
Added startpoint logging to single shower diagnosis
Jan 23, 2025
6226161
Merge branch 'develop' of https://github.com/DeepLearnPhysics/spine i…
Jan 23, 2025
8b989d8
Added more diagnosis (startpoint) to shower_dedx_singlep
Jan 23, 2025
f0f729a
Merge branch 'develop' of https://github.com/DeepLearnPhysics/spine i…
Jan 23, 2025
046c692
get the best matched particle
yeonjaej Jan 24, 2025
f4c23dd
Shower and Interaction add attributes
Jan 28, 2025
d41807a
Temporary fix for duplicate (run, subrun, event) handling
Jan 28, 2025
82e6930
Merged with duplicate handling feature branch
Jan 31, 2025
9293fd0
dedx algorithm uses PCA
yeonjaej Jan 31, 2025
fb46439
Merge remote-tracking branch 'origin/develop' into dedx
yeonjaej Feb 3, 2025
38a304f
Merge conflict due to duplicate handling
Feb 5, 2025
db2bd23
Change hard-coded max_length to 150
Feb 10, 2025
c2be426
Merge branch 'develop' of https://github.com/DeepLearnPhysics/spine i…
Feb 10, 2025
405ae8d
Merge tag 'v0.2.3' into dedx
yeonjaej Feb 10, 2025
735e7b4
tiny fix in the dedx algo
yeonjaej Feb 12, 2025
2aec1cd
Add option to skip showers in containment cut
Feb 12, 2025
e3f4bab
Merge branch 'develop' of https://github.com/DeepLearnPhysics/spine i…
Feb 12, 2025
ffdcd73
Fix for flash-matching naming fix
Feb 17, 2025
35f74c2
Merge branch 'develop' of https://github.com/DeepLearnPhysics/spine i…
Feb 17, 2025
840defe
Merge branch 'develop' of https://github.com/DeepLearnPhysics/spine i…
Feb 17, 2025
128b9b8
eps = 0.59
yeonjaej Feb 18, 2025
3c06894
Added attributes to particle / interaction used for shower cuts
Feb 19, 2025
fc716fa
Merge branch 'develop' of https://github.com/DeepLearnPhysics/spine i…
Feb 19, 2025
c91c132
Merge with yjwa dedx
Feb 19, 2025
959eba8
Added additional varaibles for shower cut
Feb 20, 2025
155b0cc
Remove unused cut parameters
Feb 22, 2025
4b7fcaf
Merge branch 'develop' of https://github.com/DeepLearnPhysics/spine i…
Feb 22, 2025
25772f4
Option for skipping containment check for showers
Feb 22, 2025
c3f7719
Add new cut varaibles to particle and interaction objects
Feb 22, 2025
5fe11ba
Added new shower spread and modifies processor to have inplace option
Feb 22, 2025
44fc4fd
Merge branch 'develop' of https://github.com/francois-drielsma/spine …
Feb 22, 2025
6a46c8d
Merge branch 'develop' of https://github.com/DeepLearnPhysics/spine i…
Feb 24, 2025
7d9885a
Add option for setting maximum michel ke for shape logic
Feb 25, 2025
3862b9d
Merge with local develop
Feb 25, 2025
c059c03
startpoint not in the voxels, taken care of
yeonjaej Feb 25, 2025
3c5f054
when used simple argument, return dedx value only
yeonjaej Feb 25, 2025
a658bac
added comments
yeonjaej Feb 25, 2025
1cb4ada
DBScan-PCA method tiny bug fix with dx estimation
yeonjaej Feb 26, 2025
3f3a5e0
Added various post-processors for computing cut variables for nue sel…
Feb 26, 2025
d7eef2e
Merge with yjwa/dedx
Feb 26, 2025
eefedd8
Added option to apply different ke thresholds in InteractionTopologyP…
Feb 27, 2025
7f3dd50
Fixed zero division error
Feb 27, 2025
6250f9d
Fixed shower spread processor crashing with tiny weights (zero division)
Feb 27, 2025
e2c73ad
Added weighted pseudovertex finder
Mar 6, 2025
ae7178d
Added alternative vertex finding algorithm processor
Mar 6, 2025
84f821a
Added additional cut variables and relaxed conversion distance
Mar 6, 2025
5b71cb3
Added sequential particle PID thresholding post processor
Mar 6, 2025
892cf80
Added additional attributes for nue diagnosis (temporary)
Mar 6, 2025
94d6558
Resolve merge conflict
Mar 6, 2025
d93da08
Diagnostic attributes (temporary)
Mar 14, 2025
1d3a2b1
Resolve merge conflict
Mar 14, 2025
9dab4a8
Added bragg peak detection
Mar 18, 2025
999c1c7
Resolve merge conflict
Mar 18, 2025
12b953b
Move assert to allow not to provide run ID from the chain in the tran…
francois-drielsma Mar 18, 2025
37adbd2
Merge branch 'develop' of https://github.com/francois-drielsma/spine …
francois-drielsma Mar 18, 2025
85b2438
Minor changes to shower quality checks
Mar 18, 2025
758445e
Added comments
Mar 19, 2025
27cf875
Changed pearsonr calculation to using start_dir, changed michel taggi…
Mar 21, 2025
a07cb2c
Clean vertex post-processing and added comments
Mar 21, 2025
10407cf
Added comments and finalized cut variables for nue analysis
Mar 21, 2025
f516851
Added comments to all shower post-processors
Mar 21, 2025
3ec8809
Clean unused attributes and remove duplicate handling
Mar 21, 2025
dea6737
Further cleaning of duplicate handling (removed)
Mar 21, 2025
5c80248
Completely removed duplicate handling
Mar 21, 2025
4d4fddd
Fix minor bugs and cleaning
Mar 21, 2025
156a31e
Logical error in configuration check of ParticleThresholdProcessor
francois-drielsma Mar 24, 2025
441f08a
Renamed post-processors, refactored reco interaction attributes
Mar 24, 2025
d022dc6
Moved vertexing utils to proper place and added option to use alterna…
Mar 24, 2025
5d93b87
Removed shower_dedx diagnosis uncleaned file
Mar 24, 2025
395ba75
Removed sequential pid thresholding (as the original is already seque…
Mar 24, 2025
7300693
Bug fix in set_visible_devices function
francois-drielsma Mar 25, 2025
0d784df
Remove bug in trunk straightness and remove inplace (that does nothin…
Mar 25, 2025
721165f
Removed cluster_dedx_legacy, which is redundant
Mar 25, 2025
fe2c6af
Changed default bogus values to have only one value
Mar 25, 2025
b44c150
Leading shower was not retriving showers
Mar 25, 2025
83653fc
Merge branch 'DeepLearnPhysics:develop' into develop
francois-drielsma Mar 25, 2025
c701098
Merge pull request #9 from dkoh0207/develop
francois-drielsma Mar 25, 2025
bb3981f
Add check in transparency calibration module that a run ID is provide…
francois-drielsma Mar 25, 2025
762ee69
Merge branch 'develop' of https://github.com/francois-drielsma/spine …
francois-drielsma Mar 25, 2025
56bf751
First pass at cleaning up new NuE selection tools
francois-drielsma Mar 27, 2025
39c5f34
Fixed track/shower merging post-processor
francois-drielsma Mar 28, 2025
58af198
Reorganized post folder
francois-drielsma Mar 28, 2025
2099f0c
Bug fixes in particle merging (start point + calo KE)
francois-drielsma Mar 29, 2025
75fad59
Removed superfluous post-processor
francois-drielsma Mar 29, 2025
d3ab7ed
Add option to draw arrows/directions in visualization tools
francois-drielsma Mar 31, 2025
4866fef
Avoid in-place modifications of calibration configuration
francois-drielsma Mar 31, 2025
b993a63
Bug fixed when running interaction clustering metric ana script on th…
francois-drielsma Mar 31, 2025
64f95a9
Typo fix in truth fragment loader
francois-drielsma Apr 1, 2025
84d958a
Added basic logical checks for post-processor ordering
francois-drielsma Apr 1, 2025
e94337b
Got rid of derived attributes of reco interactions for cleanliness
francois-drielsma Apr 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions spine/ana/metric/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def __init__(self, obj_type=None, use_objects=False, per_object=True,
self.label_key = label_key

# Parse the label_col column, if necessary
self.label_col = None
if label_col is not None:
self.label_col = enum_factory('cluster', label_col)

Expand All @@ -106,7 +107,8 @@ def __init__(self, obj_type=None, use_objects=False, per_object=True,
keys[label_key] = True
for obj in self.obj_type:
keys[f'{obj}_clusts'] = True
keys[f'{obj}_shapes'] = True
if obj != 'interaction':
keys[f'{obj}_shapes'] = True

else:
keys['points'] = True
Expand Down Expand Up @@ -150,7 +152,8 @@ def process(self, data):
label_col = self.label_col or self.label_cols[obj_type]
num_points = len(data[self.label_key])
labels = data[self.label_key][:, label_col]
shapes = data[self.label_key][:, SHAPE_COL]
if obj_type != 'interaction':
shapes = data[self.label_key][:, SHAPE_COL]
num_truth = len(np.unique(labels[labels > -1]))

else:
Expand All @@ -170,7 +173,8 @@ def process(self, data):
num_reco = len(data[f'{obj_type}_clusts'])
for i, index in enumerate(data[f'{obj_type}_clusts']):
preds[index] = i
shapes[index] = data[f'{obj_type}_shapes'][i]
if obj_type != 'interaction':
shapes[index] = data[f'{obj_type}_shapes'][i]

else:
# Use clusters from the object indexes
Expand Down
2 changes: 1 addition & 1 deletion spine/build/fragment.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ def load_truth(self, data):

def _load_truth(self, truth_fragments, points_label, depositions_label,
depositions_q_label=None, points=None, depositions=None,
points_g4=None, depositons_g4=None, sources_label=None,
points_g4=None, depositions_g4=None, sources_label=None,
sources=None):
"""Load :class:`TruthFragment` objects from their stored versions.

Expand Down
17 changes: 16 additions & 1 deletion spine/data/out/interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import numpy as np

from spine.utils.globals import PID_LABELS, PID_TAGS
from spine.utils.globals import SHOWR_SHP, PID_LABELS, PID_TAGS
from spine.utils.decorators import inherit_docstring

from spine.data.neutrino import Neutrino
Expand Down Expand Up @@ -308,6 +308,21 @@ def __str__(self):
"""
return 'Reco' + super().__str__()

@property
def leading_shower(self):
"""Leading primary shower of this interaction.

Returns
-------
RecoParticle
Primary shower with the highest kinetic energy
"""
showers = [part for part in self.primary_particles if part.shape == SHOWR_SHP]
if len(showers) == 0:
return None

return max(showers, key=lambda x: x.ke)


@dataclass(eq=False)
@inherit_docstring(TruthBase, InteractionBase)
Expand Down
104 changes: 73 additions & 31 deletions spine/data/out/particle.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from scipy.spatial.distance import cdist

from spine.utils.globals import (
TRACK_SHP, SHAPE_LABELS, PID_LABELS, PID_MASSES, PID_TO_PDG)
SHOWR_SHP, TRACK_SHP, SHAPE_LABELS, PID_LABELS, PID_MASSES, PID_TO_PDG)
from spine.utils.decorators import inherit_docstring

from spine.data.particle import Particle
Expand Down Expand Up @@ -212,19 +212,28 @@ class RecoParticle(ParticleBase, RecoBase):
(M) List of indexes of PPN points associated with this particle
ppn_points : np.ndarray
(M, 3) List of PPN points tagged to this particle
vertex_distance: float
vertex_distance : float
Set-to-point distance between all particle points and the parent
interaction vertex. (untis of cm)
shower_split_angle: float
Estimate of the opening angle of the shower. If particle is not a
shower, then this is set to -1. (units of degrees)
interaction vertex position in cm
start_dedx : float
dE/dx around a user-defined neighborhood of the start point in MeV/cm
start_straightness : float
Explained variance ratio of the beginning of the particle
directional_spread : float
Estimate of the angular spread of the particle (cosine spread)
axial_spread : float
Pearson correlation coefficient of the axial profile of the particle
w.r.t. to the distance from its start point
"""
pid_scores: np.ndarray = None
primary_scores: np.ndarray = None
ppn_ids: np.ndarray = None
ppn_points: np.ndarray = None
vertex_distance: float = -1.
shower_split_angle: float = -1.
start_dedx: float = -1.
start_straightness: float = -1.
directional_spread: float = -1.
axial_spread: float = -np.inf

# Fixed-length attributes
_fixed_length_attrs = (
Expand Down Expand Up @@ -265,19 +274,34 @@ def __str__(self):
def merge(self, other):
"""Merge another particle instance into this one.

This method can only merge two track objects with well defined start
and end points.
The merging strategy differs depending on the the particle shapes
merged together. There are two categories:
- Track + track
- The start/end points are produced by finding the combination of points
which are farthest away from each other (one from each constituent)
- The primary scores/primary status match that of the constituent
particle with the highest primary score
- The PID scores/PID value match that of the constituent particle with
the highest primary score
- Shower + Track
- The track is always merged into the shower, not the other way around
- The start point of the shower is updated to be the track end point
further away from the current shower start point
- The primary scores/primary status match that of the constituent
particle with the highest primary score
- The PID scores/PID value is kept unchanged (that of the shower)

Parameters
----------
other : RecoParticle
Other reconstructed particle to merge into this one
"""
# Check that both particles being merged are tracks
assert self.shape == TRACK_SHP and other.shape == TRACK_SHP, (
"Can only merge two track particles.")
# Check that the particles being merged fit one of two categories
assert (self.shape in (SHOWR_SHP, TRACK_SHP) and
other.shape == TRACK_SHP), (
"Can only merge two track particles or a track into a shower.")

# Check that neither particle has yet been matches
# Check that neither particle has yet been matched
assert not self.is_matched and not other.is_matched, (
"Cannot merge particles that already have matches.")

Expand All @@ -287,27 +311,45 @@ def merge(self, other):
setattr(self, attr, val)

# Select end points and end directions appropriately
points_i = np.vstack([self.start_point, self.end_point])
points_j = np.vstack([other.start_point, other.end_point])
dirs_i = np.vstack([self.start_dir, self.end_dir])
dirs_j = np.vstack([other.start_dir, other.end_dir])
if self.shape == TRACK_SHP:
# If two tracks, pick points furthest apart
points_i = np.vstack([self.start_point, self.end_point])
points_j = np.vstack([other.start_point, other.end_point])
dirs_i = np.vstack([self.start_dir, self.end_dir])
dirs_j = np.vstack([other.start_dir, other.end_dir])

dists = cdist(points_i, points_j)
max_index = np.argmax(dists)
max_i, max_j = max_index//2, max_index%2

self.start_point = points_i[max_i]
self.end_point = points_j[max_j]
self.start_dir = dirs_i[max_i]
self.end_dir = dirs_j[max_j]

dists = cdist(points_i, points_j)
max_index = np.argmax(dists)
max_i, max_j = max_index//2, max_index%2
else:
# If a shower and a track, pick track point furthest from shower
points_i = self.start_point.reshape(-1, 3)
points_j = np.vstack([other.start_point, other.end_point])
dirs_j = np.vstack([other.start_dir, other.end_dir])

dists = cdist(points_i, points_j)
max_j = np.argmax(dists)

self.start_point = points_i[max_i]
self.end_point = points_j[max_j]
self.start_dir = dirs_i[max_i]
self.end_dir = dirs_j[max_j]
self.start_point = points_j[max_j]
self.start_dir = dirs_j[max_j]

# If one of the two particles is a primary, the new one is
# Match primary/PID to the most primary particle
if other.primary_scores[-1] > self.primary_scores[-1]:
self.primary_scores = other.primary_scores
self.is_primary = other.is_primary
if self.shape == TRACK_SHP:
self.pid_scores = other.pid_scores
self.pid = other.pid

# For PID, pick the most confident prediction (could be better...)
if np.max(other.pid_scores) > np.max(self.pid_scores):
self.pid_scores = other.pid_scores
# If the calorimetric KEs have been computed, can safely sum
if other.calo_ke > 0.:
self.calo_ke += other.calo_ke

@property
def mass(self):
Expand Down Expand Up @@ -387,12 +429,12 @@ def momentum(self, momentum):
def reco_ke(self):
"""Alias for `ke`, to match nomenclature in truth."""
return self.ke

@property
def reco_momentum(self):
"""Alias for `momentum`, to match nomenclature in truth."""
return self.momentum

@property
def reco_length(self):
"""Alias for `length`, to match nomenclature in truth."""
Expand All @@ -402,7 +444,7 @@ def reco_length(self):
def reco_start_dir(self):
"""Alias for `start_dir`, to match nomenclature in truth."""
return self.start_dir

@property
def reco_end_dir(self):
"""Alias for `end_dir`, to match nomenclature in truth."""
Expand Down
14 changes: 12 additions & 2 deletions spine/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ def __init__(self, cfg, rank=None):
assert self.model is None or self.unwrap, (
"Must unwrap the model output to run post-processors.")
self.watch.initialize('post')
self.post = PostManager(post, parent_path=self.parent_path)
self.post = PostManager(
post, post_list=self.post_list, parent_path=self.parent_path)

# Initialize the analysis scripts
self.ana = None
Expand Down Expand Up @@ -354,12 +355,21 @@ def initialize_io(self, loader=None, reader=None, writer=None):
self.watch.initialize('unwrap')
self.unwrapper = Unwrapper(geometry=geo)

# If working from LArCV files, no post-processor was yet run
self.post_list = ()

else:
# Initialize the reader
self.watch.initialize('read')
self.reader = reader_factory(reader)
self.iter_per_epoch = len(self.reader)

# Fetch the list of previously run post-processors
# TODO: this only works with two runs in a row, not 3 and above
self.post_list = None
if self.reader.cfg is not None:
self.post_list = tuple(self.reader.cfg['post'])

# Fetch an appropriate common prefix for all input files
self.log_prefix, self.output_prefix = self.get_prefixes(
self.reader.file_paths, self.split_output)
Expand Down Expand Up @@ -448,7 +458,7 @@ def get_prefixes(file_paths, split_output):
log_prefix += f'--{suffix}'

# Truncate file names that are too long
max_length = 230
max_length = 150
if len(log_prefix) > max_length:
log_prefix = log_prefix[:max_length-3] + '---'

Expand Down
6 changes: 6 additions & 0 deletions spine/post/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ class PostBase(ABC):
# Units in which the post-processor expects objects to be expressed in
units = 'cm'

# Whether this post-processor needs to know where the configuration lives
need_parent_path = False

# Set of data keys needed for this post-processor to operate
_keys = ()

# Set of post-processors which must be run before this one is
_upstream = ()

# List of recognized object types
_obj_types = ('fragment', 'particle', 'interaction')

Expand Down
7 changes: 3 additions & 4 deletions spine/post/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

from spine.utils.factory import module_dict, instantiate

from . import reco, metric, optical, crt, trigger
from . import reco, truth, optical, crt, trigger

# Build a dictionary of available calibration modules
POST_DICT = {}
for module in [reco, metric, optical, crt, trigger]:
for module in [reco, truth, optical, crt, trigger]:
POST_DICT.update(**module_dict(module))


Expand All @@ -29,8 +29,7 @@ def post_processor_factory(name, cfg, parent_path=None):
cfg['name'] = name

# Instantiate the post-processor module
# TODO: This is hacky, fix it
if name == 'flash_match':
if name in POST_DICT and POST_DICT[name].need_parent_path:
return instantiate(POST_DICT, cfg, parent_path=parent_path)
else:
return instantiate(POST_DICT, cfg)
26 changes: 18 additions & 8 deletions spine/post/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,45 @@ class PostManager:
It loads all the post-processor objects once and feeds them data.
"""

def __init__(self, cfg, parent_path=None):
def __init__(self, cfg, post_list=None, parent_path=None):
"""Initialize the post-processing manager.

Parameters
----------
cfg : dict
Post-processor configurations
post_list : List[str], optional
List of post-processors which have already been run
parent_path : str, optional
Path to the analysis tools configuration file
"""
# Loop over the post-processor modules and get their priorities
cfg = deepcopy(cfg)
keys = np.array(list(cfg.keys()))
priorities = -np.ones(len(keys), dtype=np.int32)
for i, k in enumerate(keys):
if 'priority' in cfg[k]:
priorities[i] = cfg[k].pop('priority')
for i, key in enumerate(keys):
if 'priority' in cfg[key]:
priorities[i] = cfg[key].pop('priority')

# Add the modules to a processor list in decreasing order of priority
self.watch = StopwatchManager()
self.modules = OrderedDict()
keys = keys[np.argsort(-priorities)]
for k in keys:
for key in keys:
# Profile the module
self.watch.initialize(k)
self.watch.initialize(key)

# Append
self.modules[k] = post_processor_factory(
k, cfg[k], parent_path=parent_path)
self.modules[key] = post_processor_factory(
key, cfg[key], parent_path=parent_path)

# Check dependencies
if post_list is not None:
ups_post = tuple(self.modules)
for post in self.modules[key]._upstream:
assert post in (post_list + ups_post), (
f"Post-processor `{key}` is missing an essential "
f"upstream post-processor: `{post}`.")

def __call__(self, data):
"""Pass one batch of data through the post-processors.
Expand Down
3 changes: 3 additions & 0 deletions spine/post/optical/flash_matching.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class FlashMatchProcessor(PostBase):
# Alternative allowed names of the post-processor
aliases = ('run_flash_matching',)

# Whether this post-processor needs to know where the configuration lives
need_parent_path = True

def __init__(self, flash_key, volume, ref_volume_id=None,
method='likelihood', detector=None, geometry_file=None,
run_mode='reco', truth_point_mode='points',
Expand Down
2 changes: 1 addition & 1 deletion spine/post/reco/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
from .calo import *
from .pid import *
from .kinematics import *
from .label import *
from .shower import *
from .topology import *
Loading
Loading