From 83d4669a453e30395b06d229e4848d9623d35918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicholas=20Kr=C3=A4mer?= Date: Sat, 23 Apr 2022 10:46:46 +0200 Subject: [PATCH] Introduce MarkovSequence (#646) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Renamed _markov_process module to _markov * Extract reusable information from Markov processes * Distinguish MarkovSequence and MarkovProcess through transition types. * Import Markov sequence into toplevel namespace * Markov* inputs are keyword-only * Raise TypeError if MarkovProcess and MarkovSequence are mixed up. * moved sample_at_input to interface * delete redundant tests for auxiliary functions * Improved error messages. * Unified the functionality of MarkovProcess and MarkovSequence for now * Updated output of problem zoo * Fixed tests * Update EKF/UKF test to MarkovSequence interface * Correct else-return * Corrected string concatenation * Rudimentary docstring for MarkovSequence * Updated benchmarks to MarkovSequence where necessary * Use MarkovSequence in docs * Added an accidentally-deleted line back in. * Make codecov ignore an exception branch Co-authored-by: Marvin Pförtner Co-authored-by: Marvin Pförtner --- benchmarks/filtsmooth.py | 6 +- ..._linear_gaussian_filtering_smoothing.ipynb | 2 +- ...nlinear_gaussian_filtering_smoothing.ipynb | 2 +- .../filtsmooth/particle_filtering.ipynb | 2 +- .../filtsmooth/_kalman_filter_smoother.py | 19 ++-- .../zoo/filtsmooth/_filtsmooth_problems.py | 4 +- src/probnum/randprocs/markov/__init__.py | 4 +- .../markov/{_markov_process.py => _markov.py} | 99 +++++++++++++------ .../randprocs/markov/integrator/_ioup.py | 4 +- .../randprocs/markov/integrator/_iwp.py | 4 +- .../randprocs/markov/integrator/_matern.py | 4 +- .../markov/utils/_generate_measurements.py | 4 +- .../_linearization_test_interface.py | 2 +- .../test_markov/test_utils/__init__.py | 0 .../test_utils/test_generate_samples.py | 51 ---------- 15 files changed, 100 insertions(+), 107 deletions(-) rename src/probnum/randprocs/markov/{_markov_process.py => _markov.py} (73%) delete mode 100644 tests/test_randprocs/test_markov/test_utils/__init__.py delete mode 100644 tests/test_randprocs/test_markov/test_utils/test_generate_samples.py diff --git a/benchmarks/filtsmooth.py b/benchmarks/filtsmooth.py index 06e7102af..b6daab428 100644 --- a/benchmarks/filtsmooth.py +++ b/benchmarks/filtsmooth.py @@ -42,7 +42,7 @@ def setup(self, linearization_implementation): regression_problem.locations ) - prior_process = randprocs.markov.MarkovProcess( + prior_process = randprocs.markov.MarkovSequence( transition=linearized_dynmod, initrv=prior_process.initrv, initarg=regression_problem.locations[0], @@ -93,7 +93,7 @@ def setup(self, linearization_implementation): regression_problem.locations ) - prior_process = randprocs.markov.MarkovProcess( + prior_process = randprocs.markov.MarkovSequence( transition=linearized_dynmod, initrv=prior_process.initrv, initarg=regression_problem.locations[0], @@ -160,7 +160,7 @@ def setup(self, linearization_implementation, num_samples): regression_problem.locations ) - prior_process = randprocs.markov.MarkovProcess( + prior_process = randprocs.markov.MarkovSequence( transition=linearized_dynmod, initrv=prior_process.initrv, initarg=regression_problem.locations[0], diff --git a/docs/source/tutorials/filtsmooth/discrete_linear_gaussian_filtering_smoothing.ipynb b/docs/source/tutorials/filtsmooth/discrete_linear_gaussian_filtering_smoothing.ipynb index 49871a148..5d0b3ac5a 100644 --- a/docs/source/tutorials/filtsmooth/discrete_linear_gaussian_filtering_smoothing.ipynb +++ b/docs/source/tutorials/filtsmooth/discrete_linear_gaussian_filtering_smoothing.ipynb @@ -217,7 +217,7 @@ "metadata": {}, "outputs": [], "source": [ - "prior_process = randprocs.markov.MarkovProcess(\n", + "prior_process = randprocs.markov.MarkovSequence(\n", " transition=dynamics_model, initrv=initial_state_rv, initarg=0.0\n", ")" ] diff --git a/docs/source/tutorials/filtsmooth/discrete_nonlinear_gaussian_filtering_smoothing.ipynb b/docs/source/tutorials/filtsmooth/discrete_nonlinear_gaussian_filtering_smoothing.ipynb index d7f5b66ca..efa036159 100644 --- a/docs/source/tutorials/filtsmooth/discrete_nonlinear_gaussian_filtering_smoothing.ipynb +++ b/docs/source/tutorials/filtsmooth/discrete_nonlinear_gaussian_filtering_smoothing.ipynb @@ -325,7 +325,7 @@ "metadata": {}, "outputs": [], "source": [ - "prior_process = randprocs.markov.MarkovProcess(\n", + "prior_process = randprocs.markov.MarkovSequence(\n", " transition=linearised_dynamics_model, initrv=initial_state_rv, initarg=0.0\n", ")" ] diff --git a/docs/source/tutorials/filtsmooth/particle_filtering.ipynb b/docs/source/tutorials/filtsmooth/particle_filtering.ipynb index 86fdadf89..ee2d8f0fe 100644 --- a/docs/source/tutorials/filtsmooth/particle_filtering.ipynb +++ b/docs/source/tutorials/filtsmooth/particle_filtering.ipynb @@ -205,7 +205,7 @@ "outputs": [], "source": [ "num_particles = 20\n", - "prior_process = randprocs.markov.MarkovProcess(\n", + "prior_process = randprocs.markov.MarkovSequence(\n", " transition=dynamics_model, initrv=initial_state_rv, initarg=0.0\n", ")\n", "importance_distribution = filtsmooth.particle.LinearizationImportanceDistribution.from_ekf(\n", diff --git a/src/probnum/filtsmooth/_kalman_filter_smoother.py b/src/probnum/filtsmooth/_kalman_filter_smoother.py index 81ca9220e..f2ad6669f 100644 --- a/src/probnum/filtsmooth/_kalman_filter_smoother.py +++ b/src/probnum/filtsmooth/_kalman_filter_smoother.py @@ -203,23 +203,24 @@ def smooth_rts( def _setup_prior_process(F, L, m0, C0, t0, prior_model): zero_shift_prior = np.zeros(F.shape[0]) + initrv = randvars.Normal(m0, C0) + initarg = t0 if prior_model == "discrete": prior = randprocs.markov.discrete.LTIGaussian( transition_matrix=F, noise=randvars.Normal(mean=zero_shift_prior, cov=L), ) - elif prior_model == "continuous": + return randprocs.markov.MarkovSequence( + transition=prior, initrv=initrv, initarg=initarg + ) + if prior_model == "continuous": prior = randprocs.markov.continuous.LTISDE( drift_matrix=F, force_vector=zero_shift_prior, dispersion_matrix=L ) - else: - raise ValueError - initrv = randvars.Normal(m0, C0) - initarg = t0 - prior_process = randprocs.markov.MarkovProcess( - transition=prior, initrv=initrv, initarg=initarg - ) - return prior_process + return randprocs.markov.MarkovProcess( + transition=prior, initrv=initrv, initarg=initarg + ) + raise ValueError def _setup_regression_problem(H, R, observations, locations): diff --git a/src/probnum/problems/zoo/filtsmooth/_filtsmooth_problems.py b/src/probnum/problems/zoo/filtsmooth/_filtsmooth_problems.py index c8dfe09d5..ff0caaeaf 100644 --- a/src/probnum/problems/zoo/filtsmooth/_filtsmooth_problems.py +++ b/src/probnum/problems/zoo/filtsmooth/_filtsmooth_problems.py @@ -125,7 +125,7 @@ def car_tracking( # Set up regression problem time_grid = np.arange(*timespan, step=step) - prior_process = randprocs.markov.MarkovProcess( + prior_process = randprocs.markov.MarkovSequence( transition=discrete_dynamics_model, initrv=initrv, initarg=time_grid[0] ) @@ -377,7 +377,7 @@ def dh(t, x): if initarg is None: initarg = time_grid[0] - prior_process = randprocs.markov.MarkovProcess( + prior_process = randprocs.markov.MarkovSequence( transition=dynamics_model, initrv=initrv, initarg=initarg ) diff --git a/src/probnum/randprocs/markov/__init__.py b/src/probnum/randprocs/markov/__init__.py index f432e1e99..eba17c635 100644 --- a/src/probnum/randprocs/markov/__init__.py +++ b/src/probnum/randprocs/markov/__init__.py @@ -6,15 +6,17 @@ """ from . import continuous, discrete, integrator, utils -from ._markov_process import MarkovProcess +from ._markov import MarkovProcess, MarkovSequence from ._transition import Transition # Public classes and functions. Order is reflected in documentation. __all__ = [ "MarkovProcess", + "MarkovSequence", "Transition", ] # Set correct module paths. Corrects links and module paths in documentation. MarkovProcess.__module__ = "probnum.randprocs.markov" +MarkovSequence.__module__ = "probnum.randprocs.markov" Transition.__module__ = "probnum.randprocs.markov" diff --git a/src/probnum/randprocs/markov/_markov_process.py b/src/probnum/randprocs/markov/_markov.py similarity index 73% rename from src/probnum/randprocs/markov/_markov_process.py rename to src/probnum/randprocs/markov/_markov.py index 60f533245..f0320e0f5 100644 --- a/src/probnum/randprocs/markov/_markov_process.py +++ b/src/probnum/randprocs/markov/_markov.py @@ -7,48 +7,24 @@ from probnum import _function, randvars, utils from probnum.randprocs import _random_process, kernels -from probnum.randprocs.markov import _transition +from probnum.randprocs.markov import _transition, continuous, discrete from probnum.typing import ShapeLike InputType = Union[np.floating, np.ndarray] OutputType = Union[np.floating, np.ndarray] -class MarkovProcess(_random_process.RandomProcess): - r"""Random processes with the Markov property. - - A Markov process is a random process with the additional property that - conditioned on the present state of the system its future and past states are - independent. This is known as the Markov property or as the process being - memoryless. A Markov process can be fully defined via an initial state and a - state transition. - - Parameters - ---------- - initarg - Initial starting input of the process. - initrv - Random variable describing the initial state. - transition - State transition of the system. - - See Also - -------- - RandomProcess : Random processes. - GaussianProcess : Gaussian processes. - """ - +class _MarkovBase(_random_process.RandomProcess): def __init__( self, - initarg: np.ndarray, + *, initrv: randvars.RandomVariable, transition: _transition.Transition, + input_shape: ShapeLike = (), ): - self.initarg = initarg self.initrv = initrv self.transition = transition - input_shape = np.asarray(initarg).shape output_shape = initrv.shape super().__init__( @@ -60,7 +36,7 @@ def __init__( input_shape=input_shape, output_shape=output_shape, ), - cov=MarkovProcess.Kernel( + cov=_MarkovBase.Kernel( self.__call__, input_shape=input_shape, output_shape=2 * output_shape, @@ -124,3 +100,68 @@ def _evaluate(self, x0: np.ndarray, x1: Optional[np.ndarray]) -> np.ndarray: return self._markov_proc_call(args=x0).cov raise NotImplementedError + + +class MarkovProcess(_MarkovBase): + r"""Random processes with the Markov property. + + A Markov process is a random process with the additional property that + conditioned on the present state of the system its future and past states are + independent. This is known as the Markov property or as the process being + memoryless. A Markov process can be fully defined via an initial state and a + state transition. + + Parameters + ---------- + initarg + Initial starting input of the process. + initrv + Random variable describing the initial state. + transition + State transition of the system. + + See Also + -------- + RandomProcess : Random processes. + GaussianProcess : Gaussian processes. + """ + + def __init__( + self, + *, + initarg: np.ndarray, + initrv: randvars.RandomVariable, + transition: continuous.SDE, + ): + if not isinstance(transition, continuous.SDE): # pragma: no cover + msg = "The transition is not continuous. Did you mean 'MarkovSequence'?" + raise TypeError(msg) + + super().__init__( + initrv=initrv, + transition=transition, + input_shape=np.asarray(initarg).shape, + ) + self.initarg = initarg + + +class MarkovSequence(_MarkovBase): + """Discrete-time Markov processes.""" + + def __init__( + self, + *, + initarg: np.ndarray, + initrv: randvars.RandomVariable, + transition: continuous.SDE, + ): + if not isinstance(transition, discrete.NonlinearGaussian): # pragma: no cover + msg = "The transition is not discrete. Did you mean 'MarkovProcess'?" + raise TypeError(msg) + + super().__init__( + initrv=initrv, + transition=transition, + input_shape=np.asarray(initarg).shape, + ) + self.initarg = initarg diff --git a/src/probnum/randprocs/markov/integrator/_ioup.py b/src/probnum/randprocs/markov/integrator/_ioup.py index d9f7b02b1..e024c35b0 100644 --- a/src/probnum/randprocs/markov/integrator/_ioup.py +++ b/src/probnum/randprocs/markov/integrator/_ioup.py @@ -4,11 +4,11 @@ import numpy as np from probnum import randvars -from probnum.randprocs.markov import _markov_process, continuous +from probnum.randprocs.markov import _markov, continuous from probnum.randprocs.markov.integrator import _integrator, _preconditioner -class IntegratedOrnsteinUhlenbeckProcess(_markov_process.MarkovProcess): +class IntegratedOrnsteinUhlenbeckProcess(_markov.MarkovProcess): r"""Integrated Ornstein-Uhlenbeck process. Convenience access to :math:`\nu` times integrated (:math:`d` dimensional) diff --git a/src/probnum/randprocs/markov/integrator/_iwp.py b/src/probnum/randprocs/markov/integrator/_iwp.py index 8413e6787..b62c6ac59 100644 --- a/src/probnum/randprocs/markov/integrator/_iwp.py +++ b/src/probnum/randprocs/markov/integrator/_iwp.py @@ -7,11 +7,11 @@ import scipy.special from probnum import config, linops, randvars -from probnum.randprocs.markov import _markov_process, continuous, discrete +from probnum.randprocs.markov import _markov, continuous, discrete from probnum.randprocs.markov.integrator import _integrator, _preconditioner -class IntegratedWienerProcess(_markov_process.MarkovProcess): +class IntegratedWienerProcess(_markov.MarkovProcess): r"""Integrated Wiener process. Convenience access to :math:`\nu` times integrated (:math:`d` dimensional) diff --git a/src/probnum/randprocs/markov/integrator/_matern.py b/src/probnum/randprocs/markov/integrator/_matern.py index bcb890979..e8db12d82 100644 --- a/src/probnum/randprocs/markov/integrator/_matern.py +++ b/src/probnum/randprocs/markov/integrator/_matern.py @@ -5,11 +5,11 @@ import scipy.special from probnum import randvars -from probnum.randprocs.markov import _markov_process, continuous +from probnum.randprocs.markov import _markov, continuous from probnum.randprocs.markov.integrator import _integrator, _preconditioner -class MaternProcess(_markov_process.MarkovProcess): +class MaternProcess(_markov.MarkovProcess): r"""Matern process. Convenience access to (:math:`d` dimensional) Matern(:math:`\nu`) processes. diff --git a/src/probnum/randprocs/markov/utils/_generate_measurements.py b/src/probnum/randprocs/markov/utils/_generate_measurements.py index 3e52fc11c..8b029d55e 100644 --- a/src/probnum/randprocs/markov/utils/_generate_measurements.py +++ b/src/probnum/randprocs/markov/utils/_generate_measurements.py @@ -2,12 +2,12 @@ import numpy as np -from probnum.randprocs.markov import _markov_process, _transition +from probnum.randprocs.markov import _markov, _transition def generate_artificial_measurements( rng: np.random.Generator, - prior_process: _markov_process.MarkovProcess, + prior_process: _markov.MarkovProcess, measmod: _transition.Transition, times: np.ndarray, ): diff --git a/tests/test_filtsmooth/test_gaussian/test_approx/_linearization_test_interface.py b/tests/test_filtsmooth/test_gaussian/test_approx/_linearization_test_interface.py index ff212bb29..6f4cedf04 100644 --- a/tests/test_filtsmooth/test_gaussian/test_approx/_linearization_test_interface.py +++ b/tests/test_filtsmooth/test_gaussian/test_approx/_linearization_test_interface.py @@ -84,7 +84,7 @@ def test_filtsmooth_pendulum(self, rng): ) initrv = prior_process.initrv - prior_process = randprocs.markov.MarkovProcess( + prior_process = randprocs.markov.MarkovSequence( transition=ekf_dyna, initrv=initrv, initarg=regression_problem.locations[0] ) method = filtsmooth.gaussian.Kalman(prior_process) diff --git a/tests/test_randprocs/test_markov/test_utils/__init__.py b/tests/test_randprocs/test_markov/test_utils/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_randprocs/test_markov/test_utils/test_generate_samples.py b/tests/test_randprocs/test_markov/test_utils/test_generate_samples.py deleted file mode 100644 index 161fb1f91..000000000 --- a/tests/test_randprocs/test_markov/test_utils/test_generate_samples.py +++ /dev/null @@ -1,51 +0,0 @@ -import numpy as np -import pytest - -from probnum import randprocs, randvars - - -class MockTransition(randprocs.markov.Transition): - """Empty transition object to test the generate() function.""" - - # pylint: disable=signature-differs - def __init__(self, dim): - super().__init__(input_dim=dim, output_dim=dim) - - def forward_realization(self, realization, **kwargs): - return randvars.Constant(realization), {} - - def forward_rv(self, rv, **kwargs): - return rv, {} - - def backward_realization(self, *args, **kwargs): - raise NotImplementedError - - def backward_rv(self, *args, **kwargs): - raise NotImplementedError - - -def times_array(): - return np.arange(0.0, 13.0, 1.0) - - -def times_single_point(): - return np.array([0.0]) - - -@pytest.mark.parametrize("times", [times_array(), times_single_point()]) -@pytest.mark.parametrize("test_ndim", [0, 1, 2]) -def test_generate_shapes(times, test_ndim, rng): - """Output shapes are as expected.""" - mocktrans = MockTransition(dim=test_ndim) - initrv = randvars.Constant(np.random.rand(test_ndim)) - proc = randprocs.markov.MarkovProcess( - initarg=times[0], initrv=initrv, transition=mocktrans - ) - states, obs = randprocs.markov.utils.generate_artificial_measurements( - rng, prior_process=proc, measmod=mocktrans, times=times - ) - - assert states.shape[0] == len(times) - assert states.shape[1] == test_ndim - assert obs.shape[0] == len(times) - assert obs.shape[1] == test_ndim