$$\newcommand{\vec}[1]{{\bf #1} }
\newcommand{\real}{\mathbb{R} }
\newcommand{\expect}[1]{\mathbb{E}[#1] }
\DeclareMathOperator*{\argmin}{arg\,min}
\vec{x}
\real
$$

# Probabilistic filtering for intention inference

#### Inferring user intention in a noisy world
----

    All theorems are true.
    All models are wrong.
    And all data are inaccurate.

    What are we to do?
    We must be sure to remain uncertain.

-- *[Leonard A. Smith, Proc. International School of Physics ``Enrico Fermi", (1997)](http://www2.maths.ox.ac.uk/~lenny/fermi96_main_abs.html)*

## Topic purpose
This section will cover probabilistic **inference**. Rather than learning a single set of parameters by optimisation, we can infer probability distributions over possible models that might be compatible with our data.   We'll develop this into **stochastic filtering** which we can use to update beliefs about intentions reliably, over time.

### Why is this relevant for computational HCI?
* We will build **statistical models** of user behavior, and estimate parameters of that model from quantitative observations of data. This is a **model-led approach** which has a rich mathematical underpinning and many powerful algorithmic tools which can be brought to bear.

* This is **robust** (it appropriately represents uncertainty) and **generative** (it can simulate behaviour compatible with observations).

### What will we *practically* do?
* We will build a model that can track and predict cursor location using a **Kalman filter**, even as noise levels increase and observations become intermittent.

## Outline

### Interaction is inference
*  Show how to represent interaction problems as inference and discuss how probabilistic filters can be used to attack these inference problems
*  Discuss *alternative* approaches to solving interaction problems
*  Discuss principles behind probabilistic tracking of belief

### Probability refresher
*  Introduce random variables and distributions
*  Outline Bayesian inference
*  Show how Bayesian inference can be used to fuse data across time and across sensors

*  Introduce the basic terminology for probabilistic filters
### Kalman filtering
*  Experiment with *noisy cursors*
*  Model the cursor problem probabilistically
*  Discuss the Kalman filter and its assumptions
*  Build and run a simple Kalman filter on offline static data
*  **Practical**: build an online Kalman filter to  recover corrupted cursor input probabilistically
*  Discuss the limitations of the Kalman filter

------

# Interaction as inference
One view on interaction is to see user intentions as **unknown values** which are partially observed through input sensors. The time series of inputs from the user only give a partial, noisy, incomplete view of intention inside the user's head.

Probabilistic filtering **(PF)** tracks the evolution of some unknown variables *[user intentions]* given observed evidence *[user input]*, in a way that is **robust**. Probabilistic filters infer a **distribution** over possible hidden (unobserved) variables, updating them over time. These filters are inherently **uncertain**, as they represent degrees of belief, and **dynamic**, as they explicitly model changing state over time.

#### Simulation viewpoint
These filters are really *simulators*. They *simulate* how possible user behaviors might unfold over time. In some probabilistic filters, hundreds of parallel simulators are run, each with slightly different parameters. In all cases, the simulations are adjusted online to better match observed reality. The internal parameters that drive the simulation are the *unknown variables* we want to infer and the *evidence* is the observed reality that adjusts the simulation parameters.

#### Properties
Probabilistic filtering is:

| Property | Why  |
|----------|------|
|**Bayesian**  |  Represents degrees of belief using probability distributions.    |
|**predictive**  |  Works by comparing predictions with reality.   |
|**generative** |  Involves generating (i.e. simulating) behavior.   |

-----
Probabilistic filtering is an **inverse probability** approach, and it requires that we think of interaction from an unique perspective. We have to explicitly be able to write down:

* what we want to know (i.e. the **state space of intention**);
* how that will change over time (i.e. the **dynamics of intention**);
*  a model that *if we knew what the user intention was, what the expected behavior would be* (i.e. a **generative function mapping intention -> expected user inputs**).

Note that this last point is the **inverse** of the typical way of approaching this problem, where we would try and find a mapping from a sensors to intention, by design or by learning.

### Why is this computational HCI?
Probabilistic filtering means writing down an **executable, statistical model** of user behavior, then **running an inference algorithm** that updates beliefs based on the way observations evolve. The **parameters** of the filter can be **learned from user data**. The effectiveness of the filter can be quantitatively measured.

### What are competitive approaches?
#### **Crafted mappings**
**where we try to find (by hand) transforms from sensors to intentions that are  simple or obvious.**

**Example:** a button, which has two physical states, and maps on to two intentional states via two electrical states. Pushed down = current flows = user intended to switch on. The mapping from electrical states to intentional states is **designed.**

*[Image credit: David Singleton via flickr.com CC-BY 2.0]*

#### **Machine learned mappings**
**where we train a system to recognize a class of input patterns as being representative of an intended behavior. **
**Example:** Finger gesture recognizer; hundreds of examples of many users performing one of N multi-touch gestures are recorded. These are used to train a random forest to classify the intended gesture. The mapping from electrical states (capacitive sensors) to intentional states is **learned**.

*[Image credit: Elisfm - via Wikimedia Commons; public domain]*

### Benefits
* **Robustness to noise** PFs work well even with input sensors that are noisy.
* **Robustness to poorly specified models** PFs can cope predictably even if our models are bad.
* **Robustness to intermittence** PFs can continue to sensibly interpolate when input cuts out.
* **Uncertainty estimates** PFs *know how certain they are* and this can be used in the interaction design.
* **Decoupled from real-time** PFs can infer past (smoothing), present (filtering) and future (forecasting).
* **Inherent fusion of multiple input sensors** PFs are often used to solely to fuse together multiple inputs from different sensors.
* **Better feedback** PFs  offer the opportunity to give users rich insight into the process of intention decoding.
* **Flexible modeling** PFs can incorporate both fundamental modeling (e.g. physiological or cognitive models) and data-driven machine learning.

# Probability refresher

## Random variables and distributions
A **random variable** is a variable that can take on different values, but we do not know what value it has; i.e. one that is "unassigned". However, we have some knowledge which captures the possible states the variable could take on, and their corresponding probabilities. Probability theory allows us to manipulate random variables without having to assign them a specific value.

A random variable is written with a capital letter, like $X$.

A random variable might represent the outcome of dice throw (discrete); whether or not it is raining outside (discrete: binary); the height of person we haven't met yet (continuous); the position of a user's hand (continuous, multivariate);.

## Distributions
A **probability distribution** defines how likely different states of a random variable are.

We can see $X$ as the the *experiment* and $x$ as the *outcome*, with a function mapping every possible outcome to a probability. We write $P(x)$ to mean the probability of $P(X=x)$ (note the case!).

$$P(X=x),\  \text{the probability of random variable X taking on value x}\\
P(X),\  \text{shorthand for probability of X=x }\\
P(x),\  \text{shorthand for probability of specific value X=x }\\
$$
We can see an outcome as a random variable taking on a specific value i.e. $P(X=x)$. Note that we use $P(A)$ to mean the probability of **event** $A$, not the random variable $A$.

### Discrete and continuous
Random variables can be continuous (e.g. the height of a person) or discrete (the value showing on the face of a dice).

* **Discrete variables** The distribution of a discrete variable is described with a **probability mass function** (PMF) which gives each outcome a specific value; imagine a dictionary mapping outcomes to probabilities. The PMF is usually written $f_X(x)$, where $P(X=x) = f_X(x)$.

* **Continuous variables** A continuous variable has a **probability density function** (PDF) which specifies the spread of the probability as a *continuous function* $f_X(x)$. It is **not** the case that $P(X=x) = f_X(x)$ for PDFs.

##### Integration to unity
A probability mass function or probability density function *must* sum/integrate to exactly 1, as the random variable under consideration must take on *some* value. Every repetition of an experiment has exactly one outcome.

$$\sum_i f_X(x_i) = 1\quad \text{for PMFs of discrete RVs}$$
$$\int_x f_X(x)\ dx = 1\quad \text{for PDFs of continuous RVs}$$

---

## PMF example: sum of dice rolls

```python
# standard imports
import sys
sys.path.append("src")
from matrices import print_matrix
import numpy as np
import matplotlib.pyplot as plt
import sys, os, time
import pandas as pd
%matplotlib inline
import matplotlib as mpl
plt.rc('figure', figsize=(8.0, 4.0), dpi=140)
import scipy.stats
import pykalman
from scipy.stats import norm
import scipy.stats
import IPython
```

```python
# the PMF of the sum of two dice rolls
def two_dice():
    # form the sum of the cross product of these possibilities
    roll_two = [i+j for i in range(1,7) for j in range(1,7)]
    # now plot the histogram
    pmf, edges, patches = plt.hist(roll_two, normed=True, bins=range(1,14))
    print("Sum of PMF %.2f" % np.sum(pmf)) # sum of probability should be *exactly* 1.0
    plt.title("PMF of sum of 2d6 dice")
    plt.xlabel("Sum of rolls x")
    plt.ylabel("P(x)")
```

```python
two_dice()
```

## Samples and sampling
**Samples** are observed outcomes of an experiment; we will use the term **observations** synonymously. We can **sample** from a distribution; this means simulating outcomes according to the probability distribution of those variables.

For example, we can sample from the sum of dice PMF by rolling two dice and summing the result. This is a sample or a draw from this distribution.

For discrete random variables, this is easy: we simply produce samples by drawing each outcome according to its probability. For continuous variables, we need to use specific algorithms to draw samples according to a distribution.

```python
# the PMF of the sum of two dice rolls
def sample_two_dice():
    roll_two = [i+j for i in range(1,7) for j in range(1,7)]
    pmf = np.histogram(roll_two, normed=True, bins=range(1,14))[0]
    cmf  = np.cumsum(pmf) # cumulative sum of the amount of probability in each bin
    uniform_samples = np.random.uniform(0, 1, 200)
    discrete_samples = np.digitize(uniform_samples, cmf) + 1 # compensate for bin starting on 1, not 0

    plt.hist(roll_two, bins=range(1,14), facecolor='C0', normed=True, alpha=0.2, label="Sampled histogram")
    plt.hist(discrete_samples, bins=range(1,14), facecolor='none', edgecolor='C1', linewidth=2, normed=True, label="True PMF")
    plt.legend()
sample_two_dice()
```

### Probability distribution functions (for continuous random variables)
The PDF $f_X(x)$ of a random variable $X$ maps a value $x$ (which might be a real number, or a vector, or any other continuous value) to a single number, the density at the point. It is a function $\real^N \rightarrow \real^+$, where $\real^+$ is the positive real numbers.

* While a PMF can have outcomes with a probability of at most 1, it is *not* the case that the maximum value of a PDF is $f_X(x) \leq 1$ -- *just that the integral of the PDF be 1.*

The value of the PDF at any point is **not** a probability, because the probability of a continuous random variable taking on any specific number must be zero.

Instead, we can say that the probability of a continuous random variable $X$ lying in a range $[a,b]$ is:
$$\begin{equation} P(X \in [a,b]) = (a < X < b)  = \int_a^b f_X(x) \end{equation}$$

(if we were doing this more formally, we'd define things in terms of the cumulative density function (CDF) instead, which makes many things much easier).

## PDF example: the normal disribution
The most ubiquitous of all continuous PDFs is the **normal** or **Gaussian** distribution. It assigns probabilities to real values $x \in {\mathbb{R}}$ (in other words, a sample space consisting of all of the real numbers). It has a density given by the PDF:

$$f_X(x) = \frac{1}{\sqrt{2\pi\sigma^2}}\, e^{-\frac{(x - \mu)^2}{2 \sigma^2}}$$

We use a shorthand notation to refer to the distribution of continuous random variables:
$$\begin{equation}X \sim \mathcal{N}(\mu, \sigma^2)\end{equation},$$
which is read as
>"Random variable X is distributed as [N]ormal with mean $\mu$ and variance $\sigma^2$"

### Location and scale
The normal distribution places most density close to its center $\mu$ (the "mean"), with a spread defined by $\sigma^2$ (the "variance"). This can be though of the **location** and **scale** of the density function. Most standard continuous random variable PDFs have a location (where density is concentrated) and scale (how spread out the density is).

```python
import scipy.stats as stats
# Plot the PDF of the normal distribution
def plot_normal():
    # plot the normal (Gaussian distibution) along with a set of points drawn from that distribution
    x = np.linspace(-4,4,100)
    y = stats.norm.pdf(x,0,1) # mean 0, std. dev. 1
    plt.plot(x,y, label="PDF")
    plt.axhline(0, color='k', linewidth=0.2) # axis line

    # mark the mean
    plt.text(0, 0.51, '$\mu$')
    plt.axvline(0, color='r')
    # highlight one std. dev. to the right
    plt.axvspan(0,1, facecolor='b', alpha=0.1, label="1 std. dev.")
    plt.text(1.2, 0.3, '$\sigma$')
    # take 1000 random samples and scatter plot them
    samples = stats.norm.rvs(0,1,1000)
    plt.scatter(samples, np.full(samples.shape, .2), s=448, c='b', alpha=0.1, marker='|', label="Samples")
    plt.xlabel("$x$")
    plt.ylabel("$P(x)$")
    plt.legend()
```

```python
plot_normal()
```

## Multivariate distributions
Continuous distributions generalise discrete variables (probability mass functions) (e.g. over $\mathbb{Z}$) to continuous spaces over $\real$ via probability density functions.

Probability densities can be further generalised to vector spaces, particularly to $\real^n$. This extends PDFs to assign probability across an entire vector space, under the constraint that the (multidimensional integral) $$\int_{x\in\real^n} f_X (x) =1, x \in \real^n.$$

Distributions with PDFs over vector spaces are called **multivariate distributions** (which isn't a very good name; vector distributions might be clearer). In many respects, they work the same as **univariate** continuous distributions. However, they typically require more parameters to specify their form, since they can vary over more dimensions.

The normal distribution is very widely used as the distribution of continuous random variables. It can be defined for a random variable of *any dimension* (a distribution over any real vector space, including infinite ones!); a **multivariate normal** in statistical terminology.

A multivariate normal is fully specified by a **mean vector** $\vec{\mu}$ and a **covariance matrix** $\Sigma$. If you imagine the normal distribution to be a ball shaped mass in space, the mean *translates* the mass, and covariance applies a *transformation* matrix (scale, rotate and shear) to the ball to stretch it out into an ellipse. All normal distributions have an elliptical shape with highest density at the mean and falling off towards the edges.

We can now talk about the **joint probability density** (density over all dimensions) and the **marginal probability density** (density over some sub-selection of dimensions).

For example, consider $X \sim N(\vec{\mu}, \Sigma), X \in \real^2$, a two dimensional ("bivariate") normal distribution. It has a distribution $P(X_0,X_1)$  or $P(X,Y)$.

```python
import scipy.stats
def demo_normal(ax, mean, cov, title):
    x,y = np.meshgrid(np.linspace(-3,3,50), np.linspace(-3,3,50))
    pos = np.empty(x.shape + (2,))
    pos[:,:,0] = x
    pos[:,:,1] = y
    joint_pdf = scipy.stats.multivariate_normal.pdf(pos, mean, cov)
    ax.pcolor(x,y,joint_pdf, cmap='viridis', vmin=0, vmax=0.25)

    ax.axhline(0, color='C1', linewidth=0.2)
    ax.axvline(0, color='C1', linewidth=0.2)
    ax.text(0, 3.2, title, ha='center')
    ax.axis("off")
    ax.axis("image")

fig = plt.figure()
ax = fig.add_subplot(2,3,1)
demo_normal(ax, [0,0], [[1,0],[0,1]], "Unit")
ax = fig.add_subplot(2,3,2)
demo_normal(ax, [0,0], [[0.25,0],[0,0.25]], "Tighter")
ax = fig.add_subplot(2,3,3)
demo_normal(ax, [1,-0.5], [[2,0],[0,2]], "Off-centre")
ax = fig.add_subplot(2,3,4)
demo_normal(ax, [0,0], [[2,0],[0,1]], "Stretched")
ax = fig.add_subplot(2,3,5)
demo_normal(ax, [0,0], [[2,0.1],[1,1]], "Skewed")
ax = fig.add_subplot(2,3,6)
demo_normal(ax, [0,0], [[2,-0.9],[0.4,0.2]], 'Skewed')
```

## Joint, conditional, marginal

The **joint probability** of two random variables is written $$P(X,Y)$$ and gives the probability that $X$ and $Y$ take the specific values *simultaneously* (i.e. $P(X=x) \land P(Y=y)$).

The **marginal probability** is the derivation of $P(X)$ from $P(X,Y)$ by integrating (summing) over all the possible outcomes of $Y$:
$$P(X) = \int_y P(X,Y=y) dy\  \text{for a PDF.}$$
$$P(X) = \sum_y P(X,Y=y)\  \text{for a PMF.}$$

**Marginalisation** just means integration over one or more variables from a joint distribution: it *removes* those variables from the distribution.

Two random variables are **independent** if the they do not have any dependence on each other. If this is the case then the joint distribution is just the product of the individual distributions:
$P(X,Y) = P(X)P(Y).$ This is not true in the general case where the variables have dependence.

The **conditional probability** of $X$ *given* $Y$ is written as $$P(X|Y)$$ and can be computed as $$\begin{equation} P(X|Y) = \frac{P(X,Y)}{P(Y)}. \end{equation}$$ This tells us how likely $X$ is to occur *if we already know*  (or fix) the value of $Y$.

Because we can see a distribution over $R^2$ as a distribution over two variables $X$ and $Y$, we can talk about things like marginalising normal distributions:

```python
def joint_marginal(cov):
    # create an independent 2D normal distribution
    x,y = np.meshgrid(np.linspace(-3,3,50), np.linspace(-3,3,50))
    pos = np.empty(x.shape + (2,))
    pos[:,:,0] = x
    pos[:,:,1] = y
    joint_pdf = scipy.stats.multivariate_normal.pdf(pos, [0,0], cov)
    fig = plt.figure()
    # plot the joint
    ax = fig.add_subplot(2,2,1)
    ax.axis('equal')
    plt.title("Joint p(x,y)")
    ax.pcolor(x,y,joint_pdf, cmap='viridis')
    # plot the marginals
    ax = fig.add_subplot(2,2,3)
    ax.axis('equal')
    plt.title("Marginal $P(x) = \int\  P(x,y) dy$")
    ax.plot(x[0,:], np.sum(joint_pdf, axis=0))
    ax = fig.add_subplot(2,2,2)
    ax.axis('equal')
    plt.title("Marginal $P(y) = \int\  P(x,y) dx$")
    ax.plot(np.sum(joint_pdf, axis=1), x[0,:])
    # plot p(x|y)
    ax = fig.add_subplot(2,2,4)
    ax.axis('equal')
    plt.title("Conditional $P(x|y) = \\frac{P(x,y)}{P(y)}$")
    marginal = np.tile(np.sum(joint_pdf, axis=0), (joint_pdf.shape[0],1))
    ax.pcolor(x,y,joint_pdf/marginal, cmap='viridis')
joint_marginal([[1,0],[0.5,1]])
plt.tight_layout()
```

## Probability theory and Bayesian inference

#### Probability as a calculus of belief
*Bayesians* treat probability as a **calculus of belief**; in this model of thought, probabilities are measures of degrees of belief. $P(A)=0$ means a belief that $A$ cannot be true and $P(A)=1$ is a belief that $A$ is absolutely certain.

#### Probability as the optimal way of representing uncertainty
Other representations of uncertainty are strictly inferior to probabilistic methods *in the sense that* a person, agent, computer placing "bets" on future events using probabilistic models has the best possible return out of all decision systems when there is uncertainty.

*Bayesians* allow for belief in states to be combined and manipulated via the rules of probability. The key process in Bayesian logic is *updating of beliefs*. Given some *prior* belief (it's Glasgow, it's not likely to be sunny) and some new evidence (there seems to be a bright reflection inside) we can update our belief to calculate the *posterior* -- our new probability that it is sunny outside. Bayesian inference requires that we accept priors over events, i.e. that we must explicitly quantify our assumptions with probability distributions.

#### Prior, likelihood, posterior, evidence

We often want to know the probability of a some outcome $A$ given some other outcome $B$; that is $P(A|B)$. But we are often in the situation that we can only compute $P(B|A)$.

In general $P(A|B) \neq P(B|A);$ and the two expressions can be completely different.

Typically, this type of problem occurs where we:
* want to know the probability of some event given some *evidence*
* but we only know the probability of the evidence given the event

**Bayes' rule** gives a consistent way to invert the probability distribution:
$$ \begin{equation} P(A|B) = \frac{P(B|A) P(A)}{P(B)} \end{equation}$$

This follows directly from the axioms of probability. Bayes' Rule is a very important rule, and has some surprising results.

* $P(A|B)$ is called the **posterior** -- what we want to know, or will know after the computation
* $P(B|A)$ is called the **likelihood** -- how likely the event $A$ is to produce the evidence we see
* $P(A)$ is the **prior**  -- how likely the event $A$ is regardless of evidence
* $P(B)$ is the **evidence** -- how likely the evidence $B$ is regardless of the event.

Bayes' rule gives a consistent rule to take some prior belief and combine it with observed data to estimate a new distribution which combines them.

We often phrase this as some **hypothesis** $H$ we want to know, given some **data** $D$ we observe, and we write Bayes' Rule as:
$$ \begin{equation}P(H|D) = \frac{P(D|H) P(H)}{P(D)} \end{equation}$$

(the probability of the hypothesis given the data) is equal to (the probability of the data given the hypothesis) times (the probability of the hypothesis) divided by (the probability of the data).

In other words, if we want to work out how likely a hypothesis is to be true given observations, but we only know how likely we are to have seen those observations if that hypothesis *was* true, we can use Bayes' rule to solve the problem.

## Bayes' rule for combining evidence
Bayes' rule is the correct way to combine prior belief and observation to update beliefs. This can be used to "learn", where "learning" means updating a probability distribution based on observations. It has enormous applications anywhere uncertain information must be fused together, whether from multiple sources (e.g. sensor fusion) or over time (e.g. probabilistic filtering).

```python
import time
import scipy.stats

def prior_posterior(prior_mean=0, prior_std=1, sonar_std=1, n=10, anim=False):
    mean = prior_mean
    std = prior_std
    var = std*std
    prior = scipy.stats.norm(mean,std)
    evidence = scipy.stats.norm(1, 0.25)
    fig = plt.figure()
    ax = fig.add_subplot(1,1,1)

    xs = np.linspace(-5,5,200)
    ax.fill_between(xs, prior.pdf(xs), label="Prior belief", alpha=0.3)
    ax.fill_between(xs, evidence.pdf(xs), label="True generating PDF", alpha=0.3)

    sample_var = sonar_std**2 # the *expected* variance of our observations
    # note that changing this allows us to continously adjust our belief
    # in our observations
    ax.plot([0,0],[0,-0.1], 'c', alpha=0.7, label="Evidence")
    ax.plot([0,0],[0,-0.1], 'k:', alpha=0.7, label="Posterior belief")
    ax.set_title("Recursive Bayesian estimation")

    ax.set_xlabel("x")
    ax.set_ylabel("PDF $f_X(x)$")
    ax.axvline(1.0, label='True')
    ax.legend()
    for i in range(n):

        sample = evidence.rvs()
        # single step update for a normal distribution
        mean = (var * sample + sample_var * mean) / (sample_var + var)
        var = (var*sample_var) / (sample_var+var)

        sample_pdf = scipy.stats.norm(sample, sonar_std).pdf

        # plot the sample and the resulting pdf
        ax.plot([sample,sample],[0,-0.5], 'c', alpha=0.7)
        if anim:
            ax.plot(xs,-sample_pdf(xs), 'c', alpha=0.25)
        ax.plot(xs, scipy.stats.norm(mean,np.sqrt(var)).pdf(xs), 'k:', alpha=0.25)
        if anim:
            time.sleep(1.0)
            fig.canvas.draw()

    ax.fill_between(xs, scipy.stats.norm(mean,np.sqrt(var)).pdf(xs), color='g', label="Final posterior", alpha=0.2)
    ax.legend()

```

```python

prior_posterior(0,0.75)
```

```python

prior_posterior(0,3)
```

```python

prior_posterior(-3,0.5)
```

```python

prior_posterior(-3,0.5, n=100)
```

# Probabilistic filtering
We will use this recursive form of Bayesian updating to estimate user intentions online. This is a **probabilistic filter**, as described in the introduction.

This filter maintains a state distribution, which is used as prior for the next step of estimation. Evidence is observed, and a posterior is computed; this becomes the prior for the next step, after a **prediction** step is used to align the prior with the known or estimated behaviour.

Unlike other filters, such filters maintain a **distribution** over some hidden variable we are trying to estimate. This makes it possible for them to cope with noise and uncertainty robustly. It also complicates their implementation, but their are good models which are readily available.

In HCI, at the very highest level, we want to estimate **intention $X_t$** given **sensor input $Y_t$** $P(X_t|Y_t)$, both of which change over time.

* **Abstraction** $X_t$ might be expressed across multiple levels of abstraction. For example, "targeting a point", "entering a letter", "entering a command", "opening a file", "rearranging a document"
* **Sensing** $Y_t$ might be distributed over timescales (e.g. immediate cursor information versus learned language model) and over sensor modalities (e.g. combining information from a pose sensor with microphone data to estimate surface contact events).

### Overview diagram

## Probabilistic filtering terminology

Notation:
* We have a sequence of states over time, indexed by $t$
* $X_t$ the variable we want to know (at time $t$) (e.g. an intention inside a user's head).
* $Y_t$ the variable we can observe (e.g. a sensor we can get readings from).
* For computational simplicity, we assume **discrete time**, i.e. we observe sensors in a discrete, regularly sampled way.

* We want to compute $P(X_t|Y_t)$ (the **inverse problem**).
* We use a **forward model** $P(Y_t|X_t)$ to infer this.
* We need to define two functions: ${\bf\hat{y_t}} = f({\bf \hat{x}}_t)$ (the **observation function**) and $\hat{\bf x}_{t} = g(\hat{\bf x}_{t-1})$ (the **dynamics** or **process function**).
* We also need to compute the likelihood of the real observation given our model: $p(\bf\hat{y_t}|{\bf y_t})$.

* $f$, $g$ are often very simple functions.

#### Recursive filtering

Probabilistic filters are sometimes called **recursive Bayesian filters**.
* They are **Bayesian** because they represent belief about states via probability distributions.
* They are **recursive** because they take a *prior*, condition on *evidence* and compute a *posterior*; this *posterior* then becomes the *prior* at the next time step.

As well as straightforward conditioning on observed evidence, probabilistic filters incorporate dynamics which form predictions of the world at the next time step.

#### Predictor-corrector
**This is a predictor-corrector model**; the dynamics model supplies predictions, and corrections to those predictions are applied by the observation model.

## Uses of probabilistic filters
Probabilistic filters are applicable in many HCI tasks, wherever there is a process evolving over time and uncertainty about what users want to do.

For example, we have used them extensively to track finger configurations when using capacitive sensors. In this case, we have a finger pose state space (hidden) and a sensor matrix (observed), and the filter estimates pose in real-time.

[See the AnglePose video](http://www.dcs.gla.ac.uk/~jhw/AnglePose-final.mov)

(we have much better 3D tracking with probabilistic filters incorporating deep learning now, but I don't have a video :( )

We will examine a simple case -- the **noisy cursor problem**, where we robustly estimate cursor trajectories under the influence of noise. We will see how this can be used to make a reliable input device from a crude, unreliable sensing mechanism.

## The problem
**We want to track the position of a cursor; a 2D point under the control of a user.**

We will take the case of a mouse (or touchpad). A mouse is usually very reliable and outputs data that is easy to reconstruct into a cursor trajectory; just integrate up the average flow vector seen by the optical sensor.

[img]

We will simulate some of the issues that might happen with less reliable sensors, such as tracking an object with a camera-based system. This means we might encounter:
* **noise**: continuous random variations in the measured position
* **dropout**: complete loss of measurement or tracking
* **glitches**: random spikes of sensing that are not due to intentional movement (e.g. when the camera has a false recognition and the tracking suddenly jumps).

## The cursor problem
We will use a simple simulator which will corrupt mouse input with these different sources of noise, and also allow us to apply processing to the position signal to attempt to restore the intended position.

```python

import noise_cursor
noise_cursor = reload(noise_cursor)
from noise_cursor import NoiseCursorDemo
```

```python
# no noise
n = NoiseCursorDemo()
%gui tk
```

```python
# some noise
n = NoiseCursorDemo(noise=20)
%gui tk
```

## Why not just smooth things with a simple linear filter?
We can write a very simple smoothing filter, using the equation:

$$y_t = \alpha y_{t-1} + \alpha x_t,$$ where $x_t$ is the input (noisy position) and $y_t$ is the smoothed output.

This adds some dynamics to our estimation system. This is a one-pole IIR filter, or an **exponential smooth**.

```python
# Creates a simple one-pole IIR smoothing filter,
# with a cutoff set by alpha (closer to 1 is more extreme filtering)
def mk_lowpass(alpha):
    state = [0,0]
    def update(x,y):
        if x==x and y==y: # nan test
            state[0] = alpha*state[0] + (1-alpha)*x
            state[1] = alpha*state[1] + (1-alpha)*y
        return list(state)
    return update

```

```python
n = NoiseCursorDemo(filter=mk_lowpass(alpha=0.9), noise=30)
%gui tk
```

### Spike noise
This isn't a bad solution for Gaussian noise, though we do have some significant lag. But when we start to encounter disturbances beyond simple noise, the filter begins to break down. For example, jump (spike) noise that we might see when a tracker temporarily locks on to a false target.

```python
# and some mistracks
n = NoiseCursorDemo(filter=mk_lowpass(alpha=0.95), noise=30,
                    jump=0.05, jump_scale=5000)
%gui tk
```

### Signal dropout
If we now experience signal drop out, then we have the problem that the cursor freezes in place (or disappears entirely if we have a particularly poorly implemented system)

```python
# and some tracking losses
n = NoiseCursorDemo(filter=mk_lowpass(alpha=0.95), noise=30,
                    jump=0.05, jump_scale=5000,
                    dropout=[0.05, 0.1])
%gui tk
```

## Maybe we need a better filter?
The 1Euro filter, from [Casie et. al (CHI 2012)](http://cristal.univ-lille.fr/~casiez/acm.php?id=N05397) is an adaptive (nonlinear) filter for noisy cursor tracking. This essentially adjust $\alpha$ (the smoothing parameter) so that the filter gets more or less smooth depending on how fast the cursor seems to be going. This allows heavy smoothing when the cursor is slow, and lighter smoothing during fast ballistic movements, where responsiveness is more important than precision.

```python
from oneeurofilter import OneEuroFilter

# make a 2D OneEuroFilter function
def mk_oneuro(*args, **kwargs):
    # state, which is propagated from time step to time step
    filters = [OneEuroFilter(*args, **kwargs),OneEuroFilter(*args, **kwargs)]
    state = [0,0]
    def update(x,y):
        if x==x and y==y: # nan test
            state[0] = filters[0](x)
            state[1] = filters[1](y)
        return list(state)

    return update
```

```python

n = NoiseCursorDemo(filter=mk_oneuro(freq=1.0, mincutoff=0.001, beta=0.001), noise=30)
```

```python
# but with dropout and mistracks
n = NoiseCursorDemo(filter=mk_oneuro(freq=1.0, mincutoff=0.001, beta=0.001), noise=30,
                    jump=0.05, jump_scale=5000,
                    dropout=[0.02, 0.1])
%gui tk
```

## Thoughts
These various ad hoc signal processing approaches can clean up some forms of noise. But they struggle to track the cursor well with very degraded sensing. A more principled approach can do a better job -- by *representing and propagating uncertainty*.

This has several key parts:

* **How to represent a distribution over possible states**
* **How to predict a new distribution over states, given what is currently known**
* **How to update a distribution given some specific, observed evidence**

---------------

## The task
We want to recover the **intended position** of the cursor from the **observed sensing**.

* That is, we have $\bf x_t$ be the **intended position** of the cursor at $t$ (this is the hidden variable we wish to estimate). The intended position exists in the user's head.
* We have $\bf y_t$, the observation made at time $t$, which might be the displacement vector the OS reports in our example.

We need to write down our model explicitly:

* **State space for $\bf x_t$**. $\bf x_t$ is our belief about intended location. It obviously has at least two coordinates giving an intended location in screen space. But we can do a better job at predicting motion if we assume some predictable smooth *dynamics* of the cursor. In particular, we can assume that there is some associated **velocity** and **acceleration** of the cursor, and at each time point time, ${\bf x_t} = [x_t, y_t, \dot{x}_t, \dot{y}_t, \ddot{x}_t, \ddot{y}_t]$.
($\dot{x}$ means the first time derivative of $x$, $\ddot{x}$ means the second time derivative of $x$).
* **State space for $y_t$** $y_t$ is given by our sensor configuration. The OS reports two positions , $mx_t$ and $my_t$ at each observation.
So ${\bf y_t} = [ mx_t, my_t ]$

* **Prior** *where would we believe the cursor to be if we had made no measurement? $p({\bf x_0})$*
We can assume the cursor is intended to be somewhere on screen. Beyond that, we might not have any guesses as to where the cursor might be. We could be clever and assume that the cursor is likely to be near targets of interest (e.g. close to menu headers), but for now, we will assume a simple normal prior. We can assume a simple normal distribution on velocity and acceleration.

* **Dynamics** *given a current estimate of intended position, where would we expect the next intended position to be?*
We would assume that the cursor is near where it was, but is moving smoothly some velocity and acceleration: after all, it is the result of a physical motion in the world and thus has second-order dynamics.
This is the function $f$ in $${\bf x_{t+1}} = f({\bf x_t}) + \epsilon$$

* **Observation** *given our estimate of intended position, what observations would we expect?*
We'll assume that the velocity of the cursor gives us the frame-by-frame delta in mouse position. The observation is assumed to be a noisy representation of the true velocity.
This is the function $g$ in $$\hat{\bf y_t} = g({\bf x_t}).$$

* **Likelihood** given an observation, how probable is it under compared to our expected observations? This is the likelihood function $$P({\bf y_t}|{\bf x_t}) = P({\bf y_t}|{\bf \hat{y_t}})$$

We cannot in general compute these when $x_t$ is a random variable, because we have no direct way of applying functions to *distributions* rather than to specific known points. However, if we make some simple assumptions, these equations can be applied to random variables with specific form.

## The Kalman filter
### Assumptions
We are going to model the distribution of possible states in our state space for ${\bf x_t}$, updating this over time with observations that are made ${\bf y_t}$ to compute the next step.

The Kalman filter lets us do this very efficiently, as long as we can make some fairly strong assumptions about the *form of uncertainty* and the *type of dynamics* we expect to see.

#### Normality of all distributions
The Kalman filter approximates all distributions as normal (Gaussian) distributions.
This includes:
* the *process noise*, i.e. the stochastic part of the dynamics (how much the state "blurs" out on each time step)
* the *observation noise*, i.e. the noise in the observation process (how "blurred" the observation is)
* the current *state* of the filter (the current belief is)
* the *likelihood* of the observation given the current state, which is just the likelihood of the observation under the state transformed into the observation space.

All of these are Gaussian and characterised by a **covariance matrix** $\Sigma$ and a mean vector (centre) $\mu$, which specifies the shape and position of the distribution; it can be seen as defining the shape of the ellipsoidal isosurfaces of the distribution.

**The key thing is that we can separate all computations into operations on the mean vector, and operations on the covariance matrix. We can then do all computations on these. This is simple and efficient**

The Kalman filter has a few basic stages in each update:
* prior -- current belief about some unknown state (e.g. where the user's hand is)
* prediction -- updated belief, using information about how the world evolves (e.g. the hand will keep moving with similar velocity)
* predicted observation -- estimate of what will be observed. Given where we believe the hand to be, what sensor values might we see?
* posterior. Compare with observation -- given an actual observation (which will have some uncertainty) compute how likely different states would be.
* The posterior is then fed in as the input prior for the next time step.

#### Linearity of dynamics
The Kalman filter, in its basic form, assumes that all dynamics are **linear**. That is, our next guess of $$x_{t+1} = Ax_t$$ instead of $$x_{t+1} = f(x_t)$$ for any $f$. Or alternatively the transformation function $f(x)$ from the previous state to the next must be expressible as a simple matrix multiplication.

We will assume discrete time, i.e. that we make discrete steps from one time point to the next, and our dynamic system is a function that maps from points in the state space directly to new points at the next time step.

This is a simple but powerful model of how systems evolve over time and can be applied to many contexts.

For example, basic second-order dynamics of a point  can be written as a discrete time linear system:

$$\vec{x}_t = [x_t, \dot{x_t}, \ddot{x_t}]$$

$$A = \begin{bmatrix}
1 & \Delta T & \frac{1}{2}\Delta T^2\\
0 & 1& \Delta T\\
0 & 0& 1\\
\end{bmatrix}$$

$$\vec{x}_{t+1} = A{\bf x_t}$$

This models a point with position $x_t$, velocity $\dot{x_t}$ and acceleration $\ddot{x_t}$. If we apply this matrix to a state, we get a smooth trajectory:

```python
# initial state; pos = 0, vel=-0.5, acc=0.1
x = np.array([0.0, -0.5, 0.1])
dt = 0.1 # size of one time step
n = 150  # number of timesteps
# simple 2nd order dynamics equation

A = np.array([[1, dt, 0.5*dt**2],
              [0, 1, dt],
              [0, 0, 1]])

# apply dynamics
xs = [x]
for i in range(n):
    x = np.dot(A,x)
    xs.append(x)

# plot position
plt.plot(np.array(xs)[:,0], label="Position")
plt.plot(np.array(xs)[:,1], label="Velocity")
plt.plot(np.array(xs)[:,2], label="Acceleration")
plt.legend()
plt.xlabel("Time step")
plt.ylabel("Position")
```

### Time varying state transition matrices
Note that the Kalman filter does not require $A$ to be the *same* at each timestep; we can have a time-varying $A_t$ which is different at each time step. This can be used to **locally** linearise a system with nonlinear global dynamics (i.e. to use a new linear approximation at each new timestep).

### Linearity of observations
Additionally, the mapping ${\bf x_t} \rightarrow {\bf y_t}$ (and thus (${\bf y_t} \rightarrow {\bf x_t}$) must also be linear, and described by a matrix $C$. Given a $d_x$-dimensional state and a $d_y$ dimensional observation space,  $C$ is a $d_x \times d_y$ matrix.

This matrix represents the function $g(x)$ which maps from the estimated state onto observations that would be compatible with it. **Note: the function $g(x)$** goes **from state to observations** and not the other way around! We estimate what the world would look like if these states were true, and compare that to reality.

## Why?
These restrictions seem quite limiting, but the problem with maintaining probabilistic state is that the density/mass functions could be arbitrary; and there are no direct ways to manipulate such arbitrary functions. The **linear Gaussian** model avoids this by using these remarkable properties of Gaussian functions:

* every *linear transformation* of a Gaussian is Gaussian (therefore any predictive model that can be written as a linear transform can be used to generate a new Gaussian predictive distribution, and Gaussian distributions can be freely transformed to/from observation and state space.),
    * Applying the transformation $Ax+b$ to a multivariate Gaussian parameterised by $\mu, \Sigma$ results in a new Gaussian with parameters $\mu^\prime = A\mu+b, \Sigma^\prime = A\Sigma A^T$.
* the *convolution of two Gaussians* is Gaussian, (so adding Gaussian uncertainty to a Gaussian distribution remains a Gaussian),

(see [this page](http://www.tina-vision.net/docs/memos/2003-003.pdf) for details on the mathematics for products and convolutions of multivariate Gaussians, or the excellent [Matrix Cookbook](http://compbio.fmph.uniba.sk/vyuka/ml/old/2008/handouts/matrix-cookbook.pdf) which lists numerous such useful formulae)

As a consequence, the Kalman filter can maintain the full probabilistic state and perform all of its updates just by updating the parameters of a multivariate Gaussian (a mean vector $\bf \mu$ and covariance matrix $\Sigma$). The algebra to derive this is somewhat hairy, and we will omit it. In practice, unless we have to implement a Kalman filter from scratch, we only need to provide the matrices required and let standard libraries do the update steps.

This is very computationally and inferentially efficient: it is quick to do, and the estimates can be very good even with limited data, *as long* as the problem at hand is reasonably modeled with these assumptions.

# Building a cursor Kalman filter

### Dynamics

Let's first assume we only have a 2D position, velocity and acceleration, so our state space is $[x_t, y_t, \dot{x}_t, \dot{y}_t, \ddot{x}_t, \ddot{y}_t]$, and we can write some simple second order dynamics:

$$A = \begin{bmatrix}
1 & 0 & \Delta T & 0 & \frac{1}{2}\Delta T^2 & 0 \\
0 & 1 & 0 & \Delta T & 0 & \frac{1}{2}\Delta T^2  \\
0 & 0 & 1 & 0 & \Delta T & 0\\
0 & 0 & 0 & 1 & 0 & \Delta T\\
0 & 0 & 0 & 0 & 1 & 0\\
0 & 0 & 0 & 0 & 0 & 1\\
\end{bmatrix}$$

This is just the extension of what we saw above to 2D.

These dynamics are *generic* and are not special to cursor trajectory estimation. For many 2D second-order systems, this matrix is usable as is; more complex dynamics might be involved where problems have stat variables beyond simple 2D movement (e.g. the Speed Dependent Automatic Zooming formulation given in [Eslambolchilar 2003](http://eprints.gla.ac.uk/13684/)).

#### Process noise: uncertain dynamics

We also assume that our dynamics have some **noise**; i.e. they are not fully deterministic. We can predict the future, but not exactly. This might be because our model of the system is imprecise -- maybe it is not exactly linear, but a rough approximation.

By the restrictions of the Kalman filter, this must be Gaussian (normally distributed noise), and it has a structure given by a **covariance matrix** $\Sigma_A$. We need to **specify** this covariance matrix (note that it can be *learned from data* as well).

For simplicity, we will assume the noise is uncorrelated, and is equal across $x$ and $y$ (and their derivatives), so the covariance looks like a diagonal matrix:

$$\Sigma_A = \begin{bmatrix}
\sigma_x & 0  & 0 & 0  & 0 & 0 \\
0 & \sigma_x  & 0 & 0  & 0 & 0 \\
0 & 0  & \sigma_{dx} & 0  & 0 & 0 \\
0 & 0  & 0 & \sigma_{dx} & 0 & 0 \\
0 & 0  & 0 & 0 & \sigma_{ddx} & 0 \\
0 & 0  & 0 & 0 & 0 & \sigma_{ddx} \\
\end{bmatrix}$$

#### The dynamics equation
Our whole dynamics equation is then just:

$$X_{t+1} = A{\bf x_t} + N(0,\Sigma_A) $$

(the transformation given by $A$ followed by some extra Gaussian uncertainty, specified by $\Sigma_A$).

This is the key equations that does **prediction**. It takes a distribution at one step, and finds what it should look like in the next step.

We can write this in code:

```python
sigma_x = 1
sigma_dx = 0.1
sigma_ddx = 0.001
sigma_a = np.diag([sigma_x, sigma_x, sigma_dx, sigma_dx, sigma_ddx, sigma_ddx])

dt = 0.5 # 1 / frame rate in some time units
dt2 = 0.5 * dt * dt
A = np.array([[1,0, dt,0, dt2,0],
             [0,1, 0,dt, 0,dt2],
             [0,0, 1,0, dt,0],
             [0,0, 0,1, 0,dt],
             [0,0, 0,0, 1,0],
             [0,0, 0,0, 0,1]])

print_matrix("\\sigma_a", sigma_a)
sigma_a *= 0.01
print_matrix("A", A)
```

## Forward simulation

We can now draw samples of **behaviour**, by setting some specific conditions $x_0$, fixing $A$ to some configuration, and then plot the resulting trajectories. This lets us qualitatively establish whether the dynamics we have modelled are like those that we want to estimate. This is very simple to implement:

```python
%matplotlib notebook
%matplotlib notebook

import matplotlib.pyplot as plt

def simple_simulate_dynamics(A, sigma_a, x=None, n=100):
    if x is None:
        x = np.zeros((A.shape[0],))
    xs = []
    for i in range(n):
        # x_{t+1} = A x_t + N(0, sigma_a)
        x = np.dot(A,x) + scipy.stats.multivariate_normal.rvs(cov=sigma_a)
        xs.append(x)
    return np.array(xs)

def plot_dynamics(fig, A, sigma_a, x=None, sz=40):
    ax = fig.add_subplot(1,1,1)
    ax.set_xlim(-sz, sz)
    ax.set_ylim(-sz, sz)
    fig.canvas.draw()
    for i in range(7):

        xs = simple_simulate_dynamics(A, sigma_a, x=x)
        for j in range(10):
            ax.plot(xs[j*10:(j+1)*10,0], xs[j*10:(j+1)*10,1], '.-', color ='C%d'%i)
            fig.canvas.draw()
```

```python
# some random walks with these dynamics
fig = plt.figure(figsize=(8,8))
```

```python

plot_dynamics(fig, A, sigma_a)
```

```python
# some random walks with these dynamics
fig = plt.figure(figsize=(8,8))
```

```python
# some random walks with these dynamics
# but starting from a different initial condition
plot_dynamics(fig, A, sigma_a, x=[0.0,0.0,-4.0,0.0,0.1,0.0], sz=100)
```

## Alternative dynamics (changing $\Sigma_a$)
We can postulate alternative dynamics, and observe the effect

```python
# some random walks with these dynamics
fig = plt.figure(figsize=(8,8))
```

```python
# Just acceleration; smooth trajectories
sigma_x = 0.0
sigma_dx = 0.0
sigma_ddx = 0.0002
sigma_a2 = np.diag([sigma_x, sigma_x, sigma_dx, sigma_dx, sigma_ddx, sigma_ddx])
# some random walks with some alternative dynamics
plot_dynamics(fig, A, sigma_a2)
```

```python
# some random walks with these dynamics
fig = plt.figure(figsize=(8,8))
```

```python
# no acceleration, no velocity noise, just position noise
sigma_x = 0.05
sigma_dx = 0.0
sigma_ddx = 0.0
sigma_a3 = np.diag([sigma_x, sigma_x, sigma_dx, sigma_dx, sigma_ddx, sigma_ddx])
# some random walks with some alternative dynamics
plot_dynamics(fig, A, sigma_a3)
```

## Observations
We need to be able to transform our predicted internal state $\bf \hat{x_t}$ into the observation we would expect to see given that state. (NB: **not** to translate our observation into our state space!). This is the function $g\hat{(x_t}) \rightarrow \hat{y_t}$

In this case, we're assuming we observe a 2D position. Therefore we might reasonably assume that we'd expect to see a position equal to the position term of our state. We can again write this as a matrix $C$ (i.e. a linear projection from our internal state space to the observation):

$$C = \begin{bmatrix}
1 & 0 & 0 & 0 & 0 & 0\\
0 & 1 & 0 & 0 & 0 & 0\\
\end{bmatrix}$$

This simply maps our internal state space (which also includes dynamics) to the observed space, where we only see some position.

```python
C = np.array([[1,0,0,0,0,0],
              [0,1,0,0,0,0]]).astype(np.float64)
print_matrix("C", C)
```

We also know that our observation is **noisy** (i.e. not a true measurement of the world).
We can (again) use a Gaussian to represent the noise we expect to see, characterised by a covariance $\Sigma_c$. The following matrix assumes noises is equal on $x$ and $y$ and uncorrelated.

$$\Sigma_C = \begin{bmatrix}
\sigma_c & 0 \\
0 & \sigma_c \\
\end{bmatrix}$$

```python
sig_c = 15
sigma_c = np.diag([sig_c, sig_c])
print_matrix("\\sigma_c", sigma_c)
```

The complete equation for the observations is:
$${\bf\hat{y_t}} \sim N(C {\bf\hat x_t}, \Sigma_C)$$

## Prior
We need an initial guess for the state. This isn't usually very important, as the filter will eventually converge as long as the prior isn't very far off or excessively confident. We can write this again as a multivariate normal. Here we assume no correlation among any of the variables, so they just have some mean and variance for each independent component.

We can write this as $X_0 \sim N(\mu_0, \sigma_0)$, with:

$$\mu_0 = [x_c, y_c, 0, 0, 0, 0]$$
$$\sigma_0 = \begin{bmatrix}
x_c/2 & 0  & 0 & 0  & 0 & 0 \\
0 & y_c/2  & 0 & 0  & 0 & 0 \\
0 & 0  & \sigma_v & 0  & 0 & 0 \\
0 & 0  & 0 & \sigma_v  & 0 & 0 \\
0 & 0  & 0 & 0  & \sigma_a & 0 \\
0 & 0  & 0 & 0  & 0 & \sigma_a \\
\end{bmatrix}$$

```python
xmax, ymax = 800, 800 # screen size
xc, yc = xmax/2, ymax/2  # coordinates of screen centre
mu_0 = np.array([xc, yc, 0, 0, 0, 0])
sigma_vel = 10000
sigma_acc = 10000
sigma_0 = np.diag([10000, 10000, sigma_vel, sigma_vel, sigma_acc, sigma_acc])
print_matrix("\mu_0", mu_0)
print_matrix("\sigma_0", sigma_0)
```

## Creating the filter
We can now create a complete Kalman filter. We use the `pykalman` package to implement the filter mechanics. Note that the mathematical derivation of the Kalman filter looks pretty hairy, but is in fact relatively simple to implement; we won't go into the details here.

We can use the `filter_update()` function to compute new states as data comes in.

```python

# generate a simple parabolic trajectory, with a bit of noise
def gen_path(n):
    cx, cy = 150,150
    path = [(cx,cy)]
    t = 0
    for k in range(200):
        t+= 2.5
        nx, ny = np.random.normal(0,3), np.random.normal(0,3)
        obs = np.array([1.5*t+cx+nx,5*t-0.025*t*t+cy+ny])
        path.append(obs)
    return np.array(path, dtype=object)

path = gen_path(200)
```

```python
path[35:55] = None
```

```python
from kf_display import KFDisplay
kfd = KFDisplay(A, C, sigma_a, sigma_c, mu_0, sigma_0, path)
%gui tk
```

## Rejecting observations
This filter does a very good job at reject Gaussian noise, and it can cope well when observations are missing. However, the "jump" noise we saw in the noisy cursor example, where spikes are introduced, is something the Kalman filter struggles with.

The filter will blindly follow these massive, sudden deviations and lead to very erratic control. We can see this if we slightly modify the path to have a few zero'd values:

```python
path = gen_path(300)
path[::10,1] = 0 # every `10th y value set to zero
kfd = KFDisplay(A, C, sigma_a, sigma_c, mu_0, sigma_0, path, frame_time=50)
%gui tk
```

But we can be cleverer. Because we can obtain the likelihood of any observation under our current model, we can simply ignore observations that appear to be too unlikely to be plausible.

All we need to do is to measure the likelihood, compare it to some threshold, and treat the observation as missing if the value is too unlikely. This adjustment needs care: if we are too zealous in rejecting samples our filter may end up too far away from the observations to ever recover, for example if we *intentionally* moved the mouse very quickly.
But for the extreme, instantaneous jumps we are encountering, we can be fairly lax in choosing our likelihood threshold.

```python
path = gen_path(300)
path[::10,1] = 0 # every `10th y value set to zero
# inlcude a rejection threshold
kfd = KFDisplay(A, C, sigma_a, sigma_c, mu_0, sigma_0, path, frame_time=50, reject_lik=-1000)
%gui tk
```

# Practical
Create a Kalman filter that does a good job tracking the noisy cursor, with these noisy cursor parameters:

```python
# and some tracking losses
test_cursor = NoiseCursorDemo(noise=30,
                    jump=0.08, jump_scale=2000,
                    dropout=[0.02, 0.03])
%gui tk
```

Use hits per second as the criteria for (manual) optimisation.

The code below sets up the filter from scratch, but the *parameters* need to be configured to work well

```python
## Modify this cell to adjust KF parameters

## Hints:
# adjust dt, sigma_a, sigma_c and reject_lik
# you can change A or C, but make sure you know what you are doing!
# changing mu_0 and sigma_0 probably won't have much effect, as the
# prior will be forgotten very quickly anyway

# A
dt = 1 # increasing this will speed up all dynamics, and vice versa
dt2 = 0.5 * dt * dt
A = np.array([[1,0, dt,0, dt2,0],
     [0,1, 0,dt, 0,dt2],
     [0,0, 1,0, dt,0],
     [0,0, 0,1, 0,dt],
     [0,0, 0,0, 1,0],
     [0,0, 0,0, 0,1]])

# sigma_A
sigma_x = 0.1
sigma_dx = 0.1
sigma_ddx = 0.1
sigma_a = np.diag([sigma_x, sigma_x, sigma_dx, sigma_dx, sigma_ddx, sigma_ddx])

# C
C = np.array([[1,0,0,0,0,0],
              [0,1,0,0,0,0]]).astype(np.float64)
# sigma_C
sig_c = 1
sigma_c = np.diag([sig_c, sig_c])

# mu_0
xmax, ymax = 800, 800 # screen size
xc, yc = xmax/2, ymax/2  # coordinates of screen centre
mu_0 = np.array([xc, yc, 0, 0, 0, 0])

# sigma_0
sigma_vel = 1
sigma_acc = 1
sigma_0 = np.diag([xc/2, yc/2, sigma_vel, sigma_vel, sigma_acc, sigma_acc])

# rejection threshold for observations
# if you make this too close to zero (e.g. -5) all observations will be ignored
# if you make it too large, jumps will still get through
reject_lik = -10000
```

```python
### Don't change this cell!
# creates a new Kalman filter with the given parameters
def make_kf(A, sigma_a, C, sigma_C, mu_0, sigma_0, reject_lik=-np.inf):
    state = {"mean": mu_0, "cov": sigma_0}
    # create a Kalman filter
    kf = pykalman.KalmanFilter(
        transition_matrices=A,
        observation_matrices=C,
        transition_covariance=sigma_a,
        observation_covariance=sigma_c,
        initial_state_mean=mu_0,
        initial_state_covariance=sigma_0,
    )

    def update(x, y):
        pred_obs_mean = np.dot(C, state["mean"])
        pred_obs_cov = np.dot(C, np.dot(state["cov"], C.T))
        obs_arr = np.array([x, y])
        # likelihood of this sample
        lik = scipy.stats.multivariate_normal.logpdf(
            obs_arr, mean=pred_obs_mean, cov=pred_obs_cov
        )
        if x == x and lik == lik and lik > reject_lik:  # if x is not NaN
            mean, cov = kf.filter_update(
                state["mean"], state["cov"], observation=[x, y]
            )
        else:
            # update without observation
            mean, cov = kf.filter_update(state["mean"], state["cov"])
        state["mean"] = mean
        state["cov"] = cov

        return {"mean": [mean[0], mean[1]], "cov": cov[:2, :2], "lik": lik}

    return update
```

```python
import noise_cursor
noise_cursor = reload(noise_cursor)
from noise_cursor import NoiseCursorDemo

#### Create the filter and run it
kfilter=make_kf(A,sigma_a,C,sigma_c,mu_0,sigma_0,reject_lik=reject_lik)

kalman_cursor = NoiseCursorDemo(filter=kfilter,
                    noise=30,
                    jump=0.05, jump_scale=5000,
                    dropout=[0.02, 0.15])
%gui tk
```

# Thoughts on the Kalman filter

* If you had any trouble understanding this lecture, I **highly** recommend reading this outstanding blog post by Bzarg: [Kalman Filter in Pictures](http://www.bzarg.com/p/how-a-kalman-filter-works-in-pictures/).

We've only scratched the surface of the Kalman filter. There are many other things that can be useful:
### Basic technical enhancements
* We can also introduce offsets (as well as linear transforms) to the dynamics and observations, in cases where there are constant shifts (i.e. to use $Ax+b$ and $Cx+d$ instead of $Ax$ and $Cx$).

* The Kalman filter can take a known *control* signal and use this in estimation (e.g. in a drone navigation system, where there is known human control input and an partially unknown dynamic system responding to this). This introduces a matrix $B$ to represent the control->state projection, and the state update becomes:
$$\vec{x}_{t+1} = A{\bf x_t} + b + B{\bf u_t} +  N(0,\Sigma_a) ,$$
for a control input $\bf u_t$ at time $t$.
* All of the transform matrices A,B,C, and the covariances, $\Sigma_a, \Sigma_c$, can be changed at each timestep, so we have $A_t, B_t, C_t, \Sigma_{at}, \Sigma_{ct}$.

### Extending the filter
* The Kalman filter we used is "plain". It only supports linear dynamics. The **Extended Kalman Filter** changes the transition matrix at each time step, using a local linearisation of the dynamics. The **Unscented Kalman Filter** (so-called because it doesn't smell) goes even further, and allows any arbitrary dynamics to be applied.
* Many applications use multiple Kalman filters in banks, either switching between different filters or tracking multiple discrete hypotheses. In the same we we rejected some samples, the likelihood can be used to select relevant filters in different operating conditions.

### Uncertainty
* We haven't even used the uncertainty we so carefully maintained. We still used a binary in/out test on the target for a point cursor. But we can use the whole distribution, and compute the probability that the target was intended (e.g. by integrating the posterior PDF over the target box).

### Fusion
* The Kalman filter makes is very easy to fuse multiple sensors. Since we just need to write a transformation from the hidden state space to the observation space, if we have additional sensors we can just predict them as well, concatenating the observations onto the observation vector.  For example, if we had observed the total mouse velocity (perhaps with less noise) as well as the position, we could have used to improve our estimates.

There is no special "fusion" step; it happens naturally. The Unscented Kalman Filter allows any arbitrary transform from the state space to the observation space (rather than just linear) and is particularly flexible for doing sensor fusion.

# Kalman filter drawbacks
* the **dynamics** have to be linear: we can't have complicated dynamic models (although we can linearise at each time step).

This doesn't make much sense for tracking complex gesture trajectories; a dynamic model for a complete gesture is rarely going to be linear.

* the **uncertainty** must be normal: so we can't track multiple modes, for example, because a normal distribution has exactly one mode.

Imagine an object disappearing behind an obstruction which could reappear on either side; the Kalman filter can only spread out the distribution over the whole area, with an expected location in the middle of the obstacle! We would like to instead be able to track the two possibilities here by splitting up the hypotheses.

*[Waddington's epigenetic landscape, illustrating a dynamic system which develops multiple modes as it evolves; a Gaussian approximation is wholly inappropriate]*

### The particle filter
The **particle filter** extends Monte Carlo sampling to probabilistic filtering, and can track multiply hypotheses, with arbitrary dynamics and observation systems. It is particularly simple to implement and is a very powerful algorithm. However, it can be computationally expensive to run, and it is *inferentially inefficient* compared to the Kalman filter -- it (usually) takes more data to accurately estimate state than the KF would, as long as the process could reasonably be modeled using a Kalman filter.

### Scope and limitations
#### Scope
* Probabilistic filters can be applied to many problems in HCI. Typically, if a process unfolds over time and there is uncertainty, a probabilistic filter is a strong candidate for inference.
* The fact that inference is performed over time is a potential advantage over "static" classification approaches, as feedback can be generated on the fly, instead only after completion of an action.
* In the specific context of gestures, the ability to infer the start and end-point of gestures can solve the "segmentation problem" or "gesture spotting problem" that is often awkward and leads to kludges like button presses to segment actions.
* Probabilistic motion models can easily be linked to higher-order probabilistic models which infer long-term actions on the part of the user. Because everything is a probability distribution, there is a simple common basis for integrating such models. This, for example, can include language models which estimate a distribution over text that is likely to be entered given both user input and a statistical model of language.

#### Limitations
* PFs can be computationally intensive to run.
* Curse-of-dimensionality can make the attractive simplicity of PFs work poorly in practice as the state space expands (although often better than you might expect).
* Sometimes the inverse probability model can be hard to formulate. Conversely, it is sometimes very much easier.
* Particle filters are simple and elegant, but inferentially weak.
* Kalman filters are rigid and restrictive, but very inferentially efficient.
* Hybrid approaches (Ensemble Kalman filter, Unscented Kalman Filter, hybrid particle/Kalman filters, Rao-Blackwellized filters) can trade these qualities off, but they aren't off the shelf solutions (i.e. you need an expert!).

### Resources
#### Basic
* Read the [Condensation paper](http://vision.stanford.edu/teaching/cs231b_spring1415/papers/isard-blake-98.pdf).
* Read [the Kalman filter in pictures](http://www.bzarg.com/p/how-a-kalman-filter-works-in-pictures/)
* Watch [the particle filter without equations](https://www.youtube.com/watch?v=aUkBa1zMKv4)

#### Advanced
* [A technical but succinct and clear explanation of the particle filter](http://www.cns.nyu.edu/~eorhan/notes/particle-filtering.pdf)
* [A bibliography of particle filter papers](http://www.stats.ox.ac.uk/~doucet/smc_resources.html)

# Putting it together

For the final practical, we will combine probabilistic inference with unsupervised learning.

Steps:
* Learn a manifold from some data. This could be webcam data (e.g. the beard pointer) or the keyboard data, or some other source, for example using SOM or ISOMAP (or something fancier like a VAE or UMAP if you know how). For example, if you are using the keyboard input, the output should be a function that takes a 128D vector and outputs a 2D one. Use the `key_live_process()` function to apply the function to incoming vectors and get a 2D output vector.

* Use the state in the 2D manifold as the observations which estimate position, and build a Kalman filter to clean up the signal and recover a smooth, consistent estimate of where the finger is moving over the keyboard surface.

Congratulations, you've built a new input device from the ground up!

### Things we did not do
There are many things we have not covered here:
* Much better semi-supervised learning can be used to "pin" parts of the learned manifold to useful spaces or actions.
* We did no learning on the dynamics of the sensors. This is critical in many applications, and could be solved with delay embeddings or derivative augmentations.
* There is much more we could do to analyse the signals we are getting to build good recognition algorithms.
* The Kalman filter could be extended to deal with richer dynamics, or replaced with a **particle filter** to deal with multiple hypotheses or richer sensor information. It's even possible to build a particle filter which uses the low-d project directly to predict in the keyboard vector space -- this is much more robust.
* We haven't *done* anything with the inputs. We've inferred position, but that's not a user's real intention. We need to fuse these estimates of low-level intentions into higher-level components; for example selecting characters to form words. The nice thing about this being a probabilistic framework is that it is easy to integrate with a probabilistic language model to combine inputs consistently to estimate what the user wants to do at a higher level.


```python
from sympy import *
from sympy.abc import *
from sympy.galgebra.ga import *
import numpy as np
from numpy import linalg as LA
from __future__ import print_function
init_printing()
```

## PDE
The acoustic wave equation for the square slowness m and a source q is given in 3D by :

\begin{cases}
 &m \frac{d^2 u(x,t)}{dt^2} - \nabla^2 u(x,t) =q  \\
 &u(.,0) = 0 \\
 &\frac{d u(x,t)}{dt}|_{t=0} = 0
\end{cases}

with the zero initial conditons to guaranty unicity of the solution

```python
p=Function('p')
m,s,h = symbols('m s h')
m=M(x,y,z)
q=Q(x,y,z,t)
d=D(x,y,z,t)
e=E(x,y,z)
```

```python
# Choose dimension (2 or 3)
dim = 3
# Choose order
time_order = 2
space_order = 2

# half width for indexes, goes from -half to half
width_t = int(time_order/2)
width_h = int(space_order/2)
solvep = p(x,y,z,t+width_t*s)
solvepa = p(x,y,z,t-width_t*s)
```

```python
# Indexes for finite differences
indx = []
indy = []
indz = []
indt = []
for i in range(-width_h,width_h+1):
    indx.append(x + i * h)
    indy.append(y + i * h)
    indz.append(z + i* h)

for i in range(-width_t,width_t+1):
    indt.append(t + i * s)
```

```python
# Finite differences
dtt=as_finite_diff(p(x,y,z,t).diff(t,t),indt)
dxx=as_finite_diff(p(x,y,z,t).diff(x,x), indx)
dyy=as_finite_diff(p(x,y,z,t).diff(y,y), indy)
dzz=as_finite_diff(p(x,y,z,t).diff(z,z), indz)
dt=as_finite_diff(p(x,y,z,t).diff(t), indt)
lap = dxx + dyy + dzz
```

```python
arglamb=[]
arglamba=[]
for i in range(-width_t,width_t):
    arglamb.append( p(x,y,z,indt[i+width_t]))
    arglamba.append( p(x,y,z,indt[i+width_t+1]))

for i in range(-width_h,width_h+1):
    arglamb.append( p(indx[i+width_h],y,z,t))
    arglamba.append( p(indx[i+width_h],y,z,t))

for i in range(-width_h,width_h+1):
    arglamb.append( p(x,indy[i+width_h],z,t))
    arglamba.append( p(x,indy[i+width_h],z,t))

for i in range(-width_h,width_h+1):
    arglamb.append( p(x,y,indz[i+width_h],t))
    arglamba.append( p(x,y,indz[i+width_h],t))

arglamb.extend((q , m, s, h, e))
arglamb=tuple(arglamb)
arglamba.extend((q , m, s, h, e))
arglamba=tuple(arglamba)

arglamb=[ii for n,ii in enumerate(arglamb) if ii not in arglamb[:n]]
```

```python
arglamb
```

```python
# Forward wave equation
wave_equation = m*dtt- lap - q  + e*dt
stencil = solve(wave_equation,solvep)[0]
ts=lambdify(arglamb,stencil,"numpy")
stencil
```

```python
# Adjoint wave equation
wave_equationA = m*dtt- lap - d - e*dt
stencilA = solve(wave_equationA,solvepa)[0]
tsA=lambdify(arglamba,stencilA,"numpy")
stencilA
```

# Forward modelling
Point source on the grid
No receivers
Non cube grid (to make it general)

```python
import matplotlib.pyplot as plt
from matplotlib import animation

hstep=25 #space increment d  = minv/(10*f0);
tstep=2 #time increment dt < .5 * hstep /maxv;
tmin=0.0 #initial time
tmax=300 #simulate until
xmin=-500.0 #left bound
xmax=500.0 #right bound...assume packet never reaches boundary
ymin=-600.0 #left bound
ymax=600.0 #right bound...assume packet never reaches boundary
zmin=-250.0 #left bound
zmax=400.0 #right bound...assume packet never reaches boundary
f0=.010
t0=1/.010
nbpml=10
nx = int((xmax-xmin)/hstep) + 1 #number of points on x grid
ny = int((ymax-ymin)/hstep) + 1 #number of points on x grid
nz = int((zmax-zmin)/hstep) + 1 #number of points on x grid
nt = int((tmax-tmin)/tstep) + 2 #number of points on t grid
xsrc=0.0
ysrc=0.0
zsrc=25.0
#set source as Ricker wavelet for f0
def source(x,y,z,t):
    r = (np.pi*f0*(t-t0))
    val = (1-2.*r**2)*np.exp(-r**2)
    if abs(x-xsrc)<hstep/2 and abs(y-ysrc)<hstep/2 and abs(z-zsrc)<hstep/2:
        return val
    else:
        return 0.0

def dampx(x):
    dampcoeff=1.5*np.log(1.0/0.001)/(5.0*hstep);
    if x<nbpml:
        return dampcoeff*((nbpml-x)/nbpml)**2
    elif x>nx-nbpml-1:
        return dampcoeff*((x-nx+nbpml)/nbpml)**2
    else:
        return 0.0
def dampy(y):
    dampcoeff=1.5*np.log(1.0/0.001)/(5.0*hstep);
    if y<nbpml:
        return dampcoeff*((nbpml-y)/nbpml)**2
    elif y>ny-nbpml-1:
        return dampcoeff*((y-ny+nbpml)/nbpml)**2
    else:
        return 0.0

def dampz(z):
    dampcoeff=1.5*np.log(1.0/0.001)/(5.0*hstep);
    if z<nbpml:
        return dampcoeff*((nbpml-z)/nbpml)**2
    elif z>nz-nbpml-1:
        return dampcoeff*((z-ny+nbpml)/nbpml)**2
    else:
        return 0.0
```

```python
# True velocity
vel=np.ones((nx,ny,nz)) + 2.0
vel[:,:,int(nz/2):nz]=4.5
mt=vel**-2
```

```python
def Forward(nt,nx,ny,nz,m):
    u=np.zeros((nt+2,nx,ny,nz))
    for ti in range(2,nt+2):
        for a in range(1,nx-1):
            for b in range(1,ny-1):
                for c in range(1,nz-1):
                    src = source(xmin+a*hstep,ymin+b*hstep,zmin+c*hstep,tstep*ti)
                    damp=dampx(a)+dampy(b)+dampz(c)
                    u[ti,a,b,c]=ts(u[ti-2,a,b,c],
                                   u[ti-1,a,b,c],
                                   u[ti-1,a-1,b,c],
                                   u[ti-1,a+1,b,c],
                                   u[ti-1,a,b-1,c],
                                   u[ti-1,a,b+1,c],
                                   u[ti-1,a,b,c-1],
                                   u[ti-1,a,b,c+1],
                                   src , m[a,b,c], tstep, hstep, damp)
    return u

```

```python
u = Forward(nt,nx,ny,nz,mt)
```

```python

```


# Finite Elements Lab 1 Worksheet

##### Georges Limbert

###### nCATS, Faculty of Engineering and the Environment, University of Southampton

```
from IPython.core.display import HTML
css_file = '../ipython_notebook_styles/ngcmstyle.css'
HTML(open(css_file, "r").read())
```

<link href='http://fonts.googleapis.com/css?family=Open+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Nixie+One' rel='stylesheet' type='text/css'>
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 1000px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1200px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
margin-top:0.8em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Open Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:900px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Arvo', serif;
    font-style:regular;
    font-weight: 400;
    font-size: 45pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}
.text_cell_render h2 {
    font-family: 'Arvo', serif;
    font-weight: 400;
    font-size: 30pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Arvo', serif;
    margin-top:16px;
	font-size: 22pt;
    font-weight: 600;
    margin-bottom: 3px;
    font-style: regular;
    color: rgb(102,102,0);
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Arvo', serif;
    font-size: 14pt;
    text-align: center;
    margin-top: 0em;
    margin-bottom: 2em;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Arvo', sans-serif;
    font-weight: 400;
    font-size: 16pt;
    color: rgb(163,0,0);
    font-style: italic;
    margin-bottom: .1em;
    margin-top: 0.8em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'PT Mono', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

.CodeMirror{
        font-family: "PT Mono";
        font-size: 90%;
}

</style>

## Aims

The aim of this computer laboratory is to gain a practical insight into the fundamental steps required to solve a partial differential equation (PDE) over a simple geometric domain using a finite element approach. In doing so, you will learn:

* How to recast the PDE problem into a variational form
* How to define simple domains
* How to deal with Dirichlet boundary conditions
* How to numerically solve the variational problem.
* How to compute derived quantities from the solution.
* How to visualise the mesh, the solution and derived quantities

The symbolic‐numeric environment chosen for this laboratory is [FEniCS](http://fenicsproject.org/) which is a powerful and user‐friendly tool for solving PDEs. *However*, FEniCS is not compatible with Anaconda on a Mac it would seem; to use it, you must temporarily comment out the line setting up Anaconda in your `.bash_profile`. To get the notebook  working together with FEniCS you need to

1. Comment out the line setting up conda in your `.bash_profile`
2. Launch a new terminal
3. `sudo easy_install pip`
4. `sudo pip install ipython[all] --upgrade`

This will be an old, but functional, version of the notebook that can be used when not using conda.

## Basic problem: the Poisson equation

The Poisson equation is encountered in a wide range of domains in physics from heat conduction, electrostatics, diffusion and elasticity. The Poisson problem is defined as follows:

$$
\begin{align}
  - {\nabla ^2}u(x) &= f(x) & x & \text{ in } \Omega \\
  u(x) &= {u_0}(x) & x &\text{ on } \partial \Omega
\end{align}
$$

Here, $u(x)$ is the unknown function, $f$ and ${u_0}$ are prescribed functions, ${\nabla^2} = \Delta $ is the Laplace operator, $\Omega $ is the spatial domain and $\partial \Omega $ is its boundary.

A *time-independent* or *stationary* PDE like the Poisson equation combined with boundary conditions such as those above constitute a **boundary-value problem**.

## FEnics workflow

Solving a physical problem with FEnics involves the following workflow:

1. Identify the PDE and its boundary conditions
2. Recast the PDE problem into a variational problem
3. Write a Python programme encoding the :

   1. variational problem
   2. definition of input data: e.g. $f$ and ${u_0}$
   3. finite element mesh for the spatial domain $\Omega $:

4. Add statements to the Python programme for:

   1. solving the variational problem
   2. computing derived quantities such as the gradient of the unknown function
   3. visualising the results

## Variational formulation

The core of the recipe for turning a PDE into a variational problem is to multiply the PDE by a function $\upsilon $, integrate the resulting equation over $\Omega $, and perform integration by parts of terms with second-order derivatives.

The function $\upsilon$ which multiplies the PDE is in the mathematical finite element literature called a **test function**. The unknown function $u$ to be approximated is referred to as a **trial function**.

The terms test and trial function are used in FEniCS programs too.

The test function $\upsilon$ is required to vanish on the parts of the boundary where $u$ is known

**Suitable function spaces** must be specified for the test and trial functions. For standard PDEs arising in physics and mechanics such spaces are well known.

$$
\begin{equation}
  - \int_\Omega  \nabla^2 u(x)\upsilon \, dx  = \int_\Omega  f(x)\upsilon \, dx
\end{equation}
$$

### Question 1

Apply integration by parts to the PDE $ - {\nabla ^2}u(x) = f(x)$ with the
divergence theorem (relation between the flow (flux) of a vector field though a
surface and the behavior inside the surface) to recast it into its variational form.
You will also take into account the conditions about the test function $\upsilon
$ on the boundary where $u$ is prescribed.

You have now derived the variational formulation of the Poisson problem. This
variational form is what is commonly called a **weak form** of the
boundary-value problem ("weak" because of the reduced continuity requirements
placed on $u$). It contains the basic equation $ - {\nabla ^2}u(x) = f(x)$ and
the condition $u = {u_0} \text{ on }\partial \Omega $.

This continuous variational problem must be discretised to be solved using the
finite element method. The continuous version of the unknown function $u$ will be
denoted by ${u_e}$ (the index $e$ stands for "exact") to make the
distinction with the discretised solution of the problem that is typically
denoted by ${u_h}$.

**NB:** In the context of FEnics, $u$ will be meant to represent the
discretised approximate solution of the weak form.

For a linear weak form like the one just established it proves convenient to
introduce the following notation:

$$
\begin{equation}
a(u,\upsilon ) = \int_{\Omega}  {\nabla u \cdot \nabla \upsilon \, dx}
\end{equation}
$$

$$
\begin{equation}
L(\upsilon ) = \int_{\Omega}  {f\upsilon \, dx}
\end{equation}
$$

Where $a$ and $L$ are respectively a **bilinear** and **linear operator**.

The weak form of the Poisson problem (or any linear weak form) can therefore be
expressed as:

$$
\begin{equation}
a(u,\upsilon ) = L(\upsilon )
\end{equation}
$$

For every linear problem to be solved:

* all the terms containing the unknown $u$  must be collected in $a(u,\upsilon )$
* all terms with only known functions must be collected in $L(\upsilon )$

The explicit formulas for $a$ and $L$ are then coded in the programme.

Solving the discretised weak form consists in finding:

$$
\begin{equation}
  u \in V \text{ such that } a(u, \upsilon) = L(\upsilon) \quad \forall \upsilon \in V
\end{equation}
$$

Note that

$$
\begin{equation}
  V = \{ v \in H^1(\Omega): v = u_0 \text{ on } \partial \Omega \}
\end{equation}
$$

is the space of **trial** functions, and

$$
\begin{equation}
  \hat{V} = \{ v \in H^1(\Omega): v = 0 \text{ on } \partial \Omega \}
\end{equation}
$$

is the space of **test** functions.

Specifying $V$ and $\hat{V}$ consists in choosing the mesh and the type of interpolation functions.

## Practical implementation on a concrete example

Here we are going to particularise the Poisson problem (reproduced here for
convenience):

$$
\begin{align}
  - {\nabla ^2}u(x) &= f(x) & x & \text{ in } \Omega \\
  u(x) &= {u_0}(x) & x &\text{ on } \partial \Omega
\end{align}
$$

This decision means that $\{ {u_0},f,\Omega \} $ needs to be defined.

We choose a simple 2D domain: the unit square $\Omega  = [0,1] \times [0,1]$ .

In order to compare the finite element solution to the exact solution we also
*a priori* select a particular form for $u$. We choose:

$$
\begin{equation}
{u_e}(x,y) = 1 + {x^2} + 2{y^2}
\end{equation}
$$

If we inject equation the exact solution into Poisson problem we find that ${u_e}$  is solution if:

$$
\begin{equation}
f(x,y) =  - 6; \qquad {u_0}(x,y) = {u_e}(x,y) = 1 + {x^2} + 2{y^2}
\end{equation}
$$

### Question 2

Using FEnics write an algorithm to solve the boundary-value problem described above.

1.	Create and visualise the mesh using first-order (linear) triangular elements. You will use (6,4) as arguments for the mesh function which means that the computational domain will be first divide into 24 rectangles which will be further split into triangles
2.	Set up the boundary conditions and visualise them
3.	Solve the problem
4.	Plot the approximate solution over the domain using colour plots.
5.	Plot the error between the exact and approximate solution over the domain using colour plots
6.	Calculate the maximum error for the whole domain
7.	Calculate and plot the gradient of $u$

```

```

## A more concrete example: a pressurised elastic membrane

Here, we study the deflection $D(x,y)$ of an elastic circular membrane with
radius $R$, subject to a localized perpendicular pressure force, modelled as a
Gaussian function. The PDE representing this physical process is:

$$
\begin{equation}
- T{\nabla ^2}D = p(x,y) \text{ in } \Omega  = \{ (x,y)|\,{x^2} + {y^2} \le R\}
\end{equation}
$$

where $p$ is the external pressure defined as:

$$
\begin{equation}
p(x,y) = \frac{A}{{2\pi \sigma }}\exp \left[ { - \frac{1}{2}{{\left( {\frac{{x -
{x_0}}}{\sigma }} \right)}^2} - \frac{1}{2}{{\left( {\frac{{y - {y_0}}}{\sigma }}
\right)}^2}} \right]
\end{equation}
$$

$T$ the constant tension in the membrane, $A$ the amplitude of the pressure,
$({x_0},{y_0})$ the localisation of the Gaussian pressure function and $\sigma $
its width.

You will use the following values:

$$
\begin{align}
T &= 10 \\ A &= 1 \\ R &= 0.3 \\ \theta &= 0.2 \\ {x_0} &= 0.6R\cos (\theta ) \\ {y_0} &= 0.6R\sin
(\theta ) \\ \sigma  &= 0.025
\end{align}
$$

### Question 3

1.	Determine an analytical solution for the above equation equation
2.	Using the previously written Python programme, make appropriate modifications to solve the membrane problem.
3.	Solve the problem
4.	Calculate the maximum deflection
5.	Plot the approximate solution over the domain using colour plots.
6.	Plot the error between the exact and approximate solution over the domain using colour plots
7.	Calculate the maximum error for the whole domain
8.	Calculate and plot the gradient of $u$.

```

```

FEEG6016 Simulation and Modelling/2014/Finite Elements Lab 1.ipynb