```python
#from google.colab import drive
#drive.mount('gdrive')
```

```python
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.layers import Dense, Input, Flatten, Conv1D, AveragePooling1D, Dropout
from tensorflow.keras.layers import concatenate, Conv2D, MaxPooling2D, MaxPooling1D, BatchNormalization, Softmax
from tensorflow.keras.models import Model, Sequential
import pandas as pd
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
import seaborn as sns

import tensorflow_probability as tfp
tfd = tfp.distributions
tfpl = tfp.layers
tfb = tfp.bijectors
```

# Bijectors and Normalizing Flows
- Build generative models, which aim to learn the underlying data distribution itself
- Useful because it is able to "generate" samples that plausibly could have come from the dataset
- Tells you if a given instance is likely (useful for anomaly detection)

## Change of variables formula
- Computes the probability density function of a random variable under a smooth invertible transformation
- Consider change of variables $X = f(Z)$ where where $Z,X \in \mathbb{R}^d$ and $f: \mathbb{R}^d \rightarrow \mathbb{R}^d$ with probability density functions for $Z$ and $X$ denoted by $p_z$ and $p_x$
- Change of variables formula:
$p_x(x) = p_z(z) \cdot |det \, J_f(z)|^{-1}$ where J is the Jacobian

## Normalizing Flows
- class of models that exploit change of variables formula to estimate an unknwon target data density
- models the distribution of data $p_x$ using a random variable $Z$ with a simple distribution $p_z$ (e.g. isotropic Gaussian) such that $X$ can be written as $X=f_{\theta}(Z)$ where $\theta$ is a parameter vector that parameterizes the smooth invertible function $f_\theta$
- $f_\theta$ is modeled using a neural network with parameters $\theta$ which are learned from the data
- neural netork must be designed to be invertible (not usually the case)
- in practice, we often construct neural networks by composing multiple simpler blocks together. In TensorFlow Probability, these simpler blocks are called *bijectors*
- apply principle of maximum likelihood to learn $\theta_{ML}$:
$$
\begin{align}
\theta_{ML} &= arg \, max \, log \, P(D;\theta) \\
where \, log \, P(D;\theta) &= \sum_{x \in D} log \, p_z(f_{\theta}^{-1}(x)) \, - \, log \, |det \, J_{f_{\theta}^{-1}}(x)| \text{ (change of variables formula)}
\end{align}
$$

## Bijectors

```python
tfb = tfp.bijectors # used to transform tensors - they have methods to perform forward and inverse transformations

z = tf.constant([1., 2., 3.])

scale = tfb.Scale(2.)
x = scale.forward(z)
x
```

    <tf.Tensor: shape=(3,), dtype=float32, numpy=array([2., 4., 6.], dtype=float32)>

```python
scale.inverse(tf.constant([5., 3., 1.]))
```

    <tf.Tensor: shape=(3,), dtype=float32, numpy=array([2.5, 1.5, 0.5], dtype=float32)>

```python
scale = tfb.Scale(2.)
shift = tfb.Shift(1.)
scale_and_shift = tfb.Chain([shift, scale]) # applies sequence of bijector objects in REVERSE order

scale_and_shift.forward(z) # scale, then shift
```

    <tf.Tensor: shape=(3,), dtype=float32, numpy=array([3., 5., 7.], dtype=float32)>

```python
scale_and_shift.inverse(tf.constant([21., 14., 7.])) # inverse shift, then inverse scale
```

    <tf.Tensor: shape=(3,), dtype=float32, numpy=array([10. ,  6.5,  3. ], dtype=float32)>

```python
normal = tfd.Normal(loc=0., scale=1.)
z = normal.sample(3)
x = scale_and_shift.forward(z)
x
```

    <tf.Tensor: shape=(3,), dtype=float32, numpy=array([-1.1484928,  4.0239944, -0.3260641], dtype=float32)>

```python
log_prob_z = normal.log_prob(z)

# change of variables formula:
log_prob_x = normal.log_prob(z) - scale_and_shift.forward_log_det_jacobian(z, event_ndims=0)
# normal.log_prob knows how to reduce dimensions of event space but need to explicity tell forward_log_det_jacobian the # of event dims
log_prob_x
```

    <tf.Tensor: shape=(3,), dtype=float32, numpy=array([-2.1890883, -2.7551532, -1.8318914], dtype=float32)>

```python
n = 10000
z = normal.sample(n)

scale = 4.5
shift = 7
scale_and_shift = tfb.Chain([tfb.Shift(shift), tfb.Scale(scale)])

# instead of Chain, can also call a bijector wihtin another bijector
scale_transf = tfb.Scale(scale)
shift_transf = tfb.Shift(shift)
scale_and_shift = shift_transf(scale_transf) # equivalent to Chain
```

```python
x = scale_and_shift.forward(z)
np.allclose(x , scale*z + shift)  # confirm that it worked
```

    True

```python
fig, [ax1, ax2] = plt.subplots(1,2, figsize=(10,5), sharey=True, sharex=True)
ax1.hist(z, bins=60, density=True)
ax2.hist(x, bins=60, density=True)
plt.show()
```

```python
inv_x = scale_and_shift.inverse(x)
np.allclose(inv_x, z)
```

    True

```python
log_prob_x = normal.log_prob(z) - scale_and_shift.forward_log_det_jacobian(z, event_ndims=0) # in terms of z

log_prob_x = (normal.log_prob(scale_and_shift.inverse(x))
              + scale_and_shift.inverse_log_det_jacobian(x, event_ndims=0)) # in terms of x
```

```python
# Broadcasting
x = tf.random.normal(shape=(100,1))

softfloor = tfb.Softfloor(temperature=0.01) # differentiable approximation to tf.math.floor
y = softfloor.forward(x)
print(y.shape)
```

    (100, 1)

```python
softfloor = tfb.Softfloor(temperature=[0.01, 0.1, 1.]) # apply transformation with different parameter values
y = softfloor.forward(x)
print(y.shape)
```

    (100, 3)

```python
def make_plot(nparams, bijector, params, x):
  bijector_params = tuple(getattr(bijector, name) for name in params)
  upper_params = [name[0].upper() + name[1:] for name in params]
  fig = plt.figure(figsize=(14,5))
  lines = plt.plot(np.tile(x, nparams), bijector.forward(x))
  for l in zip(lines, *bijector_params):
    labels = ': {:.2f}, '.join(upper_params) + ': {:.2f}'
    l[0].set_label(labels.format(*l[1:]))
  plt.legend()
  plt.show()
```

```python
x = np.linspace(-2, 2, 2000)[..., np.newaxis]
make_plot(3, softfloor, ['temperature'], x)
```

```python
exps = tfb.GumbelCDF(loc=[0.5, 1, 1.5, 2, 3], scale=[1, 2, 2, 3, 4])
x = np.linspace(-10, 10, 2000, dtype=np.float32)[..., np.newaxis]
make_plot(5, exps, ['loc', 'scale'], x)
```

```python
# higher dimensional bijectors
# represent scale bijectors as a square matrix, B, where y = Bx as opposed to just a scalar as shown previously
bijector = tfb.ScaleMatvecDiag(scale_diag=[1.5, -0.5]) # B is a diagonal matrix (non-diagonal entries are zero)

uniform = tfd.Uniform(low=[0., 0.], high=[1., 1.]) # sample from the unit square (two dimensional)
x = uniform.sample()

y = bijector(x) # maps from rectangle to rectangle
y
```

    <tf.Tensor: shape=(2,), dtype=float32, numpy=array([ 0.6959637 , -0.27992177], dtype=float32)>

```python
bijector = tfb.ScaleMatvecTriL(scale_tril=[[-1., 0.],
                                           [-1., -1.]]) # B is a lower triangular matrix (upper right entries are zero)
y = bijector(x) # maps from rectangle to quadrilateral
y
```

    <tf.Tensor: shape=(2,), dtype=float32, numpy=array([-0.4639758, -1.0238193], dtype=float32)>

## LinearOperator and ScaleMatvecLinearOperator

```python
# these allow more flexibility than ScaleMatvecDiag and caleMatvecTriL
scale = tf.linalg.LinearOperatorDiag(diag=[1.5, -0.5]) # subclass of LinearOperator class
bijector = tfb.ScaleMatvecLinearOperator(scale) # convert to bijector using ScaleMatvecLinearOperator (same as the one made before)
y = bijector(x)
y
```

    <tf.Tensor: shape=(2,), dtype=float32, numpy=array([ 0.6959637 , -0.27992177], dtype=float32)>

```python
# can do same process for full matrix (rather than just diagonal or lower triangular)
B = [[0.5, 1.5],
     [1.5, 0.5]]
scale = tf.linalg.LinearOperatorFullMatrix(matrix=B)
bijector = tfb.ScaleMatvecLinearOperator(scale)
y = bijector(x)
y
```

    <tf.Tensor: shape=(2,), dtype=float32, numpy=array([1.0717533 , 0.97588545], dtype=float32)>

```python
# batch operations and broadcasting
x = uniform.sample(10) # B.shape[-1] must equal x.shape[-1], in this case 2=2
x
```

    <tf.Tensor: shape=(10, 2), dtype=float32, numpy=
    array([[0.44886053, 0.37525928],
           [0.10732555, 0.2851348 ],
           [0.31860888, 0.3733406 ],
           [0.661337  , 0.1269263 ],
           [0.31118476, 0.9802486 ],
           [0.13889289, 0.9075177 ],
           [0.9656341 , 0.4473244 ],
           [0.9066206 , 0.6285068 ],
           [0.6656736 , 0.45307875],
           [0.80676186, 0.3017167 ]], dtype=float32)>

```python
scale = tf.linalg.LinearOperatorDiag(diag=[1.5, -0.5])
scale.to_dense()
```

    <tf.Tensor: shape=(2, 2), dtype=float32, numpy=
    array([[ 1.5,  0. ],
           [ 0. , -0.5]], dtype=float32)>

```python
bijector = tfb.ScaleMatvecLinearOperator(scale)
y = bijector(x) # output will be batch shape by event shape
y
```

    <tf.Tensor: shape=(10, 2), dtype=float32, numpy=
    array([[ 0.6732908 , -0.18762964],
           [ 0.16098833, -0.1425674 ],
           [ 0.47791332, -0.1866703 ],
           [ 0.9920055 , -0.06346315],
           [ 0.46677715, -0.4901243 ],
           [ 0.20833933, -0.45375884],
           [ 1.4484512 , -0.2236622 ],
           [ 1.359931  , -0.3142534 ],
           [ 0.9985104 , -0.22653937],
           [ 1.2101429 , -0.15085834]], dtype=float32)>

```python
# batched bijector to apply multiple bijectors to x
diag = tf.stack((tf.constant([1.,-1.]),
                tf.constant([-1., 1.]))) # (2,2)
scale = tf.linalg.LinearOperatorDiag(diag=diag) # (2,2,2) two bijectors where B = 2x2 matrix
bijector = tfb.ScaleMatvecLinearOperator(scale=scale)

# broadcast samples across both bijectors and broadcast bijectors across all samples (requires x to have a batch dim)
x = tf.expand_dims(x, axis=1)
x.shape
```

    TensorShape([10, 1, 2])

```python
y = bijector(x)
y.shape # sample dim, bijector batch dim, event dim
```

    TensorShape([10, 2, 2])

## TransformedDistribution

```python
normal = tfd.Normal(loc=0., scale=1.)
z = normal.sample(3)

scale_and_shift = tfb.Chain([tfb.Shift(1.), tfb.Scale(2.)])

x = scale_and_shift.forward(z)

log_prob_z = normal.log_prob(z)

log_prob_x1 = log_prob_z - scale_and_shift.forward_log_det_jacobian(z, event_ndims=0)
log_prob_x2 = log_prob_z + scale_and_shift.inverse_log_det_jacobian(x, event_ndims=0)
log_prob_x3 = normal.log_prob(scale_and_shift.inverse(x)) + scale_and_shift.inverse_log_det_jacobian(x, event_ndims=0)
# typically calculate it using only x (log_prob_x3) rather than z --> use this form to minimize nll to optimize bijector parameters
np.allclose(log_prob_x1, log_prob_x2)
```

    True

```python
# base distribution --> transformation --> data distribution
# can sample from base distribution (usually something simple like diagonal Gaussian) and apply transformation to use
                                                                   # generative model to 'generate' plausible samples
# forward pass of bijector is used for sampling
# inverse and inverse Jacobian are used for calculating log likelihood during training

normal = tfd.Normal(loc=0., scale=1.) # base distribution
z = normal.sample(3)

exp = tfb.Exp() # transformation
x = exp.forward(z) # data sample

log_normal = tfd.TransformedDistribution(normal, exp) # define data distribution with distribution object
log_normal
```

    <tfp.distributions.TransformedDistribution 'expNormal' batch_shape=[] event_shape=[] dtype=float32>

```python
# equivalently:
log_normal = exp(normal) # same as using TransformedDistribution
log_normal
```

    <tfp.distributions.TransformedDistribution 'expNormal' batch_shape=[] event_shape=[] dtype=float32>

```python
log_normal.sample() # sample from base disatribution (normal) and perform forward pass using bijector (exp)
```

    <tf.Tensor: shape=(), dtype=float32, numpy=2.0651793>

```python
log_normal.log_prob(x) # apply change of variables formula using inverse and inverse_log_get_jacobian
```

    <tf.Tensor: shape=(3,), dtype=float32, numpy=array([-2.385951  , -1.496285  , -0.43637705], dtype=float32)>

```python
n = 10000
loc = 0
scale = 0.5

normal = tfd.Normal(loc=loc, scale=scale)
print('batch shape:', normal.batch_shape)
print('event shape: ', normal.event_shape)
```

    batch shape: ()
    event shape:  ()

```python
exp = tfb.Exp()
log_normal_td = exp(normal) # create TransformedDistribution object
```

```python
print('batch shape:', log_normal_td.batch_shape)
print('event shape: ', log_normal_td.event_shape)
```

    batch shape: ()
    event shape:  ()

```python
z = normal.sample(n) # sample from base distribution
plt.hist(z, bins=100, density=True)
plt.show()
```

```python
x = log_normal_td.sample(n) # sample from transformed distribution
plt.hist(x, bins=100, density=True)
plt.show()
```

```python
# compare to log normal distribution built into TensorFlow
log_normal = tfd.LogNormal(loc=loc, scale=scale)
l = log_normal.sample(n)
plt.hist(l, bins=100, density=True)
plt.show()
```

```python
log_prob = log_normal.log_prob(x)
log_prob_td = log_normal_td.log_prob(x)
np.allclose(log_prob, log_prob_td)
```

    True

```python
tril = tf.random.normal((2,4,4))
scale_low_tri = tf.linalg.LinearOperatorLowerTriangular(tril)
# ^ converts tril into lower triangular matrix by converting upper right part of last two dims to zero
# ^ in this case, it creates two 4x4 lower triangular matrices
scale_low_tri.to_dense() # visualize
```

    <tf.Tensor: shape=(2, 4, 4), dtype=float32, numpy=
    array([[[-1.7952086 ,  0.        ,  0.        ,  0.        ],
            [-0.8528584 , -1.4626194 ,  0.        ,  0.        ],
            [ 1.3494258 ,  0.4181135 , -1.417949  ,  0.        ],
            [-0.17694473, -0.16085903, -0.46292174, -2.0953758 ]],

           [[ 0.11327876,  0.        ,  0.        ,  0.        ],
            [-0.9314292 , -0.5139704 ,  0.        ,  0.        ],
            [-0.14283006, -0.8427318 , -1.3306483 ,  0.        ],
            [ 0.99171776, -1.4028883 ,  0.8568086 , -0.00419014]]],
          dtype=float32)>

```python
# define scaling bijector (y = Bx where B is the lower triangular matrices)
scale_lin_op = tfb.ScaleMatvecLinearOperator(scale_low_tri)
```

```python
# create TransformedDistribution where we can override batch/event shapes of normal (since its shapes are ())
#mvn = tfd.TransformedDistribution(normal, scale_lin_op, batch_shape=[2], event_shape=[4])
# overriding batch/event shape is deprecated
```

```python
# instead of above, you can equivalently do this:
mvn2 = tfd.MultivariateNormalLinearOperator(loc=0, scale=scale_low_tri)
print(mvn2)
```

    tfp.distributions.MultivariateNormalLinearOperator("MultivariateNormalLinearOperator", batch_shape=[2], event_shape=[4], dtype=float32)

## Subclassing bijectors

```python
class MySigmoid(tfb.Bijector): # note sigmoid bijector already exists so this is just for learning purposes
  def __init__(self, validate_args=False, name='sigmoid'):
    super(MySigmoid, self).__init__(
        validate_args=validate_args, forward_min_event_ndims=0, name=name)
    # validate_args - check any inputs passed to the class method are valid
    # forward_min_event_ndims - need to tell the bijector the min event shape it needs to act upon

  def _forward(self, x):
    return tf.math.sigmoid(x)

  def _inverse(self, y):
    return tf.math.log(y) - tf.math.log(1-y) # logit is reverse of sigmoid

  def _inverse_log_det_jacobian(self, y):
    return -tf.math.log(y) - tf.math.log(1-y)
    # return -self._forward_log_det_jacobian(self.inverse(y)) # could do it this way if _forward version is correctly calculated

  def _forward_log_det_jacobian(self, x):
    # return -tf.math.softplus(-x) - tf.math.softplus(x)
    return -self._inverse_log_det_jacobian(self.forward(x)) # only need to know how to do one of these since can use one to find the other
```

```python
class Cubic(tfb.Bijector): # (a*x+b)^3
  def __init__(self, a, b, validate_args=False, name='Cubic'):
    super(Cubic, self).__init__(validate_args=validate_args, forward_min_event_ndims=0, name=name)
    self.a = tf.cast(a, tf.float32)
    self.b = tf.cast(b, tf.float32)
    if validate_args:
      assert tf.reduce_mean(tf.cast(tf.math.greater_equal(tf.abs(self.a), 1e-5), tf.float32)) == 1.0
      assert tf.reduce_mean(tf.cast(tf.math.greater_equal(tf.abs(self.b), 1e-5), tf.float32)) == 1.0

  def _forward(self, x):
    x = tf.cast(x, tf.float32)
    return tf.squeeze(tf.pow(self.a * x + self.b, 3))

  def _inverse(self, y):
    y = tf.cast(y, tf.float32)
    return (tf.math.sign(y) * tf.pow(tf.abs(y), 1/3) - self.b) / self.a

  def _forward_log_det_jacobian(self, x):
    x = tf.cast(x, tf.float32)
    return tf.math.log(3. * tf.abs(self.a)) + 2. * tf.math.log(tf.abs(self.a * x + self.b))
```

```python
cubic = Cubic(a=[1.0, -2.0], b=[-1.0, 0.4], validate_args=True)
```

```python
x = tf.constant([[1, 2], [3, 4]])
y = cubic.forward(x)
y
```

    <tf.Tensor: shape=(2, 2), dtype=float32, numpy=
    array([[   0.      ,  -46.655994],
           [   8.      , -438.97598 ]], dtype=float32)>

```python
cubic.inverse(y)
```

    <tf.Tensor: shape=(2, 2), dtype=int32, numpy=
    array([[1, 2],
           [3, 4]], dtype=int32)>

```python
x = np.linspace(-10, 10, 500).reshape(-1,1)
plt.plot(x, cubic.forward(x))
```

```python
cubic.forward(x).shape # two lines for two values of a/b
```

    TensorShape([500, 2])

```python
plt.plot(x, cubic.inverse(x))
```

```python
plt.plot(x, cubic.forward_log_det_jacobian(x, event_ndims=0))
```

```python
# create transformed distribution
normal = tfd.Normal(loc=[0., 0.], scale=[1., 1.])
cubed_normal = tfd.TransformedDistribution(normal, cubic)
```

```python
n = 1000
g = cubed_normal.sample(n)
g.shape
```

    TensorShape([1000, 2])

```python
xx = np.linspace(-0.5, 0.5, 100)
yy = np.linspace(-0.5, 0.5, 100)
X, Y = np.meshgrid(xx, yy)

fig, ax = plt.subplots(1,1)
Z = cubed_normal.prob(np.dstack((X, Y)))
Z = tf.reduce_mean(Z, axis=-1)
cp = ax.contourf(X, Y, Z)
fig.colorbar(cp)
```

```python
# Invert bijector - inverses another bijector
inverse_cubic = tfb.Invert(cubic)
inv_cubed_normal = inverse_cubic(normal) # create TransformedDistribution

n = 1000
g = inv_cubed_normal.sample(n)
g.shape
```

    TensorShape([1000, 2])

```python
xx = np.linspace(-3, 3, 100)
yy = np.linspace(-2, 2, 100)
X, Y = np.meshgrid(xx, yy)

fig, ax = plt.subplots(1,1)
Z = inv_cubed_normal.prob(np.dstack((X, Y)))
Z = tf.reduce_mean(Z, axis=-1)
cp = ax.contourf(X, Y, Z)
fig.colorbar(cp)
```

```python
plt.figure(figsize=(12,4))
plt.subplot(1,2,1)
plt.hist(g[...,0], bins=50, density=True)

plt.subplot(1,2,2)
plt.hist(g[...,1], bins=50, density=True)
plt.show()
```

```python
# training bijectors
# create misture of Gaussians
probs = [0.45, 0.55]
mix_gauss = tfd.Mixture(cat=tfd.Categorical(probs=probs),
                        components=[tfd.Normal(loc=2.3, scale=0.4),
                                    tfd.Normal(loc=-0.8, scale=0.4)])
# create dataset
X_train = mix_gauss.sample(10000)
X_train = tf.data.Dataset.from_tensor_slices(X_train)
X_train = X_train.batch(128)

X_test = mix_gauss.sample(1000)
X_test = tf.data.Dataset.from_tensor_slices(X_test)
X_test = X_test.batch(128)

print(X_train.element_spec)
print(X_test.element_spec)
```

    TensorSpec(shape=(None,), dtype=tf.float32, name=None)
    TensorSpec(shape=(None,), dtype=tf.float32, name=None)

```python
x = np.linspace(-5,5,100)
plt.plot(x, mix_gauss.prob(x))
plt.show()
```

```python
# make trainable bijector to fit data
trainable_inv_cubcic = tfb.Invert(Cubic(tf.Variable(0.25), tf.Variable(-0.1)))
trainable_inv_cubcic.trainable_variables
```

    (<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.25>,
     <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=-0.1>)

```python
# make trainable TransformedDistribution
normal = tfd.Normal(loc=0., scale=1.)
trainable_dist = tfd.TransformedDistribution(normal, trainable_inv_cubcic)
```

```python
x = np.linspace(-5,5,100)
plt.plot(x, mix_gauss.prob(x), label='data')
plt.plot(x, trainable_dist.prob(x), label='trainable')
plt.legend()
plt.show()
```

```python
num_epochs = 10
opt = tf.keras.optimizers.Adam()
train_losses = []
test_losses = []

for epoch in range(num_epochs):
  print('Epoch {}...'.format(epoch))
  train_loss = tf.keras.metrics.Mean()
  test_loss = tf.keras.metrics.Mean()

  for train_batch in X_train:
    with tf.GradientTape() as tape:
      tape.watch(trainable_inv_cubcic.trainable_variables)
      loss = -trainable_dist.log_prob(train_batch)
    train_loss(loss)
    grads = tape.gradient(loss, trainable_inv_cubcic.trainable_variables)
    opt.apply_gradients(zip(grads, trainable_inv_cubcic.trainable_variables))
  train_losses.append(train_loss.result().numpy())

  for test_batch in X_test:
    loss = -trainable_dist.log_prob(test_batch)
    test_loss(loss)
  test_losses.append(test_loss.result().numpy())
```

    Epoch 0...
    Epoch 1...
    Epoch 2...
    Epoch 3...
    Epoch 4...
    Epoch 5...
    Epoch 6...
    Epoch 7...
    Epoch 8...
    Epoch 9...

```python
# plot learning curves
plt.plot(train_losses, label='train')
plt.plot(test_losses, label='test')
plt.legend()
plt.show()
```

```python
x = np.linspace(-5,5,100)
plt.plot(x, mix_gauss.prob(x), label='data')
plt.plot(x, trainable_dist.prob(x), label='trainable')
plt.legend()
plt.show()
```

```python
trainable_inv_cubcic.trainable_variables
```

    (<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.56060535>,
     <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=-0.39960486>)

## Normalizing Flows
- create compicated distributions by apply bijectors chained together to a simple, well-understood distribution (e.g. Gaussian)
- Calculating determinant is computationally complex: $O(D^3)$, so simplifications are required
- The determinant of a upper/lower triangular matrix is the product of the diagonal entries so complexity scales linearly
- Force lower triangular matrix using autoregressive flows where variables only depend on components lower than or equal to them.
  - masked autoregressive flows: mask weights so that $x_i$ only depends on $x_j$ with $j<i$
    - forward pass through bijector is slow but inverse pass is fast

### Autoregressive flows

```python
# let's say data is D-dimensional and is represented by vector x
# we define an ordereing to the data features, indexed by i
# express joint distribution of all data features as a product of conditional distributions where each conditional probability of
      # x_i depends only on x_0 : x_i-1
# z ~ N(0,I)
# x[i] = z[i] * scale(z[0:i-1]) + loc(x[0:i-1]) # each x is modeled as gaussian with mean/loc functions of x_0 : x_i-1
# this is a masked autoregressive flow
# Jacobian determinant is easy to compute since the Jacobian is lower triangular so it is just a product of diagonal (linear complexity)

# use this to define autoregressive flow bijector
made = tfb.AutoregressiveNetwork(params=2, event_shape=[3], hidden_units=[16, 16], activation='sigmoid')
# made --> masked auto encoder for distribution estimation
# feed forward neural network, in this case, with 2 hidden layers with size 16
made(tf.random.normal([2,3])) # input batch of 2, with input len 3 (necessary since event_shape=3)
# output has same shape as input with extra dimension on right of size 2 because params=2
# think of this as implementing the scale/loc functions described above
# network outputs two parameters, each with length 3 --> can use first as mean and second as log of stddev
# so each feature has a mean and stddev
# network respects the autoregressive property
```

    <tf.Tensor: shape=(2, 3, 2), dtype=float32, numpy=
    array([[[ 0.        ,  0.        ],
            [-0.2949697 , -0.22095796],
            [-0.5112145 , -0.6393012 ]],

           [[ 0.        ,  0.        ],
            [-0.29510465, -0.22086605],
            [-0.5108534 , -0.63928604]]], dtype=float32)>

```python
# we want to use parameters output by made network to shift and scale z[i]
maf_bijector = tfb.MaskedAutoregressiveFlow(shift_and_log_scale_fn=made) # this is a bijector that does that
# input z into bijector and it applies shift and log scale based on parameters of made network
# has to be performed in a loop to calculate earlier features before later features can be determined
# inverse does not require loop since we already have the entire vector x, so calculating z is easy
normal = tfd.MultivariateNormalDiag(loc=0, scale_diag=[1]*3)
maf = tfd.TransformedDistribution(normal, maf_bijector)
maf
```

    <tfp.distributions.TransformedDistribution 'masked_autoregressive_flowMultivariateNormalDiag' batch_shape=[] event_shape=[3] dtype=float32>

```python
# can also specify bijector to just shift using the following (only passing in first parameter of made) (or alternatively just scale)
maf_bijector = tfb.MaskedAutoregressiveFlow(lambda y: (made(y)[...,0], None), is_constant_jacobian=True)
```

```python
# RealNVP
# z ~ N(0,I)
# x[0: d] = z[0: d]
# x[d: D] = z[d: D] * scale(z[0: d]) + loc(z[0: d])
shift_and_log_scale_fn = tfb.real_nvp_default_template(hidden_layers=[32, 32], activation=tf.nn.relu)
# input of this function is z[0: d] and output is scale(z[0: d]) and loc(z[0: d])

shift_and_log_scale_fn = tfb.real_nvp_default_template(hidden_layers=[32, 32], activation=tf.nn.relu, shift_only=True)
# returns tuple, but second one is None since it is shift only
# this is the NICE model - Jacobian is identity matrix so it is volume preserving

realnvp_bijector = tfb.RealNVP(num_masked=2, shift_and_log_scale_fn=shift_and_log_scale_fn)
# num_masked is little d above (can also use fraction_masked)
# forward and inverse functions do not require loop to compute
# downside is it is not as expressive as autoregressive flow

mvn = tfd.MultivariateNormalDiag(loc=[0., 0., 0.])
realnvp = tfd.TransformedDistribution(mvn, realnvp_bijector)
```

```python
# because RealNVP doesn't change some of the inputs, we need to combine multiple chained bijectors with
          # permutation bijector in between which allows all features to have a chance to be in d:D range
permute = tfp.bijectors.Permute(permutation=[1, 2, 0]) # moves axis 0 to last dimension each time it is called

realnvp1 = tfb.RealNVP(fraction_masked=0.5, shift_and_log_scale_fn=tfb.real_nvp_default_template(hidden_layers=[32,32]))
realnvp2 = tfb.RealNVP(fraction_masked=0.5, shift_and_log_scale_fn=tfb.real_nvp_default_template(hidden_layers=[32,32]))
realnvp3 = tfb.RealNVP(fraction_masked=0.5, shift_and_log_scale_fn=tfb.real_nvp_default_template(hidden_layers=[32,32]))

chained_bijector = tfb.Chain([realnvp3, permute, realnvp2, permute, realnvp1])

mvn = tfd.MultivariateNormalDiag(loc=[0., 0., 0.])
realnvp = tfd.TransformedDistribution(mvn, chained_bijector)
```

```python
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
n_samples=1000
noisy_moons = datasets.make_moons(n_samples=n_samples, noise=0.05)
X, y = noisy_moons
X_data = StandardScaler().fit_transform(X)
xlim, ylim = [-2, 2], [-2, 2]
y_label = y.astype(np.bool)
X_train, y_train = X_data[..., 0], X_data[..., 1]
plt.scatter(X_train[y_label], y_train[y_label], s=10, color='blue')
plt.scatter(X_train[~y_label], y_train[~y_label], s=10, color='red')
plt.xlim(xlim)
plt.ylim(ylim)
```

```python
base_distribution = tfd.Independent(tfd.Normal(loc=[0, 0], scale=[1,1]), reinterpreted_batch_ndims=1)

def make_masked_autoregressive_flow(hidden_units=[16, 16], activation='relu'):
  made = tfb.AutoregressiveNetwork(
      params=2, event_shape=[2], hidden_units=hidden_units, activation=activation
  )
  return tfb.MaskedAutoregressiveFlow(shift_and_log_scale_fn=made)

trainable_distribution = tfd.TransformedDistribution(base_distribution, make_masked_autoregressive_flow())
```

```python
from mpl_toolkits.axes_grid1 import make_axes_locatable
from tensorflow.compat.v1 import logging
logging.set_verbosity(logging.ERROR)
```

```python
def plot_contour_prob(dist, rows=1, title=[''], scale_fig=4):
  cols = int(len(dist) / rows)
  xx = np.linspace(-5, 5, 100)
  yy = np.linspace(-5, 5, 100)
  X, Y = np.meshgrid(xx, yy)

  fig, ax = plt.subplots(rows, cols, figsize=(scale_fig * cols, scale_fig * rows))
  fig.tight_layout(pad=4.5)

  i = 0
  for r in range(rows):
    for c in range(cols):
      Z = dist[i].prob(np.dstack((X,Y)))
      if len(dist) == 1:
        axi = ax
      elif rows == 1:
        axi = ax[c]
      else:
        axi = ax[r, c]
      p = ax.contourf(X, Y, Z)
      i += 1
```

```python
activation = 'sigmoid'
maf = tfd.TransformedDistribution(base_distribution, make_masked_autoregressive_flow(activation=activation))
plot_contour_prob([maf], scale_fig=6)
```

```python
X = base_distribution.sample((1000, 2))
names = [base_distribution.name, trainable_distribution.name]
samples = [X, trainable_distribution.bijector.forward(X)]
```

```python
def _plot(results, rows=1, legend=False):
  cols = int(len(results) / rows)
  f, arr = plt.subplots(rows, cols, figsize=(4*cols, 4*rows))
  i = 0
  for r in range(rows):
    for c in range(cols):
      res = results[i]
      X, Y = res[..., 0].numpy(), res[..., 1].numpy()
      if rows == 1:
        p = arr[c]
      else:
        p = arr[r, c]
      p.scatter(X, Y, s=10, color='red')
      p.set_xlim([-5, 5])
      p.set_ylim([-5, 5])
      p.set_title(names[i])
      i += 1
```

```python
_plot(samples)
```

```python
from tensorflow.keras.callbacks import LambdaCallback

def train_dist_routine(trainable_distribution, n_epochs=200, batch_size=None, n_disp=100):
  x_ = Input(shape=(2,), dtype=tf.float32)
  log_prob = trainable_distribution.log_prob(x_)
  model = Model(x_, log_prob)

  model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.0005), loss = lambda _, log_prob: -log_prob)

  ns = X_data.shape[0]
  if batch_size is None:
    batch_size = ns

  # display loss every n_disp epoch
  epoch_callback = LambdaCallback(
      on_epoch_end = lambda epoch, logs:
      print('\n Epoch {}/{}'.format(epoch+1, n_epochs, logs),
            '\n\t ' + (': {:.4f}, '.join(logs.keys()) + ': {:.4f}').format(*logs.values()))
      if epoch % n_disp == 0 else False)

  history = model.fit(X_data,
                      y=np.zeros((ns, 0), dtype=np.float32),
                      batch_size=batch_size,
                      epochs=n_epochs,
                      validation_split=0.2,
                      shuffle=True,
                      verbose=False,
                      callbacks=[epoch_callback])
  return history
```

```python
history = train_dist_routine(trainable_distribution, n_epochs=600, n_disp=50)
```

     Epoch 1/600
    	 loss: 2.1344, val_loss: 2.1201

     Epoch 51/600
    	 loss: 2.0969, val_loss: 2.0867

     Epoch 101/600
    	 loss: 2.0777, val_loss: 2.0618

     Epoch 151/600
    	 loss: 2.0606, val_loss: 2.0427

     Epoch 201/600
    	 loss: 2.0478, val_loss: 2.0307

     Epoch 251/600
    	 loss: 2.0377, val_loss: 2.0224

     Epoch 301/600
    	 loss: 2.0294, val_loss: 2.0170

     Epoch 351/600
    	 loss: 2.0219, val_loss: 2.0132

     Epoch 401/600
    	 loss: 2.0154, val_loss: 2.0107

     Epoch 451/600
    	 loss: 2.0098, val_loss: 2.0065

     Epoch 501/600
    	 loss: 2.0045, val_loss: 2.0022

     Epoch 551/600
    	 loss: 2.0002, val_loss: 1.9975

```python
train_losses = history.history['loss']
valid_losses = history.history['val_loss']

plt.plot(train_losses, label='train')
plt.plot(valid_losses, label='valid')
plt.legend()
plt.show()
```

```python
X = base_distribution.sample((1000, 2))
names = [base_distribution.name, trainable_distribution.name]
samples = [X, trainable_distribution.bijector.forward(X)]
_plot(samples)
```

```python
plot_contour_prob([trainable_distribution], scale_fig=6)
```

```python
# chained masked autoregressive flow bijectors

num_bijectors = 6
bijectors = []

for i in range(num_bijectors):
  masked_auto_i = make_masked_autoregressive_flow(hidden_units=[256, 256], activation='relu')
  bijectors.append(masked_auto_i)
  bijectors.append(tfb.Permute(permutation=[1,0]))

flow_bijector = tfb.Chain(list(reversed(bijectors[:-1])))
```

```python
trainable_distribution = tfd.TransformedDistribution(base_distribution, flow_bijector)
```

```python
def make_samples():
  x = base_distribution.sample((1000, 2))
  samples = [x]
  names = [base_distribution.name]
  for bijector in reversed(trainable_distribution.bijector.bijectors):
    x = bijector.forward(x)
    samples.append(x)
    names.append(bijector.name)
  return names, samples

names, samples = make_samples()
```

```python
_plot(samples, rows=3)
```

```python
history = train_dist_routine(trainable_distribution, n_epochs=600, n_disp=50)
```

     Epoch 1/600
    	 loss: 2.9017, val_loss: 2.7577

     Epoch 51/600
    	 loss: 2.1249, val_loss: 2.1591

     Epoch 101/600
    	 loss: 2.0991, val_loss: 2.1085

     Epoch 151/600
    	 loss: 2.0852, val_loss: 2.0715

     Epoch 201/600
    	 loss: 3.3094, val_loss: 3.2642

     Epoch 251/600
    	 loss: 2.2898, val_loss: 2.2906

     Epoch 301/600
    	 loss: 1.9766, val_loss: 2.1654

     Epoch 351/600
    	 loss: 1.8411, val_loss: 2.0193

     Epoch 401/600
    	 loss: 1.6904, val_loss: 2.0140

     Epoch 451/600
    	 loss: 1.5796, val_loss: 1.9824

     Epoch 501/600
    	 loss: 1.5000, val_loss: 2.5046

     Epoch 551/600
    	 loss: 1.4864, val_loss: 6.3518

```python
train_losses = history.history['loss']
valid_losses = history.history['val_loss']

plt.plot(train_losses, label='train')
plt.plot(valid_losses, label='valid')
plt.legend()
plt.show()
```

```python
names, samples = make_samples()
_plot(samples, rows=3)
```

```python
plot_contour_prob([trainable_distribution], scale_fig=6)
```

```python

```


# Discrete Signals

*This Jupyter notebook is part of a [collection of notebooks](../index.ipynb) in the bachelors module Signals and Systems, Comunications Engineering, Universität Rostock. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## Elementary Operations

Operations like superposition, shifting and flipping can be used to construct signals with a more complex structure than by the [standard signals](standard_signals.ipynb) alone. In the following, a set of elementary operations is introduced  that are frequently used in discrete signal processing for this purpose. Note that the equivalent operation to the [temporal scaling of a continuous signal](../continuous_signals/operations.ipynb#Temporal-Scaling) is not defined for a discrete signal.

### Superposition

The weighted superposition $x[k]$ of two signals $x_1[k]$ and $x_2[k]$ is given as

\begin{equation}
x[k] = A \cdot x_1[k] + B \cdot x_2[k]
\end{equation}

with the complex weights $A, B \in \mathbb{C}$.

**Example**

The following example illustrates the superposition of two harmonic signals $x(t) = A \cdot \cos[\Omega_1 k] + B \cdot \cos[\Omega_2 k]$ with weights $A$, $B$ and normalized frequencies $\Omega_1$ and $\Omega_2$.

```python
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

k = np.arange(0, 51)
x = np.cos(2 * np.pi / 10 * k) + 2 * np.cos(2 * np.pi / 15 * k)

plt.figure(figsize=(6, 3))
plt.stem(k, x)
plt.xlabel('$k$')
plt.ylabel('$x[k]$')
plt.gca().margins(y=0.1)
```

### Shift

The shift of a signal $s[k]$ by the index $\kappa$ is defined as

\begin{equation}
x[k] = s[k-\kappa]
\end{equation}

with $\kappa \in \mathbb{Z}$. The signal $s[k]$ is

* shifted to the right for $\kappa > 0$
* shifted to the left for $\kappa < 0$

The shift of a signal is a frequently applied operation in discrete signal processing. For instance for the description of systems by linear difference equations with constant coefficients. For a discrete signal which has been derived by [temporal sampling from a continuous signal](../sampling/ideal.ipynb), the shift can be interpreted as [temporal shift](../continuous_signals/operations.ipynb#Temporal-Shift) by the time $\tau = \kappa \cdot T$ where $T$ denotes the sampling interval.

**Example**

In order to illustrate the shifting of signals, the construction of a [sawtooth signal](https://en.wikipedia.org/wiki/Sawtooth_wave) by a superposition of shifted ramp signals $k \cdot \text{rect}_N[k]$ is shown. The sawtooth signal is given as periodic continuation of the ramp signal

\begin{equation}
x[k] = \sum_{\nu = -\infty}^{\infty} (k - \nu \cdot N) \cdot \text{rect}_N[k - \nu \cdot N]
\end{equation}

The signal can be computed efficiently using the [modulo operation](https://en.wikipedia.org/wiki/Modulo_operation)

\begin{equation}
x[k] = k \bmod N
\end{equation}

which is illustrated in the following

```python
def sawtooth(k, N):
    return np.mod(k, N)

k = np.arange(-10, 40)
x = sawtooth(k, 10)

plt.figure(figsize=(6, 3))
plt.stem(k, x)
plt.xlabel('$k$')
plt.ylabel('$x[k]$')
plt.gca().margins(y=0.1)
```

### Flipping

The flipping of a signal $s[k]$ is defined as

\begin{equation}
x[k] = s[\kappa - k]
\end{equation}

with $\kappa \in \mathbb{Z}$. The flipping operation can also be represented as a reversal of the index $k$ of the signal $s[k]$ followed by a shift of $\kappa$ of the reversed signal, as $s[\kappa - k] = s[- (k - \kappa)]$. The operation can interpreted geometrically as a mirroring of the signal $s[k]$ at the vertical axis $k = \frac{\kappa}{2}$.

For $\kappa = 0$ this results in a reversal of the signal. The reversal can be interpreted as time-reversal for a discrete signal which has been derived by temporal sampling from a continuous signal.

**Example**

The following example illustrates the temporal flipping of the sawtooth signal $x[k]$ introduced above for $\kappa = 3$.

```python
x = sawtooth(3 - k, 10)

plt.figure(figsize=(6, 3))
plt.stem(k, x)
plt.xlabel('$k$')
plt.ylabel('$x[k]$')
plt.gca().margins(y=0.1)
```

**Excercise**

* Realize the temporal flipping by splitting it into two operations:
    1. time-reversal and
    2. temporal shift.

**Copyright**

This notebook is provided as [Open Educational Resource](https://en.wikipedia.org/wiki/Open_educational_resources). Feel free to use the notebook for your own purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Sascha Spors, Continuous- and Discrete-Time Signals and Systems - Theory and Computational Examples*.


# Polar and Cylindrical Frame of Reference

> Renato Naville Watanabe, Marcos Duarte
> [Laboratory of Biomechanics and Motor Control](http://pesquisa.ufabc.edu.br/bmclab)
> Federal University of ABC, Brazil

Consider that we have the position vector $\bf\vec{r}$ of a particle, moving in a circular path indicated in the figure below by a dashed line. This vector $\bf\vec{r(t)}$ is described in a fixed reference frame as:

\begin{equation}
{\bf\vec{r}}(t) = x(t){\bf\hat{i}} + y(t){\bf\hat{j}} + z(t){\bf\hat{k}}
\label{eq_1}
\end{equation}

Figure. Position vector of a moving particle moving in a circular path.

Naturally, we could describe all the kinematic variables in the fixed reference frame. But in circular motions, is convenient to define a basis with a vector in the direction of the position vector  $\bf\vec{r}$. So, the vector  $\bf\hat{e_R}$ is defined as:

\begin{equation}
{\bf\hat{e_R}} =  \frac{\bf\vec{r}}{\Vert{\bf\vec{r} }\Vert}
\label{eq_2}
\end{equation}

The second vector of the basis can be obtained by the cross multiplication between $\bf\hat{k}$ and $\bf\hat{e_R}$:

\begin{equation}
{\bf\hat{e_\theta}} = {\bf\hat{k}} \times {\bf\hat{e_R}}
\label{eq_3}
\end{equation}

The third vector of the basis is the conventional ${\bf\hat{k}}$ vector.

Figure. Position vector of a particle moving in a circular path and a time-varying basis>

This basis can be used also for non-circular movements. For a 3D movement, the versor ${\bf\hat{e_R}}$ is obtained by removing the projection of the vector ${\bf\vec{r}}$  onto the versor ${\bf\hat{k}}$:

\begin{equation}
{\bf\hat{e_R}} =  \frac{\bf\vec{r} - ({\bf\vec{r}.{\bf\hat{k}}){\bf\hat{k}}}}{\Vert\bf\vec{r} - ({\bf\vec{r}.{\bf\hat{k}}){\bf\hat{k}}\Vert}}
\label{eq_4}
\end{equation}

Figure. Position vector of a moving particle and a time-varying basis>

Note that if the movement is on a plane, the expression above is equal to ${\bf\hat{e_R}} =  \frac{\bf\vec{r}}{\Vert{\bf\vec{r} }\Vert}$ since the projection of $\bf\vec{r}$ on the $\bf\hat{k}$ versor is zero.

## Time-derivative of the versors ${\bf\hat{e_R}}$ and ${\bf\hat{e_\theta}}$

To obtain the expressions of the velocity and acceleration vectors, it is necessary to obtain the expressions of the time-derivative of the vectors ${\bf\hat{e_R}}$ and ${\bf\hat{e_\theta}}$.

This can be done by noting that:

\begin{align}
{\bf\hat{e_R}} &= \cos(\theta){\bf\hat{i}} + \sin(\theta){\bf\hat{j}}\\
{\bf\hat{e_\theta}} &= -\sin(\theta){\bf\hat{i}} + \cos(\theta){\bf\hat{j}}
\label{eq_5}
\end{align}

Deriving ${\bf\hat{e_R}}$ we obtain:

\begin{equation}
\frac{d{\bf\hat{e_R}}}{dt} = -\sin(\theta)\dot\theta{\bf\hat{i}} + \cos(\theta)\dot\theta{\bf\hat{j}} = \dot{\theta}{\bf\hat{e_\theta}}
\label{eq_6}
\end{equation}

Similarly, we obtain the time-derivative of ${\bf\hat{e_\theta}}$:

\begin{equation}
    \frac{d{\bf\hat{e_\theta}}}{dt} = -\cos(\theta)\dot\theta{\bf\hat{i}} - \sin(\theta)\dot\theta{\bf\hat{j}} = -\dot{\theta}{\bf\hat{e_R}}
\label{eq_7}
\end{equation}

## Position, velocity and acceleration

### Position

The position vector $\bf\vec{r}$, from the definition of $\bf\hat{e_R}$, is:

\begin{equation}
{\bf\vec{r}} = R{\bf\hat{e_R}} + z{\bf\hat{k}}
\label{eq_8}
\end{equation}

where $R = \Vert\bf\vec{r} - ({\bf\vec{r}.{\bf\hat{k}}){\bf\hat{k}}\Vert}$.

### Velocity

The velocity vector $\bf\vec{v}$ is obtained by deriving the vector $\bf\vec{r}$:

\begin{equation}
{\bf\vec{v}} = \frac{d(R{\bf\hat{e_R}})}{dt} + \dot{z}{\bf\hat{k}} = \dot{R}{\bf\hat{e_R}}+R\frac{d\bf\hat{e_R}}{dt}=\dot{R}{\bf\hat{e_R}}+R\dot{\theta}{\bf\hat{e_\theta}}+ \dot{z}{\bf\hat{k}}
\label{eq_9}
\end{equation}

### Acceleration

The acceleration vector $\bf\vec{a}$ is obtained by deriving the velocity vector:

\begin{align}
{\bf\vec{a}} =& \frac{d(\dot{R}{\bf\hat{e_R}}+R\dot{\theta}{\bf\hat{e_\theta}}+\dot{z}{\bf\hat{k}})}{dt}=\\\nonumber
    =&\ddot{R}{\bf\hat{e_R}}+\dot{R}\frac{d\bf\hat{e_R}}{dt} + \dot{R}\dot{\theta}{\bf\hat{e_\theta}} + R\ddot{\theta}{\bf\hat{e_\theta}} + R\dot{\theta}\frac{d{\bf\hat{e_\theta}}}{dt} + \ddot{z}{\bf\hat{k}}=\\\nonumber
    =&\ddot{R}{\bf\hat{e_R}}+\dot{R}\dot{\theta}{\bf\hat{e_\theta}} + \dot{R}\dot{\theta}{\bf\hat{e_\theta}} + R\ddot{\theta}{\bf\hat{e_\theta}} - R\dot{\theta}^2{\bf\hat{e_R}}+ \ddot{z}{\bf\hat{k}} =\\
    =&\ddot{R}{\bf\hat{e_R}}+2\dot{R}\dot{\theta}{\bf\hat{e_\theta}}+ R\ddot{\theta}{\bf\hat{e_\theta}} - {R}\dot{\theta}^2{\bf\hat{e_R}}+ \ddot{z}{\bf\hat{k}} =\\\nonumber
    =&(\ddot{R}-R\dot{\theta}^2){\bf\hat{e_R}}+(2\dot{R}\dot{\theta} + R\ddot{\theta}){\bf\hat{e_\theta}}+ \ddot{z}{\bf\hat{k}}\nonumber
\label{eq_10}
\end{align}

+ The term $\ddot{R}$ is  an acceleration in the radial direction.

+ The term $R\ddot{\theta}$ is an angular acceleration.

+ The term $\ddot{z}$ is an acceleration in the $\bf\hat{k}$ direction.

+ The term $-R\dot{\theta}^2$ is the well known centripetal acceleration.

+ The term $2\dot{R}\dot{\theta}$ is known as Coriolis acceleration. This term may be difficult to understand intuitively. It appears when there is displacement in the radial and angular directions at the same time.

## Important to note

The reader must bear in mind that the use of a different basis to represent the position, velocity or acceleration vectors is only a different representation of the same vector. For example, for the acceleration vector:

\begin{equation}
{\bf\vec{a}} = \ddot{x}{\bf\hat{i}}+ \ddot{y}{\bf\hat{j}} + \ddot{z}{\bf\hat{k}}=(\ddot{R}-R\dot{\theta}^2){\bf\hat{e_R}}+(2\dot{R}\dot{\theta} + R\ddot{\theta}){\bf\hat{e_\theta}} + \ddot{z}{\bf\hat{k}}=\dot{\Vert\bf\vec{v}\Vert}{\bf\hat{e}_t}+{\Vert\bf\vec{v}\Vert}^2\Vert{\bf\vec{C}} \Vert{\bf\hat{e}_n}
\label{eq_11}
\end{equation}

In which the last equality is the acceleration vector represented in the path-coordinate of the particle (see http://nbviewer.jupyter.org/github/BMClab/bmc/blob/master/notebooks/Time-varying%20frames.ipynb).

## Example

Consider a particle following the spiral path described below:

\begin{equation}
{\bf\vec{r}}(t) = (2\sqrt{t}\cos(t)){\bf\hat{i}}+ (2\sqrt{t}\sin(t)){\bf\hat{j}}
\label{eq_12}
\end{equation}

```python
import numpy as np
import sympy as sym
from sympy.plotting import plot_parametric,plot3d_parametric_line
from sympy.vector import CoordSys3D
import matplotlib.pyplot as plt
# from matplotlib import rc
# rc('text', usetex=True)
sym.init_printing()
```

### Solving numerically

```python
t = np.linspace(0.01,10,30).reshape(-1,1) #create a time vector and reshapes it to a column vector
R = 2*np.sqrt(t)
theta = t
rx = R*np.cos(t)
ry = R*np.sin(t)
r = np.hstack((rx, ry)) # creates the position vector by stacking rx and ry horizontally
```

```python
e_r = r/np.linalg.norm(r, axis=1, keepdims=True) # defines e_r vector
e_theta = np.cross([0,0,1],e_r)[:,0:-1] # defines e_theta vector

```

```python
dt = t[1] #defines delta_t
Rdot = np.diff(R, axis=0)/dt #find the R derivative
thetaDot = np.diff(theta, axis=0)/dt #find the angle derivative
v = Rdot*e_r[0:-1,:] +R[0:-1]*thetaDot*e_theta[0:-1,:] # find the linear velocity.
```

```python
Rddot = np.diff(Rdot, axis=0)/dt
thetaddot = np.diff(thetaDot, axis=0)/dt
```

```python
a = ((Rddot - R[1:-1]*thetaDot[0:-1]**2)*e_r[1:-1,:]
     + (2*Rdot[0:-1]*thetaDot[0:-1] + Rdot[0:-1]*thetaddot)*e_theta[1:-1,:])
```

The versors $\bf\hat{e_R}$, $\bf\hat{e_\theta}$ are plotted below at some points of the path.

```python
from matplotlib.patches import FancyArrowPatch
%matplotlib inline
plt.rcParams['figure.figsize'] = (8,8)
fig = plt.figure()

ax = fig.add_axes([0,0,1,1])
plt.plot(r[:,0],r[:,1],'.')
for i in np.arange(len(t)-2):
    vec1 = FancyArrowPatch(r[i,:],r[i,:]+e_r[i,:],mutation_scale=30,color='r', label='e_r')
    vec2 = FancyArrowPatch(r[i,:],r[i,:]+e_theta[i,:],mutation_scale=30,color='g', label='e_theta')
    ax.add_artist(vec1)
    ax.add_artist(vec2)
plt.xlim((-10,10))
plt.ylim((-10,10))
plt.grid()
plt.legend([vec1, vec2],[r'$\vec{e_r}$', r'$\vec{e_{\theta}}$'])
plt.show()
```

The velocity and acceleleration of the particle are plotted below at some points of the path.

```python
from matplotlib.patches import FancyArrowPatch
%matplotlib inline
plt.rcParams['figure.figsize'] = (8,8)
fig = plt.figure()
plt.plot(r[:,0],r[:,1],'.')
ax = fig.add_axes([0,0,1,1])
for i in np.arange(len(t)-2):
    vec1 = FancyArrowPatch(r[i,:],r[i,:]+v[i,:],mutation_scale=10,color='r')
    vec2 = FancyArrowPatch(r[i,:],r[i,:]+a[i,:],mutation_scale=10,color='g')
    ax.add_artist(vec1)
    ax.add_artist(vec2)
plt.xlim((-10,10))
plt.ylim((-10,10))
plt.grid()
plt.legend([vec1, vec2],[r'$\vec{v}$', r'$\vec{a}$'])
plt.show()
```

### Solved symbolically (extra reading)

```python
O = sym.vector.CoordSys3D(' ')
t = sym.symbols('t')
```

```python
r = 2*sym.sqrt(t)*sym.cos(t)*O.i+2*sym.sqrt(t)*sym.sin(t)*O.j
r
```

```python
plot_parametric(r.dot(O.i),r.dot(O.j),(t,0,10))
```

```python
e_r = r - r.dot(O.k)*O.k
e_r = e_r/sym.sqrt(e_r.dot(O.i)**2+e_r.dot(O.j)**2+e_r.dot(O.k)**2)
```

```python
e_r
```

```python
e_theta = O.k.cross(e_r)
e_theta
```

```python
from matplotlib.patches import FancyArrowPatch
plt.rcParams['figure.figsize'] = (8,8)
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
ax.axis("on")
time = np.linspace(0,10,30)
for instant in time:
    vt = FancyArrowPatch([float(r.dot(O.i).subs(t,instant)),float(r.dot(O.j).subs(t,instant))],
                         [float(r.dot(O.i).subs(t,instant))+float(e_r.dot(O.i).subs(t,instant)),
                          float(r.dot(O.j).subs(t, instant))+float(e_r.dot(O.j).subs(t,instant))],
                         mutation_scale=20,
                         arrowstyle="->",color="r",label='${{e_r}}$')
    vn = FancyArrowPatch([float(r.dot(O.i).subs(t, instant)),float(r.dot(O.j).subs(t,instant))],
                         [float(r.dot(O.i).subs(t, instant))+float(e_theta.dot(O.i).subs(t, instant)),
                          float(r.dot(O.j).subs(t, instant))+float(e_theta.dot(O.j).subs(t, instant))],
                         mutation_scale=20,
                         arrowstyle="->",color="g",label='${{e_{theta}}}$')
    ax.add_artist(vn)
    ax.add_artist(vt)
plt.xlim((-10,10))
plt.ylim((-10,10))
plt.legend(handles=[vt,vn],fontsize=20)
plt.grid()
plt.show()
```

```python
R = 2*sym.sqrt(t)
```

```python
Rdot = sym.diff(R,t)
Rddot = sym.diff(Rdot,t)
Rddot
```

```python
v = Rdot*e_r + R*e_theta
```

```python
v
```

```python
a = (Rddot - R)*e_r + (2*Rdot*1+0)*e_theta
aCor = 2*Rdot*1*e_theta
aCor
```

```python
a
```

```python
from matplotlib.patches import FancyArrowPatch
plt.rcParams['figure.figsize'] = (8,8)
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
ax.axis("on")
time = np.linspace(0.1,10,30)
for instant in time:
    vt = FancyArrowPatch([float(r.dot(O.i).subs(t,instant)),float(r.dot(O.j).subs(t,instant))],
                         [float(r.dot(O.i).subs(t,instant))+float(v.dot(O.i).subs(t,instant)),
                          float(r.dot(O.j).subs(t, instant))+float(v.dot(O.j).subs(t,instant))],
                         mutation_scale=20,
                         arrowstyle="->",color="r",label='${{v}}$')
    vn = FancyArrowPatch([float(r.dot(O.i).subs(t, instant)),float(r.dot(O.j).subs(t,instant))],
                         [float(r.dot(O.i).subs(t, instant))+float(a.dot(O.i).subs(t, instant)),
                          float(r.dot(O.j).subs(t, instant))+float(a.dot(O.j).subs(t, instant))],
                         mutation_scale=20,
                         arrowstyle="->",color="g",label='${{a}}$')
    vc = FancyArrowPatch([float(r.dot(O.i).subs(t, instant)),float(r.dot(O.j).subs(t,instant))],
                         [float(r.dot(O.i).subs(t, instant))+float(aCor.dot(O.i).subs(t, instant)),
                          float(r.dot(O.j).subs(t, instant))+float(aCor.dot(O.j).subs(t, instant))],
                         mutation_scale=20,
                         arrowstyle="->",color="b",label='${{a_{Cor}}}$')
    ax.add_artist(vn)
    ax.add_artist(vt)
    ax.add_artist(vc)
plt.xlim((-10,10))
plt.ylim((-10,10))
plt.legend(handles=[vt,vn,vc],fontsize=20)
plt.grid()
plt.show()
```

## Problems

1. Find the polar basis (using the computer) for a projectile motion of a particle following the parametric equation below:

\begin{equation}
\vec{r}(t) = (10t-51){\bf{\hat{i}}} + \left(-\frac{9,81}{2}t^2+50t+100\right){\bf{\hat{j}}}
\label{eq_13}
\end{equation}

2. Problems from 15.1.1 to 15.1.14 from Ruina and Rudra's book,
3. Problems from 18.1.1 to 18.1.8 and 18.1.10 from Ruina and Rudra's book.

## Reference

- Ruina A, Rudra P (2019) [Introduction to Statics and Dynamics](http://ruina.tam.cornell.edu/Book/index.html). Oxford University Press.

```python

```


```python
from sympy.physics.mechanics import ReferenceFrame,Point,dynamicsymbols
from sympy.physics.mechanics import Point
from sympy import latex,pprint,symbols,init_printing
from sympy.algebras.quaternion import Quaternion
import numpy as np
import sys
sys.path.append("../tools")
from vis import Visualizer
%matplotlib notebook
init_printing() # Para visualizar simbolos

# Aplicando esto para el caso del modelo giro-péndulo
q1,q2=dynamicsymbols('q1,q2')
# Variables de distancia l1: longitud del pendulo, R: radio del disco
l1,R=symbols('l1,R')

#Defina marcos de referencia
a=ReferenceFrame('A')
e=a.orientnew('E','Axis',(q1,a.y))
b=e.orientnew('B','Axis',(q2,-e.z))

#Defina el punto de origen
O=Point('O')
O.set_vel(a,0)
# Ahora defina el punto Q en el centro del disco
q=O.locatenew('Q',l1*e.z)
# Defina el punto P en la periferia del disco
p=q.locatenew('P',R*b.y)

vis=Visualizer(a,O)
vis.add(a,O,frame_scale=10)
vis.add(b,q,frame_scale=10)
vis.add(b,p,frame_scale=10)
vis.add(e,q,shape='assets/Atraccion_GiroPendulo_pivote.stl')
vis.add(b,q,shape='assets/Atraccion_GiroPendulo_disk.stl')
vis.plot({l1:45,R:30,q1:0.5,q2:0})
```

    <IPython.core.display.Javascript object>

```python
#Modifique las variables de movimiento para cambiar la configuracion cinematica del modelo
#Puede hacerlo desde esta celda para actualizar el modelo sin tener que volver a generar la figura.
vis.plot({l1:45,R:30,q1:0.9,q2:0.5})
```

```python
#Esto permite crear animaciones facilmente
#Ejecute esta celda y de click en la figura 1 para observar la animacion del movimiento.
import matplotlib
from matplotlib.animation import FuncAnimation
tt=np.linspace(0,10,200)
qq1=np.sin(2*np.pi*0.1*tt)
qq2=np.linspace(0,np.pi*2*10,len(tt))

def animfunc(i,qq1,qq2):
    vis.plot({l1:45,R:30,q1:qq1[i],q2:qq2[i]})
FuncAnimation(vis.fig,animfunc,fargs=(qq1,qq2),interval=5)
```

    <matplotlib.animation.FuncAnimation at 0x2346f6f2520>


## 1.2 Bayesian Framework

We are interested in beliefs, which can be interpreted as probabilities by thinking Bayesian. We have a prior belief in event $A$, beliefs formed by previous information, e.g., our prior belief about bugs being in our code before performing tests.

Secondly, we observe our evidence. To continue our buggy-code example: if our code passes $X$ tests, we want to update our belief to incorporate this. We call this new belief the posterior probability. Updating our belief is done via the following equation, known as Bayes' Theorem, after its discoverer Thomas Bayes:

$$\begin{align}
 P( A | X ) = \frac{ P(X | A) P(A) } {P(X) } \\\\[5pt]
 \propto P(X | A) P(A)\;\; (\propto \text{is proportional to })
\end{align}$$

The above formula is not unique to Bayesian inference: it is a mathematical fact with uses outside Bayesian inference. Bayesian inference merely uses it to connect prior probabilities $P(A)$ with an updated posterior probabilities $P(A | X )$.

### 1.2.1 Example: Mandatory coin-flip example
Every statistics text must contain a coin-flipping example, I'll use it here to get it out of the way. Suppose, naively, that you are unsure about the probability of heads in a coin flip (spoiler alert: it's 50%). You believe there is some true underlying ratio, call it $p$, but have no prior opinion on what $p$ might be.

We begin to flip a coin, and record the observations: either $H$ or $T$. This is our observed data. An interesting question to ask is how our inference changes as we observe more and more data? More specifically, what do our posterior probabilities look like when we have little data, versus when we have lots of data.

Below we plot a sequence of updating posterior probabilities as we observe increasing amounts of data (coin flips).

```python
%matplotlib inline
from IPython.core.pylabtools import figsize
import numpy as np
from matplotlib import pyplot as plt
figsize(11, 9)

import scipy.stats as stats

dist = stats.beta
n_trials = [0, 1, 2, 3, 4, 5, 8, 15, 50, 500]
data = stats.bernoulli.rvs(0.5, size=n_trials[-1])
x = np.linspace(0, 1, 100)

# For the already prepared, I'm using Binomial's conj. prior.
for k, N in enumerate(n_trials):
    sx = plt.subplot(len(n_trials) / 2, 2, k + 1)
    plt.xlabel("$p$, probability of heads") \
        if k in [0, len(n_trials) - 1] else None
    plt.setp(sx.get_yticklabels(), visible=False)
    heads = data[:N].sum()
    y = dist.pdf(x, 1 + heads, 1 + N - heads)
    plt.plot(x, y, label="observe %d tosses,\n %d heads" % (N, heads))
    plt.fill_between(x, 0, y, color="#348ABD", alpha=0.4)
    plt.vlines(0.5, 0, 4, color="k", linestyles="--", lw=1)

    leg = plt.legend()
    leg.get_frame().set_alpha(0.4)
    plt.autoscale(tight=True)

plt.suptitle("Bayesian updating of posterior probabilities",
             y=1.02,
             fontsize=14)

plt.tight_layout()
```

The posterior probabilities are represented by the curves, and our uncertainty is proportional to the width of the curve. As the plot above shows, as we start to observe data our posterior probabilities start to shift and move around. Eventually, as we observe more and more data (coin-flips), our probabilities will tighten closer and closer around the true value of $p=0.5$ (marked by a dashed line).

Notice that the plots are not always peaked at 0.5. There is no reason it should be: recall we assumed we did not have a prior opinion of what $p$ is. In fact, if we observe quite extreme data, say 8 flips and only 1 observed heads, our distribution would look very biased away from lumping around 0.5 (with no prior opinion, how confident would you feel betting on a fair coin after observing 8 tails and 1 head?). As more data accumulates, we would see more and more probability being assigned at $p=0.5$, though never all of it.

The next example is a simple demonstration of the mathematics of Bayesian inference.

### 1.2.2 Example: Bug, or just sweet, unintended feature?

Let $A$ denote the event that our code has no bugs in it. Let $X$ denote the event that the code passes all debugging tests. For now, we will leave the prior probability of no bugs as a variable, i.e. $P(A) = p$.

We are interested in $P(A|X)$, i.e. the probability of no bugs, given our debugging tests $X$. To use the formula above, we need to compute some quantities.

What is $P(X | A)$, i.e., the probability that the code passes $X$ tests given there are no bugs? Well, it is equal to 1, for a code with no bugs will pass all tests.

$P(X)$ is a little bit trickier: The event $X$ can be divided into two possibilities, event $X$ occurring even though our code indeed has bugs (denoted $\sim A\;$, spoken not $A$), or event $X$ without bugs ($A$). $P(X)$ can be represented as:

$$\begin{align}
P(X ) = P(X \text{ and } A) + P(X \text{ and } \sim A) \\\\[5pt]
  = P(X|A)P(A) + P(X | \sim A)P(\sim A)\\\\[5pt]
 = P(X|A)p + P(X | \sim A)(1-p)
\end{align}$$

We have already computed $P(X|A)$ above. On the other hand, $P(X | \sim A)$ is subjective: our code can pass tests but still have a bug in it, though the probability there is a bug present is reduced. Note this is dependent on the number of tests performed, the degree of complication in the tests, etc. Let's be conservative and assign $P(X|\sim A) = 0.5$. Then

$$\begin{align}
P(A | X) = \frac{1\cdot p}{ 1\cdot p +0.5 (1-p) } \\\\
 = \frac{ 2 p}{1+p}
\end{align}$$

This is the posterior probability. What does it look like as a function of our prior, $p \in [0,1]$?

```python
figsize(12.5, 4)
p = np.linspace(0, 1, 50)
plt.plot(p, 2*p/(1+p), color="#348ABD", lw=3)
#plt.fill_between(p, 2*p/(1+p), alpha=.5, facecolor=["#A60628"])
plt.scatter(0.2, 2*(0.2)/1.2, s=140, c="#348ABD")
plt.xlim(0, 1)
plt.ylim(0, 1)
plt.xlabel("Prior, $P(A) = p$")
plt.ylabel("Posterior, $P(A|X)$, with $P(A) = p$")
plt.title("Are there bugs in my code?");
```

We can see the biggest gains if we observe the $X$ tests passed when the prior probability, $p$, is low. Let's settle on a specific value for the prior. I'm a strong programmer (I think), so I'm going to give myself a realistic prior of 0.20, that is, there is a 20% chance that I write code bug-free. To be more realistic, this prior should be a function of how complicated and large the code is, but let's pin it at 0.20. Then my updated belief that my code is bug-free is 0.33.

Recall that the prior is a probability: $p$ is the prior probability that there are no bugs, so $1-p$ is the prior probability that there are bugs.

Similarly, our posterior is also a probability, with $P(A | X)$ the probability there is no bug given we saw all tests pass, hence $1-P(A|X)$ is the probability there is a bug given all tests passed. What does our posterior probability look like? Below is a chart of both the prior and the posterior probabilities.

```python
figsize(12.5, 4)
colours = ["#348ABD", "#A60628"]

prior = [0.20, 0.80]
posterior = [1. / 3, 2. / 3]
plt.bar([0, .7], prior, alpha=0.70, width=0.25,
        color=colours[0], label="prior distribution",
        lw="3", edgecolor=colours[0])

plt.bar([0 + 0.25, .7 + 0.25], posterior, alpha=0.7,
        width=0.25, color=colours[1],
        label="posterior distribution",
        lw="3", edgecolor=colours[1])

plt.ylim(0,1)
plt.xticks([0.20, .95], ["Bugs Absent", "Bugs Present"])
plt.title("Prior and Posterior probability of bugs present")
plt.ylabel("Probability")
plt.legend(loc="upper left");
```

## 1.3 Probability Distributions
Let's quickly recall what a probability distribution is: Let $Z$ be some random variable. Then associated with $Z$ is a probability distribution function that assigns probabilities to the different outcomes $Z$ can take. Graphically, a probability distribution is a curve where the probability of an outcome is proportional to the height of the curve. You can see examples in the first figure of this chapter.

We can divide random variables into three classifications:
* $Z$ is discrete: Discrete random variables may only assume values on a specified list. Things like populations, movie ratings, and number of votes are all discrete random variables. Discrete random variables become more clear when we contrast them with...

* $Z$ is continuous: Continuous random variable can take on arbitrarily exact values. For example, temperature, speed, time, color are all modeled as continuous variables because you can progressively make the values more and more precise.

* $Z$ is mixed: Mixed random variables assign probabilities to both discrete and continuous random variables, i.e. it is a combination of the above two categories.

### 1.3.1 Discrete Case
If $Z$ is discrete, then its distribution is called a probability mass function, which measures the probability $Z$ takes on the value $k$, denoted $P(Z=k)$. Note that the probability mass function completely describes the random variable $Z$, that is, if we know the mass function, we know how $Z$ should behave. There are popular probability mass functions that consistently appear: we will introduce them as needed, but let's introduce the first very useful probability mass function. We say $Z$ is Poisson-distributed if:

$$P(Z = k) =\frac{ \lambda^k e^{-\lambda} }{k!}, \; \; k=0,1,2, \dots $$

$\lambda$ is called a parameter of the distribution, and it controls the distribution's shape. For the Poisson distribution, $\lambda$ can be any positive number. By increasing $\lambda$, we add more probability to larger values, and conversely by decreasing $\lambda$ we add more probability to smaller values. One can describe $\lambda$ as the intensity of the Poisson distribution.

Unlike $\lambda$, which can be any positive number, the value $k$ in the above formula must be a non-negative integer, i.e., $k$ must take on values 0,1,2, and so on. This is very important, because if you wanted to model a population you could not make sense of populations with 4.25 or 5.612 members.

If a random variable $Z$ has a Poisson mass distribution, we denote this by writing

$$Z \sim \text{Poi}(\lambda) $$

One useful property of the Poisson distribution is that its expected value is equal to its parameter, i.e.:

$$E\large[ \;Z\; | \; \lambda \;\large] = \lambda $$

We will use this property often, so it's useful to remember. Below, we plot the probability mass distribution for different $\lambda$ values. The first thing to notice is that by increasing $\lambda$, we add more probability of larger values occurring. Second, notice that although the graph ends at 15, the distributions do not. They assign positive probability to every non-negative integer.

```python
figsize(12.5, 4)

import scipy.stats as stats
a = np.arange(16)
poi = stats.poisson
lambda_ = [1.5, 4.25]
colours = ["#348ABD", "#A60628"]

plt.bar(a, poi.pmf(a, lambda_[0]), color=colours[0],
        label="$\lambda = %.1f$" % lambda_[0], alpha=0.60,
        edgecolor=colours[0], lw="3")

plt.bar(a, poi.pmf(a, lambda_[1]), color=colours[1],
        label="$\lambda = %.1f$" % lambda_[1], alpha=0.60,
        edgecolor=colours[1], lw="3")

plt.xticks(a + 0.4, a)
plt.legend()
plt.ylabel("probability of $k$")
plt.xlabel("$k$")
plt.title("Probability mass function of a Poisson random variable; differing \
$\lambda$ values")
```

### 1.3.2 Continuous Case
Instead of a probability mass function, a continuous random variable has a probability density function. This might seem like unnecessary nomenclature, but the density function and the mass function are very different creatures. An example of continuous random variable is a random variable with exponential density. The density function for an exponential random variable looks like this:

$$f_Z(z | \lambda) = \lambda e^{-\lambda z }, \;\; z\ge 0$$
Like a Poisson random variable, an exponential random variable can take on only non-negative values. But unlike a Poisson variable, the exponential can take on any non-negative values, including non-integral values such as 4.25 or 5.612401. This property makes it a poor choice for count data, which must be an integer, but a great choice for time data, temperature data (measured in Kelvins, of course), or any other precise and positive variable. The graph below shows two probability density functions with different $\lambda$ values.

When a random variable $Z$ has an exponential distribution with parameter $\lambda$, we say $Z$ is exponential and write

$$Z \sim \text{Exp}(\lambda)$$
Given a specific $\lambda$, the expected value of an exponential random variable is equal to the inverse of $\lambda$, that is:

$$E[\; Z \;|\; \lambda \;] = \frac{1}{\lambda}$$

```python
a = np.linspace(0, 4, 100)
expo = stats.expon
lambda_ = [0.5, 1]

for l, c in zip(lambda_, colours):
    plt.plot(a, expo.pdf(a, scale=1. / l), lw=3,
             color=c, label="$\lambda = %.1f$" % l)
    plt.fill_between(a, expo.pdf(a, scale=1. / l), color=c, alpha=.33)

plt.legend()
plt.ylabel("PDF at $z$")
plt.xlabel("$z$")
plt.ylim(0, 1.2)
plt.title("Probability density function of an Exponential random variable;\
 differing $\lambda$");
```

### But what is $\lambda \;$?
This question is what motivates statistics. In the real world, $\lambda$ is hidden from us. We see only $Z$, and must go backwards to try and determine $\lambda$. The problem is difficult because there is no one-to-one mapping from $Z$ to $\lambda$. Many different methods have been created to solve the problem of estimating $\lambda$, but since $\lambda$ is never actually observed, no one can say for certain which method is best!

Bayesian inference is concerned with beliefs about what $\lambda$ might be. Rather than try to guess $\lambda$ exactly, we can only talk about what $\lambda$ is likely to be by assigning a probability distribution to $\lambda$.

This might seem odd at first. After all, $\lambda$ is fixed; it is not (necessarily) random! How can we assign probabilities to values of a non-random variable? Ah, we have fallen for our old, frequentist way of thinking. Recall that under Bayesian philosophy, we can assign probabilities if we interpret them as beliefs. And it is entirely acceptable to have beliefs about the parameter $\lambda$.

### 1.3.3 Example: Inferring behaviour from text-message data
Let's try to model a more interesting example, one that concerns the rate at which a user sends and receives text messages:

You are given a series of daily text-message counts from a user of your system. The data, plotted over time, appears in the chart below. You are curious to know if the user's text-messaging habits have changed over time, either gradually or suddenly. How can you model this? (This is in fact my own text-message data. Judge my popularity as you wish.)

```python
figsize(12.5, 3.5)
count_data = np.loadtxt("data/txtdata.csv")
n_count_data = len(count_data)
plt.bar(np.arange(n_count_data), count_data, color="#348ABD")
plt.xlabel("Time (days)")
plt.ylabel("count of text-msgs received")
plt.title("Did the user's texting habits change over time?")
plt.xlim(0, n_count_data);
```

Before we start modeling, see what you can figure out just by looking at the chart above. Would you say there was a change in behaviour during this time period?

How can we start to model this? Well, as we have conveniently already seen, a Poisson random variable is a very appropriate model for this type of count data. Denoting day $i$'s text-message count by $C_i$,

$$ C_i \sim \text{Poisson}(\lambda)  $$
We are not sure what the value of the $\lambda$ parameter really is, however. Looking at the chart above, it appears that the rate might become higher late in the observation period, which is equivalent to saying that $\lambda$ increases at some point during the observations. (Recall that a higher value of $\lambda$ assigns more probability to larger outcomes. That is, there is a higher probability of many text messages having been sent on a given day.)

How can we represent this observation mathematically? Let's assume that on some day during the observation period (call it $\tau$), the parameter $\lambda$ suddenly jumps to a higher value. So we really have two $\lambda$ parameters: one for the period before $\tau$, and one for the rest of the observation period. In the literature, a sudden transition like this would be called a switchpoint:

$$
\lambda =
\begin{cases}
\lambda_1  & \text{if } t \lt \tau \cr
\lambda_2 & \text{if } t \ge \tau
\end{cases}
$$
If, in reality, no sudden change occurred and indeed $\lambda_1 = \lambda_2$, then the $\lambda$s posterior distributions should look about equal.

We are interested in inferring the unknown $\lambda$s. To use Bayesian inference, we need to assign prior probabilities to the different possible values of $\lambda$. What would be good prior probability distributions for $\lambda_1$ and $\lambda_2$? Recall that $\lambda$ can be any positive number. As we saw earlier, the exponential distribution provides a continuous density function for positive numbers, so it might be a good choice for modeling $\lambda_i$. But recall that the exponential distribution takes a parameter of its own, so we'll need to include that parameter in our model. Let's call that parameter $\alpha$.

$$\begin{align}
\lambda_1 \sim \text{Exp}( \alpha ) \\\
\lambda_2 \sim \text{Exp}( \alpha )
\end{align}$$
$\alpha$ is called a hyper-parameter or parent variable. In literal terms, it is a parameter that influences other parameters. Our initial guess at  $\alpha$ does not influence the model too strongly, so we have some flexibility in our choice. A good rule of thumb is to set the exponential parameter equal to the inverse of the average of the count data. Since we're modeling $\lambda$ using an exponential distribution, we can use the expected value identity shown earlier to get:

$$\frac{1}{N}\sum_{i=0}^N \;C_i \approx E[\; \lambda \; |\; \alpha ] = \frac{1}{\alpha}$$
An alternative, and something I encourage the reader to try, would be to have two priors: one for each $\lambda_i$. Creating two exponential distributions with different $\alpha$ values reflects our prior belief that the rate changed at some point during the observations.

What about $\tau$? Because of the noisiness of the data, it's difficult to pick out a priori when $\tau$ might have occurred. Instead, we can assign a uniform prior belief to every possible day. This is equivalent to saying

$$\begin{align}
\tau \sim \text{DiscreteUniform(1,70) }\\\\
\Rightarrow P( \tau = k ) = \frac{1}{70}
\end{align}$$
So after all this, what does our overall prior distribution for the unknown variables look like? Frankly, it doesn't matter. What we should understand is that it's an ugly, complicated mess involving symbols only a mathematician could love. And things will only get uglier the more complicated our models become. Regardless, all we really care about is the posterior distribution.

We next turn to PyMC3, a Python library for performing Bayesian analysis that is undaunted by the mathematical monster we have created.

### 1.4 Introducing our first hammer: PyMC3
PyMC3 is a Python library for programming Bayesian analysis [3]. It is a fast, well-maintained library. The only unfortunate part is that its documentation is lacking in certain areas, especially those that bridge the gap between beginner and hacker. One of this book's main goals is to solve that problem, and also to demonstrate why PyMC3 is so cool.

We will model the problem above using PyMC3. This type of programming is called probabilistic programming, an unfortunate misnomer that invokes ideas of randomly-generated code and has likely confused and frightened users away from this field. The code is not random; it is probabilistic in the sense that we create probability models using programming variables as the model's components. Model components are first-class primitives within the PyMC3 framework.

Because of the confusion engendered by the term probabilistic programming, I'll refrain from using it. Instead, I'll simply say programming, since that's what it really is.

PyMC3 code is easy to read. The only novel thing should be the syntax. Simply remember that we are representing the model's components ($\tau, \lambda_1, \lambda_2$ ) as variables.

```python
import pymc3 as pm
import theano.tensor as tt

with pm.Model() as model:
    alpha = 1.0/count_data.mean()  # Recall count_data is the
                                   # variable that holds our txt counts
    lambda_1 = pm.Exponential("lambda_1", alpha)
    lambda_2 = pm.Exponential("lambda_2", alpha)

    tau = pm.DiscreteUniform("tau", lower=0, upper=n_count_data - 1)
```

    /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/h5py/__init__.py:36: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.
      from ._conv import register_converters as _register_converters

In the code above, we create the PyMC3 variables corresponding to $\lambda_1$ and $\lambda_2$. We assign them to PyMC3's stochastic variables, so-called because they are treated by the back end as random number generators.

```python
with model:
    idx = np.arange(n_count_data) # Index
    lambda_ = pm.math.switch(tau > idx, lambda_1, lambda_2)
```

This code creates a new function lambda_, but really we can think of it as a random variable: the random variable $\lambda$ from above. The  switch() function assigns lambda_1 or lambda_2 as the value of lambda_, depending on what side of tau we are on. The values of lambda_ up until tau are lambda_1 and the values afterwards are lambda_2.

Note that because lambda_1, lambda_2 and tau are random, lambda_ will be random. We are not fixing any variables yet.

```python
with model:
    observation = pm.Poisson("obs", lambda_, observed=count_data)
```

The variable observation combines our data, count_data, with our proposed data-generation scheme, given by the variable lambda_, through the observed keyword.

The code below will be explained in Chapter 3, but I show it here so you can see where our results come from. One can think of it as a learning step. The machinery being employed is called Markov Chain Monte Carlo (MCMC), which I also delay explaining until Chapter 3. This technique returns thousands of random variables from the posterior distributions of $\lambda_1, \lambda_2$ and $\tau$. We can plot a histogram of the random variables to see what the posterior distributions look like. Below, we collect the samples (called traces in the MCMC literature) into histograms.

```python
with model:
    step = pm.Metropolis()
    trace = pm.sample(10000, tune=5000,step=step)
```

    Multiprocess sampling (2 chains in 2 jobs)
    CompoundStep
    >Metropolis: [tau]
    >Metropolis: [lambda_2]
    >Metropolis: [lambda_1]
    Sampling 2 chains: 100%|██████████| 30000/30000 [00:17<00:00, 786.57draws/s]
    The number of effective samples is smaller than 25% for some parameters.

```python
lambda_1_samples = trace['lambda_1']
lambda_2_samples = trace['lambda_2']
tau_samples = trace['tau']
```

```python
figsize(12.5, 10)
#histogram of the samples:

ax = plt.subplot(311)
ax.set_autoscaley_on(False)

plt.hist(lambda_1_samples, histtype='stepfilled', bins=30, alpha=0.85,
         label="posterior of $\lambda_1$", color="#A60628", normed=True)
plt.legend(loc="upper left")
plt.title(r"""Posterior distributions of the variables
    $\lambda_1,\;\lambda_2,\;\tau$""")
plt.xlim([15, 30])
plt.xlabel("$\lambda_1$ value")

ax = plt.subplot(312)
ax.set_autoscaley_on(False)
plt.hist(lambda_2_samples, histtype='stepfilled', bins=30, alpha=0.85,
         label="posterior of $\lambda_2$", color="#7A68A6", normed=True)
plt.legend(loc="upper left")
plt.xlim([15, 30])
plt.xlabel("$\lambda_2$ value")

plt.subplot(313)
w = 1.0 / tau_samples.shape[0] * np.ones_like(tau_samples)
plt.hist(tau_samples, bins=n_count_data, alpha=1,
         label=r"posterior of $\tau$",
         color="#467821", weights=w, rwidth=2.)
plt.xticks(np.arange(n_count_data))

plt.legend(loc="upper left")
plt.ylim([0, .75])
plt.xlim([35, len(count_data)-20])
plt.xlabel(r"$\tau$ (in days)")
plt.ylabel("probability");
```

### Interpretation
Recall that Bayesian methodology returns a distribution. Hence we now have distributions to describe the unknown $\lambda$s and $\tau$. What have we gained? Immediately, we can see the uncertainty in our estimates: the wider the distribution, the less certain our posterior belief should be. We can also see what the plausible values for the parameters are: $\lambda_1$ is around 18 and $\lambda_2$ is around 23. The posterior distributions of the two $\lambda$s are clearly distinct, indicating that it is indeed likely that there was a change in the user's text-message behaviour.

What other observations can you make? If you look at the original data again, do these results seem reasonable?

Notice also that the posterior distributions for the $\lambda$s do not look like exponential distributions, even though our priors for these variables were exponential. In fact, the posterior distributions are not really of any form that we recognize from the original model. But that's OK! This is one of the benefits of taking a computational point of view. If we had instead done this analysis using mathematical approaches, we would have been stuck with an analytically intractable (and messy) distribution. Our use of a computational approach makes us indifferent to mathematical tractability.

Our analysis also returned a distribution for $\tau$. Its posterior distribution looks a little different from the other two because it is a discrete random variable, so it doesn't assign probabilities to intervals. We can see that near day 45, there was a 50% chance that the user's behaviour changed. Had no change occurred, or had the change been gradual over time, the posterior distribution of $\tau$ would have been more spread out, reflecting that many days were plausible candidates for $\tau$. By contrast, in the actual results we see that only three or four days make any sense as potential transition points.

### Why would I want samples from the posterior, anyways?
We will deal with this question for the remainder of the book, and it is an understatement to say that it will lead us to some amazing results. For now, let's end this chapter with one more example.

We'll use the posterior samples to answer the following question: what is the expected number of texts at day $t, \; 0 \le t \le 70$ ? Recall that the expected value of a Poisson variable is equal to its parameter $\lambda$. Therefore, the question is equivalent to what is the expected value of $\lambda$ at time $t$?

In the code below, let $i$ index samples from the posterior distributions. Given a day $t$, we average over all possible $\lambda_i$ for that day $t$, using $\lambda_i = \lambda_{1,i}$ if $t \lt \tau_i$ (that is, if the behaviour change has not yet occurred), else we use $\lambda_i = \lambda_{2,i}$.

```python
figsize(12.5, 5)
# tau_samples, lambda_1_samples, lambda_2_samples contain
# N samples from the corresponding posterior distribution
N = tau_samples.shape[0]
expected_texts_per_day = np.zeros(n_count_data)
for day in range(0, n_count_data):
    # ix is a bool index of all tau samples corresponding to
    # the switchpoint occurring prior to value of 'day'
    ix = day < tau_samples
    # Each posterior sample corresponds to a value for tau.
    # for each day, that value of tau indicates whether we're "before"
    # (in the lambda1 "regime") or
    #  "after" (in the lambda2 "regime") the switchpoint.
    # by taking the posterior sample of lambda1/2 accordingly, we can average
    # over all samples to get an expected value for lambda on that day.
    # As explained, the "message count" random variable is Poisson distributed,
    # and therefore lambda (the poisson parameter) is the expected value of
    # "message count".
    expected_texts_per_day[day] = (lambda_1_samples[ix].sum()
                                   + lambda_2_samples[~ix].sum()) / N

plt.plot(range(n_count_data), expected_texts_per_day, lw=4, color="#E24A33",
         label="expected number of text-messages received")
plt.xlim(0, n_count_data)
plt.xlabel("Day")
plt.ylabel("Expected # text-messages")
plt.title("Expected number of text-messages received")
plt.ylim(0, 60)
plt.bar(np.arange(len(count_data)), count_data, color="#348ABD", alpha=0.65,
        label="observed texts per day")

plt.legend(loc="upper left");
```

Our analysis shows strong support for believing the user's behavior did change ($\lambda_1$ would have been close in value to $\lambda_2$ had this not been true), and that the change was sudden rather than gradual (as demonstrated by $\tau$'s strongly peaked posterior distribution). We can speculate what might have caused this: a cheaper text-message rate, a recent weather-to-text subscription, or perhaps a new relationship.