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
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
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)
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