Skip to content

Commit 8c5ebee

Browse files
authored
Filter nonfinite calibration references (#2262)
* Filter nonfinite calibration references * Ignore unusable bias calibration references * Test nonfinite calibration references
1 parent 59942a4 commit 8c5ebee

3 files changed

Lines changed: 93 additions & 9 deletions

File tree

src/pyrecest/calibration/bias.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ def make_bias_training_examples(
167167
if features.shape[0] != measurements.shape[0]:
168168
raise ValueError("feature_values rows must match measurement_values rows")
169169

170-
if reference_times.size == 0:
170+
finite_reference = np.isfinite(reference_times) & np.isfinite(references).all(axis=1)
171+
if not finite_reference.any():
171172
return BiasTrainingExamples(
172173
measured=np.empty((0, measurements.shape[1])),
173174
reference=np.empty((0, measurements.shape[1])),
@@ -181,6 +182,8 @@ def make_bias_training_examples(
181182
time_delta_s=np.empty(0),
182183
)
183184

185+
reference_times = reference_times[finite_reference]
186+
references = references[finite_reference]
184187
order = np.argsort(reference_times)
185188
reference_times = reference_times[order]
186189
references = references[order]
@@ -189,7 +192,6 @@ def make_bias_training_examples(
189192
valid = (
190193
np.isfinite(measurement_times)
191194
& np.isfinite(measurements).all(axis=1)
192-
& np.isfinite(references[nearest]).all(axis=1)
193195
& (delta_s <= float(max_time_delta_s))
194196
)
195197
valid &= np.isfinite(features).all(axis=1)

src/pyrecest/calibration/time_offset.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,34 @@ def _validate_max_time_delta(max_time_delta_s: float | None) -> None:
9494
raise ValueError("max_time_delta_s must be nonnegative")
9595

9696

97+
def _finite_reference_rows(
98+
reference_times_s: np.ndarray,
99+
reference_values: np.ndarray | None = None,
100+
) -> np.ndarray:
101+
"""Return a mask selecting reference rows that are usable for matching."""
102+
103+
reference_times = np.asarray(reference_times_s, dtype=float).reshape(-1)
104+
finite = np.isfinite(reference_times)
105+
if reference_values is not None:
106+
values = np.asarray(reference_values, dtype=float)
107+
if values.ndim == 1:
108+
values = values.reshape(-1, 1)
109+
finite &= np.isfinite(values).all(axis=1)
110+
return finite
111+
112+
97113
def nearest_time_indices(
98114
reference_times_s: np.ndarray, query_times_s: np.ndarray
99115
) -> np.ndarray:
100-
"""Return original indices of nearest reference times for each query time."""
116+
"""Return original indices of nearest finite reference times for each query time."""
101117

102118
reference = np.asarray(reference_times_s, dtype=float).reshape(-1)
103119
query = np.asarray(query_times_s, dtype=float).reshape(-1)
104-
if reference.size == 0:
105-
raise ValueError("reference_times_s must not be empty")
120+
finite_reference = _finite_reference_rows(reference)
121+
if not finite_reference.any():
122+
raise ValueError("reference_times_s must contain at least one finite value")
123+
original_indices = np.flatnonzero(finite_reference)
124+
reference = reference[finite_reference]
106125
order = np.argsort(reference)
107126
sorted_reference = reference[order]
108127
insertion = np.searchsorted(sorted_reference, query)
@@ -111,7 +130,7 @@ def nearest_time_indices(
111130
use_right = np.abs(sorted_reference[right] - query) < np.abs(
112131
sorted_reference[left] - query
113132
)
114-
return order[np.where(use_right, right, left)]
133+
return original_indices[order[np.where(use_right, right, left)]]
115134

116135

117136
def interpolate_reference_values(
@@ -133,8 +152,11 @@ def interpolate_reference_values(
133152
reference_values = reference_values.reshape(-1, 1)
134153
if reference_times.size != reference_values.shape[0]:
135154
raise ValueError("reference_times_s length must match reference_values rows")
136-
if reference_times.size < 2:
137-
raise ValueError("at least two reference times are required for interpolation")
155+
finite_reference = _finite_reference_rows(reference_times, reference_values)
156+
if np.count_nonzero(finite_reference) < 2:
157+
raise ValueError("at least two finite reference rows are required for interpolation")
158+
reference_times = reference_times[finite_reference]
159+
reference_values = reference_values[finite_reference]
138160
order = np.argsort(reference_times)
139161
reference_times = reference_times[order]
140162
reference_values = reference_values[order]
@@ -145,7 +167,11 @@ def interpolate_reference_values(
145167
for dim in range(reference_values.shape[1])
146168
]
147169
)
148-
valid = (query_times >= reference_times[0]) & (query_times <= reference_times[-1])
170+
valid = (
171+
np.isfinite(query_times)
172+
& (query_times >= reference_times[0])
173+
& (query_times <= reference_times[-1])
174+
)
149175
if max_time_delta_s is not None:
150176
nearest = nearest_time_indices(reference_times, query_times)
151177
valid &= np.abs(reference_times[nearest] - query_times) <= float(

tests/calibration/test_time_offset_bias.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ def test_nearest_time_indices_accepts_unsorted_reference_times(self):
3838

3939
npt.assert_array_equal(indices, np.array([1, 2, 0]))
4040

41+
def test_nearest_time_indices_ignores_nonfinite_reference_times(self):
42+
indices = nearest_time_indices(
43+
np.array([np.nan, 10.0, 0.0]), np.array([0.2, 9.0])
44+
)
45+
46+
npt.assert_array_equal(indices, np.array([2, 1]))
47+
4148
def test_aggregate_time_offset_sweeps_preserves_rmse_and_max_semantics(self):
4249
aggregated = aggregate_time_offset_sweeps(
4350
[
@@ -133,6 +140,27 @@ def test_interpolation_rejects_negative_max_time_delta(self):
133140
max_time_delta_s=-1.0,
134141
)
135142

143+
def test_interpolation_skips_nonfinite_reference_rows(self):
144+
interpolated, valid = interpolate_reference_values(
145+
np.array([0.0, 1.0, 2.0, np.nan, 3.0]),
146+
np.array([[0.0], [np.nan], [2.0], [99.0], [3.0]]),
147+
np.array([0.5, 1.5, 2.5]),
148+
)
149+
150+
npt.assert_allclose(interpolated, np.array([[0.5], [1.5], [2.5]]))
151+
npt.assert_array_equal(valid, np.array([True, True, True]))
152+
153+
def test_interpolation_rejects_without_two_finite_reference_rows(self):
154+
with self.assertRaisesRegex(
155+
ValueError,
156+
"at least two finite reference rows are required for interpolation",
157+
):
158+
interpolate_reference_values(
159+
np.array([0.0, 1.0]),
160+
np.array([[0.0], [np.nan]]),
161+
np.array([0.5]),
162+
)
163+
136164
def test_interpolation_rejects_scalar_reference_values(self):
137165
with self.assertRaisesRegex(
138166
ValueError,
@@ -193,6 +221,34 @@ def test_make_bias_training_examples_uses_nearest_reference(self):
193221

194222
npt.assert_allclose(examples.residual, np.array([[1.0], [1.0]]))
195223

224+
def test_make_bias_training_examples_skips_nonfinite_reference_rows(self):
225+
examples = make_bias_training_examples(
226+
np.array([0.0, 1.0, 2.0]),
227+
np.array([[1.0], [3.0], [5.0]]),
228+
np.array([0.0, 1.0, 2.0, np.nan]),
229+
np.array([[0.0], [np.nan], [4.0], [99.0]]),
230+
max_time_delta_s=0.25,
231+
)
232+
233+
npt.assert_allclose(examples.measured, np.array([[1.0], [5.0]]))
234+
npt.assert_allclose(examples.reference, np.array([[0.0], [4.0]]))
235+
npt.assert_allclose(examples.residual, np.array([[1.0], [1.0]]))
236+
npt.assert_allclose(examples.time_delta_s, np.array([0.0, 0.0]))
237+
238+
def test_make_bias_training_examples_returns_empty_without_finite_reference_rows(
239+
self,
240+
):
241+
examples = make_bias_training_examples(
242+
np.array([0.0, 1.0]),
243+
np.array([[1.0], [2.0]]),
244+
np.array([np.nan, 1.0]),
245+
np.array([[0.0], [np.nan]]),
246+
feature_values=np.array([[1.0], [2.0]]),
247+
)
248+
249+
self.assertEqual(examples.measured.shape, (0, 1))
250+
self.assertEqual(examples.features.shape, (0, 1))
251+
196252
def test_make_bias_training_examples_validates_feature_rows_without_references(
197253
self,
198254
):

0 commit comments

Comments
 (0)