Update for PyTorch 0.4:

Earlier versions used ``Variable`` to wrap tensors with different properties. Since version 0.4, ``Variable`` is merged with ``tensor``, in other words, ``Variable`` is NOT needed anymore. The flag ``require_grad`` can be directly set in ``tensor``. Accordingly, this post is also updated.

Having heard about the announcement about Theano from Bengio lab , as a Theano user, I am happy and sad to see the fading of the old hero, caused by many raising stars. Sad to see it is too old to compete with its industrial competitors, and happy to have so many excellent deep learning frameworks to choose from. Recently I started translating some of my old codes to Pytorch and have been really impressed by its dynamic nature and clearness. But at the very beginning, I was very confused by the ``backward()`` function when reading the tutorials and documentations. This motivated me to write this post in order for other Pytorch beginners to ease the understanding a bit. And I'll assume that you already know the [``autograd``](http://pytorch.org/docs/master/autograd.html) module and what a [``Variable``](http://pytorch.org/docs/0.1.12/_modules/torch/autograd/variable.html) is, but are a little confused by definition of ``backward()``.

First let's recall the gradient computing under mathematical notions. For an independent variable $x$ (scalar or vector), the whatever operation on $x$ is $y = f(x)$. Then the gradient of $y$ w.r.t $x_i$s is
$$\begin{align}\nabla y&=\begin{bmatrix}
\frac{\partial y}{\partial x_1}\\
\frac{\partial y}{\partial x_2}\\
\vdots
\end{bmatrix}
\end{align}.
$$
Then for a specific point of $x=[X_1, X_2, \dots]$, we'll get the gradient of $y$ on that point as a vector. With these notions in mind, the following things are a bit confusing at the beginning

1. Mathematically, we would say "The gradients of a function w.r.t. the independent variables", whereas the ``.grad`` is attached to the leaf ``tensor``s. In Theano and Tensorflow, the computed gradients are stored separately in a variable. But with a moment of adjustment, it is fairly easy to buy that. In Pytorch it is also possible to get the ``.grad`` for intermediate ``Variable``s with help of ``register_hook`` function

2. The parameter ``grad_variables`` of the function ``torch.autograd.backward(variables, grad_tensors=None, retain_graph=None, create_graph=None, retain_variables=None, grad_variables=None)`` is not straightforward for knowing its functionality. **note that ``grad_variables`` is deprecated, use ``grad_tensors`` instead.

3. What is ``retain_graph`` doing?

```python
import torch as T
import torch.autograd
import numpy as np
```

### Simplicity of using ``backward()``

```python
'''
Define a scalar variable, set requires_grad to be true to add it to backward path for computing gradients

It is actually very simple to use backward()

first define the computation graph, then call backward()
'''

x = T.randn(1, 1, requires_grad=True) #x is a leaf created by user, thus grad_fn is none
print('x', x)
#define an operation on x
y = 2 * x
print('y', y)
#define one more operation to check the chain rule
z = y ** 3
print('z', z)

```

    x tensor([[-0.6955]], requires_grad=True)
    y tensor([[-1.3911]], grad_fn=<MulBackward>)
    z tensor([[-2.6918]], grad_fn=<PowBackward0>)

The simple operations defined a forward path $z=(2x)^3$, $z$ will be the final output ``tensor`` we would like to compute gradient: $dz=24x^2dx$, which will be passed to the parameter ``tensors`` in ``backward()`` function.

```python
#yes, it is just as simple as this to compute gradients:
z.backward()
```

```python
print('z gradient:', z.grad)
print('y gradient:', y.grad)
print('x gradient:', x.grad, 'Requires gradient?', x.grad.requires_grad) # note that x.grad is also a tensor
```

    z gradient None
    y gradient None
    x gradient tensor([[11.6105]]) Requires gradient? False

The gradients of both $y$ and $z$ are None, since the function returns the gradient for the leaves, which is $x$ in this case. At the very beginning, I was assuming something like this:

``x gradient: None
y gradient: None
z gradient: tensor([11.6105])``,
since the gradient is calculated for the final output $z$.

With a blink of thinking, we could figure out it would be practically chaos if $x$ is a multi-dimensional vector. ``x.grad`` should be interpreted as the gradient of $z$ at $x$.

### How do we use ``grad_tensors``?

``grad_tensors`` should be a list of torch tensors. In default case, the ``backward()`` is applied to scalar-valued function, the default value of ``grad_tensors`` is thus ``torch.FloatTensor([0])``. But why is that? What if we put some other values to it?

Keep the same forward path, then do ``backward`` by only setting ``retain_graph`` as ``True``.

```python
x = T.randn(1, 1, requires_grad=True) #x is a leaf created by user, thus grad_fn is none
print('x', x)
#define an operation on x
y = 2 * x
#define one more operation to check the chain rule
z = y ** 3

z.backward(retain_graph=True)
print('Keeping the default value of grad_tensors gives')
print('z gradient:', z.grad)
print('y gradient:', y.grad)
print('x gradient:', x.grad)
```

    x tensor([[-0.7207]], requires_grad=True)
    Keeping the default value of grad_tensors gives
    z gradient: None
    y gradient: None
    x gradient: tensor([[12.4668]])

Testing the explicit default value, which should give the same result. For the same graph which is retained, DO NOT forget to zero the gradient before recalculate the gradients.

```python
x.grad.data.zero_()
z.backward(T.Tensor([[1]]), retain_graph=True)
print('Set grad_tensors to 1 gives')
print('z gradient:', z.grad)
print('y gradient:', y.grad)
print('x gradient:', x.grad)
```

    Set grad_tensors to 0 gives
    z gradient: None
    y gradient: None
    x gradient: tensor([[12.4668]])

Then what about other values, let's try 0.1 and 0.5.

```python
x.grad.data.zero_()
z.backward(T.Tensor([[0.1]]), retain_graph=True)
print('Set grad_tensors to 0.1 gives')
print('z gradient:', z.grad)
print('y gradient:', y.grad)
print('x gradient:', x.grad)
```

    Set grad_tensors to 0.1 gives
    z gradient: None
    y gradient: None
    x gradient: tensor([[1.2467]])

```python
x.grad.data.zero_()
z.backward(T.FloatTensor([[0.5]]), retain_graph=True)
print('Modifying the default value of grad_variables to 0.1 gives')
print('z gradient', z.grad)
print('y gradient', y.grad)
print('x gradient', x.grad)
```

    Modifying the default value of grad_variables to 0.1 gives
    z gradient None
    y gradient None
    x gradient tensor([[6.2334]])

It looks like the elements of ``grad_tensors`` act as scaling factors. Now let's set $x$ to be a $2\times 2$matrix. Note that $z$ will also be a matrix. (Always use the latest version, ``backward`` had been improved a lot from earlier version, becoming much easier to understand.)

```python
x = T.randn(2, 2, requires_grad=True) #x is a leaf created by user, thus grad_fn is none
print('x', x)
#define an operation on x
y = 2 * x
#define one more operation to check the chain rule
z = y ** 3

print('z shape:', z.size())
z.backward(T.FloatTensor([[1, 1], [1, 1]]), retain_graph=True)
print('x gradient for its all elements:\n', x.grad)

print()
x.grad.data.zero_() #the gradient for x will be accumulated, it needs to be cleared.
z.backward(T.FloatTensor([[0, 1], [0, 1]]), retain_graph=True)
print('x gradient for the second column:\n', x.grad)

print()
x.grad.data.zero_()
z.backward(T.FloatTensor([[1, 1], [0, 0]]), retain_graph=True)
print('x gradient for the first row:\n', x.grad)

```

    x tensor([[-2.5212,  1.2730],
            [ 0.0366, -0.0750]], requires_grad=True)
    z shape: torch.Size([2, 2])
    x gradient for its all elements:
     tensor([[152.5527,  38.8946],
            [  0.0322,   0.1349]])

    x gradient for the second column:
     tensor([[ 0.0000, 38.8946],
            [ 0.0000,  0.1349]])

    x gradient for the first row:
     tensor([[152.5527,  38.8946],
            [  0.0000,   0.0000]])

 We can clearly see the gradients of $z$ are computed w.r.t to each dimension of $x$, because the operations are all element-wise.

Then what if we render the output one-dimensional (scalar) while $x$ is two-dimensional. This is a real simplified scenario of neural networks.
$$f(x)=\frac{1}{n}\sum_i^n(2x_i)^3$$
$$f'(x)=\frac{1}{n}\sum_i^n24x_i^2$$

```python
x = T.randn(2, 2, requires_grad=True) #x is a leaf created by user, thus grad_fn is none
print('x', x)
#define an operation on x
y = 2 * x
#print('y', y)
#define one more operation to check the chain rule
z = y ** 3
out = z.mean()
print('out', out)
out.backward(retain_graph=True)
print('x gradient:\n', x.grad)

```

    x tensor([[ 1.8528,  0.2083],
            [-1.5296,  0.3136]], requires_grad=True)
    out tensor(5.6434, grad_fn=<MeanBackward1>)
    x gradient:
     tensor([[20.5970,  0.2604],
            [14.0375,  0.5903]])

We will get complaints if the ``grad_tensors`` is specified for the scalar function.

```python
x.grad.data.zero_()
out.backward(T.FloatTensor([[1, 1], [1, 1]]), retain_graph=True)
print('x gradient', x.grad)
```

### What is ``retain_graph`` doing?

When training a model, the graph will be re-generated for each iteration. Therefore each iteration will consume the graph if the ``retain_graph`` is false, in order to keep the graph, we need to set it be true.

```python
x = T.randn(2, 2, requires_grad=True) #x is a leaf created by user, thus grad_fn is none
print('x', x)
#define an operation on x
y = 2 * x
#print('y', y)
#define one more operation to check the chain rule
z = y ** 3
out = z.mean()
print('out', out)
out.backward()  #without setting retain_graph to be true, it is alright for first time of backward.
print('x gradient', x.grad)

x.grad.data.zero_()
out.backward() #Now we get complaint saying that no graph is available for tracing back.
print('x gradient', x.grad)
```

### Wrap up

1. The ``backward()`` function made differentiation very simple
2. For non-scalar ``tensor``, we need to specify ``grad_tensors``
3. If you need to backward() twice on a graph or subgraph, you will need to set ``retain_graph`` to be true.
4. Note that grad will accumulate from excuting the graph multiple times.

```python

```

_drafts/understanding backward() in Pytorch.ipynb


# The PO algorithm, a python illustration

This notebook is an illustration of the Perturbation-Optimisation algorithm (PO). The PO algorithm simulates large non-centered non-stationary Gaussian samples when the precision $Q \in R^{N \times N}$ (inverse covariance) writes
\begin{equation}
Q = \sum_{k=1}^K H_k^t R_k^{-1} H_k
\end{equation}
often found in inverse problems (deconvolution, super-resolution, tomography, ...). For doing so, the algorithm supposes that

1. you can easily simulate centered gaussian law with covariance $R$ (when $R$ is diagonal for instance)
2. you can run a quadratic optimization algorithm (a conjugate gradient for instance) or a linear solver.

We suppose that the reader has sufficient knowledge of what is deconvolution, inverse problems or the problem of stochastic simulation of large and correlated Gaussian law.

This notebook illustrates this algorithm on a deconvolution problem with python and the numpy/scipy stack only (and matplotlib for display). No extra dependencies are required. **We do not go into details and many aspects can be extended, detailed, optimized, etc. This is postponed to other work.**

For more details on linear solver, optimization algorithm and MCMC algorithm we refer the reader to

- *Iterative Methods for Sparse Linear Systems* by Yousef Saad (free)
- *Numerical Optimization* by Stephen J. Wright and J. Nocedal
- *Monte Carlo Statistical Methods* by Christian Robert, Christian and George Casella
- *Probabilistic Programming & Bayesian Methods for Hackers* by Cam Davidson-Pilon (free)

This notebook has been written by *F. Orieux*, assistant professor at University Paris-Saclay, associated with the Laboratory of Signal and System (L2S, Univ. Paris-Saclay - CNRS - CentraleSupélec), with help from Olivier Féron (EDF Lab) and Jean-François Giovannelli (IMS - Univ. Bordeaux). If you use any part of this work, please mention it. Static html and pdf version are available in the same [github](https://github.com/orieux/notebooks) repository.

## Publication

The PO algorithm has been published in

[1] François Orieux, Olivier Féron and Jean-François Giovannelli, **Sampling High-Dimensional Gaussian Distributions for General Linear Inverse Problems**, IEEE Signal Processing Letters, may, 2012. [[doi](https://doi.org/10.1109/LSP.2012.2189104)]
```
@Article{orieux2012,
    title = {Sampling high-dimensional Gaussian distributions
             for general linear inverse problems},
    author = {François Orieux and Olivier Féron and Jean-François Giovannelli},
    journaltitle = {IEEE Signal Processing Letters},
    year = {2012},
    date = {05-2012},
    number = {5},
    pages = {251--254},
    volume = {19},
    doi = {10.1109/LSP.2012.2189104},
    keywords = {Bayesian, high-dimensional sampling, inverse problem,
                stochastic sampling, unsupervised}
}
```
It has been however published many times in the literature with different presentation, variation or extention. We can mention

- [2] Lalanne, P.; Prévost, D. & Chavel, P. - *Stochastic artificial retinas: algorithm, optoelectronic circuits, and implementation* - Applied Optics, 2001, 40
- [3] Tan, X.; Li, J. & Stoica, P. - *Efficient sparse Bayesian learning via Gibbs sampling* - ICASSP, IEEE, 2010.
- [4] Bardsley *et. al*, *Randomize-Then-Optimize : A Method for Sampling from Posterior Distributions in Nonlinear Inverse Problems*, 2017
- [5] Gilavert, C.; Moussaoui, S. & Idier, J. - *Efficient Gaussian Sampling for Solving Large-Scale Inverse Problems Using MCMC* - IEEE Transactions on Signal Processing, 2015.

Nevertheless, this notebook illustrates the PO algorithm as described in [1] but on deconvolution, a simpler to code problem, instead of super-resolution.

## Deconvolution

The deconvolution imaging problem suppose a blurred image $y \in R^M$ from which you want a deblurred version $\hat{x} \in R^N$. Images have not the same size and we will not suppose the periodic hypothesis. Fourier transform is therefor not permitted (this is not exactly true and another notebook will explain that more in detail). Then, the model is linear and not stationary (because of edge) and writes
$$y = Hx + n$$
where $H$ is a convolution matrix $\in R^{M\times N}$ and $n \in R^M$ an unknown noise. It is known that deconvolution is an ill-posed inverse problem, naive inversion like minimization of least-square provides an unstable solution, and noise is amplified with unusable results. A classical well-known solution is the introduction of additional information through model, also called prior information.

We suppose here

- a centered white Gaussian prior model for the noise $n$ of precision $\gamma_n I$ and
- a centered circulant Gaussian prior model for $x$ of precision $\gamma_xD^tD$, where $D$ is a convolution operator with laplacian impulsionnal response (second-order differences in line and column, or equivalently a high-pass filter).

This leads to the posterior law
$$p(x \mid y) \propto \exp \left( -\frac{\gamma_n}{2} \|y - Hx \|_2^2 \right) \exp \left( -\frac{\gamma_x}{2} \|Dx \|_2^2 \right).$$
This posterior law is a gaussian law with mean
$$ \mu = \Sigma H^t y$$
and covariance
$$\Sigma^{-1} = Q = \gamma_n H^tH + \gamma_x D^tD$$
that clearly fall under the condition of PO with $H_1 = H$, $R_1 = I$, $H_2 = D$ and $R_2 = I$.

The direct inversion of $Q$ is not feasible but the mean $\mu$ can be computed thanks to iterative algorithm like conjugate gradient that use $Q$ instead of $\Sigma$.

## Implementation

We use numpy to illustrate the algorithm with the following modules.

- `partial` simplify the definition of single argument function from multiple argument functions.
- `scipy.misc` is used to load the true image.
- `convolve2d` (abbreviated as conv2) is the convolution function with zero padding.
- `cg` is the conjugate gradient algorithm and `LinearOperator` a python object that mimic linear operator needed by `cg`.

```python
from functools import partial

import numpy as np
import numpy.random as npr

# For 'ascent' image
import scipy.misc
# The non stationnary convolution operator
from scipy.signal import convolve2d as conv2

# The conjugate gradient solver
from scipy.sparse.linalg import cg, LinearOperator

# For plotting
import matplotlib.pyplot as plt
```

We take the scipy `ascent` image as a true image with a square impulsionnal response.

```python
true = scipy.misc.ascent()[::2, ::2]  # Decimation to reduce size and time computation
blur_size = 5
ir = np.ones((blur_size, blur_size)) / blur_size**2

# The Laplacian IR
reg = np.array([[0, -1, 0],
                [-1, 4, -1],
                [0, -1, 0]], dtype=np.float32)

im = plt.imshow(true)
plt.axis('off')
title = plt.title("Ground truth")
```

The noise precision is set to 10 ($\sigma \approx 0.3$).

```python
true_gamma_n = 10
```

## Linear restoration

At this point the MAP estimator is
$$\hat{x}_{MAP} = \text{arg max}_{x} p(x \mid y) = \text{arg min}_{x} \|y - Hx \|_2^2 + \lambda \|Dx \|_2^2$$
with $\lambda = \gamma_x / \gamma_n$ that is the classical regularized least square linear estimator, which is also the mean of the gaussian law above
$$\hat{x}_{MAP} = \mu = (H^tH + \lambda D^t D)^{-1} H^t y.$$
Since the inversion of the matrix is not feasible, a the linear solver of the system $Qx = b$, with $Q = H^tH + \lambda D^t D$ and $b = H^t y$, is required. The solver needs a way to apply $Q$ and therefor $H$ and $D$, as well as their adjoint.

In the case of convolution without periodic hypothesis, the operator corresponds to the `convolve2d` function (`conv2` in Matlab) with `valid` and `full` parameter.

```python
def forward(ir, image):
    """Apply H operator"""
    return conv2(image, ir, 'valid')

def backward(ir, image):
    """Apply H^t operator"""
    return conv2(image, np.fliplr(np.flipud(ir)), 'full')

def forward_backward(ir, image):
    """Apply H^tH operator"""
    return backward(ir, forward(ir, image))

# Abreviation
H = partial(forward, ir)
Ht = partial(backward, ir)
HtH = partial(forward_backward, ir)

# Simulate noisy data
data = H(true)
data = data + np.random.standard_normal(data.shape) / np.sqrt(true_gamma_n)

# Check size
print("True shape:", true.shape)
print("Data shape:", data.shape)
print("Transpose shape:", backward(data, ir).shape)

im = plt.imshow(data)
plt.axis('off')
title = plt.title('Data')
```

We must do the same thing for the regularization.

```python
def reg_forward(reg, image):
    """Apply D operator"""
    return conv2(image, reg, 'valid')

def reg_backward(reg, image):
    """Apply D^t operator"""
    return conv2(image, np.fliplr(np.flipud(reg)), 'full')

def reg_forward_bacward(reg, image):
    """Apply D^tD operator"""
    return reg_backward(reg, reg_forward(reg, image))

# Abreviation
D = partial(reg_forward, reg)
Dt = partial(reg_backward, reg)
DtD = partial(reg_forward_bacward, reg)
```

The mean (that corresponds to the least square solution) can be computed with the conjugate gradient available in scipy. The API of this solver needs a `LinearOperator` python instance that applies the $Q$ operator. This API is not well suited for image since it must work on vectorized variables so we embed the code within unvectorization and vectorization operation.

```python
def hess(HtH, DtD, hyper, image):
    """Apply Q = H^tH + lambda * D^tD on image"""
    return HtH(image) + hyper * DtD(image)

def mv(HtH, DtD, hyper, shape, image):
    """Apply Q on vectorized image"""
    # vector to image, apply Q = H^tH + lambda * D^tD, then vectorize
    return hess(HtH, DtD, hyper, image.reshape(shape)).reshape((-1, 1))

# The Q operator on vectorized unkown
# The matvec API is limited to function of one parameter so we use functools.partial
Q = LinearOperator(dtype=np.float32,
                   shape=(true.size, true.size),
                   matvec=partial(mv, HtH, DtD, 1e-10, true.shape))
# Vectorized $H^t y$
b = Ht(data).reshape((-1, 1))
```

The solver can be run now (with $b = H^ty$ as init).

```python
sol, _ = cg(Q, b, x0=b)
sol = sol.reshape(true.shape)
```

```python
fig, axes = plt.subplots(1, 3, figsize=(10, 10))
im = axes[0].imshow(true[100:300, 100:300], vmin=true.min(), vmax=true.max())
title = axes[0].set_title('Ground truth')
axes[0].axis('off')
im = axes[1].imshow(data[100:300, 100:300], vmin=true.min(), vmax=true.max())
title = axes[1].set_title('Data')
axes[1].axis('off')
im = axes[2].imshow(sol[100:300, 100:300], vmin=true.min(), vmax=true.max())
title = axes[2].set_title('RLS')
axes[2].axis('off')
plt.tight_layout()
```

Two questions arise.

1. In the figure above, the deconvolution seems under regularized. We may want to estimate the hyperparameter value $\lambda$.
2. We have no uncertainty about the estimation.

The Bayesian approach and algorithms are a possibility (among others) to answer these questions.

## Bayesian restoration

With the Bayesian approach we can state the problem as inference on the extended posterior law
$$p(x, \gamma_n, \gamma_n \mid y) \propto \gamma_n^{\frac{M}{2} - 1} \gamma_x^{\frac{N-1}{2} - 1}\exp \left( -\frac{\gamma_n}{2} \|y - Hx \|_2^2 \right) \exp \left( -\frac{\gamma_x}{2} \|Dx \|_2^2 \right)$$
and to choose the posterior mean as estimator
$$\hat{x}, \hat{\gamma_n}, \hat{\gamma_x} = \int [x, \gamma_n, \gamma_x]\ p(x, \gamma_n, \gamma_x \mid y)\ d x\ d \gamma_n\ d \gamma_x.$$

One way to compute this integral is stochastic sampling with MCMC and more specifically the Gibbs sampler that leads to the following algorithm.

## The Gibbs sampler algorithm

Initialize $k \gets 0$, $\gamma_n^{(0)}$ and $\gamma_x^{(0)}$ (for instance at $1$). Then
1. draw $x^{(k)} \sim p\left(x \mid \gamma_n^{(k)}, \gamma_x^{(k)}, y\right)$
2. draw $\gamma_n^{(k+1)} \sim p\left(\gamma_n \mid x^{(k)}, y\right)$
3. draw $\gamma_x^{(k+1)} \sim p\left(\gamma_x \mid x^{(k)}\right)$
4. $k \gets k + 1$ and return to step 1 except if stopping condition is reached.

Finally we estimate the posterior mean with the big number law from the last $P$ samples
$$\hat x = \frac{1}{P} \sum_{i=k-1-P}^{k-1} x^{(i)}.$$

With the above model, $p(\gamma_n \mid x, y)$ and $p(\gamma_x \mid x^{(k)})$ are Gamma law that can be easily simulated with numpy toolbox.

However, the conditional law $p(x \mid \gamma_n, \gamma_x, y)$ is a high dimensional Gaussian law with non-stationary correlation. Classical approaches like Cholesky factorization are unfeasible because of the size of the problem. However, the PO algorithm is

## The PO sampler

The conditionnal law $p(x \mid \gamma_n, \gamma_x, y)$ is
$$p(x \mid \gamma_n, \gamma_x, y) \propto \exp \left( -\frac{\gamma_n}{2} \|y - Hx \|_2^2 \right) \exp \left( -\frac{\gamma_x}{2} \|Dx \|_2^2 \right).$$
This posterior law is a gaussian law with mean
$$ \mu = \gamma_n\Sigma H^t y$$
and covariance
$$\Sigma^{-1} = Q = \gamma_n H^tH + \gamma_x D^tD.$$

The PO sampler consiste, with the above model, of perturbation of the mean
1. $\tilde{y} \gets y + \epsilon_n$ with $\epsilon_n \sim \mathcal{N}\left(0, \gamma_n^{-1} I\right)$
2. $\tilde{x} \gets \epsilon_x$ with $\epsilon_x \sim \mathcal{N}\left(0, \gamma_x^{-1} I\right)$
and to solve the following optimization problem
$$x^{(k)} = \text{arg min}_x\ \frac{\gamma_n}{2} \|\tilde{y} - Hx \|_2^2 + \frac{\gamma_x}{2} \|D(x - \tilde{x})\|_2^2$$

In conclusion, in comparison to supervised deconvolution, the changes are just
1. simulate the hyperparameter according to the model (scalar Gamma law here with the standard toolbox)
2. simulate perturbation of the previous criterions and optimize it as before with a linear solver like conjugate gradient.

## Implementation

Almost everything is already in place except the $Q$ operator that must be slightly modified to take two hyperparameters.

```python
def mv(HtH, DtD, gamma_n, gamma_x, image):
    # vector to image
    image = image.reshape(true.shape)

    # Apply H^tH + mu * D^tD
    out = gamma_n * HtH(image) + gamma_x * DtD(image)

    # then vectorize
    return out.reshape((-1, 1))
```

We can now define our Gibbs sampler with `for` loops (I set the maximum number of iteration for the CG to 50 because it seems sufficient. The RJ-PO algorithm allows us to automatically tunes this value).

```python
gamma_n, gamma_x = 1, 1
gamma_n_chain, gamma_x_chain = [gamma_n], [gamma_x]

burnin, max_iter, acc = 20, 100, 0

data_t = Ht(data)
mean = np.zeros(data_t.shape)
sample = np.zeros(data_t.shape)
cum2 = np.zeros(data_t.shape)
pshape = tuple(s - 2 for s in data_t.shape)  # depends on the regularization IR

for iteration in range(max_iter):
    # Perturbation
    data_tilde = data + npr.standard_normal(data.shape) / np.sqrt(gamma_n)
    x_tilde = npr.standard_normal(pshape) / np.sqrt(gamma_x)

    b = gamma_n * Ht(data_tilde) + gamma_x * Dt(x_tilde)
    Q = LinearOperator(dtype=np.float32, shape=(true.size, true.size),
                       matvec=partial(mv, HtH, DtD, gamma_n, gamma_x))

    # Optimization
    opt, _ = cg(Q, b.reshape((-1, 1)), x0=sample.reshape((-1, 1)), maxiter=50)
    sample = opt.reshape(true.shape)

    # Hyperparameter simulation
    gamma_n = npr.gamma(data.size / 2, 2 / np.sum(abs(data - H(sample))**2))
    gamma_x = npr.gamma((true.size - 1) / 2, 2 / np.sum(abs(D(sample))**2))

    # Keep in memory all the values
    gamma_n_chain.append(gamma_n)
    gamma_x_chain.append(gamma_x)

    # Keep in memory the full set of image sample can take a lot of space
    # We accumulate instead the samples to compute the mean and variance instead
    if iteration >= burnin:
        mean = mean + sample
        cum2 = cum2 + sample**2
        acc += 1

mean = mean / acc
std = np.sqrt(cum2 / acc - mean**2)
```

```python
fig, axes = plt.subplots(2, 2, figsize=(10, 10))

axes[0][0].imshow(true[100:300, 100:300], vmin=true.min(), vmax=true.max())
axes[0][0].set_title('Ground truth $x$')
axes[0][0].axis('off')

axes[0][1].imshow(data[100:300, 100:300], vmin=true.min(), vmax=true.max())
axes[0][1].set_title('Data $y$')
axes[0][1].axis('off')

axes[1][0].imshow(mean[100:300, 100:300], vmin=true.min(), vmax=true.max())
axes[1][0].set_title('$\hat{x}$')
axes[1][0].axis('off')

im = axes[1][1].imshow(std[100:300, 100:300])
axes[1][1].set_title('$\hat{\sigma_x}$')
axes[1][1].axis('off')

plt.tight_layout()
```

```python
gn_mean = np.mean(gamma_n_chain[burnin:])
gn_std = np.std(gamma_n_chain[burnin:])
gx_mean = np.mean(gamma_x_chain[burnin:])
gx_std = np.std(gamma_x_chain[burnin:])

fig, axes = plt.subplots(1, 2, figsize=(12, 6))
axes[0].axhline(true_gamma_n, color='k', label='$\gamma_n$')
axes[0].axhline(gn_mean, color='g', label='$\hat{\gamma_n}$')
axes[0].axhline(gn_mean + 3 * gn_std, color='g', ls='dashed')
axes[0].axhline(gn_mean - 3 * gn_std, color='g', ls='dashed',
                label='$\pm 3 \hat\sigma$')
axes[0].plot(gamma_n_chain)
axes[0].set_ylim([0, 11])
axes[0].set_title('$\gamma_n$ chain')
axes[0].set_xlabel('Iteration')
axes[0].legend()

axes[1].axhline(gx_mean, color='g', label='$\hat{\gamma_x}$')
axes[1].axhline(gx_mean + 3 * gx_std, color='g', ls='dashed')
axes[1].axhline(gx_mean - 3 * gx_std, color='g', ls='dashed',
                label='$\pm 3 \hat\sigma$')
axes[1].plot(gamma_x_chain)
axes[1].set_ylim([0, 0.0008])
axes[1].set_title('$\gamma_x$ chain')
axes[1].legend()
xlabel = axes[1].set_xlabel('Iteration')
```

## Conclusion

We show in this notebook that the PO algorithm allows the simulation of large gaussian law if a linear solver (or quadratic optimization algorithm) is available, which is a common case. This possibility allows the use of a Bayesian algorithm that can, for instance, estimate the hyperparameter or the uncertainty. We demonstrate on a deconvolution problem that the method is effective, does not require many extra steps, and is easily extendable to other problems. Other illustration can be found in mentionned references.


```python
#hide code segments from display
from IPython.display import HTML
HTML('''
The raw code for this notebook is hidden for easier reading.
To toggle code display, click <a href="javascript:code_toggle()">here</a>.''')
```

# Pen Spring Diffraction Experiment
In this experiment, we illustrate the diffraction of a pen spring (a regular helix) and discuss its implications.

We will first import `numpy` to aid us in our calculations and `matplotlib` to aid us in visualisation.

```python
import numpy as np
import matplotlib.pyplot as plt
import math
%config InlineBackend.figure_format = 'retina'

def sinc(x):
    if (x != 0):
        return np.sin(np.pi * x) / (np. pi * x)
    else:
        return 1
sinc = np.vectorize(sinc)
```

# 1. Fill in: Experiment Variables
Enter the values here to match your experimental apparatus.

1. `slitWidth` is the gap in between each individual coil of the spring. From now on, it will be referred to as the *pitch* of the spring.
2. `wavelength` is the wavelength of the laser that is emitted by the laser pointer. It is usually marked near the emitter, on a warning label.
3. `distance` is the distance between the center of the spring and the surface on which the pattern is projection upon.

Units of the values below are given in **micrometers** ($10^{-6}$ m, also refered to as the **micron**).

```python
'''
slitWidth  = 2500          # 2.5mm
wavelength = 0.650         # 650nm
distance   = 10.3 * 10**6  # 10.3m
'''
slitWidth  = float(input("slitWidth: ")) #2500          # 2.5mm
wavelength = float(input("wavelength: ")) #0.650         # 650nm
distance   = float(eval(input("distance: "))) #10.3 * 10**6  # 10.3m
```

# 2. Fill in: Experiment Results

Measure the distance from the center to the $n-$th maxima. Choose any reasonable number!

Change the variable here to match your result in **mm**. It will be plotted on the visualisation later on.

```python
#result_mm = 25.5
result_mm = float(input("result: "))
```

# Visualising a Single-slit Diffraction
We will first set up some parameters, and then proceed to plot the intensity over distance from the center of the pattern.

```python
amplitude = 5 # the power of the laser (mW) as marked on the laser point. The accuracy of this value is trivial.
x = np.arange(-50000, 50000, 10) # numerical bounds and resolution of the graph (micron)
F = sinc(slitWidth * x / wavelength / distance)
intensity = amplitude / (wavelength * distance) * (slitWidth * F)**2
```

### Plotting the graph
See if your result (marked by blue line) indeed corresponds to the maxima from which you measured earlier on

```python
plt.figure(1, (12, 6))
plt.plot(x, intensity, linewidth=1, color='r') # red indicates the intensity
result = result_mm * 1000
plt.axvline(x = result, color='b', label="Obtained result: "+str(result_mm)+"mm") # blue indicated your result indicated earlier
plt.xlim((0, 50000))
plt.ylim((0, 0.06))
plt.xlabel(r'Position ($\mu$m)m)')
plt.ylabel(r'Intensity (mW)')
plt.legend(loc='lower right')
plt.title("Single-slit Diffraction (intensity / distance)")
plt.show()
```

# Angle between the Lines
Now, we will attempt to calculate the angle between the lines that form the X-shape. It corresponds to the *helix angle* on the spring.

The laser will illuminate one pitch (complete curl) of the spring.

**Babinet’s principle** states that the diffraction pattern of an obstacle is the same as the diffraction pattern of an aperture of the same shape. Hence, we will expect to see *two* single slit diffraction patterns angled to each other.

When viewed from the side, the spring (or any regular helix) resembles a sine wave. We also will make use of this property later on.

```python
plt.figure(1, (3, 4))
t = np.arange(0, 2.5, 0.04)
s = np.sin(2*np.pi*t)
plt.plot(s, t, color='k', linewidth=2)
x_p = np.linspace(-0.9, 0.9, 3)
line1,  = plt.plot(x_p, 0.17*x_p + 1, '--', linewidth=4, color='r', label='Illuminated Section')
line2,  = plt.plot(x_p, -0.17*x_p + 0.98+np.pi/6, '--', linewidth=4, color='r')
plt.axis('off')
plt.legend(loc='lower right')
plt.title("Sideways projection of the Spring")
plt.show()
```

# 3. Fill In: Parameters of the Spring
Enter the dimensions of the spring below, measured with a pair of vernier calipers. The units here are given in **mm**.

```python
'''
pitch_mm  = 2.5   # p
radius_mm = 2.05  # R
'''
pitch_mm  = float(input("pitch_mm: "))
radius_mm = float(input("radius_mm: "))
```

We orientate the spring in the $y-z$ plane to make it easier to work with.

The code below generates a graphical representation of your spring projected onto the $y-z$ plane.

```python
plt.figure(2, (12, 8))
z_q = np.arange(-2*pitch_mm, 2*pitch_mm, 0.05)
y_q = radius_mm * np.sin(2*np.pi * z_q/pitch_mm)
plt.plot(z_q, y_q, color = 'k', linewidth=4 , label='Spring')

y_1 = radius_mm * (2*np.pi * z_q/pitch_mm - 2*np.pi)
y_2 = radius_mm * (2*np.pi * z_q/pitch_mm)
y_3 = radius_mm * (2*np.pi * z_q/pitch_mm + 2*np.pi)

alpha = np.pi/2 - math.atan(radius_mm * 2*np.pi / pitch_mm)

plt.plot(z_q, y_1, "--", color='r', label = r'$y=\frac{2\pi R}{p}z+2n\pi$')
plt.plot(z_q, y_2, "--", color='r')
plt.plot(z_q, y_3, "--", color='r')

plt.text(0.1, 2, r'$\alpha$', fontsize=20)

d = pitch_mm*math.cos(alpha)
x_d2 = pitch_mm*(math.cos(alpha)**2)

x_d = np.linspace(0, x_d2, 3)
plt.plot(x_d, -1*(pitch_mm/(2*np.pi*radius_mm)) * x_d, "--", color='b')
plt.text(0.1, -radius_mm/3, "$d$ = " + str('%.1f'%d) + "mm", color="b", fontsize=14)

x_p1 = pitch_mm/4
x_p2 = x_p1 + pitch_mm
x_pitch = np.linspace(x_p1, x_p2, 3)
plt.plot(x_pitch, radius_mm + x_pitch*0, "--", color='g')
plt.text((x_p1+x_p2)/2-0.6, radius_mm + 0.1, "$p$ = " + str(pitch_mm) + "mm", color="g", fontsize=14)

plt.xlabel('$z$ (mm)')
plt.axvline(x=0, color='k', alpha=0.5)
plt.ylabel('$y$ (mm)')
plt.axhline(y=0, color='k', alpha=0.5)
plt.axis('equal')
plt.xlim((-2*pitch_mm, 2*pitch_mm))
plt.ylim((-radius_mm*1.2, radius_mm*1.2))
plt.legend(loc='lower right')
ax = plt.gca()
ax.set_xticks(np.arange(-2*pitch_mm, 2*pitch_mm, 1))
ax.set_yticks(np.arange(-radius_mm*2, radius_mm*2, 1))
plt.grid(True, linestyle='--', linewidth=1, alpha=0.5)
plt.title(r"Projection of Spring [$p=$"+str(pitch_mm)+r"mm, $R=$"+str(radius_mm)+r"mm] in the $y-z$ plane")
plt.show()
```

Perform some symbolic differentiation on the equation of the spring:

```python
from sympy import *
init_printing()
R, p, z = symbols('R p z')
eqn_y = R * sin(2 * pi * z / p)
eqn_y.simplify()
```

```python
n = symbols('n', integer=True)
deriv_y = diff(eqn_y, z)
deriv_y.simplify()
```

Substitute $z=n \times p$:

```python
deriv_y = deriv_y.subs(z, n*p) #z = n*p
deriv_y.simplify()
```

The angle $\alpha$ is equal to the following expression:

```python
eqn_a = -1 * atan(deriv_y) + pi/2
eqn_a.simplify()
```

```python
deriv_y = deriv_y.subs(p, pitch_mm * 10**-3)
deriv_y = deriv_y.subs(R, radius_mm * 10**-3)
a = -1 * math.atan(deriv_y) + math.pi/2
print(r'The angle(α) between the lines is '
      + str('%.3f'%(math.degrees(a)) + " degrees (to 3dp)."))
```

# Visualising the Actual Diffraction Pattern
Now that we have the angle and the intensity, we can attempt to visualise the pattern seen.

```python
angle = math.pi/2 - a

plt.figure(3, (12, 12))

# transform the intensity line by rotating it about the center
x_tf = ( x / math.sqrt((math.tan(angle))**2 + 1) )
y_tf = x_tf * math.tan(angle)
plt.scatter(x_tf, y_tf, color='r', s=intensity, alpha=0.05) # plot the first line

y_tf = x_tf * math.tan(-angle)
plt.scatter(x_tf, y_tf, color='r', s=intensity, alpha=0.1) # plot the second line

# plot the circle which marks our result from earlier (gray)
ax = plt.gca()
circle_result = plt.Circle((0, 0), result, color='b', fill=False, alpha=0.2)
ax.add_artist(circle_result)

plt.xlabel("$x$")
plt.ylabel("$y$")
plt.axis('equal')
plt.xlim((-30000, 30000))
plt.ylim((-30000, 30000))
plt.show()
```

#### Zoomed in version
This is a close-up look at the first quadrant of the above figure.

It allows us to properly judge the maxima that might not be obvious above.

```python
plt.figure(4, (12, 12))

x_tf = ( x / math.sqrt((math.tan(angle))**2 + 1) )
y_tf = x_tf * math.tan(angle)
plt.scatter(x_tf, y_tf, color='r', s=intensity*100, alpha=0.05)

# circle which marks the result (blue)
ax = plt.gca()
circle_result = plt.Circle((0, 0), result, color='b', fill=False)
ax.add_artist(circle_result)

plt.xlabel("$x$")
plt.ylabel("$y$")
plt.axis('equal')
plt.xlim((0, 1.1*result))
plt.ylim((0, 30000))
plt.grid(True, linestyle='--', linewidth=1, alpha=0.5)
plt.show()
```

# Significance of this Result

**Photograph 51** is the nickname given to an X-ray diffraction image of DNA taken by Raymond Gosling in May 1952, working as a PhD student under the supervision of Rosalind Franklin.

Does it not resemble the result you have gotten from the pen spring?

Since objects of similar shape produce similar diffraction patterns (Babinet's principle), this result was critical evidence in identifying the structure of DNA as a regular helix.

```python
import os
print(os.popen("cowsay 'Can MatLab do this?'").read())
```

     _____________________
    < Can MatLab do this? >
     ---------------------
            \   ^__^
             \  (oo)\_______
                (__)\       )\/\
                    ||----w |
                    ||     ||


# Pg. 360 # 1-19 odd, 31

```
Name: Eric Nguyen
Date: 10 Jan 2019
```

```python
from numpy import e, exp, log, linspace
from matplotlib.pyplot import plot
%matplotlib inline
```

#### *Solution 1a*
**1a)** $N(t) = N_0e^{-0.096t}$

#### *Solution 1b*
$\begin{align}
N_0 &= 500 \text{g} \\
t &= 4 \text{ days} \\
N(4) &= 500e^{-0.096(4)} &\quad \text{Plug given variables into N.}
\end{align}$

```python
500 * exp(-0.096 * 4)
```

    340.56571358977357

**1b)** After 4 days, approximately **340.566 g** of the iodine-131 remains.

#### *Solution 1c*
$\begin{align}
T &= \frac{\ln 2}{0.096} &\quad \text{Theorem 10 (p. 354).}
\end{align}$

```python
log(2) / 0.096
```

    7.2202831308327635

**1c)** The half-life of iodine-131  is approximately **7.22 days.**

#### *Solution 3a*
**3a)** $A(t) = A_0e^{-kt}$

#### *Solution 3b*
$\begin{align}
5 &= 10e^{-k(3.3)} \\
\frac{5}{10} &= e^{-k(3.3)} &\quad \text{Divide both sides by 10.} \\
\ln \frac{1}{2} &= -k(3.3) &\quad \text{Take the natural log of both sides.} \\
k &= -\frac{\ln \frac{1}{2}}{3.3} &\quad \text{Divide both sides by 3.3} \\
\\\\
1 &= 10e^{-kt} \\
\frac{1}{10} &= e^{-kt} &\quad \text{Divide both sides by 10.} \\
\ln{\frac{1}{10}} &= -kt &\quad \text{Take the natural log of both sides.} \\
t &= \frac{\ln{\frac{1}{10}}}{-k} &\quad \text{Divide both sides by -k.} \\
t &= \frac{\ln{\frac{1}{10}}}{-\left(-\frac{\ln \frac{1}{2}}{3.3}\right)} &\quad \text{Plug k in.}
\end{align}$

```python
log(0.1) / (log(0.5) / 3.3)
```

    10.962362713128295

**3b)** There will only be 1 lb left after approximately **11 hours**.

#### *Solution 5*
$k = \dfrac{\ln{2}}{3 \text{ min}} \quad \text{ Theorem 10 (p. 354).}$

```python
log(2) / 3
```

    0.23104906018664842

**5)** The decay rate for polonium-218 is approximately **23.1%/min**

#### *Solution 7*
$T = \dfrac{\ln{2}}{0.0315} \quad \text{Theorem 10 (p. 354).}$

```python
log(2) / .0315
```

    22.00467239872842

**7)** The half-life for lead-210 is approximately **22 years**.

#### *Solution 9*
$\begin{align}
N(100) &= 1000e^{-0.0315(100)} \\
\end{align}$

```python
1000 * exp(-0.0315 * 100)
```

    42.852126867040184

**9)** Of an initial amount of 1000g of lead-210, approximately **42.85 g** will remain after 100 years.

#### *Solution 11*
$\begin{align}
(1-0.4)N_0 &= N_0e^{-kt} \\
k &= \frac{\ln{2}}{5750 \text{ years}} &\quad \text{Theorem 10 (p. 354).} \\
0.6 &= e^{-kt} \\
\ln{0.6} &= -kt &\quad \text{Take the natural log of both sides.} \\
t &= \frac{\ln{0.6}}{-k} &\quad \text{Divide both sides by } {-k}. \\
t &= \frac{\ln{0.6}}{-\left(\frac{\ln{2}}{5750}\right)} &\quad \text{Plug in } k.
\end{align}$

```python
log(0.6) / -(log(2) / 5750)
```

    4237.552166455685

**11)** An ivory tusk that has lost 40% of its carbon-14 is approximately **4237.55 years** old.

#### *Solution 13*
$\begin{align}
k &= \frac{\ln{2}}{60.1 \text{ days}} &\quad \text{Theorem 10 (p. 354).} \\
(1 - 0.25) &= e^{-kt} \\
\ln{0.75} &= -kt \\
t &= \frac{\ln{0.75}}{-k} \\
t &= \frac{\ln{0.75}}{-\left(\frac{\ln{2}}{60.1}\right)}
\end{align}$

```python
log(0.75) / -(log(2) / 60.1)
```

    24.943753706658512

**13)** The sample of iodine-125 was sitting on the shelf for approximately **24.94 days**.

#### *Solution 15*
$\begin{align}
(1-0.381)N_0 &= N_0e^{-kt} \\
k &= \frac{\ln{2}}{5750 \text{ years}} &\quad \text{Theorem 10 (p. 354).} \\
0.6 &= e^{-kt} \\
\ln{0.619} &= -kt &\quad \text{Take the natural log of both sides.} \\
t &= \frac{\ln{0.619}}{-k} &\quad \text{Divide both sides by } {-k}. \\
t &= \frac{\ln{0.619}}{-\left(\frac{\ln{2}}{5750}\right)} &\quad \text{Plug in } k.
\end{align}$

```python
log(0.619) / -(log(2) / 5750)
```

    3978.934941324979

**15)** The pollen is approximately **3978.93 years** old.

#### *Solution 17*
$\begin{align}
\$40,000 &= P_0e^{0.053(20)} \\
P_0 &= \frac{\$40000}{e^{0.053(20)}} &\quad \text{Divide both sides by } {e^{0.053(20)}}.
\end{align}$

```python
40000 / exp(0.053 * 20)
```

    13858.232413202297

**17)** The initial investment should be around **\$13858.23**.

#### *Solution 19*

*Salary in millions of dollars*

$\begin{align}
9 &= P_0e^{0.057(6)} \\
P_0 &= \frac{9}{e^{0.057(6)}} &\quad \text{Divide both sides by } {e^{0.057(6)}}.
\end{align}$

```python
9 / exp(0.057 * 6)
```

    6.393133842382595

**19)** The present value of that years salary is **\$6,393,133.84**.

#### *Solution 31a*
$\begin{align}
102 &= a + 75 \\
a &= 27 &\quad \text{Subtract both sides by 75.} \\
\end{align}$

**31a)** $27$

#### *Solution 31b*
$\begin{align}
90 &= 27e^{-k(10)} + 75 \\
15 &= 27e^{-10k} &\quad \text{Subtract 75 from both sides.} \\
\frac{15}{27} &= e^{-10k} &\quad \text{Divide both sides by 27.} \\
\ln{\frac{15}{27}} &= -10k &\quad \text{Take the natural log of both sides.} \\
k &= \frac{\ln{\frac{15}{27}}}{-10} &\quad \text{Divide both sides by -10.}
\end{align}$

```python
log(15 / 27) / -10
```

    0.05877866649021189

**31b)** $0.05878$

#### *Solution 31c*
$T(20) = 27e^{-0.05878(20)} + 75$

```python
27 * exp(-0.05878 * 20) + 75
```

    83.3331110846657

**31c)** $83^\text{o}$

#### *Solution 31d*
$\begin{align}
80 &= 27e^{-0.05875t} + 75 \\
5 &= 27e^{-0.05875t} &\quad \text{Subtract 75 from both sides.} \\
\frac{5}{27} &= e^{-0.05875t} &\quad \text{Divide 27 from both sides.} \\
\ln{\frac{5}{27}} &= -0.05875t &\quad \text{Take the natural log of both sides.} \\
t &= \frac{\ln{\frac{5}{27}}}{-0.05875} &\quad \text{Divide both sides by -0.05875.}
\end{align}$

```python
log(5 / 27) / -0.05875
```

    28.70466303949326

#### *Solution 31e*
$\begin{align}
T'(t) &= 27e^{-0.05875t} \cdot -0.05875 &\quad \text{Chain rule.} \\
T'(t) &= -1.58625e^{-0.05875t} &\quad \text{Simplify.}
\end{align}$

```python
T = lambda t: -1.58625 * exp(-0.05875 * t)
xp = linspace(0, 100, 100)
_ = plot(xp, T(xp))
```

**31c)** $T'(t) = -1.58625e^{-0.05875t}$ **; The water temperature's rate of change rapidly approaches zero over time.**


# 3D EKF

In this exercise, you will learn that you can continue using the same code that you created earlier for three-dimensional flight. You have already went through a similar exercise when you transitioned from a one-dimensional to a two-dimensional example. In that case you had to change the values in several matrices, but rest of the estimation code remained the same. You will see that the same is true moving from 2D to 3D.

In this exercise, you simply test the correctness of your implementation. In the final project at the end of the lesson, you will be asked to estimate the drone's position and attitude using these same EKF equations.

For this exercise, we will use the following setup: the state will be obtained using GPS measurements and the yaw will be obtained from the magnetometer. Thus, the state vector has the next form $X=[x,y,z,\dot{x},\dot{y},\dot{z},\psi]^T$. The complementary filter will be used to determine the roll and pitch of the drone.

We will use the accelerometer and gyro measurements to determine the $\ddot{x},\ddot{y},\ddot{z}$ and $p,q,r$. These mesurements will be used as a control inputs for the filter.

We are using NED system where $z$ is directed down. The yaw will be tracked relative to the magnetic north.

Thus, $u=[\ddot{x},\ddot{y},\ddot{z},\dot{\psi}]^T$

```python
%matplotlib inline
%config InlineBackend.figure_format = 'retina'

import numpy as np
from math import sin, cos
import matplotlib.pyplot as plt
import matplotlib.pylab as pylab
import jdc
from ipywidgets import interactive
from scipy.stats import multivariate_normal
np.set_printoptions(precision=2)
import time

pylab.rcParams['figure.figsize'] = 10, 10
```

# EKF
In order to perform the predict and update steps for the 3D EKF, we need to know the mean and the standard deviation for measurement and motion. The measurement error is related to how good our measuring tool is, and the motion error is associated with how well the drone executes a given command.

```python
class EKF:
    def __init__(self,
                 motion_error,             # Motion noise
                 angle_sigma_yaw,          # Angle sigma
                 velocity_sigma,           # Velocity uncertainty
                 position_sigma,           # Position uncertainty
                 dt                        # dt time between samples
                ):

        # Sensor measurement sigma
        self.r_t = np.array([[motion_error**2]]) # THIS WILL CHANGE

        # Motion model noise
        self.q_t = np.array([[position_sigma**2,0.0,0.0,0.0,0.0,0.0,0.0],
                             [0.0,position_sigma**2,0.0,0.0,0.0,0.0,0.0],
                             [0.0,0.0,position_sigma**2,0.0,0.0,0.0,0.0],
                             [0.0,0.0,0.0,velocity_sigma**2,0.0,0.0,0.0],
                             [0.0,0.0,0.0,0.0,velocity_sigma**2,0.0,0.0],
                             [0.0,0.0,0.0,0.0,0.0,velocity_sigma**2,0.0],
                             [0.0,0.0,0.0,0.0,0.0,0.0,angle_sigma_yaw**2]])

        self.dt = dt
        self.mu = np.array([0])
        self.sigma = np.array([0])

        self.mu_bar = self.mu
        self.sigma_bar = self.sigma

        self.X=np.array([0.0,0.0,0.0,0.0,0.0,0.0,0.0])
        self.gravity=9.81
        self.phi=0.0
        self.psi=0.0
        self.theta=0.0

    def initial_values(self,mu_0, sigma_0):
        self.mu= mu_0
        self.sigma = sigma_0

```

### Transition model and predict step
We will use the rotation matrix to define the transition model from body frame to global frame $R_{bg}$. The transition function for the 3D case using the state vector that we defined will be:

$$
\begin{align}
  g(x_t, u_t, \Delta t) &=
  \left[  \begin{array}{c}
      x_{t,x} +  x_{t,\dot{x}} \Delta t \\
      x_{t,y} +  x_{t,\dot{y}} \Delta t \\
      x_{t,z} + x_{t,\dot{z}} \Delta t\\
      x_{t,\dot{x}} \\
      x_{t,\dot{y}} \\
      x_{t,\dot{z}} - g \Delta t \\
      x_{t, \psi}\\
    \end{array}\right] +
  \left[ \begin{array}{cccc}
      0&0&0&0\\
      0&0&0&0\\
      0&0&0&0\\
      R_{bg}[0:]&&&0\\
      R_{bg}[1:]&&&0\\
      R_{bg}[2:]&&&0\\
      0&0&0&1
      \end{array}
    \right]   u_t \Delta t
\end{align}
$$

And the Jacobian:

$$
g'(x_t, u_t, \Delta t) = \left [ \begin{array}{ccccccc}
      1 & 0 & 0 & \Delta t & 0 & 0 & 0\\
      0 & 1 & 0 & 0 & \Delta t & 0 & 0\\
      0 & 0 & 1 & 0 & 0 & \Delta t & 0\\
      0 & 0 & 0 & 1 & 0 & 0 & R'_{bg}[0:]u_t[0:3] \Delta t\\
      0 & 0 & 0 & 0 & 1  & 0 & R'_{bg}[1:]u_t[0:3] \Delta t\\
      0 & 0 & 0 & 0 & 0 & 1 &  R'_{bg}[2:]u_t[0:3] \Delta t\\
      0 & 0 & 0 & 0 & 0 & 0 & 1
    \end{array}
    \right]
$$

Where $R'_{bg}$ is $\frac{\partial}{\partial \psi}$ of the rotation matrix.

### New transition functions

In this section we will also implement the matrixes for $g(x_y,u_t,\Delta t) = a + b u_t \Delta t$ and $g'(x_y,u_t,\Delta t)$ also note that $u_t = [\ddot{x},\ddot{y},\ddot{z},\dot{\psi}]$ and $\Delta t= dt$.

```python
%%add_to EKF

@property
def R_bg(self):

    R_bg=np.array([[cos(self.phi)*cos(self.psi)-sin(self.phi)*cos(self.theta)*sin(self.psi),
                    cos(self.phi)*sin(self.psi)+sin(self.phi)*cos(self.theta)*cos(self.psi),
                    sin(self.phi)*sin(self.theta)],
                   [-sin(self.phi)*cos(self.psi)-cos(self.phi)*cos(self.theta)*sin(self.psi),
                    -sin(self.phi)*sin(self.psi)+cos(self.phi)*cos(self.theta)*cos(self.psi),
                    cos(self.phi)*sin(self.theta)],
                   [sin(self.theta)*sin(self.psi),-sin(self.theta)*cos(self.psi),cos(self.theta)]])

    return R_bg

@property
def R_bg_prime(self):

    R = np.array([[-cos(self.phi)*sin(self.psi)-sin(self.phi)*cos(self.theta)*cos(self.psi),
                   cos(self.phi)*cos(self.psi)-sin(self.phi)*cos(self.theta)*sin(self.psi), 0.0],
                  [sin(self.phi)*sin(self.psi)-cos(self.phi)*cos(self.theta)*cos(self.psi),
                  -sin(self.phi)*cos(self.psi),0.0],
                  [sin(self.theta)*cos(self.psi),sin(self.theta)*sin(self.psi),0.0]])

    return np.transpose(R)

@property
def a(self):
    a= np.zeros((self.X.shape[0],1))

    a = np.array([[self.mu[0] + self.mu[3] * self.dt],
                  [self.mu[1] + self.mu[4] * self.dt],
                  [self.mu[2] + self.mu[5] * self.dt],
                  [self.mu[3]],
                  [self.mu[4]],
                  [self.mu[5] + self.gravity * self.dt],
                  [self.mu[6]]])

    return a

@property
def b(self):

    b= np.zeros((self.X.shape[0],4))
    b[3:6,:3] = self.R_bg
    b[-1,-1] = 1

    return b

def g(self,u):

    g_3d = np.add(self.a[:,:,0], np.matmul(self.b, (u* self.dt)))

    return g_3d

def g_prime(self,u):

    g_prime=np.identity(self.mu.shape[0])
    g_prime[0,3] = self.dt
    g_prime[1,4] = self.dt
    g_prime[2,5] = self.dt
    g_prime[3:6,5:6] =np.matmul(self.R_bg_prime, (u[:3]*self.dt) )

    return g_prime

```

### Predict
Now that we have implemented the proper expressions for $g$ and $g'$ we can perform the prediction step. Please note that the given step is exactly like the one that you already implemented for one-dimensional case.

```python
%%add_to EKF

def predict(self, u):
    previous_covariance = self.sigma
    mu_bar = self.g(u)
    G_now  = self.g_prime(u)
    sigma_bar = np.matmul(G_now,np.matmul(previous_covariance,np.transpose(G_now))) + self.q_t

    self.mu_bar  = mu_bar
    self.sigma_bar = sigma_bar

    return mu_bar, sigma_bar

```

# Testing the Predict function

In this section, you will be given simple initial statements and asked to run the predict function and compare to the intuitive answer.

First, we will declare the initial condition and then execute the prediction, and you will be able to see that the prediction for 3D is no different from the 2D that you already have seen.

```python
x = 0.0                         # Initial position
y = 0.0                         # Initial position
z = -1.0                        # Initial position

x_dot = 2.0                     # Initial velocity
y_dot = 3.0                     # Initial velocity
z_dot = 0.0                     # Initial velocity

phi = 0.0                       # Initial roll angle
theta = 0.0                     # Initial pitch angle
psi = 0.0                       # Initial yaw angle

dt = 1.0                        # The time difference between measurements

motion_error = 0.01             # Motion error
angle_error_yaw = 0.001         # Angle uncertainty
velocity_sigma = 0.01           # Velocity uncertainty
position_sigma = 0.02           # Position uncertainty

mu_0 = np.array([[x],[y],[z],[x_dot],[y_dot],[z_dot],[psi]])
cov_0 = np.array([[position_sigma**2,0.0,0.0,0.0,0.0,0.0,0.0],
                  [0.0,position_sigma**2,0.0,0.0,0.0,0.0,0.0],
                  [0.0,0.0,position_sigma**2,0.0,0.0,0.0,0.0],
                  [0.0,0.0,0.0,velocity_sigma**2,0.0,0.0,0.0],
                  [0.0,0.0,0.0,0.0,velocity_sigma**2,0.0,0.0],
                  [0.0,0.0,0.0,0.0,0.0,velocity_sigma**2,0.0],
                  [0.0,0.0,0.0,0.0,0.0,0.0,angle_error_yaw**2]])

u = np.array([[0.0],
              [0.0],
              [-9.81],
              [0.0]])           # no command is given

```

The prediction step consists of declaring the object, and then initializing and calling the prediction function.

```python
# initilize the object
myEKF=EKF(motion_error,angle_error_yaw,velocity_sigma,position_sigma,dt)

myEKF.psi=psi
myEKF.phi=phi
myEKF.theta=theta
# input the initial values
myEKF.initial_values(mu_0, cov_0)

# call the predict function

mu_bar, sigma_bar = myEKF.predict(u)

print('mu_bar = \n',mu_bar)
print('\nsigma_bar = \n', sigma_bar)
```

    mu_bar =
     [[ 2.]
     [ 3.]
     [-1.]
     [ 2.]
     [ 3.]
     [ 0.]
     [ 0.]]
    sigma_bar =
     [[  9.00e-04   0.00e+00   0.00e+00   1.00e-04   0.00e+00   0.00e+00
        0.00e+00]
     [  0.00e+00   9.00e-04   0.00e+00   0.00e+00   1.00e-04   0.00e+00
        0.00e+00]
     [  0.00e+00   0.00e+00   9.00e-04   0.00e+00   0.00e+00   0.00e+00
        0.00e+00]
     [  1.00e-04   0.00e+00   0.00e+00   2.00e-04   0.00e+00   0.00e+00
        0.00e+00]
     [  0.00e+00   1.00e-04   0.00e+00   0.00e+00   2.00e-04   0.00e+00
        0.00e+00]
     [  0.00e+00   0.00e+00   0.00e+00   0.00e+00   0.00e+00   1.00e-04
        0.00e+00]
     [  0.00e+00   0.00e+00   0.00e+00   0.00e+00   0.00e+00   0.00e+00
        2.00e-06]]

# Measurement Update

We assume that our observation function is,

$$
z_t = \begin{bmatrix}x\\y\\z \\ \dot{x}\\ \dot{y} \\ \dot{z}\end{bmatrix}
$$

Thus, $h'$ will be,

$$
h'=\begin{bmatrix} 1 & 0 & 0 & 0 & 0 & 0 & 0  \\ 0 & 1 & 0 & 0 & 0 & 0 & 0  \\ 0 & 0 & 1 & 0 & 0 & 0 & 0  \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0  \end{bmatrix}
$$

```python
%%add_to EKF

def h(self,x):

    h=x

    return h

def h_prime(self):

    h_prime = np.array([[1,0,0,0,0,0,0],
                        [0,1,0,0,0,0,0],
                        [0,0,1,0,0,0,0],
                        [0,0,0,1,0,0,0],
                        [0,0,0,0,1,0,0],
                        [0,0,0,0,0,1,0]])

    return h_prime
```

Please note that the update step is identical to the one you implemented for the one-dimensional case.

```python
%%add_to EKF

def update(self, z):

    H = self.h_prime()

    S = np.matmul(np.matmul(H,self.sigma_bar),np.transpose(H)) + self.r_t

    K = np.matmul(np.matmul(self.sigma_bar,np.transpose(H)),np.linalg.inv(S))

    mu = self.mu_bar + np.matmul(K,(z-self.h(self.mu_bar[:-1])))

    sigma = np.matmul((np.identity(7) - np.matmul(K,H)),self.sigma_bar)

    self.mu=mu
    self.sigma=sigma

    return mu, sigma

```

# Results

```python
measure =np.array([[ 2.05],
 [ 3.1],
 [-1.0],
 [ 2.1],
 [ 3.05],
 [ 0.]])

mu_updated, sigma_updated = myEKF.update(measure)
print('updated mean = \n',mu_updated)
print('\nupdated sigma = \n', sigma_updated)
```

    updated mean =
     [[ 2.02]
     [ 3.07]
     [-1.03]
     [ 2.07]
     [ 3.02]
     [-0.03]
     [ 0.  ]]

    updated sigma =
     [[  3.15e-05   3.15e-05   3.15e-05   3.15e-05   3.15e-05   3.15e-05
        0.00e+00]
     [  3.15e-05   3.15e-05   3.15e-05   3.15e-05   3.15e-05   3.15e-05
        0.00e+00]
     [  3.15e-05   3.15e-05   3.15e-05   3.15e-05   3.15e-05   3.15e-05
        0.00e+00]
     [  3.15e-05   3.15e-05   3.15e-05   3.15e-05   3.15e-05   3.15e-05
        0.00e+00]
     [  3.15e-05   3.15e-05   3.15e-05   3.15e-05   3.15e-05   3.15e-05
        0.00e+00]
     [  3.15e-05   3.15e-05   3.15e-05   3.15e-05   3.15e-05   3.15e-05
        0.00e+00]
     [  0.00e+00   0.00e+00   0.00e+00   0.00e+00   0.00e+00   0.00e+00
        2.00e-06]]

```python

```


# MACD Analysis and Buy/Sell Signals

> MACD, short for moving average convergence/divergence, is a trading indicator used in technical analysis of stock prices.
Source: https://en.wikipedia.org/wiki/MACD

We analyse stock data using MACD and generate buy and sell signals.

```python
# Parameters for MACD computation

# fast_window: window length of the fast EMA
# slow_window: window length of the slow EMA
# signal_window: window length of the signal EMA

fast_window = 12
slow_window = 26
signal_window = 9

# stock's data as csv file from ariva.de
input_data_file = '../data/external/data.csv'

# notebook stores the result in output_data_path
output_data_path = '../data/interim'

# notebook name; required for outfile naming convention
# Can't be acquired by javascript approaches when 'run all'
# see: https://github.com/jupyter/notebook/issues/1622
nb_file = 'MACD_BuySell'
```

#### Some installs and imports

```python
# Install a pip module in the current Jupyter kernel
import sys

try:
    from stockstats import StockDataFrame as Sdf
except ImportError as err:
    print("Handling run-time error: ", err)
    print("Will now install missing module.")
    !{sys.executable} -m pip install stockstats

try:
    import seaborn as sns
except ImportError as err:
    print("Handling run-time error: ", err)
    print("Will now install missing module.")
    !{sys.executable} -m pip install seaborn

try:
    import papermill as pm
except ImportError as err:
    print("Handling run-time error: ", err)
    print("Will now install missing module.")
    !{sys.executable} -m pip install papermill

```

```python
# other imports

# manipulating data
import pandas as pd
import numpy as np
from stockstats import StockDataFrame as Sdf

# plotting
from matplotlib import pyplot as plt
%matplotlib inline
import seaborn as sns
import matplotlib.style as style

# notebook parameterizing and automation
import papermill as pm
```

## MACD Function using Parameters

Below there is an example of MACD calculation:

    The MACD line (macd): 12-day EMA - 26-day EMA
    Signal Line (macds): 9-day EMA of MACD Line

The parameter are (fast, slow, signal) = (12, 26, 9).
We generalize the example above and compute the parameterize MACD as follows:

\begin{align}
MACD^{fast, slow}(t) & = EMA^{fast}(close, t) - EMA^{slow}(close, t) \\
Signal^{sig}(t) & = EMA^{sig}(MACD^{fast, slow}(t), t) \\
\end{align}

```python
#
# original from stockstats source
# modified to paramterize it
#

def macd_params(df, fast_window=12, slow_window=26, signal_window=9):
    """ Moving Average Convergence Divergence

    This function will initialize all following columns.

    MACD Line (macd): (fast_window EMA - slow_window EMA)
    Signal Line (macds): signal_window EMA of MACD Line
    MACD Histogram (macdh): MACD Line - Signal Line

    :param df: data
    :param fast_window: window length of the fast EMA
    :param slow_window: window length of the slow EMA
    :param signal_window: window length of the signal EMA

    :return: None
    """

    # parameter check
    if (fast_window > slow_window) or (signal_window > fast_window):
        # error
        raise ValueError('Parameter values do not fit.')

    fast = df['close_' + str(fast_window) + '_ema']
    slow = df['close_' + str(slow_window) + '_ema']
    df['macd'] = fast - slow
    df['macds'] = df['macd_' + str(signal_window) + '_ema']
    df['macdh'] = (df['macd'] - df['macds'])

    del df['macd_' + str(signal_window) + '_ema']
    del fast
    del slow

```

## Data Import

The data download is from ariva.de. The import adjust some formatting in order to work with python's `stockstats` package.

* delimiter is ';'
* header line uses english idenifiers
* decimal separator is specified as ','
* thousands separator is '.'

```python
# Path to data is in parameter 'data_file'

# import data
colnames = ['date', 'open', 'high', 'low', 'close', 'shares', 'volume']
data = pd.read_csv(input_data_file, delimiter=';', header=0, names=colnames, decimal=',', thousands='.')
stock  = Sdf.retype(data)

# sort by date ascending
stock.sort_index(ascending=True, inplace=True)
```

## Data Visualization

Let's quickly have an overview about the stock data.

```python
# reset the index to use it as x-axis data
stock.reset_index(inplace=True)
# reindex the dataframe
stock.index = [stock['date']]

style.use('seaborn-notebook')
style.use('seaborn-white')

# 2x1 plot
stock_fig = plt.figure()
stock_fig1 = stock_fig.add_subplot(211) # 3x1, fig.1
stock_fig2 = stock_fig.add_subplot(212) # 3x1, fig.2

# line plot for close
sns.lineplot(data=stock, x='date', y='close', label='Close', ax=stock_fig1)
# remove ticks and labels
stock_fig1.tick_params(labelbottom=False, bottom=False)

# box plot for open, close, high, low
stock_prices = pd.DataFrame({'price': stock['high'], 'label': 'high'})
stock_prices = pd.concat([stock_prices, pd.DataFrame({'price': stock['low'], 'label': 'low'})], sort=False)
stock_prices = pd.concat([stock_prices, pd.DataFrame({'price': stock['open'], 'label': 'open'})], sort=False)
stock_prices = pd.concat([stock_prices, pd.DataFrame({'price': stock['close'], 'label': 'close'})], sort=False)
sns.boxplot(x="label", y="price", data=stock_prices)
```

## Compute MACD and Visualize

```python
# compute macd with standard param (12 / 26 / 9)
macd_params(stock, fast_window, slow_window, signal_window)
```

```python
stock.head()
```

      date
      open
      high
      low
      close
      shares
      volume
      close_12_ema
      close_26_ema
      macd
      macds
      macdh

      date

      2017-08-21
      2017-08-21
      174.3375
      175.6815
      173.1374
      174.0495
      776936
      135380672
      174.049500
      174.049500
      0.000000
      0.000000
      0.000000

      2017-08-22
      2017-08-22
      175.2015
      176.1615
      174.8655
      175.5855
      714906
      125526592
      174.881500
      174.847038
      0.034462
      0.019145
      0.015316

      2017-08-23
      2017-08-23
      175.5375
      176.0655
      174.0014
      174.3375
      800080
      139733784
      174.669177
      174.663966
      0.005210
      0.013434
      -0.008224

      2017-08-24
      2017-08-24
      174.4815
      176.5935
      174.4815
      175.4415
      725802
      127510400
      174.912969
      174.881331
      0.031638
      0.019601
      0.012037

      2017-08-25
      2017-08-25
      175.3455
      176.3535
      174.8175
      174.9135
      673465
      118082856
      174.913113
      174.888791
      0.024322
      0.021005
      0.003317

```python
# visualize MACD and MACD signal
style.use('seaborn-notebook')
style.use('seaborn-white')

#stock.reset_index(inplace=True)
sns.lineplot(data=stock, x='date', y='macds', label='MACD Signal')
sns.lineplot(data=stock, x='date', y='macd', label='MACD')
l=stock.date.count()/12
plt.xticks(stock.date[0::int(l)], rotation=45)
plt.show()
```

## Compute Buy/Sell Signals

The buy/sell signals are the result when the MACD and the MACD-Signal cross each other.

* MACD - Moving Average Convergence/Divergence
* MACD Signal - EMA of MACD

**BUY** Signal: $ MACD(t-1) < Signal(t-1) \textrm{ and } MACD(t) > Signal(t) $

**SELL** Signal: $ MACD(t-1) > Signal(t-1) \textrm{ and } MACD(t) < Signal(t) $

**HOLD** Signal: else

**Note:** In the code below the variable are assigned

* MACD -> macd
* MACD Signal -> macds
* BUY/SELL/HOLD -> signal

Finally, complement the dataframe with buy/sell/hold signal.

```python
macd = stock['macd']
macds = stock['macds']
macd.sort_index(ascending=True, inplace=True)
macds.sort_index(ascending=True, inplace=True)

# sort stock ascending and reset to standard integer index
stock.sort_index(ascending=True, inplace=True)
stock.reset_index(inplace=True, drop=True)
stock['signal'] = ""
stock.at[0, 'signal']="No data"

for t in range(1, len(macds)):
    #                          # If the MACD crosses the signal line upward
    if macd[t - 1] < macds[t - 1] and macd[t] > macds[t]:
        stock.at[t, 'signal']="BUY"
    #                          # The other way around
    elif macd[t - 1] > macds[t - 1] and macd[t] < macds[t]:
        stock.at[t, 'signal']="SELL"
    #                          # Do nothing if not crossed
    else:
        stock.at[t, 'signal']="HOLD"
```

## Visualize all Data

Plot the MACD, MACD signal and the buy/sell/hold signals.

```python
# replace BUY/SELL/HOLD by 1/-1/0 for plotting
stock['signal'] = stock['signal'].replace('BUY', 1).replace('SELL', -1).replace('HOLD', 0).replace('No data', 0)

# set plot style
style.use('seaborn-poster')
style.use('seaborn-white')

# Create a figure instance, and the two subplots
fig = plt.figure()
ax1 = fig.add_subplot(311) # 3x1, fig.1
ax2 = fig.add_subplot(312) # 3x1, fig.2
ax3 = fig.add_subplot(313) # 3x1, fig.3

# Stock data
sns.lineplot(data=stock, x='date', y='close', label='Close', ax=ax1)
# MACD, MACD Signal
sns.lineplot(data=stock, x='date', y='macds', label='MACD Signal', ax=ax2)
sns.lineplot(data=stock, x='date', y='macd', label='MACD', ax=ax2)
# Buy/Sell Signal
sns.lineplot(data=stock, x='date', y='signal', label='Buy/Sell Signal', ax=ax3)

# remove ticks and labels
ax1.tick_params(labelbottom=False, bottom=False)
ax2.tick_params(labelbottom=False, bottom=False)

# format ticks
l=stock.date.count()/12
plt.xticks(stock.date[0::int(l)], rotation=45)
plt.show()
```

## Stats and Data stored for later Use

We store some statistics data.
Additionally, we record values in the notebook using Papermill to be consumed by other notebooks later on.

```python
# compute stats
BUY_cnt = stock[stock['signal']==1].shape[0]
SELL_cnt = stock[stock['signal']==-1].shape[0]
HOLD_cnt = stock[stock['signal']==0].shape[0]
STATS = stock.describe()
# print stats
print("BUY Signals: ", BUY_cnt)
print("SELL Signals: ", SELL_cnt)
print("HOLD Signals: ", HOLD_cnt)
print("Statistics:")
print(STATS)
```

    BUY Signals:  11
    SELL Signals:  12
    HOLD Signals:  229
    Statistics:
                 open        high         low       close        shares  \
    count  252.000000  252.000000  252.000000  252.000000  2.520000e+02
    mean   184.629700  185.828262  183.221285  184.497138  1.323365e+06
    std      6.438659    6.375852    6.556162    6.482004  5.376838e+05
    min    171.073400  172.897400  169.057400  170.929400  6.302010e+05
    25%    179.698575  181.010000  178.225500  179.375100  1.015912e+06
    50%    185.070300  186.094400  183.720000  184.810350  1.212792e+06
    75%    189.457200  191.262400  188.365600  189.436000  1.436933e+06
    max    197.953600  198.577700  196.753600  197.521600  4.478718e+06

                 volume  close_12_ema  close_26_ema        macd       macds  \
    count  2.520000e+02    252.000000    252.000000  252.000000  252.000000
    mean   2.438393e+08    184.247229    184.007997    0.239232    0.217932
    std    9.737240e+07      5.889254      5.303585    1.775688    1.620324
    min    1.098735e+08    172.884507    173.101244   -3.318409   -2.689569
    25%    1.880567e+08    179.462166    179.949202   -1.379656   -1.221922
    50%    2.241906e+08    184.368493    184.218385    0.344283    0.285781
    75%    2.709752e+08    189.810828    188.696025    1.817561    1.607280
    max    8.140323e+08    195.266718    193.579560    3.271141    2.991973

                macdh      signal
    count  252.000000  252.000000
    mean     0.021300   -0.003968
    std      0.637962    0.302684
    min     -2.051876   -1.000000
    25%     -0.308985    0.000000
    50%      0.056835    0.000000
    75%      0.470143    0.000000
    max      1.100467    1.000000

```python
# csv file naming convention
# <nb_name>_<data_name>_<fast>_<slow>_<signal>.csv

import os
data_name = os.path.basename(input_data_file).replace('.csv','')
nb_name = os.path.basename(nb_file).replace('.ipynb','')
csv = nb_name + '_' + data_name + '_' + str(fast_window) + '_' + str(slow_window) + '_' + str(signal_window) + '.csv'
```

```python
# here, we record the data for later use

pm.record("csv", csv)

# We don't need them, because pm stores parameters anyway
#pm.record("fast_window", fast_window)
#pm.record("slow_window", slow_window)
#pm.record("signal_window", signal_window)
```

## Save Results as CSV

We store the complete dataframe in the `output_data_path` directory.

```python
csv_file = os.path.join(output_data_path, csv )
stock.to_csv(csv_file, sep=';')
#csv_file
```

    '../data/interim/MACD_BuySell_data_12_26_9.csv'

```python

```