Skip to content

Commit c09ac5a

Browse files
committed
Replace bias_state with a bias_track in BiasFeeder and BiasUpdaters
1 parent 923e1be commit c09ac5a

File tree

5 files changed

+58
-57
lines changed

5 files changed

+58
-57
lines changed

docs/examples/sensorfusion/senor_bias.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,16 +159,15 @@
159159
# measurements.
160160
#
161161
# These are all added to a MultiDataFeeder to combine them into single detection feed.
162-
import copy
163162
from stonesoup.predictor.kalman import KalmanPredictor
164163
from stonesoup.feeder.bias import TranslationBiasFeeder
165164
from stonesoup.feeder.multi import MultiDataFeeder
166165

167166
bias_state = GaussianState([[0.], [0.]], np.diag([5**2, 5**2]), start_time)
168-
bias_track = Track([copy.copy(bias_state)])
167+
bias_track = Track([bias_state])
169168

170169
bias_predictor = KalmanPredictor(CombinedLinearGaussianTransitionModel([RandomWalk(1e-1)]*2))
171-
bias_feeder = TranslationBiasFeeder(measurements[0], bias_state)
170+
bias_feeder = TranslationBiasFeeder(measurements[0], bias_track)
172171

173172
# %%
174173
# These are all added to a MultiDataFeeder to combine them into single detection feed.
@@ -201,7 +200,7 @@
201200
from stonesoup.models.measurement.bias import TranslationBiasModelWrapper
202201

203202
bias_updater = GaussianBiasUpdater(
204-
bias_state, bias_predictor, TranslationBiasModelWrapper, updater)
203+
bias_track, bias_predictor, TranslationBiasModelWrapper, updater)
205204
bias_hypothesiser = DistanceHypothesiser(
206205
predictor, bias_updater, measure=Mahalanobis(), missed_distance=5)
207206
bias_data_associator = GNNWith2DAssignment(bias_hypothesiser)
@@ -228,11 +227,10 @@
228227
track.append(hyp.prediction)
229228

230229
# Adjust measurement models by removing relative bias for plotting later
231-
rel_bias_vector = bias_track.state_vector - bias_state.state_vector
230+
rel_bias_vector = bias_track[-2].state_vector - bias_track[-1].state_vector
232231
for model in {d.measurement_model for d in detections}:
233232
model.translation_offset -= rel_bias_vector
234233
model.applied_bias += rel_bias_vector # No longer used, but for completeness
235-
bias_track.append(copy.copy(bias_state))
236234
else:
237235
# Standard track update if no bias applied i.e. unbiased sensors
238236
hypotheses = data_associator.associate(tracks, detections, time)

stonesoup/feeder/bias.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
from ..base import Property
66
from ..buffered_generator import BufferedGenerator
77
from ..types.state import GaussianState
8+
from ..types.track import Track
89

910

1011
class _BiasFeeder(DetectionFeeder):
11-
bias_state: GaussianState = Property(doc="Bias State")
12+
bias_track: Track[GaussianState] = Property(doc="Track of bias states")
1213

1314
@property
1415
def bias(self):
15-
return self.bias_state.mean
16+
return self.bias_track.state.mean
1617

1718
@abstractmethod
1819
@BufferedGenerator.generator_method
@@ -25,13 +26,13 @@ class TimeBiasFeeder(_BiasFeeder):
2526
2627
Apply bias to detection timestamp and overall timestamp yielded.
2728
"""
28-
bias_state: GaussianState = Property(
29-
doc="Bias state with state vector shape (1, 1) in units of seconds")
29+
bias_track: Track[GaussianState] = Property(
30+
doc="Track of bias states with state vector shape (1, 1) in units of seconds")
3031

3132
@BufferedGenerator.generator_method
3233
def data_gen(self):
3334
for time, detections in self.reader:
34-
bias = self.bias_state.state_vector[0, 0]
35+
bias = self.bias_track.state_vector[0, 0]
3536
bias_delta = datetime.timedelta(seconds=float(bias))
3637
time -= bias_delta
3738
models = set()
@@ -48,13 +49,13 @@ class OrientationBiasFeeder(_BiasFeeder):
4849
4950
Apply bias to detection measurement model rotation offset
5051
"""
51-
bias_state: GaussianState = Property(
52-
doc="Bias state with state vector shape (3, 1) is expected")
52+
bias_track: Track[GaussianState] = Property(
53+
doc="Track of bias states with state vector shape (3, 1) is expected")
5354

5455
@BufferedGenerator.generator_method
5556
def data_gen(self):
5657
for time, detections in self.reader:
57-
bias = self.bias_state.state_vector.copy()
58+
bias = self.bias_track.state_vector
5859
models = set()
5960
for detection in detections:
6061
models.add(detection.measurement_model)
@@ -69,13 +70,14 @@ class TranslationBiasFeeder(_BiasFeeder):
6970
7071
Apply bias to detection measurement model translation offset
7172
"""
72-
bias_state: GaussianState = Property(
73-
doc="Bias state with state vector shape (n, 1), where n is dimensions of the model")
73+
bias_track: Track[GaussianState] = Property(
74+
doc="Track of bias states with state vector shape (n, 1), "
75+
"where n is dimensions of the model")
7476

7577
@BufferedGenerator.generator_method
7678
def data_gen(self):
7779
for time, detections in self.reader:
78-
bias = self.bias_state.state_vector.copy()
80+
bias = self.bias_track.state_vector
7981
models = set()
8082
for detection in detections:
8183
models.add(detection.measurement_model)
@@ -90,14 +92,14 @@ class OrientationTranslationBiasFeeder(_BiasFeeder):
9092
9193
Apply bias to detection measurement model rotation and translation offset
9294
"""
93-
bias_state: GaussianState = Property(
94-
doc="Bias state with state vector shape (3+n, 1), 3 for rotation and where n is "
95+
bias_track: Track[GaussianState] = Property(
96+
doc="Track of bias states with state vector shape (3+n, 1), 3 for rotation and where n is "
9597
"dimensions of the model")
9698

9799
@BufferedGenerator.generator_method
98100
def data_gen(self):
99101
for time, detections in self.reader:
100-
bias = self.bias_state.state_vector.copy()
102+
bias = self.bias_track.state_vector
101103
models = set()
102104
for detection in detections:
103105
models.add(detection.measurement_model)

stonesoup/feeder/tests/test_bias.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from stonesoup.functions import build_rotation_matrix
1010
from stonesoup.types.detection import Detection
1111
from stonesoup.types.state import GaussianState, StateVector
12+
from stonesoup.types.track import Track
1213

1314

1415
def make_dummy_detection(state_vector, measurement_model):
@@ -49,7 +50,7 @@ def test_translation_gaussian_bias_feeder_iter():
4950
detection = make_dummy_detection(StateVector([[10.], [20.], [30.]]), measurement_model)
5051
feeder = TranslationBiasFeeder(
5152
reader=[(datetime.datetime(2025, 9, 10), [detection])],
52-
bias_state=bias_state,
53+
bias_track=Track([bias_state]),
5354
)
5455
# Iterate over feeder
5556
for time, detections in feeder:
@@ -69,7 +70,7 @@ def test_orientation_gaussian_bias_feeder_iter():
6970
detection = make_dummy_detection(StateVector([[10.], [20.], [30.]]), measurement_model)
7071
feeder = OrientationBiasFeeder(
7172
reader=[(datetime.datetime(2025, 9, 10), [detection])],
72-
bias_state=bias_state,
73+
bias_track=Track([bias_state]),
7374
)
7475
for time, detections in feeder:
7576
for det in detections:
@@ -92,7 +93,7 @@ def test_orientation_translation_gaussian_bias_feeder_iter():
9293
detection = make_dummy_detection(StateVector([[10.], [20.], [30.]]), measurement_model)
9394
feeder = OrientationTranslationBiasFeeder(
9495
reader=[(datetime.datetime(2025, 9, 10), [detection])],
95-
bias_state=bias_state,
96+
bias_track=Track([bias_state]),
9697
)
9798
for time, detections in feeder:
9899
for det in detections:
@@ -117,7 +118,7 @@ def test_time_gaussian_bias_feeder_iter():
117118
orig_timestamp = detection.timestamp
118119
feeder = TimeBiasFeeder(
119120
reader=[(datetime.datetime(2025, 9, 10), [detection])],
120-
bias_state=bias_state,
121+
bias_track=Track([bias_state]),
121122
)
122123
for time, detections in feeder:
123124
for det in detections:

stonesoup/updater/bias.py

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from ..types.detection import Detection
1212
from ..types.hypothesis import SingleHypothesis
1313
from ..types.state import GaussianState
14+
from ..types.track import Track
1415
from ..types.update import Update
1516
from ..updater import Updater
1617
from ..updater.kalman import UnscentedKalmanUpdater
@@ -28,7 +29,7 @@ class GaussianBiasUpdater(Updater):
2829
bias i.e. all measurements from the same sensor.
2930
"""
3031
measurement_model = None
31-
bias_state: GaussianState = Property(doc="Prior bias")
32+
bias_track: Track[GaussianState] = Property(doc="Prior bias")
3233
bias_predictor: KalmanPredictor = Property(doc="Predictor for bias")
3334
bias_model_wrapper: BiasModelWrapper = Property()
3435
updater: Updater = Property(
@@ -44,15 +45,15 @@ def __init__(self, *args, **kwargs):
4445

4546
def predict_measurement(
4647
self, predicted_state, measurement_model=None, measurement_noise=True, **kwargs):
47-
ndim_bias = self.bias_state.ndim
48-
48+
bias_state = self.bias_track.state
49+
ndim_bias = bias_state.ndim
4950
# Predict bias
50-
if self.bias_state.timestamp is None:
51-
pred_bias_state = copy.copy(self.bias_state)
51+
if bias_state.timestamp is None:
52+
pred_bias_state = copy.copy(bias_state)
5253
pred_bias_state.timestamp = predicted_state.timestamp
5354
else:
5455
pred_bias_state = self.bias_predictor.predict(
55-
self.bias_state, timestamp=predicted_state.timestamp)
56+
bias_state, timestamp=predicted_state.timestamp)
5657

5758
applied_bias = getattr(measurement_model, 'applied_bias', np.zeros((ndim_bias, 1)))
5859
delta_bias = pred_bias_state.state_vector - applied_bias
@@ -75,18 +76,15 @@ def predict_measurement(
7576
def update(self, hypotheses, **kwargs):
7677
if any(not hyp for hyp in hypotheses):
7778
raise ValueError("Must provide only non-missed detection hypotheses")
78-
79-
ndim_bias = self.bias_state.ndim
79+
bias_state = self.bias_track.state
80+
ndim_bias = bias_state.ndim
8081

8182
# Predict bias
8283
pred_time = max(hypothesis.prediction.timestamp for hypothesis in hypotheses)
83-
if self.bias_state.timestamp is None:
84-
self.bias_state.timestamp = pred_time
84+
if bias_state.timestamp is None:
85+
bias_state.timestamp = pred_time
8586
else:
86-
new_bias_state = self.bias_predictor.predict(self.bias_state, timestamp=pred_time)
87-
self.bias_state.state_vector = new_bias_state.state_vector
88-
self.bias_state.covar = new_bias_state.covar
89-
self.bias_state.timestamp = new_bias_state.timestamp
87+
bias_state = self.bias_predictor.predict(bias_state, timestamp=pred_time)
9088

9189
# Create joint state
9290
states = [hypothesis.prediction for hypothesis in hypotheses]
@@ -95,8 +93,8 @@ def update(self, hypotheses, **kwargs):
9593
for h in hypotheses
9694
if hasattr(h.measurement.measurement_model, 'applied_bias')),
9795
np.zeros((ndim_bias, 1)))
98-
delta_bias = self.bias_state.state_vector - applied_bias
99-
states.append(GaussianState(delta_bias, self.bias_state.covar))
96+
delta_bias = bias_state.state_vector - applied_bias
97+
states.append(GaussianState(delta_bias, bias_state.covar))
10098
combined_pred = GaussianState(
10199
np.vstack([state.state_vector for state in states]).view(StateVector),
102100
block_diag(*[state.covar for state in states]).view(CovarianceMatrix),
@@ -123,12 +121,13 @@ def update(self, hypotheses, **kwargs):
123121
# Update bias
124122
update = self.updater.update(SingleHypothesis(combined_pred, combined_meas), **kwargs)
125123
rel_delta_bias = update.state_vector[-ndim_bias:, :] - delta_bias
126-
self.bias_state.state_vector = self.bias_state.state_vector + rel_delta_bias
124+
bias_state.state_vector = bias_state.state_vector + rel_delta_bias
127125
if self.max_bias is not None:
128-
self.bias_state.state_vector = \
129-
np.min([abs(self.bias_state.state_vector), self.max_bias], axis=0) \
130-
* np.sign(self.bias_state.state_vector)
131-
self.bias_state.covar = update.covar[-ndim_bias:, -ndim_bias:]
126+
bias_state.state_vector = \
127+
np.min([abs(bias_state.state_vector), self.max_bias], axis=0) \
128+
* np.sign(bias_state.state_vector)
129+
bias_state.covar = update.covar[-ndim_bias:, -ndim_bias:]
130+
self.bias_track.append(bias_state)
132131

133132
# Create update states
134133
offset = 0

stonesoup/updater/tests/test_bias.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from ...types.detection import Detection
1616
from ...types.hypothesis import SingleHypothesis
1717
from ...types.state import GaussianState, StateVector
18+
from ...types.track import Track
1819

1920

2021
def make_dummy_measurement_model():
@@ -58,7 +59,7 @@ def test_translation_gaussian_bias_feeder_update_bias(bias_timestamp):
5859
bias_predictor = KalmanPredictor(
5960
CombinedLinearGaussianTransitionModel([RandomWalk(1)] * 3))
6061
updater = GaussianBiasUpdater(
61-
bias_state=copy.copy(bias_prior),
62+
bias_track=Track([copy.copy(bias_prior)]),
6263
bias_predictor=bias_predictor,
6364
bias_model_wrapper=TranslationBiasModelWrapper,
6465
)
@@ -85,9 +86,9 @@ def test_translation_gaussian_bias_feeder_update_bias(bias_timestamp):
8586
# Call update_bias
8687
updates = updater.update([hyp])
8788
# The bias_state should be updated (should not be the same as initial)...
88-
assert not np.allclose(updater.bias_state.state_vector, bias_prior.state_vector)
89+
assert not np.allclose(updater.bias_track.state_vector, bias_prior.state_vector)
8990
# and should be around 0.8 ± 0.1
90-
assert np.allclose(updater.bias_state.state_vector, [[0.8], [0.8], [0.8]], atol=0.1)
91+
assert np.allclose(updater.bias_track.state_vector, [[0.8], [0.8], [0.8]], atol=0.1)
9192
# The returned updates should match the updated state shape
9293
assert updates[0].state_vector.shape == pred.state_vector.shape
9394

@@ -98,7 +99,7 @@ def test_orientation_gaussian_bias_feeder_update_bias(bias_timestamp):
9899
bias_predictor = KalmanPredictor(
99100
CombinedLinearGaussianTransitionModel([RandomWalk(1e-6)] * 3))
100101
updater = GaussianBiasUpdater(
101-
bias_state=copy.copy(bias_prior),
102+
bias_track=Track([copy.copy(bias_prior)]),
102103
bias_predictor=bias_predictor,
103104
bias_model_wrapper=OrientationBiasModelWrapper,
104105
max_bias=StateVector([[0.], [0.], [np.pi]])
@@ -123,8 +124,8 @@ def test_orientation_gaussian_bias_feeder_update_bias(bias_timestamp):
123124

124125
hyp = SingleHypothesis(pred, meas)
125126
updates = updater.update([hyp])
126-
assert not np.allclose(updater.bias_state.state_vector, bias_prior.state_vector)
127-
assert np.allclose(updater.bias_state.state_vector, [[0.], [0.], [np.pi/16]], atol=0.05)
127+
assert not np.allclose(updater.bias_track.state_vector, bias_prior.state_vector)
128+
assert np.allclose(updater.bias_track.state_vector, [[0.], [0.], [np.pi/16]], atol=0.05)
128129
assert updates[0].state_vector.shape == pred.state_vector.shape
129130

130131

@@ -136,7 +137,7 @@ def test_orientation_translation_gaussian_bias_feeder_update_bias(bias_timestamp
136137
bias_predictor = KalmanPredictor(
137138
CombinedLinearGaussianTransitionModel([RandomWalk(1e-6)]*3 + [RandomWalk(1)]*3))
138139
updater = GaussianBiasUpdater(
139-
bias_state=copy.copy(bias_prior),
140+
bias_track=Track([copy.copy(bias_prior)]),
140141
bias_predictor=bias_predictor,
141142
bias_model_wrapper=OrientationTranslationBiasModelWrapper
142143
)
@@ -158,9 +159,9 @@ def test_orientation_translation_gaussian_bias_feeder_update_bias(bias_timestamp
158159
hyp = SingleHypothesis(pred, meas)
159160
hyps.append(hyp)
160161
updates = updater.update(hyps)
161-
assert not np.allclose(updater.bias_state.state_vector, bias_prior.state_vector)
162+
assert not np.allclose(updater.bias_track.state_vector, bias_prior.state_vector)
162163
assert np.allclose(
163-
updater.bias_state.state_vector,
164+
updater.bias_track.state_vector,
164165
[[0.], [0.], [np.pi/16], [1.], [1.], [1.]],
165166
atol=0.1)
166167
assert updates[0].state_vector.shape == pred.state_vector.shape
@@ -171,7 +172,7 @@ def test_time_gaussian_bias_feeder_update_bias(bias_timestamp):
171172
bias_predictor = KalmanPredictor(
172173
CombinedLinearGaussianTransitionModel([RandomWalk(1e-3)]))
173174
updater = GaussianBiasUpdater(
174-
bias_state=copy.copy(bias_prior),
175+
bias_track=Track([copy.copy(bias_prior)]),
175176
bias_predictor=bias_predictor,
176177
bias_model_wrapper=partial(
177178
TimeBiasModelWrapper, transition_model=make_dummy_transition_model())
@@ -195,6 +196,6 @@ def test_time_gaussian_bias_feeder_update_bias(bias_timestamp):
195196

196197
hyp = SingleHypothesis(pred, meas)
197198
updates = updater.update([hyp])
198-
assert not np.allclose(updater.bias_state.state_vector, bias_prior.state_vector)
199-
assert np.allclose(updater.bias_state.state_vector, [[1.0]], atol=0.1)
199+
assert not np.allclose(updater.bias_track.state_vector, bias_prior.state_vector)
200+
assert np.allclose(updater.bias_track.state_vector, [[1.0]], atol=0.1)
200201
assert updates[0].state_vector.shape == pred.state_vector.shape

0 commit comments

Comments
 (0)