Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor UKF Components to use linearize() #635

Merged
merged 24 commits into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e9d7991
Extracted LinearisationInterface (temporary) from EKF
pnkraemer Feb 11, 2022
de55442
implemented spherical cubature integration
pnkraemer Feb 11, 2022
4b82b7a
remove UKFComponent base class
pnkraemer Feb 11, 2022
4a31aa2
remove EKFComponent base class
pnkraemer Feb 11, 2022
3c8026e
removed unscented transform
pnkraemer Feb 11, 2022
5bd25f1
refactored SCI
pnkraemer Feb 11, 2022
6a8989a
Updated docs based on pylint's suggestions
pnkraemer Feb 11, 2022
701769d
Updated ODEFilter according to the changed linearisation interface
pnkraemer Feb 11, 2022
ca71a3f
Extracted unit parameters from cubature approximation
pnkraemer Feb 11, 2022
3d0c5d0
rename UKF auxiliary functions
pnkraemer Feb 11, 2022
366d8b1
Delete not-implemented ContinuousUKFComponent
pnkraemer Feb 11, 2022
a5918af
remove unused type imports
pnkraemer Feb 11, 2022
6415983
Merge branch 'main' of https://github.com/probabilistic-numerics/prob…
pnkraemer Feb 11, 2022
a210936
removed unused import
pnkraemer Feb 11, 2022
286bf2f
removed useless print and comment
pnkraemer Feb 11, 2022
ac78359
Merge branch 'main' into ukf-via-linearisation
pnkraemer Feb 11, 2022
e628873
Merged main
pnkraemer Feb 14, 2022
ef5f4f6
Merge branch 'main' into ukf-via-linearisation
pnkraemer Feb 15, 2022
41add8c
reintroduced old UKF parametrisation
pnkraemer Feb 15, 2022
0df11a4
Merge branch 'ukf-via-linearisation' of github.com:pnkraemer/probnum …
pnkraemer Feb 15, 2022
9380a6c
new black
pnkraemer Feb 15, 2022
bdabcc9
Explain motivations in module docstring in src/probnum/filtsmooth/gau…
pnkraemer Feb 15, 2022
a470ee7
Fix too-long module docstring
pnkraemer Feb 15, 2022
b1ba2e1
Remove trailing whitespace
pnkraemer Feb 15, 2022
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
16 changes: 8 additions & 8 deletions src/probnum/diffeq/odefilter/_odefilter.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,8 @@ def attempt_step(self, state, dt):
# Read off system matrices; required for calibration / error estimation
# Use only where a full call to forward_*() would be too costly.
# We use the mathematical symbol `Phi` (and later, `H`), because this makes it easier to read for us.
# The system matrix H of the measurement model can be accessed after the first forward_*,
# therefore we read it off further below.
discrete_dynamics = self.prior_process.transition.discretise(dt)

Phi = discrete_dynamics.transition_matrix

# Split the current RV into a deterministic part and a noisy part.
Expand All @@ -204,15 +203,16 @@ def attempt_step(self, state, dt):
)

# Compute the measurements for the error-free component
pred_rv_error_free, _ = self.prior_process.transition.forward_realization(
error_free_state, t=state.t, dt=dt
pred_rv_error_free, _ = discrete_dynamics.forward_realization(
error_free_state, t=state.t
)
meas_rv_error_free, _ = self.measurement_model.forward_rv(
pred_rv_error_free, t=state.t + dt
linearized_observation = self.measurement_model.linearize(
state.t + dt, pred_rv_error_free
)
H = self.measurement_model.linearized_model.transition_matrix_fun(
t=state.t + dt
meas_rv_error_free, _ = linearized_observation.forward_rv(
pred_rv_error_free, t=state.t + dt
)
H = linearized_observation.transition_matrix_fun(t=state.t + dt)

# Compute the measurements for the full components.
# Since the means of noise-free and noisy measurements coincide,
Expand Down
1 change: 0 additions & 1 deletion src/probnum/filtsmooth/gaussian/_kalmanposterior.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
approx.DiscreteUKFComponent,
randprocs.markov.continuous.LinearSDE,
approx.ContinuousEKFComponent,
approx.ContinuousUKFComponent,
]
"""Any linear(ized) transition can define an (approximate) Gauss-Markov prior."""

Expand Down
13 changes: 2 additions & 11 deletions src/probnum/filtsmooth/gaussian/approx/__init__.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,17 @@
"""Approximate Gaussian filtering and smoothing."""

from ._extendedkalman import ContinuousEKFComponent, DiscreteEKFComponent, EKFComponent
from ._unscentedkalman import ContinuousUKFComponent, DiscreteUKFComponent, UKFComponent
from ._unscentedtransform import UnscentedTransform
from ._extendedkalman import ContinuousEKFComponent, DiscreteEKFComponent
from ._unscentedkalman import DiscreteUKFComponent

# Public classes and functions. Order is reflected in documentation.
__all__ = [
"EKFComponent",
"ContinuousEKFComponent",
"DiscreteEKFComponent",
"UKFComponent",
"ContinuousUKFComponent",
"DiscreteUKFComponent",
"UnscentedTransform",
]

# Set correct module paths (for superclasses).
# Corrects links and module paths in documentation.
EKFComponent.__module__ = "probnum.filtsmooth.gaussian.approx"
ContinuousEKFComponent.__module__ = "probnum.filtsmooth.gaussian.approx"
DiscreteEKFComponent.__module__ = "probnum.filtsmooth.gaussian.approx"
UKFComponent.__module__ = "probnum.filtsmooth.gaussian.approx"
ContinuousUKFComponent.__module__ = "probnum.filtsmooth.gaussian.approx"
DiscreteUKFComponent.__module__ = "probnum.filtsmooth.gaussian.approx"
UnscentedTransform.__module__ = "probnum.filtsmooth.gaussian.approx"
135 changes: 10 additions & 125 deletions src/probnum/filtsmooth/gaussian/approx/_extendedkalman.py
Original file line number Diff line number Diff line change
@@ -1,130 +1,13 @@
"""Gaussian filtering and smoothing based on making intractable quantities tractable
through Taylor-method approximations, e.g. linearization."""

import abc
from typing import Dict, Tuple
"""Extended Kalman filtering."""

from probnum import randprocs, randvars


class EKFComponent(abc.ABC):
"""Interface for extended Kalman filtering components."""

def __init__(
self,
non_linear_model,
) -> None:

self.non_linear_model = non_linear_model

# Will be constructed later
self.linearized_model = None

def forward_realization(
self,
realization,
t,
dt=None,
compute_gain=False,
_diffusion=1.0,
_linearise_at=None,
) -> Tuple[randvars.Normal, Dict]:
"""Approximate forward-propagation of a realization of a random variable."""
compute_jacobian_at = (
_linearise_at
if _linearise_at is not None
else randvars.Constant(realization)
)
self.linearized_model = self.linearize(at_this_rv=compute_jacobian_at)
return self.linearized_model.forward_realization(
realization=realization,
t=t,
dt=dt,
compute_gain=compute_gain,
_diffusion=_diffusion,
)

def forward_rv(
self,
rv,
t,
dt=None,
compute_gain=False,
_diffusion=1.0,
_linearise_at=None,
) -> Tuple[randvars.Normal, Dict]:
"""Approximate forward-propagation of a random variable."""

compute_jacobian_at = _linearise_at if _linearise_at is not None else rv
self.linearized_model = self.linearize(at_this_rv=compute_jacobian_at)
return self.linearized_model.forward_rv(
rv=rv,
t=t,
dt=dt,
compute_gain=compute_gain,
_diffusion=_diffusion,
)

def backward_realization(
self,
realization_obtained,
rv,
rv_forwarded=None,
gain=None,
t=None,
dt=None,
_diffusion=1.0,
_linearise_at=None,
):
"""Approximate backward-propagation of a realization of a random variable."""

return self._backward_realization_via_backward_rv(
realization_obtained,
rv=rv,
rv_forwarded=rv_forwarded,
gain=gain,
t=t,
dt=dt,
_diffusion=_diffusion,
_linearise_at=_linearise_at,
)

def backward_rv(
self,
rv_obtained,
rv,
rv_forwarded=None,
gain=None,
t=None,
dt=None,
_diffusion=1.0,
_linearise_at=None,
):
"""Approximate backward-propagation of a random variable."""

compute_jacobian_at = _linearise_at if _linearise_at is not None else rv
self.linearized_model = self.linearize(at_this_rv=compute_jacobian_at)
return self.linearized_model.backward_rv(
rv_obtained=rv_obtained,
rv=rv,
rv_forwarded=rv_forwarded,
gain=gain,
t=t,
dt=dt,
_diffusion=_diffusion,
)

@abc.abstractmethod
def linearize(
self, at_this_rv: randvars.RandomVariable
) -> randprocs.markov.Transition:
"""Linearize the transition and make it tractable."""
raise NotImplementedError
from ._interface import _LinearizationInterface


# Order of inheritance matters, because forward and backward
# are defined in EKFComponent, and must not be inherited from SDE.
class ContinuousEKFComponent(EKFComponent, randprocs.markov.continuous.SDE):
class ContinuousEKFComponent(_LinearizationInterface, randprocs.markov.continuous.SDE):
"""Continuous-time extended Kalman filter transition.

Parameters
Expand Down Expand Up @@ -163,15 +46,15 @@ def __init__(
dispersion_function=non_linear_model.dispersion_function,
drift_jacobian=non_linear_model.drift_jacobian,
)
EKFComponent.__init__(self, non_linear_model=non_linear_model)
_LinearizationInterface.__init__(self, non_linear_model=non_linear_model)

self.mde_atol = mde_atol
self.mde_rtol = mde_rtol
self.mde_solver = mde_solver

self.forward_implementation = forward_implementation

def linearize(self, at_this_rv: randvars.Normal):
def linearize(self, t, at_this_rv: randvars.Normal):
"""Linearize the drift function with a first order Taylor expansion."""

g = self.non_linear_model.drift_function
Expand Down Expand Up @@ -202,7 +85,9 @@ def dispersion_matrix_function(t):
)


class DiscreteEKFComponent(EKFComponent, randprocs.markov.discrete.NonlinearGaussian):
class DiscreteEKFComponent(
_LinearizationInterface, randprocs.markov.discrete.NonlinearGaussian
):
"""Discrete extended Kalman filter transition."""

def __init__(
Expand All @@ -220,12 +105,12 @@ def __init__(
noise_fun=non_linear_model.noise_fun,
transition_fun_jacobian=non_linear_model.transition_fun_jacobian,
)
EKFComponent.__init__(self, non_linear_model=non_linear_model)
_LinearizationInterface.__init__(self, non_linear_model=non_linear_model)

self.forward_implementation = forward_implementation
self.backward_implementation = backward_implementation

def linearize(self, at_this_rv: randvars.Normal):
def linearize(self, t, at_this_rv: randvars.Normal):
"""Linearize the dynamics function with a first order Taylor expansion."""

g = self.non_linear_model.transition_fun
Expand Down
115 changes: 115 additions & 0 deletions src/probnum/filtsmooth/gaussian/approx/_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Temporary interface."""

from typing import Dict, Tuple

from probnum import randprocs, randvars


class _LinearizationInterface:
"""Interface for extended Kalman filtering components."""

def __init__(
self,
non_linear_model,
) -> None:

self.non_linear_model = non_linear_model

def forward_realization(
self,
realization,
t,
dt=None,
compute_gain=False,
_diffusion=1.0,
_linearise_at=None,
) -> Tuple[randvars.Normal, Dict]:
"""Approximate forward-propagation of a realization of a random variable."""
compute_jacobian_at = (
_linearise_at
if _linearise_at is not None
else randvars.Constant(realization)
)
linearized_model = self.linearize(t=t, at_this_rv=compute_jacobian_at)
return linearized_model.forward_realization(
realization=realization,
t=t,
dt=dt,
compute_gain=compute_gain,
_diffusion=_diffusion,
)
Comment on lines +31 to +43
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very soon, this whole class can go, and the linearization is done from the outside in, e.g., an ExtendedKalman() object.


def forward_rv(
self,
rv,
t,
dt=None,
compute_gain=False,
_diffusion=1.0,
_linearise_at=None,
) -> Tuple[randvars.Normal, Dict]:
"""Approximate forward-propagation of a random variable."""

compute_jacobian_at = _linearise_at if _linearise_at is not None else rv
linearized_model = self.linearize(t=t, at_this_rv=compute_jacobian_at)
return linearized_model.forward_rv(
rv=rv,
t=t,
dt=dt,
compute_gain=compute_gain,
_diffusion=_diffusion,
)

def backward_realization(
self,
realization_obtained,
rv,
rv_forwarded=None,
gain=None,
t=None,
dt=None,
_diffusion=1.0,
_linearise_at=None,
):
"""Approximate backward-propagation of a realization of a random variable."""
return self._backward_realization_via_backward_rv(
realization_obtained,
rv=rv,
rv_forwarded=rv_forwarded,
gain=gain,
t=t,
dt=dt,
_diffusion=_diffusion,
_linearise_at=_linearise_at,
)

def backward_rv(
self,
rv_obtained,
rv,
rv_forwarded=None,
gain=None,
t=None,
dt=None,
_diffusion=1.0,
_linearise_at=None,
):
"""Approximate backward-propagation of a random variable."""

compute_jacobian_at = _linearise_at if _linearise_at is not None else rv
linearized_model = self.linearize(t=t, at_this_rv=compute_jacobian_at)
return linearized_model.backward_rv(
rv_obtained=rv_obtained,
rv=rv,
rv_forwarded=rv_forwarded,
gain=gain,
t=t,
dt=dt,
_diffusion=_diffusion,
)

def linearize(
self, t, at_this_rv: randvars.RandomVariable
) -> randprocs.markov.Transition:
"""Linearize the transition and make it tractable."""
raise NotImplementedError
Loading