def test_inputs(self): """Inputs rejected or accepted according to expected types.""" numpy_array = np.ones(3) * Constant(0.1) constant_list = [Constant(0.1), Constant(0.4)] number_list = [0.5, 0.41] inputs = [numpy_array, constant_list, number_list] inputs_acceptable = [False, True, False] for inputs, is_acceptable in zip(inputs, inputs_acceptable): with self.subTest(input=inputs, is_acceptable=is_acceptable): if is_acceptable: _RandomVariableList(inputs) else: with self.assertRaises(TypeError): _RandomVariableList(inputs)
def __init__(self, scipy_solution: OdeSolution, rvs: list): self.scipy_solution = scipy_solution # rvs is of the type `list` of `RandomVariable` and can therefore be # directly transformed into a _RandomVariableList rv_states = _randomvariablelist._RandomVariableList(rvs) super().__init__(locations=scipy_solution.ts, states=rv_states)
def __init__(self, kalman_posterior: filtsmooth.KalmanPosterior): self.kalman_posterior = kalman_posterior # Pre-compute projection matrices. # The prior must be an integrator, if not, an error is thrown in 'GaussianIVPFilter'. self.proj_to_y = self.kalman_posterior.transition.proj2coord(coord=0) self.proj_to_dy = self.kalman_posterior.transition.proj2coord(coord=1) states = _randomvariablelist._RandomVariableList( [_project_rv(self.proj_to_y, rv) for rv in self.kalman_posterior.states] ) derivatives = _randomvariablelist._RandomVariableList( [_project_rv(self.proj_to_dy, rv) for rv in self.kalman_posterior.states] ) super().__init__( locations=kalman_posterior.locations, states=states, derivatives=derivatives )
def approximate_solution(): """List of normals with irrelevant values.""" rvlist = [ randvars.Normal(mean=i + np.arange(2, 4), cov=np.diag(np.arange(4, 6))) for i in range(10) ] return _randomvariablelist._RandomVariableList(rvlist)
def __init__( self, locations: np.ndarray, states: _randomvariablelist._RandomVariableList, derivatives: Optional[_randomvariablelist._RandomVariableList] = None, ): super().__init__(locations=locations, states=states) self.derivatives = ( _randomvariablelist._RandomVariableList(derivatives) if derivatives is not None else None)
def __call__( self, t: float ) -> typing.Union[randvars.RandomVariable, pnrv_list._RandomVariableList]: out_rv = self.kalman_posterior(t) if np.isscalar(t): return _project_rv(self.proj_to_y, out_rv) return pnrv_list._RandomVariableList( [_project_rv(self.proj_to_y, rv) for rv in out_rv] )
def _state_rvs(self): """ :obj:`list` of :obj:`RandomVariable`: Time-discrete posterior estimates over states, without preconditioning. Note that this does not correspond to ``self._kalman_posterior.state_rvs``: Here we undo the preconditioning to make the "states" interpretable. """ state_rvs = _RandomVariableList([ self._solver.undo_preconditioning(rv) for rv in self._kalman_posterior ]) return state_rvs
def __getitem__(self, idx): """Access the discrete-time solution through indexing and slicing.""" if isinstance(idx, int): rv = self._kalman_posterior[idx] rv = self._solver.undo_preconditioning(rv) rv = self._proj_normal_rv(rv, 0) return rv elif isinstance(idx, slice): rvs = self._kalman_posterior[idx] rvs = [self._solver.undo_preconditioning(rv) for rv in rvs] f_rvs = [self._proj_normal_rv(rv, 0) for rv in rvs] return _RandomVariableList(f_rvs) else: raise ValueError("Invalid index")
def __init__(self, times, rvs, solver): # try-except is a hotfix for now: # future PR is to move KalmanPosterior-info out of here, into GaussianIVPFilter try: self._kalman_posterior = KalmanPosterior(times, rvs, solver.gfilt, solver.with_smoothing) self._t = None self._y = None except AttributeError: self._kalman_posterior = None self._t = times self._y = _RandomVariableList(rvs) self._solver = solver
def y(self): """ :obj:`list` of :obj:`RandomVariable`: Probabilistic discrete-time solution Probabilistic discrete-time solution at times :math:`t_1, ..., t_N`, as a list of random variables. To return means and covariances use ``y.mean`` and ``y.cov``. """ if self._y: # hotfix return self._y else: function_rvs = [ self._proj_normal_rv(rv, 0) for rv in self._state_rvs ] return _RandomVariableList(function_rvs)
def __call__(self, t): """ Evaluate the time-continuous posterior at location `t` Algorithm: 1. Find closest t_prev and t_next, with t_prev < t < t_next 2. Predict from t_prev to t 3. (if `self._with_smoothing=True`) Predict from t to t_next 4. (if `self._with_smoothing=True`) Smooth from t_next to t 5. Return random variable for time t Parameters ---------- t : float Location, or time, at which to evaluate the posterior. Returns ------- :obj:`RandomVariable` Estimate of the states at time ``t``. """ if not np.isscalar(t): # recursive evaluation (t can now be any array, not just length 1!) return _RandomVariableList([self.__call__(t_pt) for t_pt in np.asarray(t)]) if t < self.locations[0]: raise ValueError( "Invalid location; Can not compute posterior for a location earlier " "than the initial location" ) if t in self.locations: idx = (self.locations <= t).sum() - 1 discrete_estimate = self.state_rvs[idx] return discrete_estimate if self.locations[0] < t < self.locations[-1]: pred_rv = self._predict_to_loc(t) if self._with_smoothing: smoothed_rv = self._smooth_prediction(pred_rv, t) return smoothed_rv else: return pred_rv # else: t > self.locations[-1]: if self._with_smoothing: warn("`smoothed=True` is ignored for extrapolation.") return self._predict_to_loc(t)
def smooth_list(self, rv_list, locations, final_rv=None, **kwargs): """ Apply smoothing to a list of RVs with desired final random variable. Specification of a final RV is useful to compute joint samples from a KalmanPosterior object, because in this case, the final RV is a Dirac (over a sample from the final Normal RV) and not a Normal RV. Parameters ---------- rv_list : _RandomVariableList or array_like List of random variables to be smoothed. locations : array_like Locations of the random variables in rv_list. final_rv : RandomVariable, optional. RandomVariable at the final point. Default is None, in which case standard smoothing is applied. If a random variable is specified, the smoothing iteration is based on this one, which is used for sampling (in which case the final random variable is a Dirac that represents a sample) Returns ------- _RandomVariableList List of smoothed random variables. """ if final_rv is None: final_rv = rv_list[-1] curr_rv = final_rv out_rvs = [curr_rv] for idx in reversed(range(1, len(locations))): unsmoothed_rv = rv_list[idx - 1] pred_rv, info = self.predict( start=locations[idx - 1], stop=locations[idx], randvar=unsmoothed_rv, **kwargs ) if "crosscov" not in info.keys(): raise TypeError("Cross-covariance required for smoothing.") curr_rv = self.smooth_step( unsmoothed_rv, pred_rv, curr_rv, info["crosscov"] ) out_rvs.append(curr_rv) out_rvs.reverse() return _RandomVariableList(out_rvs)
def smooth_list(self, rv_list, locations, _previous_posterior=None): """Apply smoothing to a list of random variables, according to the present transition. Parameters ---------- rv_list : _randomvariablelist._RandomVariableList List of random variables to be smoothed. locations : Locations :math:`t` of the random variables in the time-domain. Used for continuous-time transitions. _previous_posterior Specify a previous posterior to improve linearisation in approximate backward passes. Used in iterated smoothing based on posterior linearisation. Returns ------- _randomvariablelist._RandomVariableList List of smoothed random variables. """ final_rv = rv_list[-1] curr_rv = final_rv out_rvs = [curr_rv] for idx in reversed(range(1, len(locations))): unsmoothed_rv = rv_list[idx - 1] _linearise_smooth_step_at = (None if _previous_posterior is None else _previous_posterior( locations[idx - 1])) # Actual smoothing step curr_rv, _ = self.backward_rv( curr_rv, unsmoothed_rv, t=locations[idx - 1], dt=locations[idx] - locations[idx - 1], _linearise_at=_linearise_smooth_step_at, ) out_rvs.append(curr_rv) out_rvs.reverse() return _randomvariablelist._RandomVariableList(out_rvs)
def __call__(self, t: DenseOutputLocationArgType) -> DenseOutputValueType: """Evaluate the time-continuous solution at time t. Parameters ---------- t Location / time at which to evaluate the continuous ODE solution. Returns ------- randvars.RandomVariable or _randomvariablelist._RandomVariableList Estimate of the states at time ``t`` based on a fourth order polynomial. """ states = self.scipy_solution(t).T if np.isscalar(t): solution_as_rv = randvars.Constant(states) else: solution_as_rv = _randomvariablelist._RandomVariableList( [randvars.Constant(state) for state in states]) return solution_as_rv
def __call__(self, t): """Evaluate the time-continuous posterior at location `t` Algorithm: 1. Find closest t_prev and t_next, with t_prev < t < t_next 2. Predict from t_prev to t 3. (if `self._with_smoothing=True`) Predict from t to t_next 4. (if `self._with_smoothing=True`) Smooth from t_next to t 5. Return random variable for time t Parameters ---------- t : float Location, or time, at which to evaluate the posterior. Returns ------- :obj:`RandomVariable` Estimate of the states at time ``t``. """ # Recursive evaluation (t can now be any array, not just length 1) if not np.isscalar(t): return _RandomVariableList([self.__call__(t_pt) for t_pt in t]) # t is left of our grid -- raise error # (this functionality is not supported yet) if t < self.locations[0]: raise ValueError( "Invalid location; Can not compute posterior for a location earlier " "than the initial location" ) # Early exit if t is in our grid -- no need to interpolate if t in self.locations: idx = self._find_index(t) discrete_estimate = self.state_rvs[idx] return discrete_estimate return self.interpolate(t)
def __call__(self, t): """ Evaluate the time-continuous solution at time t. `KalmanPosterior.__call__` does the main algorithmic work to return the posterior for a given location. All that is left to do here is to (1) undo the preconditioning, and (2) to slice the state_rv in order to return only the rv for the function value. Parameters ---------- t : float Location / time at which to evaluate the continuous ODE solution. Returns ------- :obj:`RandomVariable` Probabilistic estimate of the continuous-time solution at time ``t``. """ out_rv = self._kalman_posterior(t) if np.isscalar(t): out_rv = self._solver.undo_preconditioning(out_rv) return self._proj_normal_rv(out_rv, 0) return _RandomVariableList(self._project_rv_list(out_rv))
def y(self) -> pnrv_list._RandomVariableList: y_rvs = [ _project_rv(self.proj_to_y, rv) for rv in self.kalman_posterior.state_rvs ] return pnrv_list._RandomVariableList(y_rvs)
def __init__(self, locations, state_rvs, gauss_filter, with_smoothing): self._locations = np.asarray(locations) self.gauss_filter = gauss_filter self._state_rvs = _RandomVariableList(state_rvs) self._with_smoothing = with_smoothing
def setUp(self): self.rv_list = _RandomVariableList([Constant(0.1), Constant(0.2)])
def setUp(self): self.rv_list = _RandomVariableList([])
def __init__(self, locations: np.ndarray, states: np.ndarray) -> None: self.locations = np.asarray(locations) self.states = _randomvariablelist._RandomVariableList(states)
def __call__(self, t: DenseOutputLocationArgType) -> DenseOutputValueType: """Evaluate the time-continuous posterior at location `t` Algorithm: 1. Find closest t_prev and t_next, with t_prev < t < t_next 2. Predict from t_prev to t 3. (if `self._with_smoothing=True`) Predict from t to t_next 4. (if `self._with_smoothing=True`) Smooth from t_next to t 5. Return random variable for time t Parameters ---------- t : Location, or time, at which to evaluate the posterior. Returns ------- randvars.RandomVariable or _randomvariablelist._RandomVariableList Estimate of the states at time ``t``. """ # The variable "squeeze_eventually" indicates whether # the dimension of the time-array has been promoted # (which is there for a well-behaved loop). # If this has been the case, the final result needs to be # reshaped ("squeezed") accordingly. if np.isscalar(t): t = np.atleast_1d(t) squeeze_eventually = True else: squeeze_eventually = False if not np.all(np.diff(t) >= 0.0): raise ValueError("Time-points have to be sorted.") # Split left-extrapolation, interpolation, right_extrapolation t0, tmax = np.amin(self.locations), np.amax(self.locations) t_extra_left = t[t < t0] t_extra_right = t[t > tmax] t_inter = t[(t0 <= t) & (t <= tmax)] # Indices of t where they would be inserted # into self.locations ("left": right-closest states) indices = np.searchsorted(self.locations, t_inter, side="left") interpolated_values = [ self.interpolate( t=ti, previous_location=prevloc, previous_state=prevstate, next_location=nextloc, next_state=nextstate, ) for ti, prevloc, prevstate, nextloc, nextstate in zip( t_inter, self.locations[indices - 1], self._states_left_of_location[indices - 1], self.locations[indices], self._states_right_of_location[indices], ) ] extrapolated_values_left = [ self.interpolate( t=ti, previous_location=None, previous_state=None, next_location=t0, next_state=self._states_right_of_location[0], ) for ti in t_extra_left ] extrapolated_values_right = [ self.interpolate( t=ti, previous_location=tmax, previous_state=self._states_left_of_location[-1], next_location=None, next_state=None, ) for ti in t_extra_right ] dense_output_values = extrapolated_values_left dense_output_values.extend(interpolated_values) dense_output_values.extend(extrapolated_values_right) if squeeze_eventually: return dense_output_values[0] return _randomvariablelist._RandomVariableList(dense_output_values)
def setUp(self): self.rv_list = _RandomVariableList([Dirac(0.1), Dirac(0.2)])
def dy(self): """ :obj:`list` of :obj:`RandomVariable`: Derivatives of the discrete-time solution """ dy_rvs = [self._proj_normal_rv(rv, 1) for rv in self._state_rvs] return _RandomVariableList(dy_rvs)
def __init__(self, locations, state_rvs, transition): self._locations = np.asarray(locations) self.transition = transition self._state_rvs = _RandomVariableList(state_rvs)