Skip to content

Commit c9bc0b8

Browse files
authored
Merge pull request #1014 from dstl/akkf
Adaptive kernel Kalman filter (AKKF)
2 parents 812d87a + 881eca3 commit c9bc0b8

17 files changed

+1397
-8
lines changed

docs/source/stonesoup.kernel.rst

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Kernel
2+
======
3+
4+
.. automodule:: stonesoup.kernel

docs/source/stonesoup.predictor.rst

+6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ Particle
1919
.. automodule:: stonesoup.predictor.particle
2020
:show-inheritance:
2121

22+
Kernel
23+
------
24+
25+
.. automodule:: stonesoup.predictor.kernel
26+
:show-inheritance:
27+
2228
Ensemble
2329
--------
2430

docs/source/stonesoup.rst

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Algorithm Components
4242
stonesoup.dataassociator
4343
stonesoup.deleter
4444
stonesoup.gater
45+
stonesoup.kernel
4546
stonesoup.hypothesiser
4647
stonesoup.initiator
4748
stonesoup.mixturereducer

docs/source/stonesoup.updater.rst

+6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ Particle
1919
.. automodule:: stonesoup.updater.particle
2020
:show-inheritance:
2121

22+
Kernel
23+
------
24+
25+
.. automodule:: stonesoup.updater.kernel
26+
:show-inheritance:
27+
2228
Ensemble
2329
--------
2430

docs/tutorials/filters/AKKF.py

+383
Large diffs are not rendered by default.

stonesoup/kernel.py

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
from abc import abstractmethod
2+
3+
import numpy as np
4+
5+
from .base import Base, Property
6+
from .types.array import StateVectors
7+
from .types.state import State
8+
9+
10+
class Kernel(Base):
11+
"""Kernel base type
12+
13+
A Kernel provides a means to translate state space or measurement space into kernel space.
14+
"""
15+
16+
@abstractmethod
17+
def __call__(self, state1, state2=None):
18+
r"""
19+
Compute the kernel state of a pair of :class:`~.State` objects
20+
21+
Parameters
22+
----------
23+
state1 : :class:`~.State`
24+
state2 : :class:`~.State`
25+
26+
Returns
27+
-------
28+
StateVectors
29+
kernel state of a pair of input :class:`~.State` objects
30+
31+
"""
32+
raise NotImplementedError
33+
34+
@staticmethod
35+
def _get_state_vectors(state1, state2):
36+
if isinstance(state1, State):
37+
state_vector1 = state1.state_vector
38+
else:
39+
state_vector1 = state1
40+
if state2 is None:
41+
state_vector2 = state_vector1
42+
else:
43+
if isinstance(state2, State):
44+
state_vector2 = state2.state_vector
45+
else:
46+
state_vector2 = state2
47+
return state_vector1, state_vector2
48+
49+
50+
class QuadraticKernel(Kernel):
51+
r"""Quadratic Kernel type
52+
53+
This kernel returns the quadratic kernel state vector from a pair of
54+
:class:`~.KernelParticleState` state vectors.
55+
56+
The Quadratic kernel of state vectors :math:`\mathbf{x}` and
57+
:math:`\mathbf{x}'` is defined as:
58+
59+
.. math::
60+
\mathtt{k}\left(\mathbf{x}, \mathbf{x}'\right) =
61+
\left(\alpha \langle \mathbf{x}, \mathbf{x}' \rangle + c\right)^2
62+
"""
63+
c: float = Property(
64+
default=1,
65+
doc="Free parameter trading off the influence of higher-order versus lower-order "
66+
"terms in the polynomial. Default is 1.")
67+
ialpha: float = Property(default=1e1, doc="Slope. Range is [1e0, 1e4].")
68+
69+
def __call__(self, state1, state2=None):
70+
r"""Calculate the Quadratic Kernel transformation for a pair of state vectors
71+
72+
Parameters
73+
----------
74+
state1 : :class:`~.KernelParticleState`
75+
state2 : :class:`~.KernelParticleState`
76+
77+
Returns
78+
-------
79+
StateVectors
80+
Transformed state vector in kernel space.
81+
"""
82+
state_vector1, state_vector2 = self._get_state_vectors(state1, state2)
83+
return (state_vector1.T@state_vector2/self.ialpha + self.c) ** 2
84+
85+
86+
class QuarticKernel(Kernel):
87+
r"""Quartic Kernel
88+
89+
This kernel returns the quartic kernel state from a pair of
90+
:class:`~.KernelParticleState` objects.
91+
92+
The Quartic kernel of state vectors :math:`\mathbf{x}` and
93+
:math:`\mathbf{x}'` is defined as:
94+
95+
.. math::
96+
\mathtt{k}(\mathbf{x}, \mathbf{x}') =
97+
\left(\alpha \langle \mathbf{x}, \mathbf{x}' \rangle + c\right)^4
98+
"""
99+
c: float = Property(
100+
default=1,
101+
doc="Free parameter trading off the influence of higher-order versus lower-order "
102+
"terms in the polynomial. Default is 1.")
103+
ialpha: float = Property(default=1e1, doc="Slope. Range is [1e0, 1e4].")
104+
105+
def __call__(self, state1, state2=None):
106+
r"""Calculate the Quartic Kernel transformation for a pair of state vectors
107+
108+
Parameters
109+
----------
110+
state1 : :class:`~.KernelParticleState`
111+
state2 : :class:`~.KernelParticleState`
112+
113+
Returns
114+
-------
115+
StateVectors
116+
Transformed state in kernel space.
117+
"""
118+
state_vector1, state_vector2 = self._get_state_vectors(state1, state2)
119+
return (state_vector1.T@state_vector2/self.ialpha + self.c) ** 4
120+
121+
122+
class GaussianKernel(Kernel):
123+
r"""Gaussian Kernel
124+
125+
This kernel returns the Gaussian kernel state vector from a pair of
126+
:class:`~.KernelParticleState` state vectors.
127+
128+
The Gaussian kernel of state vectors :math:`\mathbf{x}` and
129+
:math:`\mathbf{x}'` is defined as:
130+
131+
.. math::
132+
\mathtt{k}(\mathbf{x}, \mathbf{x}') =
133+
\mathrm{exp}\left(-\frac{||\mathbf{x} - \mathbf{x}'||^{2}}{2\pi\sigma^2}\right)
134+
"""
135+
variance: float = Property(
136+
default=1e1,
137+
doc=r"Denoted as :math:`\sigma^2` in the equation above. Determines the width of the "
138+
r"Gaussian kernel. Range is [1e0, 1e2].")
139+
140+
def __call__(self, state1, state2=None):
141+
r"""Calculate the Gaussian Kernel transformation for a pair of state vectors
142+
143+
Parameters
144+
----------
145+
state1 : :class:`~.KernelParticleState`
146+
state2 : :class:`~.KernelParticleState`
147+
148+
Returns
149+
-------
150+
StateVectors
151+
Transformed state vector in kernel space.
152+
"""
153+
state_vector1, state_vector2 = self._get_state_vectors(state1, state2)
154+
diff_tilde_x = (state_vector1.T[:, :, None] - state_vector2.T[:, None, :]) ** 2
155+
diff_tilde_x_sum = np.sum(diff_tilde_x, axis=0)
156+
157+
k_tilde_x = np.exp(-diff_tilde_x_sum/(2*self.variance)) / np.sqrt(2*np.pi*self.variance)
158+
159+
return StateVectors(k_tilde_x)

stonesoup/predictor/kernel.py

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import copy
2+
3+
import numpy as np
4+
5+
from ._utils import predict_lru_cache
6+
from .kalman import KalmanPredictor
7+
from ..base import Property
8+
from ..kernel import Kernel, QuadraticKernel
9+
from ..types.prediction import Prediction
10+
from ..types.state import State
11+
from ..types.update import KernelParticleStateUpdate
12+
13+
14+
class AdaptiveKernelKalmanPredictor(KalmanPredictor):
15+
r"""An implementation of the adaptive kernel Kalman filter (AKKF) predictor. Here, the AKKF draws
16+
inspiration from the concepts of kernel mean embeddings (KME) and Kalman Filter to address
17+
tracking problems in nonlinear systems.
18+
19+
In the state space, at time :math:`k`, the prior state
20+
particles are generated by passing the proposal particles at time :math:`k-1`, i.e.,
21+
:math:`\tilde{\mathbf{x}}_{k-1}^{\{i=1:M\}}`, through the motion model as
22+
23+
.. math::
24+
25+
\mathbf{x}_k^{\{i\}} = \mathtt{f}\left(\tilde{\mathbf{x}}_{k-1}^{\{i\}},
26+
\mathbf{u}_{k}^{\{i\}} \right).
27+
28+
In the kernel space, :math:`{\mathbf{x}}_{k}^{\{i=1:M_{\text{A}}\}}` are mapped as feature
29+
mappings :math:`\Phi_k`.
30+
Then, the predictive kernel weight vector :math:`\mathbf{w}^{-}_{k}`, and covariance matrix
31+
:math:`{S}_{k}^{-}`, are calculated as
32+
33+
.. math::
34+
\mathbf{w}^{-}_{k} &= \Gamma_{k} \mathbf{w}^{+}_{k-1}\\
35+
{S}_{k}^{-} &= \Gamma_{k} {S}^{+}_{k-1} \Gamma_{k} ^{\mathrm{T}} +V_{k}.
36+
37+
Here, :math:`\mathbf{w}^{+}_{k-1}` and :math:`{S}_{k-1}^{+}` are the posterior kernel weight
38+
mean vector and covariance matrix at time :math:`k-1`, respectively.
39+
The transition matrix :math:`\Gamma_{k}` represents the change of sample representation, and
40+
:math:`{V}_{k}` represents the finite matrix representation of the transition residual matrix.
41+
"""
42+
kernel: Kernel = Property(
43+
default=None,
44+
doc="Default is None. If None, the default :class:`~QuadraticKernel` is used.")
45+
lambda_predictor: float = Property(
46+
default=1e-3,
47+
doc=r":math:`\lambda_{\tilde{K}}`. Regularisation parameter used to stabilise the inverse "
48+
r"Gram matrix. Range is :math:`\left[10^{-4}, 10^{-2}\right]`")
49+
50+
def __init__(self, *args, **kwargs):
51+
super().__init__(*args, **kwargs)
52+
53+
if self.kernel is None:
54+
self.kernel = QuadraticKernel()
55+
56+
@predict_lru_cache()
57+
def predict(self, prior, timestamp=None, proposal=None, **kwargs):
58+
r"""The adaptive kernel version of the predict step
59+
60+
Parameters
61+
----------
62+
prior : :class:`~.KernelParticleState`
63+
Prior state, :math:`\mathbf{x}_{k-1}`
64+
timestamp : :class:`datetime.datetime`
65+
Time to transit to (:math:`k`)
66+
proposal : :class:`~.KernelParticleState`
67+
Proposal state, :math:`\mathbf{x}_{k-1}`
68+
**kwargs : various, optional
69+
These are passed to :meth:`~.TransitionModel.covar`
70+
71+
Returns
72+
-------
73+
: :class:`~.KernelParticleStatePrediction`
74+
The predicted state :math:`\mathbf{x}_{k|k-1}` and the predicted
75+
state covariance :math:`P_{k|k-1}`
76+
"""
77+
if proposal is None:
78+
if isinstance(prior, KernelParticleStateUpdate):
79+
proposal = State(state_vector=prior.proposal)
80+
else:
81+
proposal = copy.copy(prior)
82+
83+
# Get the prediction interval
84+
predict_over_interval = self._predict_over_interval(prior, timestamp)
85+
new_state_vector = self.transition_model.function(
86+
proposal,
87+
time_interval=predict_over_interval,
88+
**kwargs)
89+
90+
k_tilde_tilde = self.kernel(proposal)
91+
k_tilde_nontilde = self.kernel(proposal, prior)
92+
93+
I = np.identity(len(prior)) # noqa: E741
94+
inv_val = np.linalg.pinv(k_tilde_tilde + self.lambda_predictor * I)
95+
96+
kernel_t = inv_val @ k_tilde_nontilde
97+
prediction_weights = kernel_t @ prior.weight
98+
new_val = inv_val @ k_tilde_tilde - I
99+
v = new_val@new_val.T / len(prior)
100+
101+
prediction_covariance = kernel_t @ prior.kernel_covar @ kernel_t.T + v
102+
return Prediction.from_state(prior,
103+
state_vector=new_state_vector,
104+
weight=prediction_weights,
105+
kernel_covar=prediction_covariance,
106+
timestamp=timestamp,
107+
transition_model=self.transition_model)

0 commit comments

Comments
 (0)