```python
from IPython.core.display import HTML
HTML("<style>.container { width:95% !important; }</style>")
```

# Lecture 9: Methods using KKT conditions

# Reminder: optimality conditions
We consider an optimization problem of the form:
$$
\begin{align} \
\min \quad &f(x)\\
\text{s.t.} \quad & g_j(x) \geq 0\text{ for all }j=1,\ldots,J\\
& h_k(x) = 0\text{ for all }k=1,\ldots,K\\
&x\in \mathbb R^n.
\end{align}
$$

Necessary KKT conditions require for an optimal solution to satisfy

$$
\begin{align}
&\nabla_xL(x^*,\mu^*,\lambda^*) = 0\\
&\mu_j^*\geq0,\text{ for all }j=1,\ldots,J\\
&\mu_j^*g_j(x^*)=0,\text{for all }j=1,\ldots,J,
\end{align}
$$

where $\mu^*=(\mu_1^*,\ldots,\mu_J^*)$ and $\lambda^* = (\lambda^*_1,\ldots,\lambda_K^*)$ are called Lagrance multiplier vectors for the inequality and the equality constraints, respectively.

Function $L$ is the *Lagrangian function* $$L(x,\mu,\lambda) = f(x)- \sum_{j=1}^J\mu_jg_j(x) -\sum_{k=1}^K\lambda_kh_k(x)$$.

### Geometry of the KKT conditions

## Example:

Consider the following optimization problem

$$
min 𝑓(\mathbf{x}) = 𝑥_1 + 𝑥_2 \\
s.t. \\
g_1(x) = 𝑥_1^2 + 𝑥_2^2 \geq 5,\\
g_2(x) = 𝑥_1 + 2𝑥_2 \geq 4,\\
g_3(x) = 𝑥_1 \geq 0,\\
g_4(x) = 𝑥_2 \geq 0
$$

at $x^* = (2,1)$.

* The stationary rule says that, at the optimum ($x^*$), there is no feasible descent direction ($F\cap D = \varnothing $), i.e.,

$$
-\nabla f = \sum_j - \mu_j \nabla g_j\text{ , for } \mu_j \geq 0 \text{ , i.e., } g_j \text{ is active at } x^* \text{. }
$$

* In other words, $-\nabla f$ belongs to the cone defined by the (-) gradients of the active constraints

* The complementary rules make sure only the active constraints are considered.

```python
from IPython.display import Image
Image(filename = "Images\kkt_vis.jpg", width = 600, height = 600)
```

### Note.

* $min f(x)$ $\rightarrow $  $-\nabla f$ (descent direction)
* $max f(x)$ $\rightarrow $  $ \nabla f$ (ascent direction)

* $g(x) \geq 0$ $\rightarrow $  $-\nabla g$ (infeasible direction)
* $g(x) \leq 0$ $\rightarrow $  $ \nabla g$ (infeasible direction)

## Sequential Quadratic Programming (SQP)

**Idea is to generate a sequence of quadratic optimization problems whose solutions approach the solution of the original problem**

**Quadratic problems are based on applying KKT conditions to the original problem**

* Minimize a quadratic approximation of the Lagrangian function with respect to linear approximation of the constraints
* Also referred as **projected Lagrangian method**

Let us consider problem

$$
\min f(x)\\
\text{s.t. }h_k(x) = 0\text{ for all }k=1,\ldots,K,
$$

where the objective function and the equality constraints are twice differentiable.

Inequality constraints can be handled e.g. by using active set methods. See e.g. https://optimization.mccormick.northwestern.edu/index.php/Sequential_quadratic_programming

**Note that constraints can be nonlinear.**

Because we know that the optimal solution of this problem satisfies the KKT conditions, we know that

$$
\left\{\begin{array}{l}
\nabla_xL(x,\lambda)=\nabla_x f(x) + \lambda\nabla_x h(x) = 0\\
h(x) = 0
\end{array}\right.
$$

Let us assume that we have a current estimation for the solution of the equality constraints $(x_k,\lambda_k)$, then according to the Newton's method for root finding (see e.g., https://en.wikipedia.org/wiki/Newton%27s_method#Generalizations ), we have another solution $(x_k,\lambda_k)^T+(p,v)^T$ of the problem by solving system of equations

$$
\nabla_{x,\lambda} S(x_k,\lambda_k)\left[\begin{align}p^T\\v^T\end{align}\right] = -S(x_k,\lambda_k),
$$

where $S(x_k,\lambda_k)=\left[
\begin{array}{c}
\nabla_{x}L(x_k,\lambda_k)\\
h(x_k)
\end{array}
\right]
$.

This can be written as

$$
\left[
\begin{array}{cc}
\nabla^2_{xx}L(x_k,\lambda_k)&\nabla_x h(x_k)\\
\nabla_x h(x_k)^T & 0
\end{array}
\right]
\left[\begin{array}{c}p^T\\v^T\end{array}\right] =
\left[
\begin{array}{c}
-\nabla_x L(x_k,\lambda_k)\\
-h(x_k)^T
\end{array}
\right].
$$

However, the above is just the solution of the quadratic problem with equality constraints
$$
\min_p \frac12 p^T\nabla^2_{xx}L(x^k,\lambda^k)p+\nabla_xL(x^k,\lambda^k)^Tp\\
\text{s.t. }h_j(x^k) + \nabla h_j(x^k)^Tp = 0.
$$

## Intuitive interpretation

We are approximating the Lagrange function quadratically around the current solution and the constraints are approximated linearly. SQP methods are also referred to as *projected Lagrangian methods*
* compare with projected gradient method from lecture 7

**Another view point to building the approximation**: Assume that we have a current solution candidate $(x_k,\lambda_k)$. Using Taylor's series for the constraints ($x^* = x_k + d$) and including only the first order term we get

$h_i(x^*)=h_i(x_k+d)\approx h_i(x_k) + \nabla h_i(x_k)^Td$

Since, $h_i(x^*)=0$ for all $i$ we have

$\nabla h(x_k)d = -h(x_k)$

For approximating the Lagrangian function, we use up to second order terms and get

$L(x_k+d,\lambda^*) \approx L(x_k,\lambda^*) + d^T\nabla_x L(x_k,\lambda^*) + \frac{1}{2}d^T\nabla_{xx}^2L(x_k,\lambda^*)d$

When combining both approximations, we get a quadratic subproblem

$$
\underset{d}{\min}d^T\nabla_x L(x_k,\lambda_k) + \frac{1}{2}d^T \nabla_{xx}^2L(x_k,\lambda_k)d\\
\text{s.t. } \nabla h(x_k)d = -h(x_k)
$$

It can be shown (under some assumptions) that solutions of the quadratic subproblems approach $x^*$ and Lagrange multipliers approach $\lambda^*$.

Note that we can either use the exact Hessian of the Lagrange function (requires second derivatives) or some approximation of it (compare Newton's method vs. quasi-Newton ideas).

## Implementation

Define an optimization problem, where
* $f(x) = \|x\|^2 = \sum_{i=1}^n x_i^2$
* $h(x) = \sum_{i=1}^nx_i=n$

What is $x^*$?

```python
def f_constrained(x):
    return sum([i**2 for i in x]),[],[sum(x)-len(x)]
#    return sum([i**2 for i in x]),[],[sum(x)-len(x),x[0]**2+x[1]-2]

```

```python
print(f_constrained([1,0,1]))
print(f_constrained([1,2,3,4]))
```

    (2, [], [-1])
    (30, [], [6])

```python
import numpy as np
import ad

#if k=0, returns the gradient of lagrangian, if k=1, returns the hessian
def diff_L(f,x,l,k):
    #Define the lagrangian for given f and Lagrangian multiplier vector l
    L = lambda x_: f(x_)[0] + (np.matrix(f(x_)[2])*np.matrix(l).transpose())[0,0]
    return ad.gh(L)[k](x)

#Returns the gradients of the equality constraints
def grad_h(f,x):
    return  [ad.gh(lambda y:
                   f(y)[2][i])[0](x) for i in range(len(f(x)[2]))]

#Solves the quadratic problem inside the SQP method
def solve_QP(f,x,l):
    left_side_first_row = np.concatenate((\
    np.matrix(diff_L(f,x,l,1)),\
    np.matrix(grad_h(f,x)).transpose()),axis=1)
    left_side_second_row = np.concatenate((\
    np.matrix(grad_h(f,x)),\
    np.matrix(np.zeros((len(f(x)[2]),len(f(x)[2]))))),axis=1)
    right_hand_side = np.concatenate((\
    -1*np.matrix(diff_L(f,x,l,0)).transpose(),
    -np.matrix(f(x)[2]).transpose()),axis = 0)
    left_hand_side = np.concatenate((\
                                    left_side_first_row,\
                                    left_side_second_row),axis = 0)
    temp = np.linalg.solve(left_hand_side,right_hand_side)
    return temp[:len(x)],temp[len(x):] # update for both x and l

def SQP(f,start,precision):
    x = start
    l = np.ones(len(f(x)[2])) # initialize Lagrange multiplier vector l as a vector of 1s
    f_old = float('inf')
    f_new = f(x)[0]
    while abs(f_old-f_new)>precision:
        print(x)
        f_old = f_new
        (p,v) = solve_QP(f,x,l) # obtain updates for x and l by solving the quadratic subproblem
        x = x+np.array(p.transpose())[0] # update the solution x
        l = l+v # update the Lagrange multipliers l
        f_new = f(x)[0]
    return x
```

```python
x0 = [2000.0,1000.0,3000.0,5000.0,6000.0]
SQP(f_constrained,x0,0.0001)
```

    [2000.0, 1000.0, 3000.0, 5000.0, 6000.0]
    [1. 1. 1. 1. 1.]

    array([1., 1., 1., 1., 1.])

## Lagrangian methods -- "The original method of multipliers"

Let us again consider problem

$$
\min f(x)\\
\text{s.t. }h_k(x) = 0\text{ for all }k=1,\ldots,K,
$$

where the objective function and the equality constraints are twice differentiable. Inequality constaints can be handled e.g. by first converting them into equality constraints which increases the number of variables.

We know that if $x^*$ is optimal solution, then $\nabla_x L(x^*,\lambda^*) = \nabla f(x^*) + \sum_{k=1}^K\lambda^*_k \nabla h_k(x^*) = 0$
(necessary condition).

However, if we only know that for $x^*$ it holds that $\nabla_x L(x^*,\lambda^*) = 0$, we can't be sure that $x^*$ is a local minimizer. (Why???)

Since the Hessian $\nabla^2_{xx}L(x^*,\lambda^*)$ may be indefinitive, it is not sufficient to just minimize the Langrangian function $L(x,\lambda)$ in order to minimize $f(x)$ with respect to the equality constraints $h_k(x)=0$.

Solution: Improve Lagrangian function

Define augmented Lagrangian function
$$
L_c(x,\lambda) = f(x)+\lambda h(x)+\frac12c\|h(x)\|^2.
$$
Above $c\in \mathbb R$ is a penalty parameter and $\lambda \in \mathbb R^K$ is a multiplier.

Now, we have
$$
\nabla^2_{xx}L_c(x^*,\lambda^*) = \nabla^2_{xx}L(x^*,\lambda^*) + c\nabla h(x^*)^T\nabla h(x^*)
$$
and it can be shown that for $c>\hat{c}$ the Hessian of the augmented Lagrangian is positive definite (sufficient condition for optimality).

Let us consider sequence of optimization problems
$$
\min_{x\in\mathbb R^n} f(x)+\lambda_k h(x)+\frac{1}{2}c_k\|h(x)\|^2,
$$
where $c_{k+1}>c_k$ for $k=1,2,\ldots$.

Now, if $\lambda_k=0$ for all $k=1,2,\ldots$, then we have a penalty function method, which solves the problem when $c_k\to \infty$.

However, it can be shown, that if we set $\lambda_0$ randomly and keep on updating
$\lambda_{k+1} = \lambda_k+c_kh(x_k)$, then we can show that there exists $C>0$ such that of $c_k>C$, then the optimal solution of the augmented Langrangian solves the original problem!

### Example

Let us have optimization problem
$$
\min x_1^2+x_2^2\\
\text{s.t. }x_1+x_2-1=0.
$$

Now, the minimization of the augmented Lagrangian becomes
$$
\min_{x\in\mathbb R^n} x_1^2+x_2^2+\lambda_k(x_1+x_2-1)+\frac12c_k(x_1+x_2-1)^2.\\
$$

```python
def f_constrained2(x):
    return sum([i**2 for i in x]),[],[sum(x)-1]
```

```python
def augmented_langrangian(f,x,la,c):
    second_term = float(numpy.matrix(la)*numpy.matrix(f(x)[2]).transpose())
    third_term = 0.5*c*numpy.linalg.norm(f(x)[2])**2
    return f(x)[0]+second_term+third_term
```

```python
from scipy.optimize import minimize
import numpy
def augmented_langrangian_method(f,start,la0,c0):
    x_old = [float('inf')]*2
    x_new = start
    f_old = float('inf')
    f_new = f(x_new)[0]
    la = la0
    c = c0
    steps = []
    while abs(f_old-f_new)>0.00001:
#    while numpy.linalg.norm(f(x_new)[2])>0.00001: # doesn't work as itself, see starting from any feasible point
        res = minimize(lambda x:augmented_langrangian(f,x,la,c),x_new)
        x_old = x_new
        f_old = f_new
        la = float(la+numpy.matrix(c)*numpy.matrix(f(res.x)[2]).transpose()) # update Lagrangian
        x_new = res.x
        f_new = f(x_new)[0]
        c = 2*c # increase the penalty coefficient
        steps.append(list(x_new))
    return x_new,c, steps
```

```python
x0 =[1/3,2/3]
#x0 =[10,-5]
l0 = 1.0
c0 = 1.0
[x,c,steps_ag] = augmented_langrangian_method(f_constrained2,x0,l0,c0)
print(x)
print(c)
print(len(steps_ag))
```

    [0.49999991 0.50000009]
    256.0
    8

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

def plot_2d_steps2(steps,start,interval):
    myvec = np.array([start]+steps).transpose()
    plt.plot(myvec[0,],myvec[1,],'rx')
    for label,x,y in zip([str(i) for i in range(len(steps)+1)],myvec[0,],myvec[1,]):
        plt.annotate(label,xy = (x, y))
    # plot constraint
    z = np.arange(interval[0],interval[1],0.1)
    l = len(z)
    con = [1.0-z[i] for i in range(l)]
    plt.plot(z,con,'b-')
    return plt
```

```python
plot_2d_steps2(steps_ag,x0,[0,1]).show()
```

## Compare with penalty function method

```python
from scipy.optimize import minimize
import numpy
def penalty_function_method(f,start,c0):
    x_old = float('inf')
    x_new = start
    f_old = float('inf')
    f_new = f(x_new)[0]
    c = c0
    steps = []
    while abs(f_old-f_new)>0.00001:
#    while numpy.linalg.norm(f(x_new)[2])>0.00001:
        res = minimize(lambda x:augmented_langrangian(f,x,0,c),x_new)
        x_old = x_new
        f_old = f_new
        x_new = res.x
        f_new = f(x_new)[0]
        c = 2*c
        steps.append(list(x_new))
    return x_new,c,steps
```

```python
[x,c,steps_pf] = penalty_function_method(f_constrained2,x0,c0)
print(x)
print(c)
print(len(steps_pf))
```

    [0.49999618 0.49999618]
    262144.0
    18

```python
plot_2d_steps2(steps_pf,x0,[0,1]).show()
```

## What is going on in here?

The above is a simplified representation of the augmented Lagrangian method. For example, one can use exact second derivatives for calculating $\nabla^2_{xx}L(x^*,\mu^*)$ to obtain better convergence but, also, one can approximate it by utilizing ideas from quasi-Newton methods in order to not requiring second derivatives. Efficient implementation of this (and other methods) for practical problems is not completely trivial, unfortunately. If you want to read details, please see e.g., http://www.mit.edu/~dimitrib/Constrained-Opt.pdf.

## Compare with SQP

```python
import numpy as np
import ad

#if k=0, returns the gradient of lagrangian, if k=1, returns the hessian
def diff_L(f,x,l,k):
    #Define the lagrangian for given m and f
    L = lambda x_: f(x_)[0] + (np.matrix(f(x_)[2])*np.matrix(l).transpose())[0,0]
    return ad.gh(L)[k](x)

#Returns the gradients of the equality constraints
def grad_h(f,x):
    return  [ad.gh(lambda y:
                   f(y)[2][i])[0](x) for i in range(len(f(x)[2]))]

#Solves the quadratic problem inside the SQP method
def solve_QP(f,x,l):
    left_side_first_row = np.concatenate((\
    np.matrix(diff_L(f,x,l,1)),\
    np.matrix(grad_h(f,x)).transpose()),axis=1)
    left_side_second_row = np.concatenate((\
    np.matrix(grad_h(f,x)),\
    np.matrix(np.zeros((len(f(x)[2]),len(f(x)[2]))))),axis=1)
    right_hand_side = np.concatenate((\
    -1*np.matrix(diff_L(f,x,l,0)).transpose(),
    -np.matrix(f(x)[2]).transpose()),axis = 0)
    left_hand_side = np.concatenate((\
                                    left_side_first_row,\
                                    left_side_second_row),axis = 0)
    temp = np.linalg.solve(left_hand_side,right_hand_side)
    return temp[:len(x)],temp[len(x):]

def SQP(f,start,precision):
    x = start
    l = np.ones(len(f(x)[2]))
    f_old = float('inf')
    f_new = f(x)[0]
    while abs(f_old-f_new)>precision:
        print(x)
        f_old = f_new
        (p,v) = solve_QP(f,x,l)
        x = x+np.array(p.transpose())[0]
        l = l+v
        f_new = f(x)[0]
    return x
```

```python
#x0 = [-3000,2500]
SQP(f_constrained2,x0,0.00001)
```

    [0.3333333333333333, 0.6666666666666666]
    [0.5 0.5]

    array([0.5, 0.5])

```python

```

Lecture 9, Methods using KKT conditions.ipynb


```python
# This cell is mandatory in all Dymos documentation notebooks.
missing_packages = []
try:
    import openmdao.api as om
except ImportError:
    if 'google.colab' in str(get_ipython()):
        !python -m pip install openmdao[notebooks]
    else:
        missing_packages.append('openmdao')
try:
    import dymos as dm
except ImportError:
    if 'google.colab' in str(get_ipython()):
        !python -m pip install dymos
    else:
        missing_packages.append('dymos')
try:
    import pyoptsparse
except ImportError:
    if 'google.colab' in str(get_ipython()):
        !pip install -q condacolab
        import condacolab
        condacolab.install_miniconda()
        !conda install -c conda-forge pyoptsparse
    else:
        missing_packages.append('pyoptsparse')
if missing_packages:
    raise EnvironmentError('This notebook requires the following packages '
                           'please install them and restart this notebook\'s runtime: {",".join(missing_packages)}')
```

(examples:the_robertson_problem)=

# The Robertson Problem

The [Robertson Problem](https://en.wikipedia.org/w/index.php?title=Stiff_equation&oldid=1017928453#Motivating_example) is a famous example for a [stiff ODE](https://en.wikipedia.org/w/index.php?title=Stiff_equation&oldid=1017928453). Solving stiff ODEs with [explicit integration methods](https://en.wikipedia.org/w/index.php?title=Explicit_and_implicit_methods&oldid=1036001392) leads to unstable behaviour unless an extremly small step size is choosen. [Implicit methods](https://en.wikipedia.org/w/index.php?title=Explicit_and_implicit_methods&oldid=1036001392) such as the [Radau](https://en.wikipedia.org/w/index.php?title=Runge%E2%80%93Kutta_methods&oldid=1052924118#Implicit_Runge%E2%80%93Kutta_methods), [BDF](https://en.wikipedia.org/w/index.php?title=Backward_differentiation_formula&oldid=1027943694) and LSODA methods can help solve such problems. The following example shows how to solve the Robertson Problem using [SciPys LSODA method](https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html#scipy.integrate.solve_ivp) and Dymos.

## The ODE system

The ODE of the Robertson Problem is

\begin{align}
    \dot x = &        - 0.04 x + 10^4 y \cdot z &                          \\
    \dot y = & \;\;\;\: 0.04 x - 10^4 y \cdot z &        - 3\cdot 10^7 y^2 \\
    \dot z = &                                  & \;\;\;\: 3\cdot 10^7 y^2 \\
\end{align}

where $x$, $y$ and $z$ are arbitrary states. The initial conditions are

\begin{align}
    x_0 &= 1 \\
    y_0 &= z_0 = 0.
\end{align}

The problem is solved for the time interval $t\in[0,40)$. There are no controls and constraints.

```python
import numpy as np
import openmdao.api as om

class RobertsonODE(om.ExplicitComponent):
    """example for a stiff ODE from Robertson.
    """

    def initialize(self):
        self.options.declare('num_nodes', types=int)

    def setup(self):
        nn = self.options['num_nodes']

        # input: state
        self.add_input('x', shape=nn, desc="state x", units=None)
        self.add_input('y', shape=nn, desc="state y", units=None)
        self.add_input('z', shape=nn, desc="state z", units=None)

        # output: derivative of state
        self.add_output('xdot', shape=nn, desc='derivative of x', units="1/s")
        self.add_output('ydot', shape=nn, desc='derivative of y', units="1/s")
        self.add_output('zdot', shape=nn, desc='derivative of z', units="1/s")

        r = np.arange(0, nn)
        self.declare_partials(of='*', wrt='*', method='exact',  rows=r, cols=r)

    def compute(self, inputs, outputs):

        x = inputs['x']
        y = inputs['y']
        z = inputs['z']

        xdot = -0.04 * x + 1e4 * y * z
        zdot = 3e7 * y ** 2
        ydot = - ( xdot + zdot )

        outputs['xdot'] = xdot
        outputs['ydot'] = ydot
        outputs['zdot'] = zdot

    def compute_partials(self, inputs, jacobian):

        x = inputs['x']
        y = inputs['y']
        z = inputs['z']

        xdot_y = 1e4 * z
        xdot_z = 1e4 * y

        zdot_y = 6e7 * y

        jacobian['xdot', 'x'] = -0.04
        jacobian['xdot', 'y'] = xdot_y
        jacobian['xdot', 'z'] = xdot_z

        jacobian['ydot', 'x'] = 0.04
        jacobian['ydot', 'y'] = - ( xdot_y + zdot_y )
        jacobian['ydot', 'z'] = - xdot_z

        jacobian['zdot', 'x'] = 0.0
        jacobian['zdot', 'y'] = zdot_y
        jacobian['zdot', 'z'] = 0.0

```

```python
num_nodes = 3

p = om.Problem(model=om.Group())

p.model.add_subsystem('ode', RobertsonODE(num_nodes=num_nodes), promotes=['*'])

p.setup(force_alloc_complex=True)

p.set_val('x', [10., 100., 1000.])
p.set_val('y', [1, 0.1, 0.01])
p.set_val('z', [1., 2., 3.])

p.run_model()
cpd = p.check_partials(method='cs', compact_print=True)
```

```python
from dymos.utils.testing_utils import assert_check_partials

assert_check_partials(cpd)
```

```python
from openmdao.utils.assert_utils import assert_near_equal

assert_near_equal(p.get_val('xdot'), [9999.6, 1996., 260.])
```

```python
from openmdao.utils.assert_utils import assert_near_equal

assert_near_equal(p.get_val('ydot'), [-3.00099996E7, -3.01996E5, -3.26E3])
```

```python
from openmdao.utils.assert_utils import assert_near_equal

assert_near_equal(p.get_val('zdot'), [3.0E7, 3.0E5, 3.0E3])
```

## Building and running the problem

```python
import openmdao.api as om
import dymos as dm

def robertson_problem(t_final=1.):
    #
    # Initialize the Problem
    #
    p = om.Problem(model=om.Group())

    #
    # Create a trajectory and add a phase to it
    #
    traj = p.model.add_subsystem('traj', dm.Trajectory())

    phase = traj.add_phase('phase0',
                           dm.Phase(ode_class=RobertsonODE,
                                    transcription=dm.GaussLobatto(num_segments=50)
                                   ))

    #
    # Set the variables
    #
    phase.set_time_options(fix_initial=True, fix_duration=True)

    phase.add_state('x0', fix_initial=True, fix_final=False, rate_source='xdot', targets='x')
    phase.add_state('y0', fix_initial=True, fix_final=False, rate_source='ydot', targets='y')
    phase.add_state('z0', fix_initial=True, fix_final=False, rate_source='zdot', targets='z')

    #
    # Setup the Problem
    #
    p.setup(check=True)

    #
    # Set the initial values
    #
    p['traj.phase0.t_initial'] = 0.0
    p['traj.phase0.t_duration'] = t_final

    p.set_val('traj.phase0.states:x0', phase.interp('x0', ys=[1.0, 0.7]))
    p.set_val('traj.phase0.states:y0', phase.interp('y0', ys=[0.0, 1e-5]))
    p.set_val('traj.phase0.states:z0', phase.interp('z0', ys=[0.0, 0.3]))

    return p

```

```python
# just set up the problem, test it elsewhere
p = robertson_problem(t_final=40)

p.run_model()

p_sim = p.model.traj.simulate(method='LSODA')
```

```python
from openmdao.utils.assert_utils import assert_near_equal

assert_near_equal(p_sim.get_val('traj.phase0.timeseries.states:x0')[-1], 0.71583161, tolerance=1E-5)
```

```python
from openmdao.utils.assert_utils import assert_near_equal

assert_near_equal(p_sim.get_val('traj.phase0.timeseries.states:y0')[-1], 9.18571144e-06, tolerance=1E-1)
```

```python
from openmdao.utils.assert_utils import assert_near_equal

assert_near_equal(p_sim.get_val('traj.phase0.timeseries.states:z0')[-1], 0.2841592, tolerance=1E-5)
```

```python
import matplotlib.pyplot as plt

t_sim = p_sim.get_val('traj.phase0.timeseries.time')

states = ['x0', 'y0', 'z0']
fig, axes = plt.subplots(len(states), 1)
for i, state in enumerate(states):
    axes[i].plot(t_sim, p_sim.get_val(f'traj.phase0.timeseries.states:{state}'), '-')
    axes[i].set_ylabel(state[0])
axes[-1].set_xlabel('time (s)')
plt.tight_layout()
plt.show()
```


# Synthetic Data Experiments (Visualizations)

```julia
include("src/Misc.jl")
using .Misc
```

```julia
import PyPlot, Seaborn
import Suppressor: @suppress_err
```

```julia
MARKER = ["o", "v", "d", "X", "p", "*", "P"];
PATH = "./results/synt";
IMG = "./img/synt";
```

## On Hyperparameters of BAM

\begin{equation}
X^{(1)} = \left(\begin{array}{cccc}
    2 & 1 & 1 & 0 \\
    0 & 0 & 1 & 2 \\
    0 & 0 & 1 & 1
    \end{array} \right)
\end{equation}

```julia
result = load_json("$PATH/effective_params.json")

R = Int.(result["R"])
As = Float64.(result["a"])
dₑₚ = Int.(result["EP"])
log_PS = hcat(result["log_PS"]...);
log_PX = logsumexp(log_PS,1);
```

```julia
@suppress_err begin
fig, ax = PyPlot.subplots(1, 1+length(As); figsize=(14,3))

ax[1].set_ylabel("# of \$ S \$")
for (i,a) ∈ enumerate(As)
    ax[i].set_title("\$ a = $a \$")
    ax[i].hist(log_PS[:,i]; bins=200, label="\$ \\log \\pi(S) \$")
    ax[i].axvline(log_PX[i]; label="\$ \\log \\mathcal{L}_X \$", linestyle="-.", linewidth=2.0, color="red")
    ax[i].set_xlabel("\$ \\log \\pi(S) \$")
    ax[i].legend()
end

ax[end].set_title("Effective Parameters")
ax[end].hist(dₑₚ; bins=200, label="\$ d_{EP} \$")
ax[end].set_xlabel("\$ d_{EP}\$")
ax[end].invert_xaxis()
ax[end].legend()

PyPlot.savefig("$IMG/alloc_tensor_dist.pdf", bbox_inches="tight");
end
```

## Comparison of SMC, VB and exact enumeration on toy data

\begin{equation}
X^{(1)} = \left(\begin{array}{cccc}
    2 & 1 & 1 & 0 \\
    0 & 0 & 1 & 2 \\
    0 & 0 & 1 & 1
    \end{array} \right)
\end{equation}

```julia
result = load_json("$PATH/marginal_lkhd.json")

As = Float64.(result["a"])
Rs = Int.(result["R"])

log_PX = hcat(result["log_PX"]...)
log_PX_smc = hcat(result["log_PX_smc"]...)
log_PX_vb = hcat(result["log_PX_vb"]...);
```

```julia
log_PR = log_PX .- logsumexp(log_PX,dims=1);
log_PR_smc = log_PX_smc .- logsumexp(log_PX_smc,dims=1);
log_PR_vb = log_PX_vb .- logsumexp(log_PX_vb,dims=1);
```

```julia
@suppress_err begin
fig, ax = PyPlot.subplots(2, 3; figsize=(12,7.5))

for (r,R) in enumerate(Rs)
    ax[1,1].semilogx(As,log_PX[r,:], label="K = $R", marker=MARKER[r])
    ax[2,1].semilogx(As,log_PR[r,:], label="K = $R", marker=MARKER[r])
    ax[1,2].semilogx(As,log_PX_smc[r,:], label="K = $R", marker=MARKER[r])
    ax[2,2].semilogx(As,log_PR_smc[r,:], label="K = $R", marker=MARKER[r])
    ax[1,3].semilogx(As,log_PX_vb[r,:], label="K = $R", marker=MARKER[r])
    ax[2,3].semilogx(As,log_PR_vb[r,:], label="K = $R", marker=MARKER[r])
end
ax[1,1].set_ylabel("\$ \\log \\mathcal{L}_X(K) \$");
ax[2,1].set_ylabel("\$ \\log \\mathrm{Pr}(K \\mid X) \$");
ax[1,1].set_title("Marginal Log-Likelihood");
ax[1,2].set_title("Marginal Log-Likelihood (SMC)");
ax[1,3].set_title("Marginal Log-Likelihood (ELBO)");
ax[2,1].set_title("Posterior Log Odds");
ax[2,2].set_title("Posterior Log Odds (SMC)");
ax[2,3].set_title("Posterior Log Odds (ELBO)");

foreach(i -> ax[2,i].set_xlabel("\$ a \$"), 1:3);
foreach(i -> ax[1,i].set_xlim(ax[1,1].get_xlim()), 1:3);
foreach(i -> ax[2,i].set_ylim(-3), 1:3);
for i ∈ 1:2, j ∈ 1:3; ax[i,j].legend() end

PyPlot.savefig("$IMG/marginal_lkhd.pdf", bbox_inches="tight");
end
```

\begin{equation}
X^{(2)} = \left(\begin{array}{ccc}
    4 & 3 & 0 \\
    0 & 0 & 3 \\
    0 & 0 & 3
    \end{array} \right)
\end{equation}

```julia
result = load_json("$PATH/marginal_lkhd2.json")

As = Float64.(result["a"])
Rs = Int.(result["R"])

log_PX = hcat(result["log_PX"]...)
log_PX_smc = hcat(result["log_PX_smc"]...)
log_PX_vb = hcat(result["log_PX_vb"]...);
```

```julia
log_PR = log_PX .- logsumexp(log_PX,dims=1);
log_PR_smc = log_PX_smc .- logsumexp(log_PX_smc,dims=1);
log_PR_vb = log_PX_vb .- logsumexp(log_PX_vb,dims=1);
```

```julia
@suppress_err begin
fig, ax = PyPlot.subplots(2, 3; figsize=(12,7.5))

for (r,R) in enumerate(Rs)
    ax[1,1].semilogx(As,log_PX[r,:], label="K = $R", marker=MARKER[r])
    ax[2,1].semilogx(As,log_PR[r,:], label="K = $R", marker=MARKER[r])
    ax[1,2].semilogx(As,log_PX_smc[r,:], label="K = $R", marker=MARKER[r])
    ax[2,2].semilogx(As,log_PR_smc[r,:], label="K = $R", marker=MARKER[r])
    ax[1,3].semilogx(As,log_PX_vb[r,:], label="K = $R", marker=MARKER[r])
    ax[2,3].semilogx(As,log_PR_vb[r,:], label="K = $R", marker=MARKER[r])
end
ax[1,1].set_ylabel("\$ \\log \\mathcal{L}_X(K) \$");
ax[2,1].set_ylabel("\$ \\log \\mathrm{Pr}(K \\mid X) \$");
ax[1,1].set_title("Marginal Log-Likelihood");
ax[1,2].set_title("Marginal Log-Likelihood (SMC)");
ax[1,3].set_title("Marginal Log-Likelihood (ELBO)");
ax[2,1].set_title("Posterior Log Odds");
ax[2,2].set_title("Posterior Log Odds (SMC)");
ax[2,3].set_title("Posterior Log Odds (ELBO)");

foreach(i -> ax[2,i].set_xlabel("\$ a \$"), 1:3);
foreach(i -> ax[1,i].set_xlim(ax[1,1].get_xlim()), 1:3);
foreach(i -> ax[2,i].set_ylim(-3.5,0.0), 1:3);
for i ∈ 1:2, j ∈ 1:3; ax[i,j].legend() end

PyPlot.savefig("$IMG/marginal_lkhd2.pdf", bbox_inches="tight");
end
```

## Posterior Distribution of the number of tokens $S_+$

\begin{equation}
X^{(3)} = \left(\begin{array}{cc}
    3 & ? \\
    3 & 3 \\
    \end{array} \right)
\end{equation}

```julia
result₁ = load_json("$PATH/Sp_dist.json")

Ts = Int.(result₁["T"])
Rs = Int.(result₁["R"])
log_PT = hcat(result₁["log_PT"]...);
```

```julia
fig, ax = PyPlot.subplots(1, 1; figsize=(5,3))
for (r,R) in enumerate(Rs)
    ax.plot(Ts,log_PT[r,:], label="K = $R", marker=MARKER[r])
end
ax.set_title("Distribution of \$ \\mathbf{S}_+ \$");
ax.set_ylabel("\$ \\log \\mathrm{Pr}(\\mathbf{S}_+ = T,  X^{(3)} \\mid K) \$");
ax.set_xlabel("\$ T \$")
ax.legend();

PyPlot.savefig("$IMG/Sp_dist.pdf", bbox_inches="tight");
```

\begin{equation}
X^{(4)} = \left(\begin{array}{ccc}
    4 & ? \\
    4 & 1
    \end{array} \right)
\end{equation}

```julia
result₂ = load_json("$PATH/Sp_dist2.json")

Ts = Int.(result₂["T"])
Rs = Int.(result₂["R"])
log_PT = hcat(result₂["log_PT"]...);
```

```julia
fig, ax = PyPlot.subplots(1, 1; figsize=(5,3))
for (r,R) in enumerate(Rs)
    ax.plot(Ts,log_PT[r,:], label="K = $R", marker=MARKER[r])
end
ax.set_title("Distribution of \$ \\mathbf{S}_+ \$");
ax.set_ylabel("\$ \\log \\mathrm{Pr}(\\mathbf{S}_+ = T,  X^{(4)} \\mid K) \$");
ax.set_xlabel("\$ T \$")
ax.legend();

PyPlot.savefig("$IMG/Sp_dist2.pdf", bbox_inches="tight");
```

## Model Order Selection for CP/PARAFAC

### Confusion Matrix

```julia
result = load_json("$PATH/parafac_confusion.json")

I, J, K, T = result["I"], result["J"], result["K"], result["T"]
Rs = Int.(result["R"])
As = Float64.(result["a"])

R_true = Int.(result["R_true"])
R_smc = Int.(result["R_smc"])
R_vb = Int.(result["R_vb"]);
```

```julia
conf_smc = zeros(Int,length(Rs),length(Rs));
conf_vb = zeros(Int,length(Rs),length(Rs));
```

```julia
for i ∈ 1:length(R_true)
    conf_smc[R_true[i],R_smc[i]] += 1
    conf_vb[R_true[i],R_vb[i]] += 1
end
```

```julia
@suppress_err begin
fig, ax = PyPlot.subplots(1; figsize=(4,3))
Seaborn.heatmap(conf_vb; annot=true, cmap="YlGnBu", cbar=false, xticklabels=Rs, yticklabels=Rs, linewidths=.4, ax=ax)

ax.set_title("Confusion Matrix of Model Orders")
ax.set_ylabel("\$ R_\\mathrm{true} \$")
ax.set_xlabel("\$ R_\\mathrm{VB} \$")

PyPlot.savefig("$IMG/parafac_confusion_vb.pdf", bbox_inches="tight");
end
```

```julia
@suppress_err begin
fig, ax = PyPlot.subplots(1; figsize=(4,3))
Seaborn.heatmap(conf_smc; annot=true, cmap="YlGnBu", cbar=false, xticklabels=Rs, yticklabels=Rs, linewidths=.4, ax=ax)

ax.set_title("Confusion Matrix of Model Orders")
ax.set_ylabel("\$ R_\\mathrm{true} \$")
ax.set_xlabel("\$ R_\\mathrm{SMC} \$")

PyPlot.savefig("$IMG/parafac_confusion_smc.pdf", bbox_inches="tight");
end
```

### An Example

```julia
result = load_json("$PATH/parafac_model_selection.json")

I, J, K, R_true = result["I"], result["J"], result["K"], result["R_true"]
Rs = Int.(result["R"])
a = result["a"]
log_PX_smc, log_PX_vb = Float64.(result["log_PX_smc"]), Float64.(result["log_PX_vb"]);
```

```julia
PyPlot.figure(figsize=(5,3))

PyPlot.axvline(R_true; label="G. Rank", color="black", linestyle="--")
PyPlot.plot(Rs, log_PX_vb; label="ELBO", marker=MARKER[5]);
PyPlot.plot(Rs, log_PX_smc; label="SIS/R", marker=MARKER[6]);

PyPlot.xlabel("\$R\$ (Model order)")
PyPlot.ylabel("\$ \\log \\mathcal{L}_X(R) \$")
PyPlot.title("Model Scoring Example (PARAFAC)");
PyPlot.legend();

PyPlot.savefig("$IMG/cp_model_selection.pdf", bbox_inches="tight");
```

### Runtimes

```julia
result = load_json("$PATH/parafac_runtime.json")

T, P, R, a = result["T"], result["P"], result["R"], result["a"]
IJK = Array{Array{Int,1},1}(result["IJK"])
smc_times = hcat(result["smc"]...)
vb_times = hcat(result["vb"]...);
```

```julia
PyPlot.figure(figsize=(5,3))
PyPlot.plot(1:length(IJK),nanmean(vb_times,2); label="VB", marker=MARKER[5])
PyPlot.plot(1:length(IJK),nanmean(smc_times,2); label="SIS/R", marker=MARKER[6])
PyPlot.title("Average Runtimes (PARAFAC)")
PyPlot.ylabel("sec.")
PyPlot.xlabel(" \$ I_1 \\times I_2 \\times I_3 \$ (Dimensions of \$X\$)")
PyPlot.xticks(1:2:length(IJK),["\$ {$(4*i)}^3 \$" for i ∈ 1:2:length(IJK)])
PyPlot.legend()

PyPlot.savefig("$IMG/runtimes.pdf", bbox_inches="tight");
```


# Lección 2: Los postulados de la mecánica cuántica #

La mecánica cuántica es el marco teórico en el que los científicos trabajan el desarrollo de teorías físicas relacionadas con el mundo microscópico. Este marco de trabajo matemático, por si solo, no indica las leyes que un sistema debería cumplir, sin embargo, ofrece el panorama de cómo este sistema puede cambiar.

Los postulados de la mecánica cuántica son aquellos que establecen la relación entre el mundo físico y el formalismo matemático de la mecánica cuántica.
Es importante tener en cuenta que el desarrollo de estos postulados se dio a traves del ensayo y el error, de forma tal que estos parecen partir de motivaciones no muy claras, sin embargo, nuestro objetivo en esta sesión es aprender el cómo aplicarlos y cuándo hacerlo.

**SUPER IMPORTANTE:** En este ejemplo intentaremos presentar al lector el formalismo necesario para estudiar la computación cuántica en forma teórica, así como una posible intuición física sobre la misma. Esta sesióne stá disponible en nuestro servidor de MyBinder.

Primero importaremos alguna librerías que son necesarias para desarrollar nuestro experimento

```python
from qiskit import QuantumRegister, ClassicalRegister
from qiskit import QuantumCircuit, execute, Aer
from qiskit.circuit import Parameter
from qiskit.visualization import plot_histogram,state_visualization
from qiskit.quantum_info.operators import Operator
import matplotlib.pyplot as plt
import numpy as np

pi=np.pi
```

Ahora definimos una función con un parametro $\phi$ que nos servirá para hacer una evolución temporal en una simulación de un interferómetro de Mach-Zehnder.

```python
def MZ_interferometer(phi):
    ## La variable phi esta relacionada al cambio de fase en uno de los dos caminos
    MZ_circuit=QuantumCircuit(1)

    ## Se define una compuerta de Hadamard la cual cumple la función de beam splitter
    MZ_circuit.h(0)

    ## Luego se hace una diferencia de camino a traves del uno de los caminos del campo,
    ## En este caso el que esta en el estado que llamamos |1>

    MZ_circuit.p(phi,0)

    ## finalmente ponemos el siguiente BS para terminar con el interferómetro
    MZ_circuit.h(0)

    MZ_gate=MZ_circuit.to_gate()
    MZ_gate.name = "MZ-Interferometer"

    return MZ_gate
```

### Postulado 1 ###

El primer postulado de la mecánica cuántica nos indica el espacio matemático en el que, dado un sistema, se trabaja el formalismo de la mecánica cuántica.
Éste dicta que:
>Dado un sistema físico aislado hay un espacio vectorial complejo con un producto interno definido (un espacio de Hilbert) conocido como ***espacio de estados***  del sistema. El sistema en este espacio se representa a partir de un ***vector de estado***, el cual es unitario en este espacio, es decir.

$$|\psi\rangle = \sum_k c_k|k\rangle \hspace{2em} \text{donde} \hspace{2em} \langle\psi|\psi\rangle = \sum_k |c_k|^2 = 1.$$

La notación que se utiliza en la mecánica cuántica para escribir estos estados se conoce como la notación bra-ket de Dirac. En esta notación el estado bra se escribe, $\langle \psi |$ y su correspondiente ket es $|\psi\rangle$.

En el caso de la computación cuántica nosotros vamos a estar interesados en un espacio de estados particular, un espacio de en el que el sistema tiene dos estados bien definidos $|0\rangle$ y $|1\rangle$, este espacio de estados lo llamaremos el espacio de qubit. En este caso los estados $|0\rangle$ y $|1\rangle$ son la base de nuestro espacio vectorial, y dado que estos se definen ortonormales, podemos representarlos en una forma vectorial

$$|0\rangle = \begin{bmatrix} 1 \\ 0 \end{bmatrix} \hspace{2em} \text{y} \hspace{2em} |1\rangle = \begin{bmatrix} 0 \\ 1 \end{bmatrix}$$

Por lo tanto, el ket asociado a un estado arbitrario $|\psi\rangle$ se puede representar de la forma

$$ |\psi\rangle = \alpha|0\rangle + \beta|1\rangle = \begin{bmatrix} \alpha \\ \beta \end{bmatrix} $$

Los coeficientes $\alpha$ y $\beta$ representan las amplitudes de los estados $|0\rangle$ y $|1\rangle$ respectivamente. Estos números Estan relacionados con las *probabilidades* de encontrar el sistema, en este caso el qubit, en un estado o el otro. Más en detalle, las probabilidades de encontrar el qubit $|\psi\rangle$ en el estado $|0\rangle$ o $|1\rangle$ después de realizar la medición son

$$p(|0\rangle) = |\alpha|^2 \hspace{2em}\text{y}\hspace{2em} p(|1\rangle) = |\beta|^2$$

Antes de continuar hagamos un pequeño experimento.

Preparemos un estado de una forma partícular, en concreto vamos a prepararlo en el estado

$$|\psi\rangle = \frac{1}{\sqrt{2}}\left(|0\rangle + |1\rangle\right) $$

```python
## Inicializamos los registros necesarios
qr=QuantumRegister(1)
cr=ClassicalRegister(1)
qc=QuantumCircuit(qr,cr)

## Aplicamos una compuerta de Hadamard
qc.h(qr[0])
qc.measure(qr[0],cr[0])
qc.draw(output='mpl')
```

De acuerdo con lo que vimos anteriormente entonces $\alpha = \beta = \frac{1}{\sqrt{2}}$. Por lo tanto, las probabilidades de que el qubit esté en los estados $|0\rangle$ o $|1\rangle$ son $p(|0\rangle) = \frac{1}{2}$ y $p(|1\rangle) = \frac{1}{2}$ respectivamente. Ahora realizamos una medición.

```python
## Usamos el simulador qasm
backend = Aer.get_backend('qasm_simulator')
results = execute(qc,backend,shots=10000).result().get_counts()
plot_histogram(results)
```

Lo que haremos ahora es agregar una fase de la forma $e^{i\phi}$ en la amplitud del estado $|1\rangle$. Por lo cual el estado toma la forma

$$|\psi\rangle = \frac{1}{\sqrt{2}}\left(|0\rangle + i|1\rangle\right) .$$

```python
qr=QuantumRegister(1)
cr=ClassicalRegister(1)
qc=QuantumCircuit(qr,cr)

qc.h(qr[0])
qc.p(pi/2,qr[0])
qc.measure(qr[0],cr[0])
qc.draw(output='mpl')
```

Midiendo nuevamente tenemos.

```python
backend = Aer.get_backend('qasm_simulator')
results = execute(qc,backend,shots=10000).result().get_counts()
plot_histogram(results)
```

Es decir que, así se añadiera un factor de fase las distribuciones de probabilidad luego de medir son iguales, por lo tanto, los números $\alpha$ y $\beta$ tienen que ser números de la forma $\alpha = |\alpha|e^{i\theta_\alpha}$ y $\beta = |\beta|e^{i\theta_\beta}$, es decir, estos números pertenecen a $\mathbb{C}$.

Con base a este resultado, podemos escribir apropiadamente el bra asociado al estado $|\psi\rangle$. Este es

$$\langle \psi | =  \alpha^* \langle 0 | + \beta^*\langle 1 | = \begin{bmatrix}\alpha^* ,\beta^*\end{bmatrix} $$

Estos estados cumplen con las relaciones

$$\langle 0|0 \rangle =\begin{bmatrix}1 ,0\end{bmatrix}\begin{bmatrix}1 \\0\end{bmatrix}  = 1$$

$$\langle 1|1\rangle = \begin{bmatrix}0 ,1\end{bmatrix}\begin{bmatrix}0 \\1\end{bmatrix}  = 1$$

$$\langle 0|1\rangle = \begin{bmatrix}1 ,0\end{bmatrix}\begin{bmatrix}0 \\1\end{bmatrix}  = 0$$

$$\langle 1|0\rangle = \begin{bmatrix}0 ,1\end{bmatrix}\begin{bmatrix}1 \\0\end{bmatrix}  = 0$$

Por lo cual el Bra-Ket del estado $|\psi\rangle$, el cual representa el producto punto en el espacio de estados, se escribe:

$$\langle\psi|\psi\rangle = \left( \alpha^* \langle 0 | + \beta^*\langle 1 | \right) \left(\alpha|0\rangle + \beta|1\rangle\right)$$

$$ \langle\psi|\psi\rangle =|\alpha|^2 + |\beta|^2 = 1 $$

O vectorialmente

$$\langle\psi|\psi\rangle =  \begin{bmatrix}\alpha^* ,\beta^*\end{bmatrix}\begin{bmatrix}\alpha \\\beta\end{bmatrix} = |\alpha|^2 + |\beta|^2 = 1$$

Adicionalmente, así como definimos el Bra-ket podemos definir un Ket-Bra, que tendría la forma

$$|\psi\rangle\langle\psi| = \begin{bmatrix}\alpha\alpha^*&\alpha\beta^*\\\beta\alpha^*&\beta\beta^*\end{bmatrix}$$

Este tipo de producto, que es similar al producto tensorial, se conoce en este contexto como el producto de Kronecker.

Dado que los números $\alpha$ y $\beta$ son complejos entonces un sistema de dos estados, como es el caso de un qubit, puede representarse sobre la superficie de una esfera de radio $1$, esta esfera se conoce como la *Esfera de Bloch*.

### Postulado 2 ###

>La evolución temporal de un sistema cuántico ***cerrado*** se describe a traves de una ***transformación unitaria***. esto implica que si en un tiempo $t_{1}$ el sistema se encuentra en el estado $|\psi\rangle$. Luego, en un instante $t_2$ el sistema estará en el estado $|\psi '\rangle$. Esta evolución se da por un operador unitario $U(t_1,t_2)$, que solo depende de los dos tiempos ya mencionados. Esta evolución se puede escribir como
$$ |\psi'\rangle = U |\psi\rangle. $$

Así mismo, teniendo en mente el operador Hamiltoniano de un sistema cerrado, se puede escribir la evolución temporal de este a traves de la ecuación de Schrödinger,
$$ i\hbar \frac{d|\psi\rangle}{dt} = \mathcal{H} |\psi\rangle $$

Como hemos visto, así como los estados tienen una representación vectorial, los operadores que actuan sobre los estados se pueden representar matricialmente para que modifiquen estos estados. En el caso de la computación cuántica cada una de las compuertas que se usan tiene una representación matricial. El primer operador que veremos es el operador identidad $\mathbb{1}$. Este operador no modifica el estado de ninguna forma, y se puede representar matricialmente como

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

Otra transformación se representa mediante el símbolo $X$, esta transformación lo que hace es intercambiar los elementos de la base, es decir

$$\begin{equation}X|0\rangle\rightarrow|1\rangle, \hspace{2em}X|1\rangle\rightarrow|0\rangle\end{equation}$$

Matricialmente se puede representar como

$$\begin{equation}X=\begin{bmatrix}0 & 1 \\ 1 & 0\end{bmatrix}\end{equation}$$

Esta transformación se llama phase-flip y se puede representar mediante el operador $Z$. esta cambia la fase relativa entre los estados  $|0\rangle$ y $|1\rangle$ en un factor de $\pi$, es decir

$$\begin{equation}Z|0\rangle\rightarrow|0\rangle, \hspace{2em}	Z|1\rangle\rightarrow -|1\rangle\end{equation} $$

Matricialmente se representa por

$$\begin{equation}Z=\begin{bmatrix}1 & 0 \\ 0 & -1\end{bmatrix} \end{equation}$$

Los operadores $X$ y $Z$ son dos de los conocidos operadores de Pauli, Estos se representan por las llamadas matrices de Pauli. Adicionalmente hay otra transformación que viene de la combinación entre $X$ y $Z$ de la forma $ZX$, esta actúa sobre el estado de forma tal que

$$\begin{equation}ZX|0\rangle\rightarrow-|1\rangle, \hspace{2em}	ZX|1\rangle\rightarrow |0\rangle.\end{equation} $$

Se puede representar matricialmente de la forma

$$\begin{equation}ZX=\begin{bmatrix}0 & 1 \\ -1 & 0\end{bmatrix} = iY\end{equation}$$

Con $Y$ La tercera matriz de Pauli. Esta tiene la forma

$$\begin{equation}Y=\begin{bmatrix}0 & -i \\ i & 0\end{bmatrix}\end{equation}$$

En este punto es importante definir la operación $^\dagger$ que se realiza sobre un operador. Esta operación implica tomar el operador transpuesto y complejo conjugado, por ejemplo para el operador $Y$ tenemos

$$\begin{equation}Y=\begin{bmatrix}0 & -i \\ i & 0\end{bmatrix}\Rightarrow Y^\dagger=\begin{bmatrix}0 & i \\ -i & 0\end{bmatrix}^* = \begin{bmatrix}0 & -i \\ i & 0\end{bmatrix}\end{equation}$$

Como vimos, $Y = Y^\dagger$, esta es una propiedad llamada *Hermiticidad*. Cuando un operador en mecánica cuántica es hermítico, entonces es posible medir los observables asociados a ese operador.

Además de los operadores de Pauli hay otro par de operadores que son importantes para futuros desarrollos. El primero es el operador de Hadamard, el cual tiene la forma

$$H =\frac{1}{\sqrt{2}} \begin{bmatrix}1 & 1\\1 & -1 \end{bmatrix}$$

Esta compuerta lo que hace es rotar los estados en un ángulo de 45°. Es decir

$$|0\rangle\rightarrow \frac{1}{\sqrt{2}}\left(|0\rangle + |1\rangle\right)\hspace{2em} \text{y}\hspace{2em}|1\rangle \rightarrow \frac{1}{\sqrt{2}} \left(|0\rangle - |1\rangle\right)$$

Un ultimo operador que nos sirve en este momento es el cambio de fase. Este operador lo que hace es añadir una fase cuando el sistema se encuentra en el estado $|1\rangle$. Matricialmente se puede representar como

$$P_\phi =\begin{bmatrix}1 & 0\\0 & e^{i\phi} \end{bmatrix}$$

Para ilustrar este principio lo que haremos será simular un interferómetro de Mach-Zehnder como el que se muestra en la siquiente figura.

En este caso, solo se envía un fotón por la entrada que representa el estado $|0\rangle$. El estado inicial se puede representar de la forma
$$ |\psi_i\rangle = |0\rangle$$.

En esta figura se tiene que

Ahora debemos representar este interferómetro mediante un circuito cuántico y encontrar la probabilidad con la que alguno de los medidores va a detectar el fotón, es decir, determinar la probabilidad de que esté en el estado $|0\rangle$ o $|1\rangle$.
Para esto se tiene el haz que ingresa. Luego, este pasa por un divisor de haz, este divisor de haz lo que hace es dar al fotón la posibilidad de seguir su camino (camino $|0\rangle$) o ser reflejado (camino $|1\rangle$) con probabilidad de 50\%. Esto se puede representar mediante una compuerta de Hadamard $H$. Luego de esto, el fotón puede o no ser desfasado, la condición de que el desfasador $\phi$ actue depende de si el fotón va o no por el camino $|1\rangle$. Este desfasador se puede representar por la compuerta de fase $P_\phi$. Finalmente, sin importar el camino, el haz pasa nuevamente por un divisor de haz, es decir, una nueva compuerta Hadamard. Por lo cual el interferómetro se podría visualizar de la siguiente forma

```python
## Primero definiremos el fotón que unicia por el camino que definimos como el estado inicial $|0>$
qr=QuantumRegister(1)
cr=ClassicalRegister(1)
qc=QuantumCircuit(qr,cr)

## definimos el parametro
phi=Parameter('$\phi$')

## Definimos el espaciamiento deseado
angulos = np.linspace(0,2*pi,100)

## Aplicamos el interferómetro de MZ a nuestro estado inicial
MZ_I = MZ_interferometer(phi)

qc.append(MZ_I,[0])
qc.measure(qr[0],cr[0])
qc.decompose().draw(output='mpl')
```

Luego de pasar por el interferómetro, el estado final toma la forma

$$|\psi_f\rangle = \cos\frac{\phi}{2}|0\rangle - i\sin\frac{\phi}{2} |1\rangle$$

Por lo tanto, las probabilidades de medir el estado $|0\rangle$, es decir, que se haga una detección en el detector $D_4$  y de medir $|1\rangle$, es decir, hacer una detección en el detector $D_5$ son:

$$P_0 = \cos^2 \frac{\phi}{2} \hspace{2em} \text{y}\hspace{2em} P_1 = \sin^2 \frac{\phi}{2}$$

De acá se ve claramente que

$$ P_0 + P_1 = \cos^2 \frac{\phi}{2} +\sin^2 \frac{\phi}{2} = 1 $$

```python
shots=1024
backend = Aer.get_backend('qasm_simulator')
resultados = execute(qc,backend,parameter_binds=[{phi:angulo}for angulo in angulos],shots=shots).result().get_counts()

## Capturamos la probabilidad de en el medidor 1
P_0 = np.array([resultado.get('0',0)/shots for resultado in resultados])

## Capturamos la probabilidad de en el medidor 2
P_1 = np.array([resultado.get('1',0)/shots for resultado in resultados])

## Graficamos
plt.title(r'Probabilidad de medición por detector en el interferometro Mach-Zehnder')
plt.xlabel(r'$\phi$ (radianes)')
plt.ylabel(r'$Probabilidades$')
plt.plot(angulos,P_0,label='P_0',color='k')
plt.plot(angulos,P_1,label='P_1',color='r')
plt.plot(angulos,P_0 + P_1,label='P_0 + P_1',color='b')
plt.legend(bbox_to_anchor = (1, 1))
plt.show()
```

### Postulado 3 ###

>Las mediciones a un sistema cuántico se describen a traves de una colección ${M_{m}}$ de ***operadores de medición***. Estos operadores actuan sobre el es estacio de estados del sistema. El indice $m$ se refiere a las posibles mediciones que pueden resultar en el experimento. Es decir, si un sistema se encuentra en el estado $|\psi>$ justo antes de la medición, la probabilidad de que se de el resultado $m$ es

$$p(m) = \langle\psi|M_m^{\dagger}M_m|\psi\rangle.$$

Luego de la medición el estado del sistema colapsa al estado

$$\frac{M_m |\psi\rangle}{\sqrt{\langle\psi|M_m^{\dagger}M_m|\psi\rangle}}.$$

Es importante tener en cuenta que los operadores de medición satisfacen la relación de completez

$$\sum_m M_m^{\dagger}M_m = I .$$

Esta relación de completez muestra también el hecho de que la suma de las probabilidades de que el sistema este en el estado $m$ sea uno:

$$\sum_m p(m) = \sum_m \langle\psi|M_m^{\dagger}M_m|\psi\rangle = 1$$

En el caso de un qubit, tenemos dos operadores de medición, el que mide el estado $|0\rangle$ que llamamos $M_0$ y el que mide el estado $|1\rangle$ que llamamos $M_1$. Se representan de la forma

$$M_0= |0\rangle\langle 0| = \begin{bmatrix}1 & 0 \\ 0 & 0\end{bmatrix} \hspace{2em} \text{y} \hspace{2em} M_1= |1\rangle\langle 1| = \begin{bmatrix}0 & 0 \\ 0 & 1\end{bmatrix} $$

Entonces, por ejemplo, tomemos el estado final presentado en el caso del interferómetro de Mach-Zehnder

$$|\psi\rangle = \cos\frac{\phi}{2}|0\rangle - i\sin\frac{\phi}{2} |1\rangle$$

Primero supongamos que al realizar la medición el estado resulta ser $|0\rangle$, entonces el estado luego de la medición es

$$|\psi\rangle \rightarrow \frac{M_0 |\psi\rangle}{\sqrt{\langle\psi|M_0^{\dagger}M_0|\psi\rangle}}$$

$$= \frac{\cos\frac{\phi}{2} |0\rangle}{\sqrt{\cos^2\frac{\phi}{2}}} = |0\rangle$$

De igual forma, suponiendo que al medir el estado colapsó al estado $|1\rangle$, se tiene

$$|\psi\rangle \rightarrow \frac{M_1 |\psi\rangle}{\sqrt{\langle\psi|M_1^{\dagger}M_1|\psi\rangle}}$$

$$= \frac{\sin\frac{\phi}{2} |1\rangle}{\sqrt{\sin^2\frac{\phi}{2}}} = |1\rangle$$

### Postulado 4 ###
>El espacio de los estados de un sistema físico compuesto es el producto tensorial entre los espacios de estados de los componentes de este sistema compuesto. Esto quiere decir que, si tenemos $1,2,...,n$ sistemas que se encuentran en los estados $|\psi_1>,|\psi_2>,...,|\psi_n>$ respectivamente, entonces, la decripción de el sistema físico compuesto $|\Psi>$ sería

$$|\Psi> = |\psi_1>\otimes |\psi_2> \otimes ... \otimes |\psi_n>$$

Para ilustrar este postulado, vamos a pensar en dos sistemas de 1 qubit los cuales vamos a nombrar $|\psi_1\rangle$ y $|\psi_2\rangle$ por lo cual el sistema completo se puede ver de la forma $|\Psi\rangle = |\psi_1\rangle \otimes |\psi_2\rangle$. Una base para expandir estos estados es la de {$|0\rangle$, $|1\rangle$}, esta base para cada uno de los qubits, entonces, el estado total se puede expandir en la base:

$$ \{|0\rangle,|1\rangle\} \otimes \{|0\rangle,|1\rangle\} = \{|0\rangle\otimes |0\rangle, |0\rangle\otimes |1\rangle, |1\rangle\otimes |0\rangle, |1\rangle\otimes |1\rangle\} $$

Usando la notación $|0\rangle\otimes |0\rangle = |0 0\rangle$ y así para los otros sistemas, donde el primer y segundo número corresponden a los sistemas $|\psi_1\rangle$ y $|\psi_2\rangle$ respectivamente. Este producto se puede visualizar vectorialmente de la forma

$$|0 0\rangle = |0\rangle\otimes |0\rangle = \begin{bmatrix}1\\0\end{bmatrix}\otimes\begin{bmatrix}1\\0\end{bmatrix} = \begin{bmatrix}1\begin{bmatrix}1\\0\end{bmatrix}\\0\begin{bmatrix}1\\0\end{bmatrix}\end{bmatrix} = \begin{bmatrix}1\\0\\0\\0\end{bmatrix}$$

Realizando los otros productos de la base se puede llegar a

$$ |0 0\rangle =  \begin{bmatrix}1\\0\\0\\0\end{bmatrix},\hspace{1em}|0 1\rangle =  \begin{bmatrix}0\\1\\0\\0\end{bmatrix},\hspace{1em}|1 0\rangle = \begin{bmatrix}0\\0\\1\\0\end{bmatrix},\hspace{1em}|1 1\rangle =  \begin{bmatrix}0\\0\\0\\1\end{bmatrix}.$$

Sin embargo, como bien sabemos, hay infinitos conjuntos de vectores que pueden ser la base del espacio en el que estamos trabajando, del mismo modo, hay diferentes estados de dos partículas que pueden funcionar como base para el espacio de Hilbert en el que los posibles estados de estas partículas reciden. Una base de interes para la computación cuántica es la conocida *Base de Bell*. La base de Bell para dos qubits representa los posibles estados *entrelazados* que pueden haber entre ellas. Los 4 elementos de la base de Bell para 2 qubits son

$$|\Psi^+\rangle_{1,2} = \frac{1}{\sqrt{2}}\left(|01 \rangle + |10\rangle\right)$$
$$|\Psi^-\rangle_{1,2} = \frac{1}{\sqrt{2}}\left(|01 \rangle - |10\rangle\right)$$
$$|\Phi^+\rangle_{1,2} = \frac{1}{\sqrt{2}}\left(|00 \rangle + |11 \rangle\right)$$
$$|\Phi^-\rangle_{1,2} = \frac{1}{\sqrt{2}}\left(|00 \rangle - |11 \rangle\right)$$

Como pueden notar, NO es posible expresar alguno de los anteriores estados de la forma $|\psi\rangle_1 \otimes |\psi\rangle_1$, es decir, no se puede expresar como el producto de estados de las partículas singulares. Este tipo de estados es único de la mecánica cuántica, y son estados en los que, una vez se haga la medición de una de las dos partículas, o uno de los dos qubits, el estado del segundo quedará totalmente determinado por el resultado de la medición hecha en el primer qubit.

Ahora vamos a cunstruir estos estados, para esto tenemos el siguiente cuadro

|Estado inicial $|ij\rangle$|$\left(H_1 \otimes I_2\right)$|$|\Psi\rangle_{i,j}$|
|:------------:|:---------------:|:--------------:|
|$ |00 \rangle$|$\frac{1}{\sqrt{2}}\left(|00 \rangle + |10\rangle\right)$|$|\Phi^+\rangle_{1,2}$|
|$ |01 \rangle$|$\frac{1}{\sqrt{2}}\left(|01 \rangle + |11\rangle\right)$|$|\Psi^+\rangle_{1,2}$|
|$ |10 \rangle$|$\frac{1}{\sqrt{2}}\left(|00 \rangle - |10\rangle\right)$|$|\Phi^-\rangle_{1,2}$|
|$ |11 \rangle$|$\frac{1}{\sqrt{2}}\left(|01 \rangle - |11\rangle\right)$|$|\Psi^-\rangle_{1,2}$|

$$\hspace{2em}\overset{\text{Hadamard sobre } q_1}{\longrightarrow}\hspace{2em}\overset{\text{CNOT} q_1,q_2}{\longrightarrow}$$

A continuación vamos a construir el primer estado de la base de Bell

```python
## Iniciamos los registros necesarios
qr=QuantumRegister(2)
cr=ClassicalRegister(2)
qc=QuantumCircuit(qr,cr)

## Ahora realizamos la operación de Hadamard sobre el primer qubit de abajo hacia arriba, es decir, el qubit[1]
qc.h(qr[1])

## Ahora aplicamos la compuerta CNOT con controlada por el primer qubit, apuntando al segundo, es decir
qc.cx(qr[1],qr[0])

qc.measure(qr[0],cr[0])
qc.measure(qr[1],cr[1])
qc.draw(output='mpl')
```

```python
backend = Aer.get_backend('qasm_simulator')
results = execute(qc,backend,shots=10000).result().get_counts()
plot_histogram(results)
```

Como vemos, cada vez que el qubit 1 está en el estado $|i\rangle$ con $i = 0,1$, el qubit 0 siempre está en su mismo estado, tal cuál como nos indica el estado de Bell $|\Phi^+\rangle_{1,2} = \frac{1}{\sqrt{2}}\left(|00 \rangle + |11 \rangle\right)$.

## Referencias

* Nielsen & Chuang. **Quantum Computation and Quantum Information**. Cambridge University Press, 2010.
* Beck. **Quantum Mechanics Theory and Experiments**. Cambridge University Press, 2012.
* Ataman. **The quantum optical description of a double Mach-Zehnder interferometer**. arXiv:1407.1704 [physics.optics]. 2014.
* Ekert. **From Interferometers to Quantum Computers**. Supplementary material, Mathematical Institute, University of Oxford, 2010.
* Wilde. **Quantum Information Theory**. Cambridge University Press, 2013.

Muchas gracias por leer esta publicación! Abajo se encuentran links a las lecciones siguientes en el Crash Course de QC-FEM.


#  CS109B Data Science 2: Advanced Topics in Data Science

## Lab 4 - Bayesian Analysis

**Harvard University**

**Spring 2020**

**Instructors:** Mark Glickman, Pavlos Protopapas, and Chris Tanner

**Lab Instructors:** Chris Tanner and Eleni Angelaki Kaxiras

**Content:** Eleni Angelaki Kaxiras

---

```python
## RUN THIS CELL TO PROPERLY HIGHLIGHT THE EXERCISES
import requests
from IPython.core.display import HTML
styles = requests.get("https://raw.githubusercontent.com/Harvard-IACS/2019-CS109B/master/content/styles/cs109.css").text
HTML(styles)
```

```python
import pymc3 as pm
from pymc3 import summary
```

```python
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as stats
import pandas as pd
%matplotlib inline

import warnings
warnings.filterwarnings('ignore')
```

```python
print('Running on PyMC3 v{}'.format(pm.__version__))
```

    Running on PyMC3 v3.8

```javascript
%%javascript
IPython.OutputArea.auto_scroll_threshold = 20000;
```

    <IPython.core.display.Javascript object>

## Learning Objectives

By the end of this lab, you should be able to:
* Understand how probability distributions work.
* Apply Bayes Rule in calculating probabilities.
* Understand how to apply Bayesian analysis using PyMC3
* Avoid getting fired when talking to your Bayesian employer.

**This lab corresponds to Lectures 6, 7, and 8, and maps to Homework 3.**

## Table of Contents

1. The Bayesian Way of Thinking or Is this a Fair Coin?
2. [Intro to `pyMC3`](#pymc3).
3. [Bayesian Linear Regression](#blr).
4. [Try this at Home: Example on Mining Disasters](#no4).

## 1. The Bayesian way of Thinking

```
Here is my state of knowledge about the situation. Here is some data, I am now going to revise my state of knowledge.
```

Table Exercise: Discuss the statement above with your table mates and make sure everyone understands what it means and what constitutes Bayesian way of thinking. Finally, count the Bayesians among you.

### A. Bayes Rule

\begin{equation}
\label{eq:bayes}
P(A|\textbf{B}) = \frac{P(\textbf{B} |A) P(A) }{P(\textbf{B})}
\end{equation}

$P(A|\textbf{B})$ is the **posterior** distribution, prob(hypothesis | data)

$P(\textbf{B} |A)$ is the **likelihood** function, how probable is my data **B** for different values of the parameters

$P(A)$ is the marginal probability to observe the data, called the **prior**, this captures our belief about the data before observing it.

$P(\textbf{B})$ is the marginal distribution (sometimes called marginal likelihood)

Table Exercise: Solve the Monty Hall Paradox using Bayes Rule.

You are invited to play a game. There are 3 doors behind **one** of which are the keys to a brand new red Tesla. There is a goat behind each of the other two.

You are asked to pick one door, and let's say you pick **Door1**. The host knows where the keys are. Of the two remaining closed doors, he will always open the door that has a goat behind it. He'll say "I will do you a favor and open **Door2**". So he opens Door2 inside which there is, of course, a goat. He now asks you, do you want to open the initial Door you chose or change to **Door3**? Generally, in this game, when you are presented with this choice should you swap the doors?

**Initial Steps:**
- Start by defining the `events` of this probabilities game. One definition is:

    - $A_i$: car is behind door $i$

    - $B_i$ host opens door $i$

$i\in[1,2,3]$

- In more math terms, the question is: is the probability that the price is behind **Door 1** higher than the probability that the price is behind **Door2**, given that an event **has occured**?

**P(A1|B2) = P(B2|A1)P(A1) / P(B2) = (1/2)(1/3) / (1/2)** - 1/2 because if prize behind A1 as you guessed, 50/50 for either of other doors

**P(A3|B2) = P(B2|A3)P(A3) / P(B2) = (1/1)(1/3) / (1/2)** - 1/1 because if prize behind A1 as you guessed, 100% chance the host opens the only door left (after the right answer and your guess)

### B. Bayes Rule written with Probability Distributions

We have data that we believe come from an underlying distribution of unknown parameters. If we find those parameters, we know everything about the process that generated this data and we can make inferences (create new data).

$$
\begin{equation}
\label{eq:bayes}
P(\theta|\textbf{D}) = \frac{P(\textbf{D} |\theta) P(\theta) }{P(\textbf{D})}
\end{equation}
$$

#### But what is $\theta \;$?

$\theta$ is an unknown yet fixed set of parameters. In Bayesian inference we express our belief about what $\theta$ might be and instead of trying to guess $\theta$ exactly, we look for its **probability distribution**. What that means is that we are looking for the **parameters** of that distribution. For example, for a Poisson distribution our $\theta$ is only $\lambda$. In a normal distribution, our $\theta$ is often just $\mu$ and $\sigma$.

### C. A review of Common Probability Distributions

#### Discrete Distributions

The random variable has a **probability mass function (pmf)** which measures the probability that our random variable will take a specific value $y$, denoted $P(Y=y)$.

- **Bernoulli** (binary outcome, success has probability $\theta$, $one$ trial):
$
P(Y=k) =  \theta^k(1-\theta)^{1-k}
$

- **Binomial** (binary outcome, success has probability $\theta$, $n$ trials):
\begin{equation}
P(Y=k) =  {{n}\choose{k}} \cdot \theta^k(1-\theta)^{n-k}
\end{equation}

*Note*: Binomial(1,$p$) = Bernouli($p$)

- **Negative Binomial**

- **Poisson** (counts independent events occurring at a rate)
\begin{equation}
P\left( Y=y|\lambda \right) = \frac{{e^{ - \lambda } \lambda ^y }}{{y!}}
\end{equation}
y = 0,1,2,...

- **Discrete Uniform**

- **Categorical, or Multinulli** (random variables can take any of K possible categories, each having its own probability; this is a generalization of the Bernoulli distribution for a discrete variable with more than two possible outcomes, such as the roll of a die)

- **Dirichlet-multinomial** (a generalization of the beta distribution for many variables)

#### Continuous Distributions

The random variable has a **probability density function (pdf)**.
- **Uniform** (variable equally likely to be near each value in interval $(a,b)$)
\begin{equation}
P(X = x) = \frac{1}{b - a}
\end{equation}
anywhere within the interval $(a, b)$, and zero elsewhere.

- **Normal** (a.k.a. Gaussian)
\begin{equation}
X \sim  \mathcal{N}(\mu,\,\sigma^{2})
\end{equation}

    A Normal distribution can be parameterized either in terms of precision $\tau$ or standard deviation ($\sigma^{2}$. The link between the two is given by
\begin{equation}
\tau = \frac{1}{\sigma^{2}}
\end{equation}
 - Mean $\mu$
 - Variance $\frac{1}{\tau}$ or $\sigma^{2}$
 - Parameters: `mu: float`, `sigma: float` or `tau: float`

- **Beta** (variable ($\theta$) taking on values in the interval $[0,1]$, and parametrized by two positive parameters, $\alpha$ and $\beta$ that control the shape of the distribution.

*Note:*Beta is a good distribution to use for priors (beliefs) because its range is $[0,1]$ which is the natural range for a probability and because we can model a wide range of functions by changing the $\alpha$ and $\beta$ parameters.

\begin{equation}
\label{eq:beta}
P(\theta) = \frac{1}{B(\alpha, \beta)} {\theta}^{\alpha - 1} (1 - \theta)^{\beta - 1} \propto {\theta}^{\alpha - 1} (1 - \theta)^{\beta - 1}
\end{equation}

where the normalisation constant, $B$, is a beta function of $\alpha$ and $\beta$,

\begin{equation}
B(\alpha, \beta) = \int_{t=0}^1 t^{\alpha - 1} (1 - t)^{\beta - 1} dt.
\end{equation}

- **Exponential**

- **Gamma**

 #### Code Resources:
 - Statistical Distributions in numpy/scipy: [scipy.stats](https://docs.scipy.org/doc/scipy/reference/stats.html)
 - Statistical Distributions in pyMC3: [distributions in PyMC3](https://docs.pymc.io/api/distributions.html) (we will see those below).

Exercise: Plot a Discrete variable

Change the value of $\mu$ in the Poisson PMF and see how the plot changes. Remember that the y-axis in a discrete probability distribution shows the probability of the random variable having a specific value in the x-axis.

\begin{equation}
P\left( X=k \right) = \frac{{e^{ - \mu } \mu ^k }}{{k!}}
\end{equation}

**stats.poisson.pmf(x, mu)** $\mu$(mu) is our $\theta$ in this case.

```python
plt.style.use('seaborn-darkgrid')
x = np.arange(0, 30)
for m in [0, 0.5, 3, 6, 11, 14]:
    pmf = stats.poisson.pmf(x, m)
    plt.plot(x, pmf, 'o', alpha=0.5, label='$\mu$ = {}'.format(m))
plt.xlabel('random variable', fontsize=12)
plt.ylabel('probability', fontsize=12)
plt.legend(loc=1)
plt.ylim=(-0.1)
plt.show()
```

```python
# same for binomial
plt.style.use('seaborn-darkgrid')
x = np.arange(0, 22)
ns = [10, 17]
ps = [0.5, 0.7]
for n, p in zip(ns, ps):
    pmf = stats.binom.pmf(x, n, p)
    plt.plot(x, pmf, 'o', alpha=0.5, label='n = {}, p = {}'.format(n, p))
plt.xlabel('x', fontsize=14)
plt.ylabel('f(x)', fontsize=14)
plt.legend(loc=1)
plt.show()
```

```python
# discrete uniform
plt.style.use('seaborn-darkgrid')
ls = [0]
us = [3] # watch out, this number can only be integer!
for l, u in zip(ls, us):
    x = np.arange(l, u+1)
    pmf = [1.0 / (u - l + 1)] * len(x)
    plt.plot(x, pmf, '-o', label='lower = {}, upper = {}'.format(l, u))
plt.xlabel('x', fontsize=12)
plt.ylabel('probability P(x)', fontsize=12)
plt.legend(loc=1)
plt.show()
```

Exercise: Plot a continuous variable

Change the value of $\mu$ in the Uniform PDF and see how the plot changes.

Remember that the y-axis in a continuous probability distribution does not shows the actual probability of the random variable having a specific value in the x-axis because that probability is zero!. Instead, to see the probability that the variable is within a small margin we look at the integral below the curve of the PDF.

The uniform is often used as a noninformative prior.

```
Uniform - numpy.random.uniform(a=0.0, b=1.0, size)
```

$\alpha$ and $\beta$ are our parameters. `size` is how many tries to perform.
Our $\theta$ is basically the combination of the parameters a,b. We can also call it
\begin{equation}
\mu = (a+b)/2
\end{equation}

```python
from scipy.stats import uniform

r = uniform.rvs(size=1000)
plt.plot(r, uniform.pdf(r),'r-', lw=5, alpha=0.6, label='uniform pdf')
plt.hist(r, density=True, histtype='stepfilled', alpha=0.2)
plt.ylabel(r'probability density')
plt.xlabel(f'random variable')
plt.legend(loc='best', frameon=False)
plt.show()
```

```python
from scipy.stats import beta

alphas = [0.5, 1.5, 3.0]
betas = [0.5, 1.5, 3.0]
x = np.linspace(0, 1, 1000)
colors = ['red', 'green', 'blue']

fig, ax = plt.subplots(figsize=(8, 5))

for a, b, colors in zip(alphas, betas, colors):
    dist = beta(a, b)
    plt.plot(x, dist.pdf(x), c=colors,
             label=f'a={a}, b={b}')

ax.set_ylim(0, 3)

ax.set_xlabel(r'$\theta$')
ax.set_ylabel(r'$p(\theta|\alpha,\beta)$')
ax.set_title('Beta Distribution')

ax.legend(loc='best')
fig.show();
```

```python
plt.style.use('seaborn-darkgrid')
x = np.linspace(-5, 5, 1000)
mus = [0., 0., 0., -2.]
sigmas = [0.4, 1., 2., 0.4]
for mu, sigma in zip(mus, sigmas):
    pdf = stats.norm.pdf(x, mu, sigma)
    plt.plot(x, pdf, label=r'$\mu$ = '+ f'{mu},' + r'$\sigma$ = ' + f'{sigma}')
plt.xlabel('random variable', fontsize=12)
plt.ylabel('probability density', fontsize=12)
plt.legend(loc=1)
plt.show()
```

```python
plt.style.use('seaborn-darkgrid')
x = np.linspace(-5, 5, 1000)
mus = [0., 0., 0., -2.] # mean
sigmas = [0.4, 1., 2., 0.4] # std
for mu, sigma in zip(mus, sigmas):
      plt.plot(x, uniform.pdf(x, mu, sigma), lw=5, alpha=0.4, \
               label=r'$\mu$ = '+ f'{mu},' + r'$\sigma$ = ' + f'{sigma}')
plt.xlabel('random variable', fontsize=12)
plt.ylabel('probability density', fontsize=12)
plt.legend(loc=1)
plt.show()
```

### D. Is this a Fair Coin?

We do not want to promote gambling but let's say you visit the casino in **Monte Carlo**. You want to test your theory that casinos are dubious places where coins have been manipulated to have a larger probability for tails. So you will try to estimate how fair a coin is based on 100 flips.

You begin by flipping the coin. You get either Heads ($H$) or Tails ($T$) as our observed data and want to see if your posterior probabilities change as you obtain more data, that is, more coin flips. A nice way to visualize this is to plot the posterior probabilities as we observe more flips (data).

We will be using Bayes rule. $\textbf{D}$ is our data.

$$
\begin{equation}
\label{eq:bayes}
P(\theta|\textbf{D}) = \frac{P(\textbf{D} |\theta) P(\theta) }{P(\textbf{D})}
\end{equation}
$$

In the case of a coin toss when we observe $k$ heads in $n$ tosses:

$$
\begin{equation}
\label{eq:bayes}
P(\theta|\textbf{k}) = Beta(\alpha + \textbf{k}, \beta + n - \textbf{k})
\end{equation}
$$

we can say that $\alpha$ and $\beta$ play the roles of a "prior number of heads" and "prior number of tails".

```python
# play with the priors - here we manually set them but we could be sampling from a separate Beta
trials = np.array([0, 1, 3, 5, 10, 15, 20, 100, 200, 300])
heads = np.array([0, 1, 2, 4, 8, 10, 10, 50, 180, 150])
x = np.linspace(0, 1, 100)

# for simplicity we set a,b=1

plt.figure(figsize=(10,8))
for k, N in enumerate(trials):
    sx = plt.subplot(len(trials)/2, 2, k+1)
    posterior = stats.beta.pdf(x, 1 + heads[k], 1 + trials[k] - heads[k])
    plt.plot(x, posterior, alpha = 0.5, label=f'{trials[k]} tosses\n {heads[k]} heads');
    plt.fill_between(x, 0, posterior, color="#348ABD", alpha=0.4)
    plt.legend(loc='upper left', fontsize=10)
    plt.legend()
    plt.autoscale(tight=True)

plt.suptitle("Posterior probabilities for coin flips", fontsize=15);
plt.tight_layout()
plt.subplots_adjust(top=0.88)
```

 [Top](#top)

## 2. Introduction to `pyMC3`

PyMC3 is a Python library for programming Bayesian analysis, and more specifically, data creation, model definition, model fitting, and posterior analysis. It uses the concept of a `model` which contains assigned parametric statistical distributions to unknown quantities in the model. Within models we define random variables and their distributions. A distribution requires at least a `name` argument, and other `parameters` that define it. You may also use the `logp()` method in the model to build the model log-likelihood function. We define and fit the model.

PyMC3 includes a comprehensive set of pre-defined statistical distributions that can be used as model building blocks. Although they are not meant to be used outside of a `model`, you can invoke them by using the prefix `pm`, as in `pm.Normal`.

#### Markov Chain Monte Carlo (MCMC) Simulations

PyMC3 uses the **No-U-Turn Sampler (NUTS)** and the **Random Walk Metropolis**, two Markov chain Monte Carlo (MCMC) algorithms for sampling in posterior space. Monte Carlo gets into the name because when we sample in posterior space, we choose our next move via a pseudo-random process. NUTS is a sophisticated algorithm that can handle a large number of unknown (albeit continuous) variables.

```python
with pm.Model() as model:
    z = pm.Normal('z', mu=0., sigma=5.)
    x = pm.Normal('x', mu=z, sigma=1., observed=5.)
print(x.logp({'z': 2.5}))
print(z.random(10, 100)[:10])
```

    -4.043938533204672
    [ 8.33640681  3.99381712 -2.19366687  2.3737412  -9.73872099 -4.81026637
     -2.41892328  5.36819376  2.05338416  1.96679158]

**References**:

- *Salvatier J, Wiecki TV, Fonnesbeck C. 2016. Probabilistic programming in Python using PyMC3. PeerJ Computer Science 2:e55* [(https://doi.org/10.7717/peerj-cs.55)](https://doi.org/10.7717/peerj-cs.55)
- [Distributions in PyMC3](https://docs.pymc.io/api/distributions.html)
- [More Details on Distributions](https://docs.pymc.io/developer_guide.html)

Information about PyMC3 functions including descriptions of distributions, sampling methods, and other functions, is available via the `help` command.

```python
help(pm.Poisson)
```

    Help on class Poisson in module pymc3.distributions.discrete:

    class Poisson(pymc3.distributions.distribution.Discrete)
     |  Poisson(name, *args, **kwargs)
     |
     |  Poisson log-likelihood.
     |
     |  Often used to model the number of events occurring in a fixed period
     |  of time when the times at which events occur are independent.
     |  The pmf of this distribution is
     |
     |  .. math:: f(x \mid \mu) = \frac{e^{-\mu}\mu^x}{x!}
     |
     |  .. plot::
     |
     |      import matplotlib.pyplot as plt
     |      import numpy as np
     |      import scipy.stats as st
     |      plt.style.use('seaborn-darkgrid')
     |      x = np.arange(0, 15)
     |      for m in [0.5, 3, 8]:
     |          pmf = st.poisson.pmf(x, m)
     |          plt.plot(x, pmf, '-o', label='$\mu$ = {}'.format(m))
     |      plt.xlabel('x', fontsize=12)
     |      plt.ylabel('f(x)', fontsize=12)
     |      plt.ylim(0)
     |      plt.legend(loc=1)
     |      plt.show()
     |
     |  ========  ==========================
     |  Support   :math:`x \in \mathbb{N}_0`
     |  Mean      :math:`\mu`
     |  Variance  :math:`\mu`
     |  ========  ==========================
     |
     |  Parameters
     |  ----------
     |  mu : float
     |      Expected number of occurrences during the given interval
     |      (mu >= 0).
     |
     |  Notes
     |  -----
     |  The Poisson distribution can be derived as a limiting case of the
     |  binomial distribution.
     |
     |  Method resolution order:
     |      Poisson
     |      pymc3.distributions.distribution.Discrete
     |      pymc3.distributions.distribution.Distribution
     |      builtins.object
     |
     |  Methods defined here:
     |
     |  __init__(self, mu, *args, **kwargs)
     |      Initialize self.  See help(type(self)) for accurate signature.
     |
     |  logp(self, value)
     |      Calculate log-probability of Poisson distribution at specified value.
     |
     |      Parameters
     |      ----------
     |      value : numeric
     |          Value(s) for which log-probability is calculated. If the log probabilities for multiple
     |          values are desired the values must be provided in a numpy array or theano tensor
     |
     |      Returns
     |      -------
     |      TensorVariable
     |
     |  random(self, point=None, size=None)
     |      Draw random values from Poisson distribution.
     |
     |      Parameters
     |      ----------
     |      point : dict, optional
     |          Dict of variable values on which random values are to be
     |          conditioned (uses default point if not specified).
     |      size : int, optional
     |          Desired size of random sample (returns one sample if not
     |          specified).
     |
     |      Returns
     |      -------
     |      array
     |
     |  ----------------------------------------------------------------------
     |  Methods inherited from pymc3.distributions.distribution.Distribution:
     |
     |  __getnewargs__(self)
     |
     |  __latex__ = _repr_latex_(self, name=None, dist=None)
     |      Magic method name for IPython to use for LaTeX formatting.
     |
     |  default(self)
     |
     |  get_test_val(self, val, defaults)
     |
     |  getattr_value(self, val)
     |
     |  logp_nojac(self, *args, **kwargs)
     |      Return the logp, but do not include a jacobian term for transforms.
     |
     |      If we use different parametrizations for the same distribution, we
     |      need to add the determinant of the jacobian of the transformation
     |      to make sure the densities still describe the same distribution.
     |      However, MAP estimates are not invariant with respect to the
     |      parametrization, we need to exclude the jacobian terms in this case.
     |
     |      This function should be overwritten in base classes for transformed
     |      distributions.
     |
     |  logp_sum(self, *args, **kwargs)
     |      Return the sum of the logp values for the given observations.
     |
     |      Subclasses can use this to improve the speed of logp evaluations
     |      if only the sum of the logp values is needed.
     |
     |  ----------------------------------------------------------------------
     |  Class methods inherited from pymc3.distributions.distribution.Distribution:
     |
     |  dist(*args, **kwargs) from builtins.type
     |
     |  ----------------------------------------------------------------------
     |  Static methods inherited from pymc3.distributions.distribution.Distribution:
     |
     |  __new__(cls, name, *args, **kwargs)
     |      Create and return a new object.  See help(type) for accurate signature.
     |
     |  ----------------------------------------------------------------------
     |  Data descriptors inherited from pymc3.distributions.distribution.Distribution:
     |
     |  __dict__
     |      dictionary for instance variables (if defined)
     |
     |  __weakref__
     |      list of weak references to the object (if defined)

 [Top](#top)

## 3. Bayesian Linear Regression

Let's say we want to predict outcomes Y as normally distributed observations with an expected value $mu$ that is a linear function of two predictor variables, $\bf{x}_1$ and $\bf{x}_2$.

\begin{equation}
\mu = \alpha + \beta_1 \bf{x}_1 + \beta_2 x_2
\end{equation}

\begin{equation}
Y \sim  \mathcal{N}(\mu,\,\sigma^{2})
\end{equation}

where $\sigma^2$ represents the measurement error.

In this example, we will use $\sigma^2 = 10$

We also choose the parameters as normal distributions:

\begin{eqnarray}
\alpha \sim  \mathcal{N}(0,\,10) \\
\beta_i \sim  \mathcal{N}(0,\,10) \\
\sigma^2 \sim  |\mathcal{N}(0,\,10)|
\end{eqnarray}

We will artificially create the data to predict on. We will then see if our model predicts them correctly.

```python
# Initialize random number generator
np.random.seed(123)

# True parameter values
alpha, sigma = 1, 1
beta = [1, 2.5]

# Size of dataset
size = 100

# Predictor variable
X1 = np.linspace(0, 1, size)
X2 = np.linspace(0,.2, size)

# Simulate outcome variable
Y = alpha + beta[0]*X1 + beta[1]*X2 + np.random.randn(size)*sigma

fig, ax = plt.subplots(1,2, figsize=(10,6), sharex=True)
ax[0].scatter(X1,Y)
ax[1].scatter(X2,Y)
ax[0].set_xlabel(r'$x_1$', fontsize=14)
ax[0].set_ylabel(r'$Y$', fontsize=14)
ax[1].set_xlabel(r'$x_2$', fontsize=14)
ax[1].set_ylabel(r'$Y$', fontsize=14)
```

```python
from pymc3 import Model, Normal, HalfNormal

basic_model = Model()

with basic_model:

    # Priors for unknown model parameters, specifically create stochastic random variables
    # with Normal prior distributions for the regression coefficients,
    # and a half-normal distribution for the standard deviation of the observations, σ.
    alpha = Normal('alpha', mu=0, sd=10)
    beta = Normal('beta', mu=0, sd=10, shape=2)
    sigma = HalfNormal('sigma', sd=1)

    # Expected value of outcome - posterior
    mu = alpha + beta[0]*X1 + beta[1]*X2

    # Likelihood (sampling distribution) of observations
    Y_obs = Normal('Y_obs', mu=mu, sd=sigma, observed=Y)
```

```python
# model fitting with sampling
from pymc3 import NUTS, sample, find_MAP
from scipy import optimize

with basic_model:

    # obtain starting values via MAP
    start = find_MAP(fmin=optimize.fmin_powell)

    # instantiate sampler
    step = NUTS(scaling=start)

    # draw 2000 posterior samples
    trace = sample(2000, step, start=start)
```

    logp = -164.5:   5%|▌         | 270/5000 [00:00<00:00, 5084.85it/s]

    Optimization terminated successfully.
             Current function value: 164.496957
             Iterations: 6
             Function evaluations: 271

    logp = -164.5:   5%|▌         | 271/5000 [00:00<00:12, 387.03it/s]
    Multiprocess sampling (4 chains in 4 jobs)
    NUTS: [sigma, beta, alpha]
    Sampling 4 chains, 0 divergences: 100%|██████████| 10000/10000 [00:07<00:00, 1384.27draws/s]
    The number of effective samples is smaller than 25% for some parameters.

```python
from pymc3 import traceplot

traceplot(trace);
```

```python
results = pm.summary(trace,
                     var_names=['alpha', 'beta', 'sigma'])
results
```

      mean
      sd
      hpd_3%
      hpd_97%
      mcse_mean
      mcse_sd
      ess_mean
      ess_sd
      ess_bulk
      ess_tail
      r_hat

      alpha
      1.011
      0.231
      0.586
      1.448
      0.003
      0.002
      4508.0
      4504.0
      4495.0
      4119.0
      1.0

      beta[0]
      1.493
      2.026
      -2.125
      5.532
      0.065
      0.046
      961.0
      961.0
      968.0
      1198.0
      1.0

      beta[1]
      0.191
      9.902
      -19.364
      17.768
      0.318
      0.225
      972.0
      972.0
      979.0
      1149.0
      1.0

      sigma
      1.146
      0.082
      0.994
      1.300
      0.001
      0.001
      7181.0
      7089.0
      7309.0
      5219.0
      1.0

This linear regression example is from the original paper on PyMC3: *Salvatier J, Wiecki TV, Fonnesbeck C. 2016. Probabilistic programming in Python using PyMC3. PeerJ Computer Science 2:e55 https://doi.org/10.7717/peerj-cs.55*

 [Top](#top)

## 4. Try this at Home: Example on Mining Disasters
We will go over the classical `mining disasters from 1851 to 1962` dataset.

This example is from the [pyMC3 Docs](https://docs.pymc.io/notebooks/getting_started.html).

```python
import pandas as pd
disaster_data = pd.Series([4, 5, 4, 0, 1, 4, 3, 4, 0, 6, 3, 3, 4, 0, 2, 6,
                           3, 3, 5, 4, 5, 3, 1, 4, 4, 1, 5, 5, 3, 4, 2, 5,
                           2, 2, 3, 4, 2, 1, 3, np.nan, 2, 1, 1, 1, 1, 3, 0, 0,
                           1, 0, 1, 1, 0, 0, 3, 1, 0, 3, 2, 2, 0, 1, 1, 1,
                           0, 1, 0, 1, 0, 0, 0, 2, 1, 0, 0, 0, 1, 1, 0, 2,
                           3, 3, 1, np.nan, 2, 1, 1, 1, 1, 2, 4, 2, 0, 0, 1, 4,
                           0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1])
fontsize = 12
years = np.arange(1851, 1962)
plt.figure(figsize=(10,5))
# plt.scatter(years, disaster_data);
plt.bar(years, disaster_data)
plt.ylabel('Disaster count', size=fontsize)
plt.xlabel('Year', size=fontsize);
plt.title('Was there a Turning Point in Mining disasters from 1851 to 1962?', size=15);
```

#### Building the model

**Step1:** We choose the probability model for our experiment. Occurrences of disasters in the time series is thought to follow a **Poisson** process with a large **rate** parameter in the early part of the time series, and from one with a smaller **rate** in the later part. We are interested in locating the change point in the series, which perhaps is related to changes in mining safety regulations.

```
disasters = pm.Poisson('disasters', rate, observed=disaster_data)
```

We have two rates, `early_rate` if $t<=s$, and `late_rate` if $t>s$, where $s$ is the year the switch was made (a.k.a. the `switchpoint`).

**Step2:** Choose a prior distributions of the two rates, what we believe the rates were before we observed the data, and the switchpoint. We choose Exponential.
```
early_rate = pm.Exponential('early_rate', 1)
```

The parameters of this model are:

**Note:** Watch for missing values. Missing values are handled transparently by passing a MaskedArray or a pandas.DataFrame. Behind the scenes, another random variable, disasters.missing_values is created to model the missing values. If you pass a np.array with missing values you will get an error.

```python
with pm.Model() as disaster_model:

    # discrete
    switchpoint = pm.DiscreteUniform('switchpoint', lower=years.min(), upper=years.max(), testval=1900)

    # Priors for pre- and post-switch rates number of disasters
    early_rate = pm.Exponential('early_rate', 1)
    late_rate = pm.Exponential('late_rate', 1)

    # our theta - allocate appropriate Poisson rates to years before and after current
    # switch is an `if` statement in puMC3
    rate = pm.math.switch(switchpoint >= years, early_rate, late_rate)

    # our observed data as a likelihood function of the `rate` parameters
    # shows how we think our data is distributed
    disasters = pm.Poisson('disasters', rate, observed=disaster_data)
```

#### Model Fitting

```python
# there are defaults but we can also more explicitly set the sampling algorithms
with disaster_model:

    # for continuous variables
    step1 = pm.NUTS([early_rate, late_rate])

    # for discrete variables
    step2 = pm.Metropolis([switchpoint, disasters.missing_values[0]] )

#     trace = pm.sample(10000, step=[step1, step2])
    # try different number of samples
    trace = pm.sample(1, step=[step1, step2])
```

#### Posterior Analysis

On the left side plots we notice that our early rate is between 2.5 and 3.5 disasters a year. In the late period it seems to be between 0.6 and 1.2 so definitely lower.

The right side plots show the samples we drew to come to our conclusion.

```python
pm.traceplot(trace, ['early_rate', 'late_rate', 'switchpoint'], figsize=(20,10));
```

```python
results = pm.summary(trace,
                     var_names=['early_rate', 'late_rate', 'switchpoint'])
results
```


\begin{equation}
x = y
\end{equation}

$$
x = y
$$

align:

\begin{align}
\frac{x}{y}
\end{align}

# Divvy data exploration project

**authors:** Peter Carbonetto, Gao Wang

**date:** July 13, 2017

*[Photo](https://www.flickr.com/photos/jamesbondsv/9041673199) by
[Steven Vance](https://www.flickr.com/photos/jamesbondsv) /
[CC BY 2.0](https://creativecommons.org/licenses/by/2.0/)*

## Project overview

The purpose of this project is to gain some insight into city-wide biking trends by analyzing the [Divvy trip
data](https://www.divvybikes.com/system-data). We also examine trip data from a single bike station at the University of Chicago to compare the biking patterns at the university against city-wide trends.

All the results and plots presented in the pages below should be reproducable on your computer. Follow the
[Setup Instructions](setup.html) if you are interested in reproducing the results for yourself.

These are the results of our analyses. They were generated by rendering the [Jupyter notebooks](https://github.com/stephenslab/ipynb-website/tree/master/analysis) into webpages.

1. [A first glance at the Divvy data.](analysis/first-glance.html)

2. [A map of the Divvy stations in Chicago.](analysis/station-map.html)

3. [Exploring daily bike commuting trends from the Divvy data.](analysis/time-of-day-trends.html)

4. [Exploring seasonal biking trends from the Divvy data.](analysis/seasonal-trends.html)

## Credits

This Jupyter notebook website was developed by [Peter Carbonetto](http://pcarbo.github.io) and [Gao Wang](https://github.com/gaow) at the [University of Chicago](https://www.uchicago.edu).

Thanks to [John Blischak](https://github.com/jdblischak) and [Matthew Stephens](http://stephenslab.uchicago.edu) for their assistance and support. Also, thanks to [Larry Layne](https://rstudio-pubs-static.s3.amazonaws.com/63061_90f5136ffdf74740b6ba4ad8f2fd72fe.html) and [Austin Wehrwein](http://www.austinwehrwein.com/data-visualization/heatmaps-with-divvy-data) for sharing their analyses of the Divvy trip data that inspired some of the investigations here.