Skip to content

Commit d529990

Browse files
Propagate field-corrected calibration points
1 parent 71f4cb7 commit d529990

5 files changed

Lines changed: 203 additions & 13 deletions

File tree

src/spine/calib/manager.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def __init__(
4242
# Add the modules to a processor list in configuration order
4343
parsed = parse_module_config(cfg)
4444
names = [spec["name"] for spec in parsed.values()]
45+
self.update_points = "field" in names
4546

4647
# Make sure the essential calibration modules are present
4748
if not gain_applied and "recombination" in names and "gain" not in names:
@@ -83,7 +84,7 @@ def __call__(
8384
track: bool | None = None,
8485
meta: Meta | None = None,
8586
module_id: int | None = None,
86-
) -> NDArray[np.floating]:
87+
) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
8788
"""Main calibration driver.
8889
8990
Parameters
@@ -110,10 +111,13 @@ def __call__(
110111
111112
Returns
112113
-------
114+
np.ndarray
115+
(N, 3) array of calibrated point coordinates
113116
np.ndarray
114117
(N) array of calibrated depositions in ADC, e- or MeV
115118
"""
116119
# If necessary, convert all points to detector coordinates
120+
orig_points = points
117121
if meta is not None:
118122
points = meta.to_cm(points, center=True)
119123
if module_id is not None:
@@ -136,6 +140,7 @@ def __call__(
136140
tpc_indexes = self.geo.get_closest_tpc_indexes(points)
137141

138142
# Loop over the TPCs, apply the relevant calibration corrections
143+
new_points = np.copy(points) if self.update_points else orig_points
139144
new_values = np.copy(values)
140145
for t in range(self.geo.tpc.num_chambers):
141146
# Restrict to the TPC of interest
@@ -164,6 +169,14 @@ def __call__(
164169
self.watch.stop(key)
165170

166171
# Append
172+
if self.update_points:
173+
new_points[tpc_indexes[t]] = tpc_points
167174
new_values[tpc_indexes[t]] = tpc_values
168175

169-
return new_values
176+
if self.update_points:
177+
if module_id is not None:
178+
new_points = self.geo.translate(new_points, module_id, 0)
179+
if meta is not None:
180+
new_points = meta.to_px(new_points)
181+
182+
return new_points, new_values

src/spine/model/full_chain.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -950,7 +950,7 @@ def run_calibration(
950950
run_id = run_info[b // rep].run if run_info is not None else None
951951

952952
# Calibrate voxel values
953-
values_b = self.calibrator(
953+
voxels_b, values_b = self.calibrator(
954954
voxels_b,
955955
values_b,
956956
sources_b,
@@ -959,6 +959,10 @@ def run_calibration(
959959
module_id=b % rep,
960960
)
961961

962+
if self.calibrator.update_points:
963+
data.tensor[lower:upper, COORD_COLS] = torch.tensor(
964+
voxels_b, dtype=data.dtype, device=data.device
965+
)
962966
data.tensor[lower:upper, VALUE_COL] = torch.tensor(
963967
values_b, dtype=data.dtype, device=data.device
964968
)

src/spine/post/reco/calo.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,11 @@ def process(self, data):
164164

165165
# Apply calibration
166166
if not self.do_tracking or part.shape != TRACK_SHP:
167-
depositions = self.calibrator(points, deps, sources, run_id)
167+
cal_points, depositions = self.calibrator(
168+
points, deps, sources, run_id
169+
)
168170
else:
169-
depositions = self.calibrator(
171+
cal_points, depositions = self.calibrator(
170172
points, deps, sources, run_id, track=True
171173
)
172174

@@ -176,22 +178,33 @@ def process(self, data):
176178
else:
177179
setattr(part, self.truth_dep_mode, depositions)
178180

181+
if self.calibrator.update_points:
182+
if not part.is_truth:
183+
part.points = cal_points
184+
else:
185+
setattr(part, self.truth_point_mode, cal_points)
186+
data[points_key][part.index] = cal_points
179187
data[dep_key][part.index] = depositions
180188
unass_mask[part.index] = False
181189

182190
# Apply calibration corrections to unassociated depositions
183191
unass_index = np.where(unass_mask)[0]
184192
points = data[points_key][unass_index]
185193
depositions = data[dep_key][unass_index]
194+
sources = None
186195
if source_key in data:
187196
sources = data[source_key][unass_index]
188197

189-
data[dep_key][unass_index] = self.calibrator(
198+
cal_points, depositions = self.calibrator(
190199
points, depositions, sources, run_id
191200
)
201+
if self.calibrator.update_points:
202+
data[points_key][unass_index] = cal_points
203+
data[dep_key][unass_index] = depositions
192204

193205
# If requested, updated the depositions attribute of interactions
194206
for k in self.interaction_keys:
207+
points_key = "points" if not "truth" in k else self.truth_point_key
195208
dep_key = "depositions" if not "truth" in k else self.truth_dep_key
196209
for inter in data[k]:
197210
# Update depositions for the interaction
@@ -200,3 +213,10 @@ def process(self, data):
200213
inter.depositions = depositions
201214
else:
202215
setattr(inter, self.truth_dep_mode, depositions)
216+
217+
if self.calibrator.update_points:
218+
points = data[points_key][inter.index]
219+
if not inter.is_truth:
220+
inter.points = points
221+
else:
222+
setattr(inter, self.truth_point_mode, points)

test/test_calib/test_manager.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ class FakeMeta:
1212
def to_cm(self, points, center=True):
1313
return points + 1.0
1414

15+
def to_px(self, points):
16+
return points - 1.0
17+
1518

1619
def test_manager_parses_labels_and_explicit_names(monkeypatch, fake_geo):
1720
monkeypatch.setattr(manager_mod.GeoManager, "get_instance", lambda: fake_geo)
@@ -50,7 +53,7 @@ def test_manager_applies_modules_in_config_order(monkeypatch, fake_geo):
5053
values = np.array([10.0, 10.0])
5154
sources = np.array([[0, 0], [0, 1]])
5255

53-
corrected = manager(points, values, sources=sources)
56+
_, corrected = manager(points, values, sources=sources)
5457

5558
assert np.allclose(corrected, [20.0, 30.0])
5659

@@ -59,7 +62,7 @@ def test_manager_can_infer_tpc_indexes_without_sources(monkeypatch, fake_geo):
5962
monkeypatch.setattr(manager_mod.GeoManager, "get_instance", lambda: fake_geo)
6063
manager = CalibrationManager(gain={"gain": [2.0, 3.0]})
6164

62-
corrected = manager(
65+
_, corrected = manager(
6366
np.array([[1.0, 0.0, 0.0], [8.0, 0.0, 0.0]]),
6467
np.array([10.0, 10.0]),
6568
)
@@ -71,7 +74,7 @@ def test_manager_applies_meta_module_translation_and_empty_tpc(monkeypatch, fake
7174
monkeypatch.setattr(manager_mod.GeoManager, "get_instance", lambda: fake_geo)
7275
manager = CalibrationManager(gain={"gain": [2.0, 3.0]})
7376

74-
corrected = manager(
77+
_, corrected = manager(
7578
np.array([[1.0, 0.0, 0.0]]),
7679
np.array([10.0]),
7780
sources=np.array([[0, 0]]),
@@ -86,7 +89,7 @@ def test_manager_dispatches_lifetime_and_unknown_modules(monkeypatch, fake_geo):
8689
monkeypatch.setattr(manager_mod.GeoManager, "get_instance", lambda: fake_geo)
8790
manager = CalibrationManager(lifetime={"lifetime": 10.0, "driftv": 2.0})
8891

89-
corrected = manager(
92+
_, corrected = manager(
9093
np.array([[4.0, 0.0, 0.0]]),
9194
np.array([1.0]),
9295
sources=np.array([[0, 0]]),
@@ -115,7 +118,7 @@ def test_manager_dispatches_recombination(monkeypatch, fake_geo):
115118
recombination={"efield": 0.5},
116119
)
117120

118-
corrected = manager(
121+
_, corrected = manager(
119122
np.array([[1.0, 0.0, 0.0]]),
120123
np.array([1000.0]),
121124
sources=np.array([[0, 0]]),
@@ -131,7 +134,7 @@ def test_manager_dispatches_transparency(monkeypatch, fake_geo, transparency_db)
131134
transparency={"transparency_db": str(transparency_db), "run_id": 100}
132135
)
133136

134-
corrected = manager(
137+
_, corrected = manager(
135138
np.array([[0.0, 1.25, 1.25]]),
136139
np.array([12.0]),
137140
sources=np.array([[0, 1]]),
@@ -151,10 +154,47 @@ def test_manager_dispatches_field_before_lifetime(monkeypatch, fake_geo):
151154
lifetime={"lifetime": 10.0, "driftv": 2.0},
152155
)
153156

154-
corrected = manager(
157+
_, corrected = manager(
155158
np.array([[1.0, 0.0, 0.0]]),
156159
np.array([1.0]),
157160
sources=np.array([[0, 0]]),
158161
)
159162

160163
assert np.allclose(corrected, [np.exp(3.0 / 20.0)])
164+
165+
166+
def test_manager_can_return_field_corrected_points(monkeypatch, fake_geo):
167+
monkeypatch.setattr(manager_mod.GeoManager, "get_instance", lambda: fake_geo)
168+
field_map = FieldMap(
169+
np.full((1, 1, 1, 3), [2.0, 0.0, 0.0], dtype=float),
170+
[[0.0, 10.0], [-1.0, 1.0], [-1.0, 1.0]],
171+
)
172+
manager = CalibrationManager(field={"field_map": field_map})
173+
174+
points, values = manager(
175+
np.array([[1.0, 0.0, 0.0]]),
176+
np.array([1.0]),
177+
sources=np.array([[0, 0]]),
178+
)
179+
180+
assert np.allclose(points, [[3.0, 0.0, 0.0]])
181+
assert np.allclose(values, [1.0])
182+
183+
184+
def test_manager_returns_field_corrected_points_in_input_units(monkeypatch, fake_geo):
185+
monkeypatch.setattr(manager_mod.GeoManager, "get_instance", lambda: fake_geo)
186+
field_map = FieldMap(
187+
np.full((1, 1, 1, 3), [2.0, 0.0, 0.0], dtype=float),
188+
[[0.0, 10.0], [-1.0, 1.0], [-1.0, 1.0]],
189+
)
190+
manager = CalibrationManager(field={"field_map": field_map})
191+
192+
points, values = manager(
193+
np.array([[1.0, -0.5, -0.5]]),
194+
np.array([1.0]),
195+
sources=np.array([[0, 0]]),
196+
meta=FakeMeta(),
197+
)
198+
199+
assert np.allclose(points, [[3.0, -0.5, -0.5]])
200+
assert np.allclose(values, [1.0])

test/test_post/test_calo.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from types import SimpleNamespace
2+
3+
import numpy as np
4+
import pytest
5+
6+
import spine.calib.manager as manager_mod
7+
from spine.calib.field import FieldMap
8+
from spine.data.out import RecoInteraction, RecoParticle
9+
from spine.post.reco.calo import CalibrationProcessor
10+
11+
12+
class FakeTPC:
13+
def __init__(self, anode_pos, center):
14+
self.anode_pos = anode_pos
15+
self.drift_axis = 0
16+
self.drift_dir = np.array([1.0, 0.0, 0.0])
17+
self.center = np.asarray(center, dtype=float)
18+
self.dimensions = np.asarray((10.0, 10.0, 10.0), dtype=float)
19+
self.boundaries = np.vstack(
20+
(self.center - self.dimensions / 2.0, self.center + self.dimensions / 2.0)
21+
).T
22+
23+
24+
class FakeTPCSet:
25+
num_modules = 1
26+
num_chambers_per_module = 2
27+
num_chambers = 2
28+
29+
def __init__(self):
30+
self._tpcs = [
31+
[
32+
FakeTPC(0.0, center=(0.0, 0.0, 0.0)),
33+
FakeTPC(10.0, center=(10.0, 0.0, 0.0)),
34+
]
35+
]
36+
self.chambers = [tpc for module in self._tpcs for tpc in module]
37+
lower = np.min(
38+
np.vstack([tpc.boundaries[:, 0] for tpc in self.chambers]), axis=0
39+
)
40+
upper = np.max(
41+
np.vstack([tpc.boundaries[:, 1] for tpc in self.chambers]), axis=0
42+
)
43+
self.modules = [
44+
SimpleNamespace(
45+
center=(lower + upper) / 2.0,
46+
dimensions=upper - lower,
47+
boundaries=np.vstack((lower, upper)).T,
48+
)
49+
]
50+
51+
def __getitem__(self, index):
52+
return self._tpcs[index]
53+
54+
55+
class FakeGeo:
56+
def __init__(self):
57+
self.tpc = FakeTPCSet()
58+
59+
def get_volume_index(self, sources, module_id, tpc_id):
60+
return np.where((sources[:, 0] == module_id) & (sources[:, 1] == tpc_id))[0]
61+
62+
def get_closest_tpc_indexes(self, points):
63+
return [np.where(points[:, 0] <= 5.0)[0], np.where(points[:, 0] > 5.0)[0]]
64+
65+
def translate(self, points, source_module, target_module):
66+
return points + float(target_module - source_module)
67+
68+
69+
@pytest.fixture
70+
def fake_geo():
71+
return FakeGeo()
72+
73+
74+
def test_calibration_processor_updates_field_corrected_points(monkeypatch, fake_geo):
75+
monkeypatch.setattr(manager_mod.GeoManager, "get_instance", lambda: fake_geo)
76+
field_map = FieldMap(
77+
np.full((1, 1, 1, 3), [2.0, 0.0, 0.0], dtype=float),
78+
[[0.0, 10.0], [-1.0, 1.0], [-1.0, 1.0]],
79+
)
80+
processor = CalibrationProcessor(
81+
obj_type=("particle", "interaction"),
82+
field={"field_map": field_map},
83+
)
84+
85+
points = np.array([[1.0, 0.0, 0.0], [4.0, 0.0, 0.0]])
86+
depositions = np.array([10.0, 20.0])
87+
sources = np.array([[0, 0], [0, 0]])
88+
particle = RecoParticle(
89+
index=np.array([0], dtype=np.int32),
90+
points=points[[0]].copy(),
91+
depositions=depositions[[0]].copy(),
92+
sources=sources[[0]].copy(),
93+
)
94+
interaction = RecoInteraction(
95+
index=np.array([0, 1], dtype=np.int32),
96+
points=points.copy(),
97+
depositions=depositions.copy(),
98+
sources=sources.copy(),
99+
)
100+
data = {
101+
"points": points.copy(),
102+
"depositions": depositions.copy(),
103+
"sources": sources,
104+
"reco_particles": [particle],
105+
"reco_interactions": [interaction],
106+
}
107+
108+
processor.process(data)
109+
110+
expected_points = points + np.array([2.0, 0.0, 0.0])
111+
assert np.allclose(data["points"], expected_points)
112+
assert np.allclose(particle.points, expected_points[[0]])
113+
assert np.allclose(interaction.points, expected_points)

0 commit comments

Comments
 (0)