$$ \LaTeX \text{ command declarations here.}
\newcommand{\R}{\mathbb{R}}
\renewcommand{\vec}[1]{\mathbf{#1}}
\newcommand{\X}{\mathcal{X}}
\newcommand{\D}{\mathcal{D}}
\newcommand{\G}{\mathcal{G}}
\newcommand{\Parents}{\mathrm{Parents}}
\newcommand{\NonDesc}{\mathrm{NonDesc}}
\newcommand{\I}{\mathcal{I}}
\newcommand{\dsep}{\text{d-sep}}
$$

```python
from __future__ import division

# scientific
%matplotlib inline
from matplotlib import pyplot as plt;
import numpy as np;
import sklearn as skl;
import sklearn.datasets;
import sklearn.cluster;

# ipython
import IPython;

# python
import os;

#####################################################

# image processing
import PIL;

# trim and scale images
def trim(im, percent=100):
    print("trim:", percent);
    bg = PIL.Image.new(im.mode, im.size, im.getpixel((0,0)))
    diff = PIL.ImageChops.difference(im, bg)
    diff = PIL.ImageChops.add(diff, diff, 2.0, -100)
    bbox = diff.getbbox()
    if bbox:
        x = im.crop(bbox)
        return x.resize(((x.size[0]*percent)//100, (x.size[1]*percent)//100), PIL.Image.ANTIALIAS);

#####################################################

# daft (rendering PGMs)
import daft;

# set to FALSE to load PGMs from static images
RENDER_PGMS = False;

# decorator for pgm rendering
def pgm_render(pgm_func):
    def render_func(path, percent=100, render=None, *args, **kwargs):
        print("render_func:", percent);
        # render
        render = render if (render is not None) else RENDER_PGMS;

        if render:
            print("rendering");
            # render
            pgm = pgm_func(*args, **kwargs);
            pgm.render();
            pgm.figure.savefig(path, dpi=300);

            # trim
            img = trim(PIL.Image.open(path), percent);
            img.save(path, 'PNG');
        else:
            print("not rendering");

        # error
        if not os.path.isfile(path):
            raise "Error:  Graphical model image %s not found.  You may need to set RENDER_PGMS=True.";

        # display
        return IPython.display.Image(filename=path);#trim(PIL.Image.open(path), percent);

    return render_func;

######################################################
```

# EECS 445:  Machine Learning

## Lecture 16:  Latent Variables, d-Separation, Gaussian Mixture Models

- Instructor:  **Jacob Abernethy**
- Date:  November 9, 2016

## References

- **[MLAPP]** Murphy, Kevin. [*Machine Learning:  A Probabilistic Perspective*](https://mitpress.mit.edu/books/machine-learning-0).  2012.
- **[PRML]** Bishop, Christopher.  [*Pattern Recognition and Machine Learning*](http://research.microsoft.com/en-us/um/people/cmbishop/prml/).  2006.
- **[Koller & Friedman 2009]** Koller, Daphne and Nir Friedman.  [*Probabilistic Graphical Models*](https://mitpress.mit.edu/books/probabilistic-graphical-models).  2009.
- **Book Chapter**: [The language of directed acyclic
graphical models](http://idiom.ucsd.edu/~rlevy/pmsl_textbook/chapters/pmsl_12.pdf)
    - This notes are really nice

## Outline

- Review of Exponential Families
    - MLE
    - Brief discussion of conjugate priors and MAP estimation
- Probabilistic Graphical Models
    - Review of Conditional Indep. Assumptions
    - Intro to Hidden Markov Models
    - Latent Variable Models in general
    - d-separation in Bayesian Networks
- Mixture Models
    - Gaussian Mixture Model
    - Relationship to Clustering

## Exponential Family Distributions

**DEF:** $p(x | \theta)$ has **exponential family form** if:
$$
\begin{align}
p(x | \theta)
&= \frac{1}{Z(\theta)} h(x) \exp\left[ \eta(\theta)^T \phi(x) \right] \\
&= h(x) \exp\left[ \eta(\theta)^T \phi(x) - A(\theta) \right]
\end{align}
$$

- $Z(\theta)$ is the **partition function** for normalization
- $A(\theta) = \log Z(\theta)$ is the **log partition function**
- $\phi(x) \in \R^d$ is a vector of **sufficient statistics**
- $\eta(\theta)$ maps $\theta$ to a set of **natural parameters**
- $h(x)$ is a scaling constant, usually $h(x)=1$

### Exponential Family:  MLE

To find the maximum, recall $\nabla_\theta A(\theta) = E_\theta[\phi(x)]$, so
\begin{align*}
\nabla_\theta \log p(\D | \theta) & =
\nabla_\theta(\theta^T \phi(\D) - N A(\theta)) \\
& = \phi(\D) - N E_\theta[\phi(X)] = 0
\end{align*}
Which gives
$$E_\theta[\phi(X)] = \frac{\phi(\D)}{N} = \frac{1}{N} \sum_{k=1}^N \phi(x_k)$$

Obtaining the maximum likelihood is simply solving the calculus problem $\nabla_\theta A(\theta) = \frac{1}{N} \sum_{k=1}^N \phi(x_k)$ which is often easy

### Bayes for Exponential Family

Exact Bayesian analysis is considerably simplified if the prior is **conjugate** to the likelihood.
- Simply, this means that prior $p(\theta)$ has the same form as the posterior $p(\theta|\mathcal{D})$.

This requires likelihood to have finite sufficient statistics
* Exponential family to the rescue!

**Note**: We will release some notes on cojugate priors + exponential families. It's hard to learn from slides and needs a bit more description.

### Likelihood for exponential family

Likelihood:
$$ p(\mathcal{D}|\theta) \propto g(\theta)^N \exp[\eta(\theta)^T s_N]\\
s_N = \sum_{i=1}^{N}\phi(x_i)$$

In terms of canonical parameters:
$$ p(\mathcal{D}|\eta) \propto \exp[N\eta^T \bar{s} -N A(\eta)] \\
\bar s = \frac{1}{N}s_N $$

### Conjugate prior for exponential family

* The prior and posterior for an exponential family involve two parameters, $\tau$ and $\nu$, initially set to $\tau_0, \nu_0$

$$ p(\theta| \nu_0, \tau_0) \propto g(\theta)^{\nu_0} \exp[\eta(\theta)^T \tau_0] $$

* Denote $ \tau_0 = \nu_0 \bar{\tau}_0$ to separate out the size of the **prior pseudo-data**, $\nu_0$ , from the mean of the sufficient statistics on this pseudo-data, $\tau_0$ . Hence,

$$ p(\theta| \nu_0, \bar \tau_0) \propto \exp[\nu_0\eta(\theta)^T \bar \tau_0  - \nu_0 A(\eta)] $$

* Think of $\tau_0$ as a "guess" of the future sufficient statistics, and $\nu_0$ as the strength of this guess

### Prior:  Example

$$
\begin{align}
p(\theta| \nu_0, \tau_0)
&\propto (1-\theta)^{\nu_0} \exp[\tau_0\log(\frac{\theta}{1-\theta})] \\
&= \theta^{\tau_0}(1-\theta)^{\nu_0 - \tau_0}
\end{align}
$$

Define $\alpha = \tau_0 +1 $ and $\beta = \nu_0 - \tau_0 +1$ to see that this is a **beta distribution**.

### Posterior

Posterior:
$$ p(\theta|\mathcal{D}) = p(\theta|\nu_N, \tau_N) = p(\theta| \nu_0 +N, \tau_0 +s_N) $$

Note that we obtain **hyper-parameters** by adding. Hence,

$$ \begin{align}
p(\eta|\mathcal{D})
&\propto \exp[\eta^T (\nu_0 \bar\tau_0 + N \bar s) - (\nu_0 + N) A(\eta) ] \\
&= p(\eta|\nu_0 + N, \frac{\nu_0 \bar\tau_0 + N \bar s}{\nu_0 + N})
\end{align}$$
where $\bar s = \frac 1 N \sum_{i=1}^{N}\phi(x_i)$.

* *posterior hyper-parameters are a convex combination of the prior mean hyper-parameters and the average of the sufficient statistics.*

## Back to Graphical Models!

### Recall: Bayesian Networks:  Definition

A **Bayesian Network** $\mathcal{G}$ is a directed acyclic graph whose nodes represent random variables $X_1, \dots, X_n$.
- Let $\Parents_\G(X_k)$ denote the parents of $X_k$ in $\G$
- Let $\NonDesc_\G(X_k)$ denote the variables in $\G$ who are not descendants of $X_k$.

> Examples will come shortly...

### Bayesian Networks:  Local Independencies

Every Bayesian Network $\G$ encodes a set $\I_\ell(\G)$ of **local independence assumptions**:

> For each variable $X_k$, we have $(X_k \perp \NonDesc_\G(X_k) \mid \Parents_\G(X_k))$

Every node $X_k$ is conditionally independent of its nondescendants given its parents.

### Example:  Naive Bayes

The graphical model for Naive Bayes is shown below:
- $\Parents_\G(X_k) = \{ C \}$, $\NonDesc_\G(X_k) = \{ X_j \}_{j\neq k} \cup \{ C \}$
- Therefore $X_j \perp X_k \mid C$ for any $j \neq k$

### Factorization Theorem:  Statement

**Theorem:** *(Koller & Friedman 3.1)* If $\G$ is an I-map for $P$, then $P$ **factorizes** as follows:
$$
P(X_1, \dots, X_N) = \prod_{k=1}^N P(X_k \mid \Parents_\G(X_k))
$$

### Example:  Fully Connected Graph

A **fully connected graph** makes no independence assumptions.
$$
P(A,B,C) = P(A) P(B|A) P(C|A,B)
$$

### Importan PGM Example:  Markov Chain

State at time $t$ depends only on state at time $t-1$.
$$
P(X_0, X_1, \dots, X_N) = P(X_0) \prod_{t=1}^N P(X_t \mid X_{t-1})
$$

### Compact Representation of PGM:  Plate Notation

We can represent (conditionally) iid variables using **plate notation**.

A box around a variable (or set of variables), with a $K$, means that we have access to $K$ iid samples of this variable.

### Unobserved Variables:  Hidden Markov Model

Noisy observations $X_k$ generated from hidden Markov chain $Y_k$. (More on this soon)

# This Brings us to: Latent Variable Models

> Uses material from **[MLAPP]** §10.1-10.4, §11.1-11.2

### Latent Variable Models

In general, the goal of probabilistic modeling is to
> Use what we know to make *inferences* about what we don't know.

Graphical models provide a natural framework for this problem.
- Assume unobserved variables are correlated due to the influence of unobserved **latent variables**.
- Latent variables encode beliefs about the generative process.

In a graphical model, we will often **shade in** the observed variables to distinguish them from hidden variables.

### Example:  Gaussian Mixture Models

This dataset is hard to explain with a single distribution.
- Underlying density is complicated overall...
- But it's clearly three Gaussians!

```python
X, y = skl.datasets.make_blobs(1000, cluster_std=[1.0, 2.5, 0.5], random_state=170)
plt.scatter(X[:,0], X[:,1])
```

### Example:  Mixture Models

Instead, introduce a latent **cluster label** $z_j \in [K]$ for each datapoint $x_j$,
$$
\begin{align}
z_j &\sim \mathrm{Cat}(\pi_1, \dots, \pi_K)
    & \forall\, j=1,\dots,N \\
x_j \mid z_j &\sim \mathcal{N}(\mu_{z_j}, \Sigma_{z_j})
    & \forall\, j=1,\dots,N \\
\end{align}
$$

This allows us to explain a complicated density as a **mixture** of simpler densities:
$$
P(x | \mu, \Sigma) = \sum_{k=1}^K \pi_k \mathcal{N}(x | \mu_k, \Sigma_k)
$$

### Example:  Mixture Models

```python
@pgm_render
def pgm_gmm():
    pgm = daft.PGM([4,4], origin=[-2,-1], node_unit=0.8, grid_unit=2.0);
    # nodes
    pgm.add_node(daft.Node("pi", r"$\pi$", 0, 1));
    pgm.add_node(daft.Node("z", r"$Z_j$", 0.7, 1));
    pgm.add_node(daft.Node("x", r"$X_j$", 1.3, 1, observed=True));
    pgm.add_node(daft.Node("mu", r"$\mu$", 0.7, 0.3));
    pgm.add_node(daft.Node("sigma", r"$\Sigma$", 1.3, 0.3));

    # edges
    pgm.add_edge("pi", "z", head_length=0.08);
    pgm.add_edge("z", "x", head_length=0.08);
    pgm.add_edge("mu", "x", head_length=0.08);
    pgm.add_edge("sigma", "x", head_length=0.08);

    pgm.add_plate(daft.Plate([0.4,0.8,1.3,0.5], label=r"$\qquad\qquad\qquad\;\; N$",
    shift=-0.1))

    return pgm;
```

```python
%%capture
pgm_gmm("images/pgm/pgm-gmm.png")
```

### Example:  Hidden Markov Models

Noisy observations $X_k$ generated from hidden Markov chain $Y_k$.
$$
P(\vec{X}, \vec{Y}) = P(Y_1) P(X_1 \mid Y_1) \prod_{k=2}^N \left(P(Y_k \mid Y_{k-1}) P(X_k \mid Y_k)\right)
$$

```python
@pgm_render
def pgm_hmm():
    pgm = daft.PGM([7, 7], origin=[0, 0])

    # Nodes
    pgm.add_node(daft.Node("Y1", r"$Y_1$", 1, 3.5))
    pgm.add_node(daft.Node("Y2", r"$Y_2$", 2, 3.5))
    pgm.add_node(daft.Node("Y3", r"$\dots$", 3, 3.5, plot_params={'ec':'none'}))
    pgm.add_node(daft.Node("Y4", r"$Y_N$", 4, 3.5))

    pgm.add_node(daft.Node("x1", r"$X_1$", 1, 2.5, observed=True))
    pgm.add_node(daft.Node("x2", r"$X_2$", 2, 2.5, observed=True))
    pgm.add_node(daft.Node("x3", r"$\dots$", 3, 2.5, plot_params={'ec':'none'}))
    pgm.add_node(daft.Node("x4", r"$X_N$", 4, 2.5, observed=True))

    # Add in the edges.
    pgm.add_edge("Y1", "Y2", head_length=0.08)
    pgm.add_edge("Y2", "Y3", head_length=0.08)
    pgm.add_edge("Y3", "Y4", head_length=0.08)

    pgm.add_edge("Y1", "x1", head_length=0.08)
    pgm.add_edge("Y2", "x2", head_length=0.08)
    pgm.add_edge("Y4", "x4", head_length=0.08)

    return pgm;
```

```python
%%capture
pgm_hmm("images/pgm/hmm.png");
```

### Example:  Unsupervised Learning

Latent variables are fundamental to unsupervised and deep learning.
- Serve as a **bottleneck**
- Compute a **compressed** representation of data

```python
@pgm_render
def pgm_unsupervised():
    pgm = daft.PGM([6, 6], origin=[0, 0])

    # Nodes
    pgm.add_node(daft.Node("d1", r"$Z_1$", 2, 3.5))
    pgm.add_node(daft.Node("di", r"$Z_2$", 3, 3.5))
    pgm.add_node(daft.Node("dn", r"$Z_3$", 4, 3.5))

    pgm.add_node(daft.Node("f1", r"$X_1$", 1, 2.50, observed=True))
    pgm.add_node(daft.Node("fi-1", r"$X_2$", 2, 2.5, observed=True))
    pgm.add_node(daft.Node("fi", r"$X_3$", 3, 2.5, observed=True))
    pgm.add_node(daft.Node("fi+1", r"$X_4$", 4, 2.5, observed=True))
    pgm.add_node(daft.Node("fm", r"$X_N$", 5, 2.5, observed=True))

    # Add in the edges.
    pgm.add_edge("d1", "f1", head_length=0.08)
    pgm.add_edge("d1", "fi-1", head_length=0.08)
    pgm.add_edge("d1", "fi", head_length=0.08)
    pgm.add_edge("d1", "fi+1", head_length=0.08)
    pgm.add_edge("d1", "fm", head_length=0.08)

    pgm.add_edge("di", "f1", head_length=0.08)
    pgm.add_edge("di", "fi-1", head_length=0.08)
    pgm.add_edge("di", "fi", head_length=0.08)
    pgm.add_edge("di", "fi+1", head_length=0.08)
    pgm.add_edge("di", "fm", head_length=0.08)

    pgm.add_edge("dn", "f1", head_length=0.08)
    pgm.add_edge("dn", "fi-1", head_length=0.08)
    pgm.add_edge("dn", "fi", head_length=0.08)
    pgm.add_edge("dn", "fi+1", head_length=0.08)
    pgm.add_edge("dn", "fm", head_length=0.08)

    return pgm
```

```python
%%capture
pgm_unsupervised("images/pgm/unsupervised.png");
```

### Other Latent Variable Models

Many other models in machine learning involve latent variables:

- Neural Networks / Multilayer Perceptrons
- Restricted Boltzmann Machines
- Deep Belief Networks
- Probabilistic PCA

### Latent Variable Models:  Complexity

Latent variable models exhibit **emergent complexity**.
- Although each conditional distribution is simple,
- The joint distribution is capable of modeling complex interactions.

However, latent variables make learning difficult.
- Inference is challening in models with latent variables.
- They can introduce new dependencies between observed variables.

## Break time

# Bayesian Networks
## Part II:  Inference, Learning, and d-Separation

> Uses material from **[Koller & Friedman 2009]** Chapter 3, **[MLAPP]** Chapter 10, and **[PRML]** §8.2.1

### Bayesian Networks:  Terminology

Typically, our models will have
- Observed variables $X$
- Hidden variables $Z$
- Parameters $\theta$

Occasionally, we will distinguish between **inference** and **learning**.

### Bayesian Networks:  Inference

**Inference**:  Estimate hidden variables $Z$ from observed variables $X$.
$$
P(Z | X,\theta) = \frac{P(X,Z | \theta)}{P(X|\theta)}
$$

- Denominator $P(X|\theta)$ is sometimes called the probability of the **evidence**.
- Occasionally we care only about a subset of the hidden variables, and marginalize out the rest.

### Bayesian Networks:  Learning

**Learning:** Estimate parameters $\theta$ from observed data $X$.
$$
P(\theta \mid X) = \sum_{z \in Z} P(\theta, z \mid X) = \sum_{z \in Z} P(\theta \mid z, X) P(z \mid X)
$$

To Bayesians, parameters *are* hidden variables, so inference and learning are equivalent.

### Bayesian Networks:  Probability Queries

In general, it is useful to compute $P(A|B)$ for arbitrary collections $A$ and $B$ of variables.
- Both inference and learning take this form.

To accomplish this, we must understand the **independence structure** of any given graphical model.

### Review:  Local Independencies

Every Bayesian Network $\G$ encodes a set $\I_\ell(\G)$ of **local independence assumptions**:

> For each variable $X_k$, we have $(X_k \perp \NonDesc_\G(X_k) \mid \Parents_\G(X_k))$

Every node $X_k$ is conditionally independent of its nondescendants given its parents.

> For arbitrary sets of variables, when does $(A \perp B \mid C)$ hold?

### Review:  I-Maps

If $P$ satisfies the independence assertions made by $\G$, we say that
- $\G$ is an **I-Map** for $P$
- or that $P$ **satisfies** $\G$.

Any distribution satisfying $\G$ shares common structure.
- We will exploit this structure in our algorithms
- This is what makes graphical models so **powerful**!

### Review:  Factorization Theorem

Last time, we proved that for any $P$ satisfying $\G$,
$$
P(X_1, \dots, X_N) = \prod_{k=1}^N P(X_k \mid \Parents_\G(X_k))
$$

If we understand independence structure, we can factorize arbitrary conditional distributions:
$$
P(A_1, \dots, A_n \mid B_1, \dots, B_m) = \;?
$$

###  Question 1:  Is $(A \perp B)$?

```python
@pgm_render
def pgm_question1():
    pgm = daft.PGM([4, 4], origin=[0, 0])

    # Nodes
    pgm.add_node(daft.Node("c", r"$C$", 2, 3.5))
    pgm.add_node(daft.Node("a", r"$A$", 1.3, 2.5))
    pgm.add_node(daft.Node("b", r"$B$", 2.7, 2.5))

    # Add in the edges.
    pgm.add_edge("c", "a", head_length=0.08)
    pgm.add_edge("c", "b", head_length=0.08)

    return pgm;
```

```python
%%capture
pgm_question1("images/pgm/question1.png")
```

### Answer 1:  No!

No!  $A$ and $B$ are not marginally independent.
- Note $C$ is not shaded, so we don't observe it.

In general,

$$
P(A,B) = \sum_{c \in C} P(A,B,c) = \sum_{c \in C} P(A|c)P(B|c)P(c) \neq P(A)P(B)
$$

### Question 2:  Is $(A \perp B \mid C)$?

```python
@pgm_render
def pgm_question2():
    pgm = daft.PGM([4, 4], origin=[0, 0])

    # Nodes
    pgm.add_node(daft.Node("c", r"$C$", 2, 3.5,
                           observed=True))
    pgm.add_node(daft.Node("a", r"$A$", 1.3, 2.5))
    pgm.add_node(daft.Node("b", r"$B$", 2.7, 2.5))

    # Add in the edges.
    pgm.add_edge("c", "a", head_length=0.08)
    pgm.add_edge("c", "b", head_length=0.08)

    return pgm
```

```python
%%capture
pgm_question2("images/pgm/question2.png")
```

### Answer 2:  Yes!

Yes!  $(A \perp B | C)$ follows from the local independence properties of Bayesian networks.

> Every variable is conditionally independent of its nondescendants given its parents.

Observing $C$ *blocks* the path of influence from $A$ to $B$. Or, using factorization theorem:
$$
\begin{align}
P(A,B|C) & = \frac{P(A,B,C)}{P(C)}  \\
& = \frac{P(C)P(A|C)P(B|C)}{P(C)} \\
& = P(A|C)P(B|C)
\end{align}
$$

### Question 3:  Is $(A \perp B)$?

```python
@pgm_render
def pgm_question3():
    pgm = daft.PGM([4, 4], origin=[0, 0])

    # Nodes
    pgm.add_node(daft.Node("c", r"$C$", 2, 3.5))
    pgm.add_node(daft.Node("a", r"$A$", 1.3, 2.5))
    pgm.add_node(daft.Node("b", r"$B$", 2.7, 2.5))

    # Add in the edges.
    pgm.add_edge("a", "c", head_length=0.08)
    pgm.add_edge("c", "b", head_length=0.08)

    return pgm
```

```python
%%capture
pgm_question3("images/pgm/question3.png")
```

### Answer 3:  No!

Again, $C$ is not given, so $A$ and $B$ are dependent.

### Question 4:  Is $(A \perp B \mid C)$?

```python
@pgm_render
def pgm_question4():
    pgm = daft.PGM([4, 4], origin=[0, 0])

    # Nodes
    pgm.add_node(daft.Node("c", r"$C$", 2, 3.5, observed=True))
    pgm.add_node(daft.Node("a", r"$A$", 1.3, 2.5))
    pgm.add_node(daft.Node("b", r"$B$", 2.7, 2.5))

    # Add in the edges.
    pgm.add_edge("a", "c", head_length=0.08)
    pgm.add_edge("c", "b", head_length=0.08)

    return pgm
```

```python
%%capture
pgm_question4("images/pgm/question4.png")
```

### Answer 4:  Yes!

Again, observing $C$ *blocks* influence from $A$ to $B$.

> Every variable is conditionally independent of its nondescendants given its parents.

### Question 5:  Is $(A \perp B)$?

```python
@pgm_render
def pgm_question5():
    pgm = daft.PGM([4, 4], origin=[0, 0])

    # Nodes
    pgm.add_node(daft.Node("c", r"$C$", 2, 3.5))
    pgm.add_node(daft.Node("a", r"$A$", 1.3, 2.5))
    pgm.add_node(daft.Node("b", r"$B$", 2.7, 2.5))

    # Add in the edges.
    pgm.add_edge("a", "c", head_length=0.08)
    pgm.add_edge("b", "c", head_length=0.08)

    return pgm
```

```python
%%capture
pgm_question5("images/pgm/question5.png")
```

### Answer 5:  Yes!

Using the factorization rule,
$$
P(A,B,C) = P(A)P(B)P(C\mid A,B)
$$
Therefore, marginalizing out $C$,
$$
\begin{align}
P(A,B) & = \sum_{c \in C} P(A,B,c)  \\
& = \sum_{c \in C} P(A)P(B) P(c \mid A,B) \\
& = P(A)P(B) \sum_{c \in C} P(c \mid A,B) = P(A)P(B)
\end{align}
$$

### Question 6:  Is $(A \perp B \mid C)$?

```python
@pgm_render
def pgm_question6():
    pgm = daft.PGM([4, 4], origin=[0, 0])

    # Nodes
    pgm.add_node(daft.Node("c", r"$C$", 2, 3.5, observed=True))
    pgm.add_node(daft.Node("a", r"$A$", 1.3, 2.5))
    pgm.add_node(daft.Node("b", r"$B$", 2.7, 2.5))

    # Add in the edges.
    pgm.add_edge("a", "c", head_length=0.08)
    pgm.add_edge("b", "c", head_length=0.08)

    return pgm
```

```python
%%capture
pgm_question6("images/pgm/question6.png")
```

### Answer:  No!

$A$ can influence $B$ via $C$.
$$
P(A,B | C) = \frac{P(A,B,C)}{P(C)} = \frac{P(A)P(B)P(C|A,B)}{P(C)}
$$

This does not factorize in general to $P(A|C)P(B|C)$.

### Example:  Battery, Fuel, and Gauge

Consider three binary random variables
- Battery $B$ is either charged $(B=1)$ or dead, $(B=0)$
- Fuel tank $F$ is either full $(F=1)$ or empty, $(F=0)$
- Fuel gauge $G$ either indicates full $(G=1)$ or empty, $(G=0)$

Assume $(B \perp F)$ with priors
- $P(B = 1) = 0.9$
- $P(F = 1) = 0.9$

### Example:  Battery, Fuel, and Gauge

Given the state of the fuel tank and the battery, the fuel gauge reads full with probabilities:
 - $p(G = 1 \mid B = 1, F = 1) = 0.8$
 - $p(G = 1 \mid B = 1, F = 0) = 0.2$
 - $p(G = 1 \mid B = 0, F = 1) = 0.2$
 - $p(G = 1 \mid B = 0, F = 0) = 0.1$

### Example:  Battery, Fuel, and Gauge

Without any observations, the probability of an empty fuel tank is
$$
P(F=0) = 1 - P(F = 1) = 0.1
$$

```python
@pgm_render
def pgm_bfg_1():
    pgm = daft.PGM([4, 4], origin=[0, 0])

    # Nodes
    pgm.add_node(daft.Node("G", r"$G$", 2, 3.5))
    pgm.add_node(daft.Node("B", r"$B$", 1.3, 2.5))
    pgm.add_node(daft.Node("F", r"$F$", 2.7, 2.5))

    # Add in the edges.
    pgm.add_edge("B", "G", head_length=0.08)
    pgm.add_edge("F", "G", head_length=0.08)

    return pgm;
```

```python
%%capture
pgm_bfg_1("images/pgm/bfg-1.png")
```

### Example:  Empty Gauge

Now, suppose the gauge reads $G=0$.  We have
$$
P(G=0) = \sum \limits_{B \in \{0, 1\}} \sum \limits_{F \in \{0, 1\}}
 P(G = 0 \mid B, F) P(B) P(F) = 0.315
$$

> Verify this!

### Example:  Emtpy Gauge

```python
@pgm_render
def pgm_bfg_2():
    pgm = daft.PGM([4, 4], origin=[0, 0])

    # Nodes
    pgm.add_node(daft.Node("G", r"$G$", 2, 3.5, offset=(0, 20), observed=True))
    pgm.add_node(daft.Node("B", r"$B$", 1.3, 2.5, offset=(0, 20)))
    pgm.add_node(daft.Node("F", r"$F$", 2.7, 2.5, offset=(0, 20)))

    # Add in the edges.
    pgm.add_edge("B", "G", head_length=0.08)
    pgm.add_edge("F", "G", head_length=0.08)

    return pgm;
```

```python
%%capture
pgm_bfg_2("images/pgm/bfg-2.png");
```

### Example:  Empty Gauge

Now, we also have
$$
p(G = 0 \mid F = 0)
= \sum \limits_{B \in \{0, 1\}} p(G = 0 \mid B, F = 0) p(B) = 0.81
$$

Applying Bayes' Rule,
$$
\begin{align}
p(F = 0 \mid G = 0)
&= \frac{p(G = 0 \mid F = 0) p(F = 0)}{p(G = 0)} \\
&\approx 0.257 > p(F = 0) = 0.10
\end{align}
$$

Observing an empty gauge makes it more likely that the tank is empty!

### Example:  Emtpy Gauge, Dead Battery

Now, suppose we *also* observe a dead battery $B =0$.  Then,
$$
\begin{align}
p(F = 0 \mid G = 0, B = 0)
&= \frac{p(G = 0 \mid B = 0, F = 0) p(F = 0)}{\sum_{F \in \{0, 1\}} p(G = 0 \mid B = 0, F) p(F)} \\
&\approx 0.111
\end{align}
$$

### Example:  Emtpy Gauge, Dead Battery

```python
@pgm_render
def pgm_bfg_3():
    pgm = daft.PGM([4, 4], origin=[0, 0])

    # Nodes
    pgm.add_node(daft.Node("G", r"$G$", 2, 3.5, offset=(0, 20), observed=True))
    pgm.add_node(daft.Node("B", r"$B$", 1.3, 2.5, offset=(0, 20), observed=True))
    pgm.add_node(daft.Node("F", r"$F$", 2.7, 2.5, offset=(0, 20)))

    # Add in the edges.
    pgm.add_edge("B", "G", head_length=0.08)
    pgm.add_edge("F", "G", head_length=0.08)

    return pgm;
```

```python
%%capture
pgm_bfg_3("images/pgm/bfg-3.png")
```

### Example:  Empty Gauge, Dead Battery

This is the **explaining away** phenomenon.
- The probability of an empty tank has decreased from $0.257$ to $0.111$ after observing the dead battery!
- A dead battery *explans* why the guage indicates an empty tank, reducing the probability that the tank is *really* empty.

Conditioning on a common child makes its parents dependent.

### Bayesian Networks:  d-Separation

We say a trail $X \leftrightharpoons Z \leftrightharpoons Y$ is **active** when influence can flow from $X$ to $Y$ via $Z$.
- **Causal Trail:** $X \rightarrow Z \rightarrow Y$ is active iff $Z$ is *hidden* (unobserved).
- **Evidential Trail:** $X \leftarrow Z \leftarrow Y$ is active iff $Z$ is *hidden*.
- **Common Cause:** $X \leftarrow Z \rightarrow Y$ is active iff $Z$ is *hidden*.
- **Common Effect:** $X \rightarrow Z \leftarrow Y$ is active iff $Z$ or a descendant of $Z$ is *observed*.

### Bayesian Networks:  d-Separation

A longer trail $X_1 \leftrightharpoons X_2 \leftrightharpoons \cdots \leftrightharpoons X_n$ is **active** if influence can flow from $X_1$ to $X_n$ along the trail.

- Requires that every $X_{k-1} \leftrightharpoons X_k \leftrightharpoons X_{k+1}$ is active.

In general, $X_1 \leftrightharpoons X_2 \leftrightharpoons \cdots \leftrightharpoons X_n$ is **active** given observed variables $\mathcal Y$ if
- Whenever we have a v-structure $X_{k-1} \rightarrow X_k \leftarrow X_{k+1}$ then $X_k$ or one of its descendants is in $\mathcal Y$.
- No other node along the trail is in $\mathcal Y$.

### Bayesian Networks: d-Separation

Some graphs may have more than one trail between two given nodes.
- One node may inflence another if there exists an active trail between them.

Let $\mathbf{X}$, $\mathbf{Y}$, and $\mathbf{Z}$ be sets of nodes in $\G$.  We say $\mathbf{X}$ and $\mathbf{Y}$ are **d-separated** given $\mathbf{Z}$, denoted $\dsep_\G( \mathbf{X};\mathbf{Y} \mid \mathbf{Z})$ if there is no active trail between any nodes $X \in \mathbf{X}$ and $Y \in \mathbf{Y}$ given $\mathbf{Z}$.

### Bayesian Networks:  d-Separation

**Theorem:** (Koller & Friedman 3.5) For almost all distributions $P$ that factorize over $\G$, we have
$$
\dsep_\G(X; Y \mid Z) \iff (X \perp Y \mid Z)
$$

That is, except on a set of measure zero in the space of distributions, the concept of d-separation in $\G$ exactly captures the concept of conditional independence.
- To check if variables are independent, check for d-separation.

## Challenging Independence Questions:

* Why is the claim **False**? It actually isn't true that $B \perp E \mid D$!

  Notice that there is an active path from $B$ to $E$:
$$B \to \boxed{D} \leftarrow A \to C \to E$$


# ***Introduction to Radar Using Python and MATLAB***
## Andy Harrison - Copyright (C) 2019 Artech House
<br/>

# Loop Antennas
***

Similar to the linear wire antenna, the loop antenna is also easy to fabricate, easy to analyze, and inexpensive to build.  Loops are generally classified as either electrically small, where the circumference is less than a tenth of a wavelength, or electrically large, where the circumference is about a wavelength.  Small loops are not effective radiators and are usually used as probes or other receiving type elements, whereas the large circular loops are typically used in array configurations.

### Small Circular Loop
Referring to Section 3.2.2.1, assume a small circular loop in the $x$-$y$ plane, as shown in Figure 3.8.  The wire is very thin such that the current distribution is $I_{\phi} = I_0$.  The electric and magnetic fields are written as (Equations 3.42 - 3.43)

\begin{align}
    E_{\phi} &= \eta \sin\theta\, I_0 \, (ka)^2 \, \frac{e^{-jkr}}{4r} \hspace{0.5in} \text{(V/m)}\label{eq:small_loop_E}\\ \nonumber \\
    H_{\theta} &= -\sin\theta\, I_0 \, (ka)^2 \, \frac{e^{-jkr}}{4r}\label{eq:small_loop_H} \hspace{0.5in} \text{(A/m)}
\end{align}

### Constant Current Loop
Referring to Section 3.2.2.2, consider a circular loop antenna with a radius that is not considered small but does have a constant current distribution.  Following the same procedure as for the small circular loop antenna allows the fields for a constant current loop antenna to be written as

\begin{align}
    E_{\phi} &= a\, k\, \eta\, I_0\, \frac{e^{-jkr}}{2r}\, J_1(k a \sin\theta) \hspace{0.5in} \text{(V/m)} \label{eq:efield_constant_current_loop} \\ \nonumber \\
    H_{\theta} &= -a\, k\, I_0\, \frac{e^{-jkr}}{2r}\, J_1(k a \sin\theta) \hspace{0.5in} \text{(A/m)}
\end{align}
***

Begin by getting the library path

```python
import lib_path
```

Set the frequency (Hz), the radius (m) and the current (A) for the loop antenna

```python
frequency = 1e9

radius = 0.1

current = 1.0
```

Set the angular span using the `linspace` routine from `scipy`

```python
from numpy import linspace

from scipy.constants import pi

theta = linspace(0.0, 2.0 * pi, 1000)
```

Calculate the antenna parameters and antenna pattern using the `small_loop` and `circular_loop` routines

```python
from Libs.antenna.loop import small_loop, circular_loop

antenna_type = 'small_loop'

if antenna_type == 'circular_loop':

    total_power_radiated = small_loop.radiated_power(frequency, radius, current)

    radiation_resistance = small_loop.radiation_resistance(frequency, radius)

    beamwidth = small_loop.beamwidth()

    directivity = small_loop.directivity()

    maximum_effective_aperture = small_loop.maximum_effective_aperture(frequency)

    _, _, ep, _, _, _ = small_loop.far_field(frequency, radius, current, 1e9, theta)

else:

    total_power_radiated = circular_loop.radiated_power(frequency, radius, current)

    radiation_resistance = circular_loop.radiation_resistance(frequency, radius)

    beamwidth = circular_loop.beamwidth(frequency, radius)

    directivity = circular_loop.directivity(frequency, radius)

    maximum_effective_aperture = circular_loop.maximum_effective_aperture(frequency, radius)

    _, _, ep, _, _, _ = circular_loop.far_field(frequency, radius, current, 1e9, theta)
```

Display the antenna parameters

```python
print('Total Radiated Power (W)     {:.2f}'.format(total_power_radiated))

print('Radiation Resistance (Ohms)  {:.2f}'.format(radiation_resistance))

print('Beamwidth (deg)              {:.2f}'.format(beamwidth))

print('Directivity                  {:.2f}'.format(directivity))

print('Max Effective Aperture       {:.2e}'.format(maximum_effective_aperture))
```

    Total Radiated Power (W)     620.55
    Radiation Resistance (Ohms)  1241.11
    Beamwidth (deg)              128.15
    Directivity                  1.41
    Max Effective Aperture       5.04e-03

Display the antenna pattern using the `matplotlib` routines

```python
from matplotlib import pyplot as plt

# Set the figure size

plt.rcParams["figure.figsize"] = (15, 10)

# Display the results

plt.polar(theta, abs(ep), '')

# Set the plot title and labels

plt.title('Loop Antenna Pattern', size=14)

# Set the tick label size

plt.tick_params(labelsize=12)

# Turn on the grid

plt.grid(linestyle=':', linewidth=0.5)
```


# Making a random Polygon

```python
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from PyEscape.escape_plan import escape
from PyEscape.escape_points import random_points_on_hull
from PyEscape.escape_polygonhelper import make_hull_and_scale
from tqdm import tqdm
```

```python
D = 400
v = 1
a = 0.1
n_pores = 1
pts = np.random.random((10,3))
hull, pts = make_hull_and_scale(pts)
pores = random_points_on_hull(hull)

N = 10
results = []
for i in tqdm(range(N)):
    results.append(escape(D, v, a, pores, dt=1e-6, hull=hull, shape='polygon'))
```

    100%|██████████| 10/10 [01:04<00:00,  6.49s/it]

```python
mov_avg = np.cumsum(results)/(1+np.arange(len(results)))

plt.figure()
plt.plot(mov_avg, label='Simulations')
plt.axhline(0.00625, label='Schuss et al.', c='r')
plt.legend()
```

```python
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")

# Plot defining corner points
ax.plot(pts.T[0], pts.T[1], pts.T[2], "ko")

# 12 = 2 * 6 faces are the simplices (2 simplices per square face)
for s in hull.simplices:
    s = np.append(s, s[0])  # Here we cycle back to the first coordinate
    ax.plot(pts[s, 0], pts[s, 1], pts[s, 2], "r-")

# Make axis label
for i in ["x", "y", "z"]:
    eval("ax.set_{:s}label('{:s}')".format(i, i))

plt.show()
```

# Converting Polygon to Ellipsoid

```python
from tqdm import tqdm
from PyEscape.escape_polygonhelper import make_hull_and_scale
from PyEscape.escape_polygonhelper import isolate_points_from_segmented_imagestack
from PyEscape.escape_polygonhelper import fit_polygon_to_ellipsoid
from PyEscape.escape_points import random_points_on_hull, random_points_on_ellipsoid
from PyEscape.escape_plan import escape
from skimage import io
import numpy as np
import multiprocessing
from sys import argv
from os import cpu_count

img_loc = "/Users/aoife/PHD/Modelling/EscapeLeafCellsMultipleEscapes/data/Leaf segmentation.tif"
leaf_stack = io.imread(img_loc)

unique_cell_ids = np.genfromtxt("/Users/aoife/PHD/Modelling/EscapeLeafCellsMultipleEscapes/data/cells_idx.csv").astype('int')[1:]
idx=70

pts = isolate_points_from_segmented_imagestack(leaf_stack, unique_cell_ids[idx], .2768, .2768, .9766)
hull, pts = make_hull_and_scale(pts)

```

```python
def correction_SA(a,b=1,c=1, vol=1):
    def vol_Ellip(a, b, c): return 4/3*np.pi*a*b*c
    def SA_Ellip(a,b,c): return 4*np.pi*( ((a*b)**1.6 + (a*c)**1.6 + (b*c)**1.6)/3  )**(1/1.6)
    volN = vol_Ellip(a, b, c)
    cbrt_diff = vol/np.cbrt(volN)
    a, b, c = np.array([a, b, c]) * cbrt_diff
    Lin = min([a,b,c])
    Lout = max([a,b,c])
    Lin = SA_Ellip(Lin, Lin, Lin)
    Lout = SA_Ellip(Lout, Lout, Lout)
    return Lout/Lin
```

```python
ratios = []
abcs = []
for idx, cell in enumerate(unique_cell_ids):
    pts = isolate_points_from_segmented_imagestack(leaf_stack, cell, .2768, .2768, .9766)
    hull, pts = make_hull_and_scale(pts)
    a,b,c = fit_polygon_to_ellipsoid(hull)
    abcs.append([a,b,c])
    ratios.append(correction_SA(a,b,c))
    if idx == 50:
        break
```

```python
ratios = np.array(ratios)
```

```python
import seaborn as sns

plt.hist(np.around(ratios[ratios<2000]))
```

```python
from PyEscape.escape_polygonhelper import fit_polygon_to_ellipsoid
from PyEscape.escape_points import random_points_on_ellipsoid

ABC = fit_polygon_to_ellipsoid(hull)
pores = random_points_on_ellipsoid(ABC)

N = 10
results = []
for i in tqdm(range(N)):
    results.append(escape(D, v, a, pores, dt=1e-6, ABC=ABC, shape='ellipsoid'))
```

    100%|██████████| 10/10 [00:09<00:00,  1.09it/s]

```python
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np

phi = np.linspace(0,2*np.pi, 256).reshape(256, 1) # the angle of the projection in the xy-plane
theta = np.linspace(0, np.pi, 256).reshape(-1, 256) # the angle from the polar axis, ie the polar angle
radius = 4

# Transformation formulae for a spherical coordinate system.
x = ABC[0]*np.sin(theta)*np.cos(phi)
y = ABC[1]*np.sin(theta)*np.sin(phi)
z = ABC[2]*np.cos(theta)

fig = plt.figure(figsize=plt.figaspect(1))  # Square figure
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(x, y, z, color='b')

```

```python
from tqdm import tqdm
from PyEscape.escape_polygonhelper import make_hull_and_scale
from PyEscape.escape_polygonhelper import isolate_points_from_segmented_imagestack
from PyEscape.escape_polygonhelper import fit_polygon_to_ellipsoid
from PyEscape.escape_points import random_points_on_hull, random_points_on_ellipsoid
from PyEscape.escape_plan import escape
from skimage import io
import numpy as np
import multiprocessing
from sys import argv
from os import cpu_count

img_loc = "/Users/aoifehughes/PHD/Modelling/EscapeLeafCellsMultipleEscapes/data/Leaf segmentation.tif"
leaf_stack = io.imread(img_loc)

unique_cell_ids = np.genfromtxt("/Users/aoifehughes/PHD/Modelling/EscapeLeafCellsMultipleEscapes/data/cells_idx.csv").astype('int')[1:]
idx=70

pts = isolate_points_from_segmented_imagestack(leaf_stack, unique_cell_ids[idx], .2768, .2768, .9766)
hull, pts = make_hull_and_scale(pts)

ABC = fit_polygon_to_ellipsoid(hull)
ABC
```

    array([0.26467467, 0.33265048, 2.71150778])

```python
def calc_ratio(img, idx):
    pts = isolate_points_from_segmented_imagestack(img, unique_cell_ids[idx], .2768, .2768, .9766)
    hull, pts = make_hull_and_scale(pts)
    a,b,c = fit_polygon_to_ellipsoid(hull)
    return [a,b,c]
calc_ratio(leaf_stack, idx)
```

    [0.2646746720766011, 0.3326504782977085, 2.7115077795057245]

```python
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure(figsize=plt.figaspect(1))  # Square figure
ax = fig.add_subplot(111, projection='3d')

phi = np.linspace(0,2*np.pi, 256).reshape(256, 1) # the angle of the projection in the xy-plane
theta = np.linspace(0, np.pi, 256).reshape(-1, 256) # the angle from the polar axis, ie the polar angle
radius = 4

# Transformation formulae for a spherical coordinate system.
X = ABC[0]*np.sin(theta)*np.cos(phi)
Y = ABC[1]*np.sin(theta)*np.sin(phi)
Z = ABC[2]*np.cos(theta)

# Create cubic bounding box to simulate equal aspect ratio
max_range = np.array([X.max()-X.min(), Y.max()-Y.min(), Z.max()-Z.min()]).max()
Xb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][0].flatten() + 0.5*(X.max()+X.min())
Yb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][1].flatten() + 0.5*(Y.max()+Y.min())
Zb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][2].flatten() + 0.5*(Z.max()+Z.min())
# Comment or uncomment following both lines to test the fake bounding box:
for xb, yb, zb in zip(Xb, Yb, Zb):
    ax.plot([xb], [yb], [zb], 'w')

ax.plot_surface(X, Y, Z, color='b', alpha=0.9)
```

```python
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")

# 12 = 2 * 6 faces are the simplices (2 simplices per square face)
for s in hull.simplices:
    s = np.append(s, s[0])  # Here we cycle back to the first coordinate
    ax.plot(pts[s, 0], pts[s, 1], pts[s, 2], "r-")

# Make axis label
for i in ["x", "y", "z"]:
    eval("ax.set_{:s}label('{:s}')".format(i, i))

# Create cubic bounding box to simulate equal aspect ratio
max_range = np.array([X.max()-X.min(), Y.max()-Y.min(), Z.max()-Z.min()]).max()
Xb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][0].flatten() + 0.5*(X.max()+X.min())
Yb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][1].flatten() + 0.5*(Y.max()+Y.min())
Zb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][2].flatten() + 0.5*(Z.max()+Z.min())
# Comment or uncomment following both lines to test the fake bounding box:
#for xb, yb, zb in zip(Xb, Yb, Zb):
#    ax.plot([xb], [yb], [zb], 'w')
#ax.plot_surface(X, Y, Z, color='b', alpha=0.5)
plt.show()
```

```python
fig = plt.figure( figsize=(8,8), dpi=200)
ax = fig.add_subplot(111, projection="3d")

# 12 = 2 * 6 faces are the simplices (2 simplices per square face)
for idx, s in enumerate(hull.simplices):
    s = np.append(s, s[0])  # Here we cycle back to the first coordinate
    ax.plot(pts[s, 0], pts[s, 1], pts[s, 2], "r-", alpha=0.2, label=('Cell convex hull' if idx==0 else None) )

# Make axis label
for i in ["x", "y", "z"]:
    eval("ax.set_{:s}label('{:s}')".format(i, i))

# Create cubic bounding box to simulate equal aspect ratio
max_range = np.array([X.max()-X.min(), Y.max()-Y.min(), Z.max()-Z.min()]).max()
Xb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][0].flatten() + 0.5*(X.max()+X.min())
Yb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][1].flatten() + 0.5*(Y.max()+Y.min())
Zb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][2].flatten() + 0.5*(Z.max()+Z.min())
# Comment or uncomment following both lines to test the fake bounding box:
for xb, yb, zb in zip(Xb, Yb, Zb):
    ax.plot([xb], [yb], [zb], 'w', alpha=0.3)

c3=ax.plot_surface(X+1, Y, Z, color='b', alpha=0.5, label='Approximation')
c3._facecolors2d='b'
c3._edgecolors2d='b'
ax.legend()
fig.savefig("/Users/aoifehughes/PHD/Documents/Progress/2021_04/figures/approxCells.png")
```

```python
pts
```

```python
import numpy as np
from sympy import Plane, Point3D
import networkx as nx

def simplify(triangles):
    """
    Simplify an iterable of triangles such that adjacent and coplanar triangles form a single face.
    Each triangle is a set of 3 points in 3D space.
    """

    # create a graph in which nodes represent triangles;
    # nodes are connected if the corresponding triangles are adjacent and coplanar
    G = nx.Graph()
    G.add_nodes_from(range(len(triangles)))
    for ii, a in enumerate(triangles):
        for jj, b in enumerate(triangles):
            if (ii < jj): # test relationships only in one way as adjacency and co-planarity are bijective
                if is_adjacent(a, b):
                    if is_coplanar(a, b, np.pi / 180.):
                        G.add_edge(ii,jj)

    # triangles that belong to a connected component can be combined
    components = list(nx.connected_components(G))
    simplified = [set(flatten(triangles[index] for index in component)) for component in components]

    # need to reorder nodes so that patches are plotted correctly
    reordered = [reorder(face) for face in simplified]

    return reordered

def is_adjacent(a, b):
    return len(set(a) & set(b)) == 2 # i.e. triangles share 2 points and hence a side

def is_coplanar(a, b, tolerance_in_radians=0):
    a1, a2, a3 = a
    b1, b2, b3 = b
    plane_a = Plane(Point3D(a1), Point3D(a2), Point3D(a3))
    plane_b = Plane(Point3D(b1), Point3D(b2), Point3D(b3))
    if not tolerance_in_radians: # only accept exact results
        return plane_a.is_coplanar(plane_b)
    else:
        angle = plane_a.angle_between(plane_b).evalf()
        angle %= np.pi # make sure that angle is between 0 and np.pi
        return (angle - tolerance_in_radians <= 0.) or \
            ((np.pi - angle) - tolerance_in_radians <= 0.)

flatten = lambda l: [item for sublist in l for item in sublist]

def reorder(vertices):
    """
    Reorder nodes such that the resulting path corresponds to the "hull" of the set of points.

    Note:
    -----
    Not tested on edge cases, and likely to break.
    Probably only works for convex shapes.

    """
    if len(vertices) <= 3: # just a triangle
        return vertices
    else:
        # take random vertex (here simply the first)
        reordered = [vertices.pop()]
        # get next closest vertex that is not yet reordered
        # repeat until only one vertex remains in original list
        vertices = list(vertices)
        while len(vertices) > 1:
            idx = np.argmin(get_distance(reordered[-1], vertices))
            v = vertices.pop(idx)
            reordered.append(v)
        # add remaining vertex to output
        reordered += vertices
        return reordered

def get_distance(v1, v2):
    v2 = np.array(list(v2))
    difference = v2 - v1
    ssd = np.sum(difference**2, axis=1)
    return np.sqrt(ssd)
```

```python
from scipy.spatial import HalfspaceIntersection
from scipy.spatial import ConvexHull
import numpy as np
import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d as a3

w = np.array([1., 1., 1.])
# ∑ᵢ hᵢ wᵢ qᵢ - ∑ᵢ gᵢ wᵢ <= 0
#  qᵢ - ubᵢ <= 0
# -qᵢ + lbᵢ <= 0
halfspaces = np.array([
                    [1.*w[0], 1.*w[1], 1.*w[2], -10 ],
                    [ 1.,  0.,  0., -4],
                    [ 0.,  1.,  0., -4],
                    [ 0.,  0.,  1., -4],
                    [-1.,  0.,  0.,  0],
                    [ 0., -1.,  0.,  0],
                    [ 0.,  0., -1.,  0]
                    ])
feasible_point = np.array([0.1, 0.1, 0.1])
hs = HalfspaceIntersection(halfspaces, feasible_point)
verts = hs.intersections
#hull = ConvexHull(verts)
hull, pts = make_hull_and_scale(pts)

simplices = hull.simplices

org_triangles = [verts[s] for s in simplices[:10]]

class Faces():
    def __init__(self,tri, sig_dig=12, method="convexhull"):
        self.method=method
        self.tri = np.around(np.array(tri), sig_dig)
        self.grpinx = list(range(len(tri)))
        norms = np.around([self.norm(s) for s in self.tri], sig_dig)
        _, self.inv = np.unique(norms,return_inverse=True, axis=0)

    def norm(self,sq):
        cr = np.cross(sq[2]-sq[0],sq[1]-sq[0])
        return np.abs(cr/np.linalg.norm(cr))

    def isneighbor(self, tr1,tr2):
        a = np.concatenate((tr1,tr2), axis=0)
        return len(a) == len(np.unique(a, axis=0))+2

    def order(self, v):
        if len(v) <= 3:
            return v
        v = np.unique(v, axis=0)
        n = self.norm(v[:3])
        y = np.cross(n,v[1]-v[0])
        y = y/np.linalg.norm(y)
        c = np.dot(v, np.c_[v[1]-v[0],y])
        if self.method == "convexhull":
            h = ConvexHull(c)
            return v[h.vertices]
        else:
            mean = np.mean(c,axis=0)
            d = c-mean
            s = np.arctan2(d[:,0], d[:,1])
            return v[np.argsort(s)]

    def simplify(self):
        for i, tri1 in enumerate(self.tri):
            for j,tri2 in enumerate(self.tri):
                if j > i:
                    if self.isneighbor(tri1,tri2) and \
                       self.inv[i]==self.inv[j]:
                        self.grpinx[j] = self.grpinx[i]
        groups = []
        for i in np.unique(self.grpinx):
            u = self.tri[self.grpinx == i]
            u = np.concatenate([d for d in u])
            u = self.order(u)
            groups.append(u)
        return groups

f = Faces(org_triangles)
g = f.simplify()

ax = a3.Axes3D(plt.figure())

colors = list(map("C{}".format, range(len(g))))

pc = a3.art3d.Poly3DCollection(g,  facecolor=colors,
                                   edgecolor="k", alpha=0.9)
ax.add_collection3d(pc)

ax.dist=10
ax.azim=30
ax.elev=10
ax.set_xlim([0,5])
ax.set_ylim([0,5])
ax.set_zlim([0,5])
plt.show()
```

```python
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")

# 12 = 2 * 6 faces are the simplices (2 simplices per square face)
for s in hull.simplices:
    s = np.append(s, s[0])  # Here we cycle back to the first coordinate
    #ax.plot(pts[s, 0], pts[s, 1], pts[s, 2], "r-")

# Make axis label
for i in ["x", "y", "z"]:
    eval("ax.set_{:s}label('{:s}')".format(i, i))

# Create cubic bounding box to simulate equal aspect ratio
max_range = np.array([X.max()-X.min(), Y.max()-Y.min(), Z.max()-Z.min()]).max()
Xb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][0].flatten() + 0.5*(X.max()+X.min())
Yb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][1].flatten() + 0.5*(Y.max()+Y.min())
Zb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][2].flatten() + 0.5*(Z.max()+Z.min())
# Comment or uncomment following both lines to test the fake bounding box:
for xb, yb, zb in zip(Xb, Yb, Zb):
    ax.plot([xb], [yb], [zb], 'w')
ax.plot_surface(X, Y, Z, color='b', alpha=0.5)
plt.show()
```

```python

```


```python
from importlib import *
import shiroin
reload(shiroin)
from IPython.display import Latex
from shiroindev import *
from sympy import *
shiro.display=lambda x:display(Latex(x))
prove('2*(a^2+b^2+c^2-a*b-a*c-b*c)')
```

numerator: $2 a^{2} - 2 a b - 2 a c + 2 b^{2} - 2 b c + 2 c^{2}$

denominator: $1$

status: 0

From weighted AM-GM inequality:

$$2 a b \le a^{2}+b^{2}$$

$$2 a c \le a^{2}+c^{2}$$

$$2 b c \le b^{2}+c^{2}$$

$$ 0 \le 0 $$

The sum of all inequalities gives us a proof of the inequality.

    0

```python
ineqs2
```

```python
x=S("a^2+b^2+c^2")
latex(latex(x))
```

    '\\mathtt{\\text{a\\textasciicircum\\{2\\} + b\\textasciicircum\\{2\\} + c\\textasciicircum\\{2\\}}}'


# Stab mit Volumenkraft und vorgegebener Spannung

\begin{align}
-E \frac{\partial^2 u}{\partial x^2}&= f &x\in (0\text{ $m$}, 2\text{ $m$})\label{inner}\\
u(0 \text{ $m$}) & =0 \text{ $m$}&\text{Verschiebung vorgegeben}\\
E \frac{\partial u(2 \text{ $m$})}{\partial x} & =g &\text{Spannung vorgegeben}
\end{align}

### Lösen des Gleichungssystems

Analytische Lösung $$u_{ana}=-\frac{f}{2\cdot E}x^2+\left(\frac{g}{E}+2\text{ $m$}\frac{f}{E}\right)x$$ verglichen.

Als Beispiel sei der Stab aus Baustahl und $2\text{ $m$}$ lang, mit einem Querschnitt mit Radius von $10\text{ $cm$}$, an einem Ende an der Decke befestigt und am unteren Ende wird eine Spannung vorgegeben. Der Stab wird durch Schwerkraft nach unten gezogen.

* Was heißt physikalisch wenn bei 2 m die vorgegebene Spannung 0 Pa beträgt?
* Was heißt physikalisch wenn bei 2 m die vorgegebene Spannung positiv ist?
* Was heißt physikalisch wenn bei 2 m die vorgegebene Spannung negativ ist?

Für das numerische Beispiel sei das E-Modul $E=200 \text{ $MPa$}$, $u_{Dirichlet1}=0\text{ $m$}$, $f=7850\cdot 9.8066\text{ $\frac{N}{m^3}$}$ und die Spannung $E \frac{\partial u(2\text{ m})}{\partial x}=g$ bei 2 m einmal 0 Pa, einmal "echt" größer als null und einmal "echt" kleiner als null.

```python
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib as mpl
from matplotlib import ticker
import numpy as np
from scipy import integrate
from fem.funktionen import *
import seaborn as sns
#sns.set()
mpl.rcParams["figure.figsize"] = (20,7)
mpl.rcParams['lines.linewidth'] = 5
mpl.rcParams['lines.markersize'] = 15
#sns.set()
farben = sns.color_palette()
plt.style.use('seaborn-darkgrid')
farben = sns.color_palette()
```

```python

emod=200 #stahl
#emod=0.1 # gummi
#emod=0.01 # styropor, Dichte theoretisch auch anpassen
#emod=9

A = np.pi*(10/100.)**2
Laenge = 2.
E = emod*10**9
u0 = 0
g = 100*10**6
f = 7850*9.8066

```

```python
g*A
```

    3141592.6535897935

```python
x=np.linspace(0,2 ,1000)

def uAna(x):

    wert=(-f/(2*E))*x**2+(g/E+2*f/E)*x

    return wert

ax=plt.axes()
ax.tick_params(labelsize=15)
ax.plot(x,uAna(x), label='Analytisch', color=farben[0]),
ax.tick_params(labelsize=15)
box = ax.get_position()
ax.set_xlabel(xlabel='x in m',fontsize=15)
ax.set_ylabel(ylabel='u in m',fontsize=15)
plt.legend(loc='best',fontsize=15)
print(max(uAna(x)))
print(min(uAna(x)))
```

## Berechnung der Kenngrößen: Postprocessing

Nach dem Solving-Schritt ist eine FEM-Approximation $u_{FEM}$ der Verschiebung $u$ bekannt. Meistens ist die eigentliche Kenngröße jedoch nicht die Verschiebung, sondern eine Größe, die sich aus der Verschiebung berechnet. In der linearen Elastizität z.B. die Dehnung oder die Spannung.

\begin{align}
\sigma & =E \epsilon &\text{ Hook-Gesetz: Spannung-Dehnung}\\
\epsilon & = \frac{\partial u}{\partial x} &\text{ Dehnung-Verschiebung}
\end{align}

Meistens wird aus der Dehnung und Spannung noch eine Vergleichsdehnung bzw. Vergleichsspannung berechnet (z.B. von Mises Vergleichsdehnung bzw. Vergleichsspannung). In 1D ist das nur der Betrag:

\begin{align}
\sigma_V & =\sqrt{\sigma^2} &\text{ Vergleichsdehnung}\\
\epsilon_V & = \frac{1}{1+\nu}\sqrt{\epsilon^2} &\text{ Vergleichsspannung}
\end{align}

Wobei $\nu=0.3$ bei Stahl; siehe [Issler et. al, 2003].

```python
def dehnung(y):

    wert=(-f/(E))*x+(g/E+2*f/E)

    return wert

def spannung(y):
    wert=E*dehnung(y)
    return wert

ax=plt.axes()
ax.tick_params(labelsize=15)
ax.plot(x,abs(dehnung(x)), label=r'$\epsilon_V (max. {0})$'.format(max(abs(dehnung(x)))), color=farben[0])

ax.tick_params(labelsize=15)
ax.set_xlabel(xlabel='x in m',fontsize=15)
ax.set_ylabel(ylabel=r'$\epsilon_V$',fontsize=15)
plt.legend(loc='best',fontsize=15)
```

```python
ax=plt.axes()
ax.plot(x,abs(spannung(x))*10**(-5), label=r'$\sigma_V (max. {0})$'.format(max(abs(spannung(x)*10**(-5)))), color=farben[1])

ax.tick_params(labelsize=15)
ax.set_xlabel(xlabel='x in m',fontsize=15)
ax.set_ylabel(ylabel=r'$\sigma_V$ in bar',fontsize=15)

plt.legend(loc='best',fontsize=15)
```

$$\Pi=\frac{1}{2}A\int_{0}^{2}\epsilon\sigma dx$$

```python
0.5*A*integrate.simps(dehnung(x)*spannung(x), x)
```

    1573.216022862689

Ist das Ergebnis noch physiklaisch, ist die Spannung noch im linearen Bereich?

### Übungen:

* Berechnen Sie dieses Beispiel in 3D in ANSYS
    * Ist das Ergebnis ähnlich?
    * Wie erklären sich ggf. Unterschiede?

* Geben Sie postive und negative Drücke bei 2 m an
* Bei welchem Druck wird die Streckgrenze erreicht?


###### Content under Creative Commons Attribution license CC-BY 4.0, code under MIT license (c)2014 L.A. Barba, G.F. Forsyth, C.D. Cooper.

# Spreading out

Welcome back! This is the third lesson of the course [Module 4](https://github.com/numerical-mooc/numerical-mooc/tree/master/lessons/04_spreadout), _Spreading out: parabolic PDEs,_ where we study the numerical solution of diffusion problems.

In the first two notebooks, we looked at the 1D heat equation, and solved it numerically using [*explicit*](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/04_spreadout/04_01_Heat_Equation_1D_Explicit.ipynb) and [*implicit*](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/04_spreadout/04_02_Heat_Equation_1D_Implicit.ipynb) schemes. We learned that implicit schemes are unconditionally stable, and we are free to choose any time step. —Wait: _any time step?_ Remember, we still want to capture the physics of the problem accurately. So although stability concerns do not limit the time step, it still has to be small enough to satisfy any accuracy concerns.

We are now ready to graduate to two dimensions! In the remaining lessons of this course module, we will study the 2D heat equation and reaction-diffusion equation. Like before, we start with explicit methods (this lesson) and then move to implicit methods (next lesson). Let's get started.

## 2D Heat conduction

The equation of heat conduction in 2D is:

$$
\begin{equation}
\rho c_p \frac{\partial T}{\partial t} = \frac{\partial}{\partial x} \left( \kappa_x \frac{\partial T}{\partial x} \right) + \frac{\partial}{\partial y} \left(\kappa_y \frac{\partial T}{\partial y} \right)
\end{equation}
$$

where $\rho$ is the density, $c_p$ is the heat capacity and $\kappa$ is the thermal conductivity.

If the thermal conductivity $\kappa$ is constant, then we can take it outside of the spatial derivative and the equation simplifies to:

$$
\begin{equation}
\frac{\partial T}{\partial t} = \alpha \left(\frac{\partial^2 T}{\partial x^2} + \frac{\partial^2 T}{\partial y^2} \right)
\end{equation}
$$

where $\alpha = \frac{\kappa}{\rho c_p}$ is the thermal diffusivity.  The thermal diffusivity describes the ability of a material to conduct heat vs. storing it.

Does that equation have a familiar look to it?  That's because it's the same as the diffusion equation. There's a reason that $\alpha$ is called the thermal *diffusivity*! We're going to set up an interesting problem where 2D heat conduction is important, and set about to solve it with explicit finite-difference methods.

### Problem statement

Removing heat out of micro-chips is a big problem in the computer industry. We are at a point in technology where computers can't run much faster because the chips might start failing due to the high temperature. This is a big deal! Let's study the problem more closely.

We want to understand how heat is dissipated from the chip with a very simplified model. Say we consider the chip as a 2D plate of size $1{\rm cm}\times 1{\rm cm}$, made of Silicon: $\kappa = 159{\rm W/m C}$, $c_p = 0.712\cdot 10^3 {\rm J/kg C}$, $\rho = 2329{\rm kg/m}^3$, and diffusivity $\alpha \approx 10^{-4}{\rm m}^2{/\rm s}$. Silicon melts at $1414{\rm C}$, but chips should of course operate at much smaller temperatures. The maximum temperature allowed depends on the processor make and model; in many cases, the maximum temperature is somewhere between $60{\rm C}$ and $\sim70{\rm C}$, but better CPUs are recommended to operate at a [maximum of $80{\rm C}$](http://www.pugetsystems.com/blog/2009/02/26/intel-core-i7-temperatures/) (like the Intel Core i7, for example).

We're going to set up a somewhat artificial problem, just to demonstrate an interesting numerical solution. Say the chip is in a position where on two edges (top and right) it is in contact with insulating material.  On the other two edges the chip is touching other components that have a constant temperature of $T=100{\rm C}$ when the machine is operating. Initially, the chip is at room temperature $(20{\rm C})$. *How long does it take for the center of the chip to reach $70{\rm C}$?*

#### Figure 1: Simplified microchip problem setup.

Let's use what we have learned to tackle this problem!

## 2D Finite differences

Everything you learned about finite-difference schemes in [Notebook 1 of Module 2](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/02_spacetime/02_01_1DConvection.ipynb) still applies, but now there are two spatial dimensions.  We will need to build a 2D grid of discrete points to compute the solution on.

We will use a 2D Cartesian grid: one that consists of two families of (grid) lines parallel to the two spatial directions. Two lines (of different families) intersect on one and only one grid node (this is called a _structured_ grid). In the $x$ direction, the discretization uses $i=0, \cdots N_x$ lines, and in the $y$ direction we have $j=0, \cdots N_y$ lines. A given node on the grid will now have two spatial coordinates, and we need two indices: for the two lines that intersect at that node.  For example, the middle point in the figure below would be $T_{i,j}$.

#### Figure 2. Nodal coordinates in 2 dimensions

### Explicit scheme in 2D

Recall from above that the 2D heat equation is

$$
\frac{\partial T}{\partial t} = \alpha \left(\frac{\partial^2 T}{\partial x^2} + \frac{\partial^2 T}{\partial y^2} \right)
$$

Let's write this out discretized using forward difference in time, and central difference in space,  using an explicit scheme. You should be able write this out yourself, without looking—if you need to look, it means you still need to write more difference equations by your own hand!

$$
\begin{equation}
\frac{T^{n+1}_{i,j} - T^n_{i,j}}{\Delta t} = \alpha \left( \frac{T^n_{i+1, j} - 2T^n_{i,j} + T^n_{i-1,j}}{\Delta x^2} + \frac{T^n_{i, j+1} - 2T^n_{i,j} + T^n_{i,j-1}}{\Delta y^2}\right)
\end{equation}
$$

Rearranging the equation to solve for the value at the next time step, $T^{n+1}_{i,j}$, yields

$$
\begin{equation}
T^{n+1}_{i,j}= T^n_{i,j} + \alpha \left( \frac{\Delta t}{\Delta x^2} (T^n_{i+1, j} - 2T^n_{i,j} + T^n_{i-1,j}) + \\\frac{\Delta t}{\Delta y^2} (T^n_{i, j+1} - 2T^n_{i,j} + T^n_{i,j-1})\right)
\end{equation}
$$

That's a little messier than 1D, but still recognizable.

Up until now, we've used stencils to help visualize how a scheme will advance the solution for one time step.  Stencils in 2D are a little harder to draw, but hopefully the figure below will guide your understanding of this method: we are using five grid points at time step $n$ to obtain the solution on one point at time step $n+1$.

#### Figure 3: 2D Explicit Stencil

Similar to all of the 1D explicit methods we've used, the solution at $T^{n+1}_{i,j}$ is updated using only known values from the current solution at time $n$. This is straightforward to implement in code, but will be subject to stability limitations on the time step that you can choose. We'll study an implicit method in the next lesson.

### Boundary Conditions

Whenever we reach a point that interacts with the boundary, we apply the boundary condition. As in the previous notebook, if the boundary has Dirichlet conditions, we simply impose the prescribed temperature at that point. If the boundary has Neumann conditions, we approximate them with a finite-difference scheme.

Remember, Neumann boundary conditions prescribe the derivative in the normal direction. For example, in the problem described above, we have $\frac{\partial T}{\partial y} = q_y$ in the top boundary and $\frac{\partial T}{\partial x} = q_x$ in the right boundary, with $q_y = q_x = 0$ (insulation).

Thus, at every time step, we need to enforce

$$
\begin{equation}
T_{i,end} = q_y\cdot\Delta y + T_{i,end-1}
\end{equation}
$$

and

$$
\begin{equation}
T_{end,j} = q_x\cdot\Delta x + T_{end-1,j}
\end{equation}
$$

Write the finite-difference discretization of the boundary conditions yourself, and confirm that you can get the expressions above.

### Stability

Before doing any coding, let's revisit stability constraints. We saw in the first notebook of this series that the 1D explicit discretization of the diffusion equation was stable as long as $\alpha \frac{\Delta t}{(\Delta x)^2} \leq \frac{1}{2}$. In 2D, this constraint is even tighter, as we need to add them in both directions:

$$
\begin{equation}
\alpha \frac{\Delta t}{(\Delta x)^2} + \alpha \frac{\Delta t}{(\Delta y)^2} < \frac{1}{2}.
\end{equation}
$$

Say that the mesh has the same spacing in $x$ and $y$, $\Delta x = \Delta y = \delta$. In that case, the stability condition is:

$$
\begin{equation}
\alpha \frac{\Delta t}{\delta^2} < \frac{1}{4}
\end{equation}
$$

## Code implementation

### Array storage

The physical problem has two dimensions, so we also store the temperatures in two dimensions: in a 2D array.

We chose to store it with the $y$ coordinates corresponding to the rows of the array and $x$ coordinates varying with the columns (this is just a code design decision!). If we are consistent with the stencil formula (with $x$ corresponding to index $i$ and $y$ to index $j$), then $T_{i,j}$ will be stored in array format as `T[j,i]`.

This might be a little confusing as most of us are used to writing coordinates in the format $(x,y)$, but our preference is to have the data stored so that it matches the physical orientation of the problem. Then, when we make a plot of the solution, the visualization will make sense to us, with respect to the geometry of our set-up. That's just nicer than to have the plot rotated!

#### Figure 4: Row-column data storage

As you can see on Figure 4 above, if we want to access the value $18$ we would write those coordinates as $(x_2, y_3)$.  You can also see that its location is the 3rd row, 2nd column, so its array address would be `T[3,2]`.

Again, this is a design decision.  However you can choose to manipulate and store your data however you like; just remember to be consistent!

### Code time!

Now, to some coding! First, we have a little function that will advance the solution in time with a forward-time, centered-space scheme, and will monitor the center of the plate to tell us when it reaches $70{\rm C}$. Let's start by setting up our Python compute environment.

```python
import numpy
from matplotlib import pyplot
%matplotlib inline
```

```python
# Set the font family and size to use for Matplotlib figures.
pyplot.rcParams['font.family'] = 'serif'
pyplot.rcParams['font.size'] = 16
```

```python
def ftcs(T0, nt, dt, dx, dy, alpha):
    """
    Computes and returns the temperature distribution
    after a given number of time steps.
    Explicit integration using forward differencing
    in time and central differencing in space, with
    Neumann conditions (zero-gradient) on top and right
    boundaries and Dirichlet conditions on bottom and
    left boundaries.

    Parameters
    ----------
    T0 : numpy.ndarray
        The initial temperature distribution as a 2D array of floats.
    nt : integer
        Maximum number of time steps to compute.
    dt : float
        Time-step size.
    dx : float
        Grid spacing in the x direction.
    dy : float
        Grid spacing in the y direction.
    alpha : float
        Thermal diffusivity.

    Returns
    -------
    T : numpy.ndarray
        The temperature distribution as a 2D array of floats.
    """
    # Define some constants.
    sigma_x = alpha * dt / dx**2
    sigma_y = alpha * dt / dy**2
    # Integrate in time.
    T = T0.copy()
    ny, nx = T.shape
    I, J = int(nx / 2), int(ny / 2)  # indices of the center
    for n in range(nt):
        T[1:-1, 1:-1] = (T[1:-1, 1:-1] +
                         sigma_x * (T[1:-1, 2:] - 2.0 * T[1:-1, 1:-1] + T[1:-1, :-2]) +
                         sigma_y * (T[2:, 1:-1] - 2.0 * T[1:-1, 1:-1] + T[:-2, 1:-1]))
        # Apply Neumann conditions (zero-gradient).
        T[-1, :] = T[-2, :]
        T[:, -1] = T[:, -2]
        # Check if the center of the domain has reached T = 70C.
        if T[J, I] >= 70.0:
            break
    print('[time step {}] Center at T={:.2f} at t={:.2f} s'
          .format(n + 1, T[J, I], (n + 1) * dt))
    return T
```

See the [`break`](https://docs.python.org/3/tutorial/controlflow.html) statement? It exits the `for` loop at the closest time iteration when the plate reaches $70{\rm C}$.

In the code cell below, we define our initial conditions according to the problem set up, and choose the discretization parameters. We start with only 20 spatial steps in each coordinate direction and advance for 500 time steps. You should later experiments with these parameters at your leisure!

```python
# Set parameters.
Lx = 0.01  # length of the plate in the x direction
Ly = 0.01  # height of the plate in the y direction
nx = 21  # number of points in the x direction
ny = 21  # number of points in the y direction
dx = Lx / (nx - 1)  # grid spacing in the x direction
dy = Ly / (ny - 1)  # grid spacing in the y direction
alpha = 1e-4  # thermal diffusivity of the plate

# Define the locations along a gridline.
x = numpy.linspace(0.0, Lx, num=nx)
y = numpy.linspace(0.0, Ly, num=ny)

# Compute the initial temperature distribution.
Tb = 100.0  # temperature at the left and bottom boundaries
T0 = 20.0 * numpy.ones((ny, nx))
T0[0, :] = Tb
T0[:, 0] = Tb
```

We don't want our solution blowing up, so let's find a time step with $\frac{\alpha \Delta t}{\Delta x^2} = \frac{\alpha \Delta t}{\Delta y^2} = \frac{1}{4}$.

```python
# Set the time-step size based on CFL limit.
sigma = 0.25
dt = sigma * min(dx, dy)**2 / alpha  # time-step size
nt = 500  # number of time steps to compute

# Compute the temperature along the rod.
T = ftcs(T0, nt, dt, dx, dy, alpha)
```

    [time step 256] Center at T=70.02 at t=0.16 s

### Visualize the results

By now, you're no doubt *very* familiar with the `pyplot.plot` command.  It's great for line plots, scatter plots, etc., but what about when we have two spatial dimensions and another value (temperature) to display?

Are you thinking contour plot?  We're thinking contour plot.  Check out the documentation on [`pyplot.contourf`](http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.contour) (the 'f' denotes "filled" contours).

```python
# Plot the filled contour of the temperature.
pyplot.figure(figsize=(8.0, 5.0))
pyplot.xlabel('x [m]')
pyplot.ylabel('y [m]')
levels = numpy.linspace(20.0, 100.0, num=51)
contf = pyplot.contourf(x, y, T, levels=levels)
cbar = pyplot.colorbar(contf)
cbar.set_label('Temperature [C]')
pyplot.axis('scaled', adjustable='box');
```

That looks pretty cool! Note that in the call to `pyplot.contourf` you can specify the number of contour levels to display (we chose `51`).  Look at that visualization: does it make physical sense to you, considering that the upper and right sides of the chip are insulated, in our problem?

##### Dig deeper

In the problem we just demonstrated, the chip reaches a temperature of $70{\rm C}$ at a given time, but will it keep increasing? That spells trouble.

Imagine that you have a heat sink instead of an insulator acting on the upper and right sides. What should be the heat flux that the heat sink achieves there, so that the temperature does not exceed $70{\rm C}$ at the center of the chip?

---
###### The cell below loads the style of the notebook

```python
from IPython.core.display import HTML
css_file = '../../styles/numericalmoocstyle.css'
HTML(open(css_file, 'r').read())
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+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'>
<link href='https://fonts.googleapis.com/css?family=Source+Code+Pro' 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: 750px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1000px;
    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: 'Alegreya Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:600px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Nixie One', 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: 'Nixie One', 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: 'Nixie One', 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: 'Nixie One', 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: 'Nixie One', 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: "Source Code Pro";
    font-size: 90%;
}

.alert-box {
    padding:10px 10px 10px 36px;
    margin:5px;
}

.success {
    color:#666600;
    background:rgb(240,242,229);
}
</style>


## Evade Signal Classification with FGSM

This notebook shows the performance of a digital FGSM attack on an Automatic Modulation Classification neural network model.

### Assumptions
- The dataset wrangling has already been completed (and is provided here)
- The implementation of model training on this dataset has already been completed
- The plotting code has already been completed

### Components Recreated in Tutorial
- FGSM attack constrained by a power ratio

### See Also
The code in this tutorial is a stripped down version of the code in ``rfml.attack.fgsm`` that simplifies discussion.  Further detail can be provided by directly browsing the source files.

### Install the library code and dependencies

```python
# Install the library code
!pip install git+https://github.com/brysef/rfml.git@1.0.1
```

```python
# Plotting Includes
import matplotlib.pyplot as plt

# External Includes
import numpy as np

import torch
from torch.nn.functional import cross_entropy
from torch.utils.data import DataLoader, TensorDataset

# Internal Includes
from rfml.data import Dataset, Encoder
from rfml.data import build_dataset

from rfml.nbutils import plot_acc_vs_spr, plot_confusion

from rfml.nn.eval import compute_accuracy
from rfml.nn.eval.confusion import _confusion_matrix
from rfml.nn.F import energy
from rfml.nn.model import build_model
from rfml.nn.train import build_trainer, PrintingTrainingListener
```

### Configuration

```python
gpu = True       # Set to True to use a GPU for training
fig_dir = None   # Set to a file path if you'd like to save the plots generated
data_path = None # Set to a file path if you've downloaded RML2016.10A locally
```

## Train and Evaluate a Model on a Static Dataset

See ``module_2`` for a more extensive description of this procedure.
Here we just use the library for a quick implementation.
In practice, the model training would be separated from the attack, but, is juxtaposed with the attack here for simplicity as it is self-contained for demonstration.

```python
train, test, val, le = build_dataset("RML2016.10a", path=data_path)
# as_numpy returns x,y and x is shape BxCxIQxN
input_samples = val.as_numpy(le=le)[0].shape[3]
model = build_model(model_name="CNN", input_samples=input_samples, n_classes=len(le))
trainer = build_trainer(strategy="standard", max_epochs=3, gpu=gpu)
trainer.register_listener(PrintingTrainingListener())
trainer(model=model, training=train, validation=val, le=le)
acc = compute_accuracy(model=model, data=test, le=le)

print("Overall Testing Accuracy: {:.4f}".format(acc))
```

### Strip Down the Testing Data
Strip the data down to only the highest SNR (18 dB in RML2016.10a).  This ensures that the classification accuracy would have been close to the highest and gives the best evaluation of the attack because it separates the decreased accuracy from being causes by low SNR vs being caused by the attack.

```python
mask = test.df["SNR"] >= 18
dl = DataLoader(test.as_torch(le=le, mask=mask), shuffle=True, batch_size=512)
```

### Evading Signal Classification with Direct Access to the Classifier

Here, we're simply going to sweep the **intensity** of the attack, denoted in this tutorial as $E_s/E_p$ or signal-to-perturbation ratio represented in dB.

Recall that the equation for FGSM [Goodfellow et. al] is

\begin{equation}
    x^* = x + \text{sign}(\nabla_X \mathcal{L}(f(\theta, X), y_s))
\end{equation}

For the purposes of a clean implementation in software, we can split this equation into multiple steps.
First, we must compute the *signed gradient*.

\begin{equation}
    \text{"signed gradient"} = \text{sign}(\nabla_X \mathcal{L}(f(\theta, X), y_s))
\end{equation}

This is done by utilizing the model and PyTorch to backpropogate the gradient to the input.
Once the gradient is known, we can apply a simple sign operation.

Then, the signed gradient must be scaled to achieve an allowed intensity.
Traditionally, CV literature uses a norm to constrain the perturbation (e.g. $\left\lVert {x - x^*} \right\rVert_p < \epsilon$).
In the context of wireless communications, it makes more sense to constrain the power ratio of the perturbation to the underlying signal as the absolute values of either the perturbation or signal generally do not matter.
Recall that we've assumed $E_s$ is $1$ and therefore can scale the perturbation using the following equation [Flowers et. al, Sadeghi/Larson].

\begin{equation}
    p = \sqrt{\frac{10^{\frac{-E_s/E_p}{10}}}{2 \times \text{sps}}} \times \text{"signed gradient"}
\end{equation}

Because the RML2016.10A dataset is not properly normalized for all examples, especially AM-SSB, we have to normalize the input data before running the attack to have a known power -- this is the only way to ensure that the attack intensity is correct.

To complete the FGSM algorithm we simply need to add the perturbation to the original example.

\begin{equation}
    \text{"adversarial example"} = x + p
\end{equation}

#### Citations

##### Goodfellow et al

Goodfellow, I., Shlens, J., and Szegedy, C. (2015).  Explaining and harnessing adversarial examples. In Int. Conf. on Learning Representations.

##### Flowers et al

Flowers, B., Buehrer,  R.  M.,  and Headley,  W. C. (2019). Evaluating adversarial evasion attacks in the context of wireless communications. IEEE Transactions on Information Forensics and Security, pages 1–1.

##### Sadeghi/Larson

Sadeghi, M. and Larsson, E. G. (2018). Adversarial attacks on deep-learning based radio signal classification.IEEEWireless Commun. Letters, pages 1–1.

#### Helper methods for the attack

```python
def fgsm(x, y, input_size, net, spr, sps):
    p = compute_signed_gradient(x=x, y=y, input_size=input_size, net=net)
    p = scale_perturbation(sg=p, spr=spr, sps=sps)

    return x + p

def compute_signed_gradient(x, y, input_size, net):
    # Ensure that the gradient is tracked at the input, add some noise to avoid any
    # actual zeros in the signal (dithering), and then ensure its the proper shape
    x.requires_grad = True
    _x = _dither(x)

    # Put the inputs/outputs onto the most probable device that the model is currently
    # on -- this could fail if the model gets split amongst multiple devies, but, that
    # doesn't happen in this code.
    _x = _x.to(net.device)
    y = y.to(net.device)

    # Perform forward/backward pass to get the gradient at the input
    _y = net(_x)
    loss = cross_entropy(_y, y)
    loss.backward()

    # Take the sign of the gradient that can be scaled later
    ret = torch.sign(x.grad.data)

    return ret

def scale_perturbation(sg, spr, sps):
    if spr == np.inf:
        return sg * 0
    multiplier = pow(10, -spr / 10.0)
    multiplier = multiplier / (2 * sps)
    multiplier = pow(multiplier, 0.5)

    return sg * multiplier

def _dither(x):
    snr = 100
    voltage = pow(pow(10.0, -snr / 10.0), 0.5)

    noise = x.data.new(x.size()).normal_(0.0, voltage)
    return x + noise

def _normalize(x):
    power = energy(x)
    # Make the dimensions match because broadcasting is too magical to
    # understand in its entirety... essentially want to ensure that we
    # divide each channel of each example by the sqrt of the power of
    # that channel/example pair
    power = power.view([power.size()[0], power.size()[1], 1, 1])

    return x / torch.sqrt(power)

def _sanity_check(desired_spr, adv_x, x):
    signal_power = energy(x)
    perturbation_power = energy(adv_x - x)
    _spr = 10*torch.log10(signal_power / perturbation_power)
    _spr = _spr.detach().numpy().mean()
    if np.abs(_spr - desired_spr) > 0.5:
        raise RuntimeError("Calculated SPR and desired SPR does not match: "
                           "Desired SPR={:0.2f}dB, Calculated SPR={:0.2f}dB, "
                           "Signal Power={:0.2f}dB, Perturbation Power={:0.2f}dB".format(
                               desired_spr,
                               _spr,
                               10.0*np.log10(signal_power.detach().numpy().mean()),
                               10.0*np.log10(perturbation_power.detach().numpy().mean()))
                          )
```

#### Top Level control loop for the attack

```python
# Ensure that the model is in "evaluation" mode
# -- Therefore batch normalization is not computed and dropout is not performed
# -- Note: This is the cause of a lot of bugs
model.eval()

acc_vs_spr = list()
sprs = list()

for spr in np.linspace(50.0, 0.0, num=26):
    right = 0
    total = 0

    for x, y in dl:
        # Before crafting an attack on X, ensure that it is normalized to have a
        # specified power -- RML2016.10a fluctuates a bit, especially on AM-SSB,
        # which will cause the attack intensity to be incorrect.
        x = _normalize(x)
        adv_x = fgsm(x, y, spr=spr, input_size=input_samples, sps=1, net=model)

        # Ensure that we've accurately represented the attack power
        _sanity_check(desired_spr=spr, adv_x=adv_x, x=x)

        predictions = model.predict(adv_x)
        right += (predictions == y).sum().item()
        total += len(y)

    acc = float(right) / total
    acc_vs_spr.append(acc)
    sprs.append(spr)

fig = plot_acc_vs_spr(acc_vs_spr=acc_vs_spr,
                      spr=sprs,
                      title="Performance of a Digital FGSM Attack"
                     )
if fig_dir is not None:
    file_path = "{fig_dir}/direct_access_fgsm.pdf".format(fig_dir=fig_dir)
    print("Saving Figure -> {file_path}".format(file_path=file_path))
    fig.savefig(file_path, format="pdf", transparent=True)
plt.show()
```

#### Constructing a Confusion Matrix

The model breaks down in a way that generally intuitively makes sense to the expert eye with knowledge of the underlying signal formats.  The analog signals are confused for other analog signals, the PSK/QAM signals are confused for other PSK/QAMs with differing modulation orders, etc.  This behavior was examined more closely for targeted adversarial attacks in [Bair et al.] where it was found that the adversarial distance [Papernot et al.], or put more simply, the adversarial perturbation power required to "transform" 50% of the input examples from a source to target class, could be used in a spectral clustering algorithm to recover the relationships between the signal formats.

##### Bair et al.

Bair, S., Delvecchio, M., Flowers, B., Michaels, A. J., andHeadley, W. C. (2019). On the limitations of targeted adversarial evasion attacks against deep learning enabled modulation recognition. In ACM Workshop on Wireless Security and Machine Learning (WiseML 2019).

#### Papernot et al.

Papernot,  N.,  McDaniel,  P.,  Jha,  S.,  Fredrikson,  M.,Celik,  Z.  B.,  and  Swami,  A.  (2016).   The  limitations  of  deep  learningin  adversarial  settings.   InIEEE  European  Symposium  on  Security  andPrivacy (EuroS&P), pages 372–387. IEEE.

```python
spr = 10  # dB

predictions = list()
labels = list()
for x, y in dl:
    # Before crafting an attack on X, ensure that it is normalized to have a
    # specified power -- RML2016.10a fluctuates a bit, especially on AM-SSB,
    # which will cause the attack intensity to be incorrect.
    x = _normalize(x)
    adv_x = fgsm(x, y, spr=spr, input_size=input_samples, sps=1, net=model)

    # Ensure that we've accurately represented the attack power
    _sanity_check(desired_spr=spr, adv_x=adv_x, x=x)

    _predictions = model.predict(adv_x)
    predictions.extend(_predictions.detach().numpy())
    labels.extend(y)

cmn = _confusion_matrix(predictions=predictions, labels=labels, le=le)

title = "Confusion Matrix with a {spr} dB FGSM Attack".format(spr=spr)
fig = plot_confusion(cm=cmn, labels=le.labels, title=title)
if fig_dir is not None:
    file_path = "{fig_dir}/fgsm_confusion_matrix.pdf"
    print("Saving Figure -> {file_path}".format(file_path=file_path))
    fig.savefig(file_path, format="pdf", transparent=True)
plt.show()
```


# A 2D version of the Robot Parking problem

Here is a 2D version of the Robot parking problem. Credits for this implemention to  [Thijs van de Laar](https://biaslab.github.io/member/thijs) and [Burak Ergul](https://biaslab.github.io/member/burak).

```julia
using Pkg;Pkg.activate("../probprog/workspace/");Pkg.instantiate()
```

     Activating environment at `~/biaslab/repos/BMLIP/lessons/notebooks/probprog/workspace/Project.toml`

```julia
using Random
Random.seed!(87613) # Set random seed

using LinearAlgebra
using PyPlot
using ForneyLab

include("environment_2d.jl") # Include environmental dynamics
include("helpers_2d.jl") # Include helper functions for plotting
include("agent_2d.jl") # Load agent's internal beliefs over external dynamics
;
```

### Build model

We define the state of the robot (at time $k$) as the current orientation and Cartesian coordinates:
\begin{align}
    s_k = (ϕ_k, x_k, y_k)^{T}\,.
\end{align}

The control consists of a change in orientation and subsequent travel distance:
\begin{align}
    u_k = (\Delta\phi_k, r_k)^{T}\,.
\end{align}

The model factorization did not change:
\begin{align}
    p_t(o, s, u) &\propto p(s_{t-1}) \prod_{k=t}^{t+T} p(o_k | s_k)\, p(s_k | s_{k-1}, u_k)\, p(u_k)\, \tilde{p}(o_k)\,.
\end{align}

The specific model assumptions however now include a non-linear transition function $g$:
\begin{align}
    p(s_{t-1}) &= \mathcal{N}(s_{t-1} | m_{s, t-t}, V_{s, t-1})\\
    p(s_k | s_{k-1}, u_k) &= \mathcal{N}(s_k | g(s_{k-1}, u_k), \Gamma^{-1})\\
    p(o_k | s_k) &= \mathcal{N}(o_k | s_k, \Phi^{-1})\\
    p(u_k) &= \mathcal{N}(u_k | 0, \Upsilon) \text{, for } k>t\\
    \tilde{p}(o_k) &= \mathcal{N}(o_k | 0, \Sigma) \text{, for } k>t\\
    p(u_t) &= \delta(u_t - \hat{u}_t)\\
    \tilde{p}(o_t) &= \delta(o_t - \hat{o}_t)\,.
\end{align}

```julia
# Internal model perameters
Gamma   = 100.0*diageye(3) # Transition precision
Phi     = 10.0*diageye(3) # Observation precision
Upsilon = Diagonal([0.25*pi, 1.0].^2) # Control prior variance
Sigma   = Diagonal([1.0, 30.0, 30.0]) # Goal prior variance
;
```

```julia
T = 10 # Lookahead

# Build internal model
fg = FactorGraph()

o   = Vector{Variable}(undef, T) # Observed states
s_g = Vector{Variable}(undef, T) # Noiseless internal states
s   = Vector{Variable}(undef, T) # Noisy internal states
u   = Vector{Variable}(undef, T) # Control states

@RV s_t_min ~ GaussianMeanVariance(placeholder(:m_s_t_min, dims=(3,)),
                                   placeholder(:V_s_t_min, dims=(3,3))) # Prior internal state
u_t = placeholder(:u_t, dims=(2,))
@RV u[1]   ~ GaussianMeanVariance(u_t, tiny*diageye(2))
@RV s_g[1] ~ Nonlinear{Unscented}(s_t_min, u[1], g=g,g_inv=[g_inv_in1,g_inv_in2])
@RV s[1]   ~ GaussianMeanPrecision(s_g[1], Gamma)
@RV o[1]   ~ GaussianMeanPrecision(s[1], Phi)
placeholder(o[1], :o_t, dims=(3,))

s_k_min = s[1]
for k=2:T
    @RV u[k]   ~ GaussianMeanVariance(zeros(2), Upsilon)
    @RV s_g[k] ~ Nonlinear{Unscented}(s_k_min, u[k], g=g, g_inv=[g_inv_in1,g_inv_in2]) # Noiseless transition model
    @RV s[k]   ~ GaussianMeanPrecision(s_g[k], Gamma) # State transition model including noise
    @RV o[k]   ~ GaussianMeanPrecision(s[k], Phi) # Observation model
    GaussianMeanVariance(o[k],
                         placeholder(:m_o, var_id=:m_o_*k, index=k-1, dims=(3,)),
                         placeholder(:V_o, var_id=:V_o_*k, index=k-1, dims=(3,3))) # Goal prior
    s_k_min = s[k]
end
;
```

### Infer algorithm

```julia
# Schedule message passing algorithm
q = PosteriorFactorization(fg)
algo = messagePassingAlgorithm(u[2]) # Infer internal states
source = algorithmSourceCode(algo)
eval(Meta.parse(source)) # Loads the step!() function for inference
inspectSnippet(source) # Inspect a snippet of the algorithm code
;
```

    function step!(data::Dict, marginals::Dict=Dict(), messages::Vector{Message}=Array{Message}(undef, 59))

    messages[1] = ruleSPGaussianMeanVarianceOutNPP(nothing, Message(Multivariate, PointMass, m=[0.0, 0.0]), Message(MatrixVariate, PointMass, m=Diagonal([0.6168502750680849, 1.0])))
    messages[2] = ruleSPGaussianMeanVarianceOutNPP(nothing, Message(Multivariate, PointMass, m=data[:m_s_t_min]), Message(MatrixVariate, PointMass, m=data[:V_s_t_min]))
    messages[3] = ruleSPGaussianMeanVarianceOutNPP(nothing, Message(Multivariate, PointMass, m=data[:u_t]), Message(MatrixVariate, PointMass, m=Diagonal([1.0e-12, 1.0e-12])))

    ...

    marginals[:u_2] = messages[1].dist * messages[59].dist

    return marginals

    end

### Execute algorithm

```julia
# Initial state
ϕ_0 = pi # Initial orientation
x_0 = 2.0 # Initial coordinates
y_0 = 3.0
;
```

```julia
N = 20 # Total simulation time

(execute, observe)  = initializeWorld() # Let there be a world
(infer, act, slide) = initializeAgent() # Let there be an agent

# Step through action-perception loop
u_hat = Vector{Vector{Float64}}(undef, N) # Actions
o_hat = Vector{Vector{Float64}}(undef, N) # Observations
for t=1:N
    u_hat[t] = act() # Evoke an action from the agent
               execute(u_hat[t]) # The action influences hidden external states
    o_hat[t] = observe() # Observe the current environmental outcome (update p)
               infer(u_hat[t], o_hat[t]) # Infer beliefs from current model state (update q)
               slide() # Prepare for next iteration
end
;
```

### and plot the position track

```julia
# Plot active inference results
plotTrajectory(u_hat, o_hat)
;
```

```julia

```