..
    This work is licensed under the Creative Commons Attribution-
    NonCommercial-ShareAlike 3.0 Unported License. To view a copy of this
    license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a
    letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View,
    California, 94041, USA.
    
.. _timedep_guide:
    
.. currentmodule:: qinfer

Learning Time-Dependent Models
==============================

Time-Dependent Parameters
-------------------------

In addition to learning static parameters, **QInfer** can be used
to learn the values of parameters that change stochastically as a
function of time. In this case, the model parameter vector
:math:`\vec{x}` is interpreted as a time-dependent vector
:math:`\vec{x}(t)` representing the state of an underlying process.
The resulting statistical problem is often referred to as
*state-space estimation*. By using an appropriate resampling
algorithm, such as the Liu-West algorithm [LW01]_, state-space
and static parameter estimation can be combined such that 
a subset of the components of :math:`\vec{x}` are allowed to
vary with time.

**QInfer** represents state-space filtering by the use of
the :meth:`Simulatable.update_timestep` method, which samples
how a model parameter vector is updated as a function of time.
In particular, this method is used by :class:`~qinfer.SMCUpdater` to draw samples from the
distribution :math:`f`

.. math::

    \vec{x}(t_{\ell+1}) \sim f(\vec{x}(t_{\ell}, e(t_{\ell})),

where :math:`t_{\ell}` is the time at which the experiment :math:`e_{\ell}`
is measured, and where :math:`t_{\ell+1}` is the time step immediately
following :math:`t_{\ell}`. As this distribution is in general dependent
on the experiment being performed, :meth:`~Simulatable.update_timestep`
is vectorized in a manner similar to :meth:`~Model.likelihood` (see
:ref:`models_guide` for details). That is,
given a tensor :math:`X_{i,j}` of model parameter vectors and a vector
:math:`e_k` of experiments,
:meth:`~Simulatable.update_timestep` returns a tensor :math:`X_{i,j,k}'`
of sampled model parameters at the next time step.

Random Walk Models
------------------

As an example, :class:`RandomWalkModel` implements :meth:`~Simulatable.update_timestep`
by taking as an input a :class:`Distribution` specifying steps
:math:`\Delta \vec{x} = \vec{x}(t + \delta t) - \vec{x}(t)`. An
instance of :class:`RandomWalkModel` decorates another model in
a similar fashion to :ref:`other derived models <models_guide_derived>`.
For instance, the following code declares a precession model in
which the unknown frequency :math:`\omega` changes by a normal
random variable with mean 0 and standard deviation 0.005 after
each measurement. 

>>> from qinfer import (
...     SimplePrecessionModel, RandomWalkModel, NormalDistribution
... )
>>> model = RandomWalkModel(
...     underlying_model=SimplePrecessionModel(),
...     step_distribution=NormalDistribution(0, 0.005 ** 2)
... )

We can then draw simulated trajectories for the true and estimated
value of :math:`\omega` using a minor modification to the updater loop
discussed in :ref:`smc_guide`.

.. plot::

    model = RandomWalkModel(
        # Note that we set a minimum frequency of negative
        # infinity to prevent problems if the random walk
        # causes omega to cross zero.
        underlying_model=SimplePrecessionModel(min_freq=-np.inf),
        step_distribution=NormalDistribution(0, 0.005 ** 2)
    )
    prior = UniformDistribution([0, 1])
    updater = SMCUpdater(model, 2000, prior)

    expparams = np.empty((1, ), dtype=model.expparams_dtype)

    true_trajectory = []
    est_trajectory = []

    true_params = prior.sample()

    for idx_exp in range(400):
        # We don't want to grow the evolution time to be arbitrarily
        # large, so we'll instead choose a random time up to some
        # maximum. 
        expparams[0] = np.random.random() * 10 * np.pi
        datum = model.simulate_experiment(true_params, expparams)
        updater.update(datum, expparams)

        # We index by [:, :, 0] to pull off the index corresponding
        # to experiment parameters.
        true_params = model.update_timestep(true_params, expparams)[:, :, 0]

        true_trajectory.append(true_params[0])
        est_trajectory.append(updater.est_mean())

    plt.plot(true_trajectory, label='True')
    plt.plot(est_trajectory, label='Est.')
    plt.legend()
    plt.xlabel('# of Measurements')
    plt.ylabel(r'$\omega$')

    plt.show()


Specifying Custom Time-Step Updates
-----------------------------------

The :class:`RandomWalkModel` example above is somewhat unrealistic,
however, in that the step distribution is independent of the evolution
time. For a more reasonable noise process, we would expect that
:math:`\mathbb{V}[\omega(t + \Delta t) - \omega(t)] \propto \Delta t`.
We can subclass :class:`SimplePrecessionModel` to add this behavior
with a custom :meth:`~Simulatable.update_timestep` implementation.

.. plot::

    class DiffusivePrecessionModel(SimplePrecessionModel):
        diffusion_rate = 0.0005 # We'll multiply this by
                                 # sqrt(time) below.

        def update_timestep(self, modelparams, expparams):
            step_std_dev = self.diffusion_rate * np.sqrt(expparams)
            steps = step_std_dev * np.random.randn(
                # We want the shape of the steps in omega
                # to match the input model parameter and experiment
                # parameter shapes.
                # The axis of length 1 represents that this model
                # has only one model parameter (omega).
                modelparams.shape[0], 1, expparams.shape[0]
            ) 
            # Finally, we add a new axis to the input model parameters
            # to match the experiment parameters.
            return modelparams[:, :, np.newaxis] + steps 

    model = DiffusivePrecessionModel()
    prior = UniformDistribution([0, 1])
    updater = SMCUpdater(model, 2000, prior)

    expparams = np.empty((1, ), dtype=model.expparams_dtype)

    true_trajectory = []
    est_trajectory = []

    true_params = prior.sample()

    for idx_exp in range(400):
        expparams[0] = np.random.random() * 10 * np.pi
        datum = model.simulate_experiment(true_params, expparams)
        updater.update(datum, expparams)

        true_params = model.update_timestep(true_params, expparams)[:, :, 0]

        true_trajectory.append(true_params[0])
        est_trajectory.append(updater.est_mean())

    plt.plot(true_trajectory, label='True')
    plt.plot(est_trajectory, label='Est.')
    plt.legend()
    plt.xlabel('# of Measurements')
    plt.ylabel(r'$\omega$')

    plt.show()