def ivp_to_regression_problem(
    ivp: problems.InitialValueProblem,
    locations: Union[Sequence, np.ndarray],
    ode_information_operator: information_operators.InformationOperator,
    approx_strategy: Optional[approx_strategies.ApproximationStrategy] = None,
    ode_measurement_variance: Optional[FloatLike] = 0.0,
    exclude_initial_condition=False,
):
    """Transform an initial value problem into a regression problem.

    Parameters
    ----------
    ivp
        Initial value problem to be transformed.
    locations
        Locations of the time-grid-points.
    ode_information_operator
        ODE information operator to use.
    approx_strategy
        Approximation strategy to use. Optional. Default is `EK1()`.
    ode_measurement_variance
        Artificial ODE measurement noise. Optional. Default is 0.0.
    exclude_initial_condition
        Whether to exclude the initial condition from the regression problem.
        Optional. Default is False, in which case the returned measurement model list
        consist of [`initcond_mm`, `ode_mm`, ..., `ode_mm`].

    Returns
    -------
    problems.TimeSeriesRegressionProblem
        Time-series regression problem.
    """

    # Construct data and solution
    N = len(locations)
    data = np.zeros((N, ivp.dimension))
    if ivp.solution is not None:
        solution = np.stack([ivp.solution(t) for t in locations])
    else:
        solution = None

    ode_information_operator.incorporate_ode(ivp)

    # Construct measurement models
    measmod_initial_condition, measmod_ode = _construct_measurement_models(
        ivp, ode_information_operator, approx_strategy,
        ode_measurement_variance)

    if exclude_initial_condition:
        measmod_list = [measmod_ode] * N
    else:
        measmod_list = [measmod_initial_condition] + [measmod_ode] * (N - 1)

    # Return regression problem
    return problems.TimeSeriesRegressionProblem(
        locations=locations,
        observations=data,
        measurement_models=measmod_list,
        solution=solution,
    )
def test_sampling_shapes_1d(locs, size):
    """Make the sampling tests for a 1d posterior."""
    locations = np.linspace(0, 2 * np.pi, 100)
    data = 0.5 * np.random.randn(100) + np.sin(locations)

    prior = randprocs.markov.integrator.IntegratedWienerTransition(0, 1)
    measmod = randprocs.markov.discrete.LTIGaussian(
        state_trans_mat=np.eye(1),
        shift_vec=np.zeros(1),
        proc_noise_cov_mat=np.eye(1))
    initrv = randvars.Normal(np.zeros(1), np.eye(1))

    prior_process = randprocs.markov.MarkovProcess(transition=prior,
                                                   initrv=initrv,
                                                   initarg=locations[0])
    kalman = filtsmooth.gaussian.Kalman(prior_process)
    regression_problem = problems.TimeSeriesRegressionProblem(
        observations=data, measurement_models=measmod, locations=locations)
    posterior, _ = kalman.filtsmooth(regression_problem)

    size = utils.as_shape(size)
    if locs is None:
        base_measure_reals = np.random.randn(
            *(size + posterior.locations.shape + (1, )))
        samples = posterior.transform_base_measure_realizations(
            base_measure_reals, t=posterior.locations)
    else:
        locs = np.union1d(locs, posterior.locations)
        base_measure_reals = np.random.randn(*(size + (len(locs), )) + (1, ))
        samples = posterior.transform_base_measure_realizations(
            base_measure_reals, t=locs)

    assert samples.shape == base_measure_reals.shape
    def _improve(self, *, data, prior_process):

        # Measurement model for SciPy observations
        ode_dim = prior_process.transition.wiener_process_dimension
        proj_to_y = prior_process.transition.proj2coord(coord=0)
        observation_noise_std = self._observation_noise_std * np.ones(ode_dim)
        process_noise = randvars.Normal(
            mean=np.zeros(ode_dim),
            cov=np.diag(observation_noise_std**2),
            cov_cholesky=np.diag(observation_noise_std),
        )
        measmod_scipy = randprocs.markov.discrete.LTIGaussian(
            transition_matrix=proj_to_y,
            noise=process_noise,
            forward_implementation="sqrt",
            backward_implementation="sqrt",
        )

        # Regression problem
        ts, ys = data
        regression_problem = problems.TimeSeriesRegressionProblem(
            observations=ys,
            locations=ts,
            measurement_models=[measmod_scipy] * len(ts))

        # Infer the solution
        kalman = filtsmooth.gaussian.Kalman(prior_process)
        out, _ = kalman.filtsmooth(regression_problem)
        estimated_initrv = out.states[0]
        return estimated_initrv
Beispiel #4
0
def _setup_regression_problem(H, R, observations, locations):
    zero_shift_mm = np.zeros(H.shape[0])
    measmod = randprocs.markov.discrete.LTIGaussian(state_trans_mat=H,
                                                    shift_vec=zero_shift_mm,
                                                    proc_noise_cov_mat=R)
    measurement_models = [measmod] * len(locations)
    regression_problem = problems.TimeSeriesRegressionProblem(
        observations=observations,
        locations=locations,
        measurement_models=measurement_models,
    )
    return regression_problem
Beispiel #5
0
def _setup_regression_problem(H, R, observations, locations):
    zero_shift_mm = np.zeros(H.shape[0])
    measmod = randprocs.markov.discrete.LTIGaussian(transition_matrix=H,
                                                    noise=randvars.Normal(
                                                        mean=zero_shift_mm,
                                                        cov=R))
    measurement_models = [measmod] * len(locations)
    regression_problem = problems.TimeSeriesRegressionProblem(
        observations=observations,
        locations=locations,
        measurement_models=measurement_models,
    )
    return regression_problem
    def test_filtsmooth_pendulum(self, rng):
        # pylint: disable=not-callable
        # Set up test problem

        # If this measurement variance is not really small, the sampled
        # test data can contain an outlier every now and then which
        # breaks the test, even though it has not been touched.
        regression_problem, info = filtsmooth_zoo.pendulum(
            rng=rng, measurement_variance=0.0001)
        prior_process = info["prior_process"]
        measmods = regression_problem.measurement_models

        ekf_dyna = self.linearizing_component(prior_process.transition)
        ekf_meas = [self.linearizing_component(mm) for mm in measmods]

        regression_problem = problems.TimeSeriesRegressionProblem(
            locations=regression_problem.locations,
            observations=regression_problem.observations,
            measurement_models=ekf_meas,
            solution=regression_problem.solution,
        )

        initrv = prior_process.initrv
        prior_process = randprocs.markov.MarkovProcess(
            transition=ekf_dyna,
            initrv=initrv,
            initarg=regression_problem.locations[0])
        method = filtsmooth.gaussian.Kalman(prior_process)

        # Compute filter/smoother solution
        posterior, _ = method.filtsmooth(regression_problem)
        filtms = posterior.filtering_posterior.states.mean
        smooms = posterior.states.mean

        # Compute RMSEs and assert they are well-behaved.
        comp = regression_problem.solution[:, 0]
        normaliser = np.sqrt(comp.size)
        filtrmse = np.linalg.norm(filtms[:, 0] - comp) / normaliser
        smoormse = np.linalg.norm(smooms[:, 0] - comp) / normaliser
        obs_rmse = (
            np.linalg.norm(regression_problem.observations[:, 0] - comp) /
            normaliser)

        assert smoormse < filtrmse < obs_rmse, (smoormse, filtrmse, obs_rmse)
Beispiel #7
0
    def __call__(
        self,
        ivp: problems.InitialValueProblem,
        prior_process: randprocs.markov.MarkovProcess,
    ) -> randvars.RandomVariable:
        """Compute the initial distribution.

        For Runge-Kutta initialization, it goes as follows:

        1. The ODE integration problem is set up on the interval ``[t0, t0 + (2*order+1)*h0]``
        and solved with a call to ``scipy.integrate.solve_ivp``. The solver is uses adaptive steps with ``atol=rtol=1e-12``,
        but is forced to pass through the
        events ``(t0, t0+h0, t0 + 2*h0, ..., t0 + (2*order+1)*h0)``.
        The result is a vector of time points and states, with at least ``(2*order+1)``.
        Potentially, the adaptive steps selected many more steps, but because of the events, fewer steps cannot have happened.

        2. A prescribed prior is fitted to the first ``(2*order+1)`` (t, y) pairs of the solution. ``order`` is the order of the prior.

        3. The value of the resulting posterior at time ``t=t0`` is an estimate of the state and all its derivatives.
        The resulting marginal standard deviations estimate the error. This random variable is returned.

        Parameters
        ----------
        ivp
            Initial value problem.
        prior_process
            Prior Gauss-Markov process.

        Returns
        -------
        Normal
            Estimated (improved) initial random variable. Compatible with the specified prior.
        """
        f, y0, t0, df = ivp.f, ivp.y0, ivp.t0, ivp.df
        y0 = np.asarray(y0)
        ode_dim = y0.shape[0] if y0.ndim > 0 else 1
        order = prior_process.transition.num_derivatives

        # order + 1 would suffice in theory, 2*order + 1 is for good measure
        # (the "+1" is a safety factor for order=1)
        num_steps = 2 * order + 1
        t_eval = np.arange(t0, t0 + (num_steps + 1) * self.dt, self.dt)
        sol = sci.solve_ivp(
            f,
            (t0, t0 + (num_steps + 1) * self.dt),
            y0=y0,
            atol=1e-12,
            rtol=1e-12,
            t_eval=t_eval,
            method=self.method,
        )

        # Measurement model for SciPy observations
        proj_to_y = prior_process.transition.proj2coord(coord=0)
        zeros_shift = np.zeros(ode_dim)
        zeros_cov = np.zeros((ode_dim, ode_dim))
        measmod_scipy = randprocs.markov.discrete.LTIGaussian(
            proj_to_y,
            zeros_shift,
            zeros_cov,
            proc_noise_cov_cholesky=zeros_cov,
            forward_implementation="sqrt",
            backward_implementation="sqrt",
        )

        # Measurement model for initial condition observations
        proj_to_dy = prior_process.transition.proj2coord(coord=1)
        if df is not None and order > 1:
            proj_to_ddy = prior_process.transition.proj2coord(coord=2)
            projmat_initial_conditions = np.vstack((proj_to_y, proj_to_dy, proj_to_ddy))
            initial_data = np.hstack((y0, f(t0, y0), df(t0, y0) @ f(t0, y0)))
        else:
            projmat_initial_conditions = np.vstack((proj_to_y, proj_to_dy))
            initial_data = np.hstack((y0, f(t0, y0)))
        zeros_shift = np.zeros(len(projmat_initial_conditions))
        zeros_cov = np.zeros(
            (len(projmat_initial_conditions), len(projmat_initial_conditions))
        )
        measmod_initcond = randprocs.markov.discrete.LTIGaussian(
            projmat_initial_conditions,
            zeros_shift,
            zeros_cov,
            proc_noise_cov_cholesky=zeros_cov,
            forward_implementation="sqrt",
            backward_implementation="sqrt",
        )

        # Create regression problem and measurement model list
        ts = sol.t[:num_steps]
        ys = list(sol.y[:, :num_steps].T)
        ys[0] = initial_data
        measmod_list = [measmod_initcond] + [measmod_scipy] * (len(ts) - 1)
        regression_problem = problems.TimeSeriesRegressionProblem(
            observations=ys, locations=ts, measurement_models=measmod_list
        )

        # Infer the solution
        kalman = filtsmooth.gaussian.Kalman(prior_process)
        out, _ = kalman.filtsmooth(regression_problem)
        estimated_initrv = out.states[0]
        return estimated_initrv
def merge_regression_problems(
    regression_problem1: problems.TimeSeriesRegressionProblem,
    regression_problem2: problems.TimeSeriesRegressionProblem,
) -> Tuple[problems.TimeSeriesRegressionProblem]:
    """Make a new regression problem out of two other regression problems.

    Parameters
    ----------
    regression_problem1 :
        Time series regression problem.
    regression_problem2 :
        Time series regression problem.

    Raises
    ------
    ValueError
        If the locations in both regression problems are not disjoint.
        Multiple observations at a single grid point are not supported currently.

    Returns
    -------
    problem : problems.TimeSeriesRegressionProblem
        Time series regression problem.

    Note
    ----
    To merge more than two problems, combine this function with functools.reduce.

    Examples
    --------

    Create two car-tracking problems with similar parameters and disjoint locations.

    >>> import probnum.problems.zoo.filtsmooth as filtsmooth_zoo
    >>> import numpy as np
    >>> rng = np.random.default_rng(seed=1)
    >>> prob1, _ = filtsmooth_zoo.car_tracking(
    ...     rng=rng, measurement_variance=2.0, timespan=(0.0, 10.0), step=0.5
    ... )
    >>> print(prob1.locations)
    [0.  0.5 1.  1.5 2.  2.5 3.  3.5 4.  4.5 5.  5.5 6.  6.5 7.  7.5 8.  8.5
     9.  9.5]

    >>> prob2, _ = filtsmooth_zoo.car_tracking(
    ...     rng=rng, measurement_variance=2.0, timespan=(0.25, 10.25), step=0.5
    ... )
    >>> print(prob2.locations)
    [0.25 0.75 1.25 1.75 2.25 2.75 3.25 3.75 4.25 4.75 5.25 5.75 6.25 6.75
     7.25 7.75 8.25 8.75 9.25 9.75]

    Merge them with merge_regression_problems

    >>> new_prob = merge_regression_problems(prob1, prob2)
    >>> print(new_prob.locations)
    [0.   0.25 0.5  0.75 1.   1.25 1.5  1.75 2.   2.25 2.5  2.75 3.   3.25
     3.5  3.75 4.   4.25 4.5  4.75 5.   5.25 5.5  5.75 6.   6.25 6.5  6.75
     7.   7.25 7.5  7.75 8.   8.25 8.5  8.75 9.   9.25 9.5  9.75]

    If you have more than two problems that you want to merge, do this with functools.reduce.

    >>> import functools
    >>> prob3, _ = filtsmooth_zoo.car_tracking(
    ...     rng=rng, measurement_variance=2.0, timespan=(0.35, 10.35), step=0.5
    ... )
    >>> new_prob = functools.reduce(
    ...     merge_regression_problems,
    ...     (prob1, prob2, prob3),
    ... )
    >>> print(new_prob.locations)
    [0.   0.25 0.35 0.5  0.75 0.85 1.   1.25 1.35 1.5  1.75 1.85 2.   2.25
     2.35 2.5  2.75 2.85 3.   3.25 3.35 3.5  3.75 3.85 4.   4.25 4.35 4.5
     4.75 4.85 5.   5.25 5.35 5.5  5.75 5.85 6.   6.25 6.35 6.5  6.75 6.85
     7.   7.25 7.35 7.5  7.75 7.85 8.   8.25 8.35 8.5  8.75 8.85 9.   9.25
     9.35 9.5  9.75 9.85]
    """

    measurement_models1 = np.asarray(regression_problem1.measurement_models)
    measurement_models2 = np.asarray(regression_problem2.measurement_models)

    # Some shorthand improves readibility of the inserts below.
    locs1, data1, sol1 = (
        regression_problem1.locations,
        regression_problem1.observations,
        regression_problem1.solution,
    )
    locs2, data2, sol2 = (
        regression_problem2.locations,
        regression_problem2.observations,
        regression_problem2.solution,
    )

    # Merge time locations
    if np.any(np.in1d(locs1, locs2)):
        raise ValueError("Regression problems must not share time locations.")
    new_locs = np.sort(np.concatenate((locs1, locs2)))
    locs1_in_new_locs = np.searchsorted(new_locs, locs1)
    locs2_in_new_locs = np.searchsorted(new_locs, locs2)

    # Merge observations
    new_num_obs = len(data1) + len(data2)
    if not data1.shape[1:] == data2.shape[1:]:
        raise ValueError("The data sets have incompatible dimension.")
    new_data_shape = (new_num_obs,) + data1.shape[1:]
    new_data = np.zeros(new_data_shape)
    new_data[locs1_in_new_locs] = data1
    new_data[locs2_in_new_locs] = data2

    # Merge solutions.
    # The resulting problem will only have a solution of BOTH problems have one.
    if sol1 is not None and sol2 is not None:
        if not sol1.shape[1:] == sol2.shape[1:]:
            raise ValueError("The solution arrays have incompatible dimension.")
        new_sol_shape = (new_num_obs,) + sol1.shape[1:]
        new_sol = np.zeros(new_sol_shape)
        new_sol[locs1_in_new_locs] = sol1
        new_sol[locs2_in_new_locs] = sol2
    else:
        new_sol = None

    # Merge measurement models
    new_measurement_models = np.zeros((new_num_obs,), dtype=object)
    new_measurement_models[locs1_in_new_locs] = measurement_models1
    new_measurement_models[locs2_in_new_locs] = measurement_models2

    # Return merged arrays
    new_regression_problem = problems.TimeSeriesRegressionProblem(
        locations=new_locs,
        observations=new_data,
        measurement_models=new_measurement_models,
        solution=new_sol,
    )
    return new_regression_problem
def benes_daum(
    rng: np.random.Generator,
    measurement_variance: FloatLike = 0.1,
    process_diffusion: FloatLike = 1.0,
    time_grid: Optional[np.ndarray] = None,
    initrv: Optional[randvars.RandomVariable] = None,
):
    r"""Filtering/smoothing setup based on the Beneš SDE.

    A non-linear state space model for the dynamics of a Beneš SDE.
    Here, we formulate a continuous-discrete state space model:

    .. math::

        d x(t) &= \tanh(x(t)) d t + L d w(t) \\
        y_n &= x(t_n) + r_n

    for a driving Wiener process :math:`w(t)` and Gaussian distributed measurement noise
    :math:`r_n \sim \mathcal{N}(0, R)` with measurement noise
    covariance matrix :math:`R`.

    Parameters
    ----------
    rng
        Random number generator.
    measurement_variance
        Marginal measurement variance.
    process_diffusion
        Diffusion constant for the dynamics
    time_grid
        Time grid for the filtering/smoothing problem.
    initrv
        Initial random variable.

    Returns
    -------
    regression_problem
        ``TimeSeriesRegressionProblem`` object with time points and noisy observations.
    info
        Dictionary containing additional information like the prior process.

    Notes
    -----
    In order to generate observations for the returned ``TimeSeriesRegressionProblem`` object,
    the non-linear Beneš SDE has to be linearized.
    Here, a ``ContinuousEKFComponent`` is used, which corresponds to a first-order
    linearization as used in the extended Kalman filter.
    """
    def f(t, x):
        return np.tanh(x)

    def df(t, x):
        return 1.0 - np.tanh(x)**2

    def l(t, x):
        return process_diffusion * np.ones((1, 1))

    if initrv is None:
        initrv = randvars.Normal(np.zeros(1), 3.0 * np.eye(1))

    dynamics_model = randprocs.markov.continuous.SDE(
        state_dimension=1,
        wiener_process_dimension=1,
        drift_function=f,
        dispersion_function=l,
        drift_jacobian=df,
    )
    measurement_model = randprocs.markov.discrete.LTIGaussian(
        transition_matrix=np.eye(1),
        noise=randvars.Normal(mean=np.zeros(1),
                              cov=measurement_variance * np.eye(1)),
    )

    # Generate data
    if time_grid is None:
        time_grid = np.arange(0.0, 4.0, step=0.2)
    # The non-linear dynamics are linearized according to an EKF in order
    # to generate samples.
    linearized_dynamics_model = filtsmooth.gaussian.approx.ContinuousEKFComponent(
        non_linear_model=dynamics_model)

    prior_process = randprocs.markov.MarkovProcess(transition=dynamics_model,
                                                   initrv=initrv,
                                                   initarg=time_grid[0])
    prior_process_with_linearized_dynamics = randprocs.markov.MarkovProcess(
        transition=linearized_dynamics_model,
        initrv=initrv,
        initarg=time_grid[0])

    states, obs = randprocs.markov.utils.generate_artificial_measurements(
        rng=rng,
        prior_process=prior_process_with_linearized_dynamics,
        measmod=measurement_model,
        times=time_grid,
    )
    regression_problem = problems.TimeSeriesRegressionProblem(
        observations=obs,
        locations=time_grid,
        measurement_models=measurement_model,
        solution=states,
    )

    info = dict(prior_process=prior_process)
    return regression_problem, info
def pendulum(
    rng: np.random.Generator,
    measurement_variance: FloatLike = 0.1024,
    timespan: Tuple[FloatLike, FloatLike] = (0.0, 4.0),
    step: FloatLike = 0.0075,
    initrv: Optional[randvars.RandomVariable] = None,
    initarg: Optional[float] = None,
):
    r"""Filtering/smoothing setup for a (noisy) pendulum.

    A non-linear, discretized state space model for a pendulum with unknown forces
    acting on the dynamics, modeled as Gaussian noise.
    See e.g. Särkkä, 2013 [1]_ for more details.

    .. math::

        \begin{pmatrix}
          x_1(t_n) \\
          x_2(t_n)
        \end{pmatrix}
        &=
        \begin{pmatrix}
          x_1(t_{n-1}) + x_2(t_{n-1}) \cdot h \\
          x_2(t_{n-1}) - g \sin(x_1(t_{n-1})) \cdot h
        \end{pmatrix}
        +
        q_n \\
        y_n &\sim \sin(x_1(t_n)) + r_n

    for some ``step`` size :math:`h` and Gaussian process noise
    :math:`q_n \sim \mathcal{N}(0, Q)` with

    .. math::
        Q =
        \begin{pmatrix}
          \frac{h^3}{3} & \frac{h^2}{2} \\
          \frac{h^2}{2} & h
        \end{pmatrix}

    :math:`g` denotes the gravitational constant and :math:`r_n \sim \mathcal{N}(0, R)`
    is Gaussian mesurement noise with some covariance :math:`R`.

    Parameters
    ----------
    rng
        Random number generator.
    measurement_variance
        Marginal measurement variance.
    timespan
        :math:`t_0` and :math:`t_{\max}` of the time grid.
    step
        Step size of the time grid.
    initrv
        Initial random variable.
    initarg
        Initial time point of the prior process.
        Optional. Default is the left boundary of timespan.

    Returns
    -------
    regression_problem
        ``TimeSeriesRegressionProblem`` object with time points and noisy observations.
    info
        Dictionary containing additional information like the prior process.


    References
    ----------
    .. [1] Särkkä, Simo. Bayesian Filtering and Smoothing. Cambridge University Press,
        2013.

    """

    # Graviational constant
    g = 9.81

    # Define non-linear dynamics and measurements
    def f(t, x):
        x1, x2 = x
        y1 = x1 + x2 * step
        y2 = x2 - g * np.sin(x1) * step
        return np.array([y1, y2])

    def df(t, x):
        x1, _ = x
        y1 = [1, step]
        y2 = [-g * np.cos(x1) * step, 1]
        return np.array([y1, y2])

    def h(t, x):
        x1, _ = x
        return np.array([np.sin(x1)])

    def dh(t, x):
        x1, _ = x
        return np.array([[np.cos(x1), 0.0]])

    noise_cov = (np.diag(np.array([step**3 / 3, step])) +
                 np.diag(np.array([step**2 / 2]), 1) +
                 np.diag(np.array([step**2 / 2]), -1))

    dynamics_model = randprocs.markov.discrete.NonlinearGaussian(
        input_dim=2,
        output_dim=2,
        transition_fun=f,
        noise_fun=lambda t: randvars.Normal(mean=np.zeros(2), cov=noise_cov),
        transition_fun_jacobian=df,
    )

    measurement_model = randprocs.markov.discrete.NonlinearGaussian(
        input_dim=2,
        output_dim=1,
        transition_fun=h,
        noise_fun=lambda t: randvars.Normal(
            mean=np.zeros(1), cov=measurement_variance * np.eye(1)),
        transition_fun_jacobian=dh,
    )

    if initrv is None:
        initrv = randvars.Normal(np.ones(2), measurement_variance * np.eye(2))

    # Generate data
    time_grid = np.arange(*timespan, step=step)

    if initarg is None:
        initarg = time_grid[0]
    prior_process = randprocs.markov.MarkovProcess(transition=dynamics_model,
                                                   initrv=initrv,
                                                   initarg=initarg)

    states, obs = randprocs.markov.utils.generate_artificial_measurements(
        rng=rng,
        prior_process=prior_process,
        measmod=measurement_model,
        times=time_grid,
    )
    regression_problem = problems.TimeSeriesRegressionProblem(
        observations=obs,
        locations=time_grid,
        measurement_models=measurement_model,
        solution=states,
    )

    info = dict(prior_process=prior_process)
    return regression_problem, info
def car_tracking(
    rng: np.random.Generator,
    measurement_variance: FloatLike = 0.5,
    process_diffusion: FloatLike = 1.0,
    num_prior_derivatives: IntLike = 1,
    timespan: Tuple[FloatLike, FloatLike] = (0.0, 20.0),
    step: FloatLike = 0.2,
    initrv: Optional[randvars.RandomVariable] = None,
    forward_implementation: str = "classic",
    backward_implementation: str = "classic",
):
    r"""Filtering/smoothing setup for a simple car-tracking scenario.

    A discrete, linear, time-invariant Gaussian state space model for car-tracking,
    based on Example 3.6 in Särkkä, 2013. [1]_
    Let :math:`X = (\dot{x}_1, \dot{x}_2, \ddot{x}_1, \ddot{x}_2)`. Then the state
    space model has the following discretized formulation

    .. math::

        X(t_{n}) &=
        \begin{pmatrix}
          1 & 0 & \Delta t& 0 \\
          0 & 1 & 0 & \Delta t \\
          0 & 0 & 1 & 0 \\
          0 & 0 & 0 & 1
        \end{pmatrix} X(t_{n-1})
        +
        q_n \\
        y_{n} &=
        \begin{pmatrix}
          1 & 0 & 0 & 0 \\
          0 & 1 & 0 & 0 \\
        \end{pmatrix} X(t_{n})
        + r_n

    where :math:`q_n \sim \mathcal{N}(0, Q)` and :math:`r_n \sim \mathcal{N}(0, R)`
    for process noise covariance matrix :math:`Q` and measurement noise covariance
    matrix :math:`R`.

    Parameters
    ----------
    rng
        Random number generator.
    measurement_variance
        Marginal measurement variance.
    process_diffusion
        Diffusion constant for the dynamics.
    num_prior_derivatives
        Order of integration for the dynamics model. Defaults to one, which corresponds
        to a Wiener velocity model.
    timespan
        :math:`t_0` and :math:`t_{\max}` of the time grid.
    step
        Step size of the time grid.
    initrv
        Initial random variable.
    forward_implementation
        Implementation of the forward transitions inside prior and measurement model.
        Optional. Default is `classic`. For improved numerical stability, use `sqrt`.
    backward_implementation
        Implementation of the backward transitions inside prior and measurement model.
        Optional. Default is `classic`. For improved numerical stability, use `sqrt`.

    Returns
    -------
    regression_problem
        ``TimeSeriesRegressionProblem`` object with time points and noisy observations.
    info
        Dictionary containing additional information like the prior process.

    References
    ----------
    .. [1] Särkkä, Simo. Bayesian Filtering and Smoothing. Cambridge University Press,
        2013.

    """
    state_dim = 2
    model_dim = state_dim * (num_prior_derivatives + 1)
    measurement_dim = 2
    dynamics_model = randprocs.markov.integrator.IntegratedWienerTransition(
        num_derivatives=num_prior_derivatives,
        wiener_process_dimension=state_dim,
        forward_implementation=forward_implementation,
        backward_implementation=backward_implementation,
    )

    discrete_dynamics_model = dynamics_model.discretise(dt=step)

    measurement_matrix = np.eye(measurement_dim, model_dim)
    measurement_cov = measurement_variance * np.eye(measurement_dim)
    measurement_model = randprocs.markov.discrete.LTIGaussian(
        transition_matrix=measurement_matrix,
        noise=randvars.Normal(mean=np.zeros(measurement_dim),
                              cov=measurement_cov),
        forward_implementation=forward_implementation,
        backward_implementation=backward_implementation,
    )

    if initrv is None:
        initrv = randvars.Normal(
            np.zeros(model_dim),
            measurement_variance * np.eye(model_dim),
            cov_cholesky=np.sqrt(measurement_variance) * np.eye(model_dim),
        )

    # Set up regression problem
    time_grid = np.arange(*timespan, step=step)

    prior_process = randprocs.markov.MarkovProcess(
        transition=discrete_dynamics_model,
        initrv=initrv,
        initarg=time_grid[0])

    states, obs = randprocs.markov.utils.generate_artificial_measurements(
        rng=rng,
        prior_process=prior_process,
        measmod=measurement_model,
        times=time_grid,
    )
    regression_problem = problems.TimeSeriesRegressionProblem(
        observations=obs,
        locations=time_grid,
        measurement_models=measurement_model,
        solution=states,
    )

    info = dict(prior_process=prior_process)
    return regression_problem, info
def ornstein_uhlenbeck(
    rng: np.random.Generator,
    measurement_variance: FloatLike = 0.1,
    driftspeed: FloatLike = 0.21,
    process_diffusion: FloatLike = 0.5,
    time_grid: Optional[np.ndarray] = None,
    initrv: Optional[randvars.RandomVariable] = None,
    forward_implementation: str = "classic",
    backward_implementation: str = "classic",
):
    r"""Filtering/smoothing setup based on an Ornstein Uhlenbeck process.

    A linear, time-invariant state space model for the dynamics of a time-invariant
    Ornstein-Uhlenbeck process. See e.g. Example 10.19 in Särkkä et. al, 2019. [1]_
    Here, we formulate a continuous-discrete state space model:

    .. math::

        d x(t) &= \lambda x(t) d t + L d w(t) \\
        y_n &= x(t_n) + r_n

    for a drift constant :math:`\lambda` and a driving Wiener process :math:`w(t)`.
    :math:`r_n \sim \mathcal{N}(0, R)` is Gaussian distributed measurement noise
    with covariance matrix :math:`R`.
    Note that the linear, time-invariant dynamics have an equivalent discretization.

    Parameters
    ----------
    rng
        Random number generator.
    measurement_variance
        Marginal measurement variance.
    driftspeed
        Drift parameter of the Ornstein-Uhlenbeck process.
    process_diffusion
        Diffusion constant for the dynamics
    time_grid
        Time grid for the filtering/smoothing problem.
    initrv
        Initial random variable.
    forward_implementation
        Implementation of the forward transitions inside prior and measurement model.
        Optional. Default is `classic`. For improved numerical stability, use `sqrt`.
    backward_implementation
        Implementation of the backward transitions inside prior and measurement model.
        Optional. Default is `classic`. For improved numerical stability, use `sqrt`.


    Returns
    -------
    regression_problem
        ``TimeSeriesRegressionProblem`` object with time points and noisy observations.
    info
        Dictionary containing additional information like the prior process.


    References
    ----------
    .. [1] Särkkä, Simo, and Solin, Arno. Applied Stochastic Differential Equations.
        Cambridge University Press, 2019
    """

    dynamics_model = randprocs.markov.integrator.IntegratedOrnsteinUhlenbeckTransition(
        num_derivatives=0,
        wiener_process_dimension=1,
        driftspeed=driftspeed,
        forward_implementation=forward_implementation,
        backward_implementation=backward_implementation,
    )

    measurement_model = randprocs.markov.discrete.LTIGaussian(
        transition_matrix=np.eye(1),
        noise=randvars.Normal(mean=np.zeros(1),
                              cov=measurement_variance * np.eye(1)),
        forward_implementation=forward_implementation,
        backward_implementation=backward_implementation,
    )

    if initrv is None:
        initrv = randvars.Normal(10.0 * np.ones(1), np.eye(1))

    # Set up regression problem
    if time_grid is None:
        time_grid = np.arange(0.0, 20.0, step=0.2)

    prior_process = randprocs.markov.MarkovProcess(transition=dynamics_model,
                                                   initrv=initrv,
                                                   initarg=time_grid[0])
    states, obs = randprocs.markov.utils.generate_artificial_measurements(
        rng=rng,
        prior_process=prior_process,
        measmod=measurement_model,
        times=time_grid)

    regression_problem = problems.TimeSeriesRegressionProblem(
        observations=obs,
        locations=time_grid,
        measurement_models=measurement_model,
        solution=states,
    )

    info = dict(prior_process=prior_process)
    return regression_problem, info