# Model Project - Dynamic Optimization With and Without Income Risk

**Team:** Kongerækken

**Members:** Frederik Degn Pedersen and Christian Brauer Johanssen

**Project description:** Throughout this project, we employ a two period model of consumer behavior. Initially, we perform analysis using `sympy` on a simple version of the model only featuring consumption goods and an initial cash endowment. Next, we expand the model to include income in the second period as well as a utility providing altruistic/inheritance motive. Finally, we expand the model further by introducing two different risk structures and compare consumer behavior acrossspecifications.

Imports and set magics:

```python
# importing relevant packages
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
import ipywidgets as widgets
import time
from scipy import optimize
from scipy import interpolate
import sympy as sm
from IPython.display import display

%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.widgets import Slider

# autoreload modules when code is run
%load_ext autoreload
%autoreload 2

# local module
import modelproject
```

# Model Analysis - The Simple Case

To start, we consider a simple two period consumer problem without uncertainty, where the following definitions of **variables** and **parameters** apply:

* $m$ is the endowment (cash-on-hand). In this simplified version the consumer only get an endowment in period 1.
* $c$ is consumption
* $r$ is the interest rate
* $\beta > 0$, $\rho > 1$ are utility parameters (subjective discount factor and coefficient of risk aversion)
* Subscripts denotes periods

Assuming CRRA utility from consumption in both periods, the combined utility function, $U$, can be written as

$$
\begin{align}
    U &= \max_{c_1,c_2}\frac{c_1^{1-\rho}}{1-\rho}+\beta\frac{c_2^{1-\rho}}{1-\rho} \\
    \text{s.t.} \\
    c_2 &= (1+r)(m_1-c_1) \\
    c_{1} &\in [0,m_{1}] \\
\end{align}
$$

where the constraint on consumption in period 2 results from the intertemporal budget constraint. Borrowing is not possible by the second constraint.

## Analysis Using `sympy`
As this simple version of the model has a nice, closed form solution, we can use sympy to solve analytically for optimal consumption as follows:
* First, we define all symbols
* Next, we define the objective function ($U$)
* Then we define the budget constraint and solve it for $c_2$
* Ultimately we substitute for $c_2$ in the objective function and differentiate with respect to $c_1$

```python
# define symbols
m1 = sm.symbols('m_1')
c1 = sm.symbols('c_1')
c2 = sm.symbols('c_2')
r = sm.symbols('r')
beta = sm.symbols('beta')
rho = sm.symbols('rho')

# define objective
m2 = (1+r)*(m1-c1)
obj = c1**(1-rho)/(1-rho)+beta*(c2**(1-rho)/(1-rho))

# define constraint and solve for c2
cons = sm.Eq(c2,(1+r)*(m1-c1))
c2_from_cons = sm.solve(cons,c2)

# substitute for c2 and take first order conditions
obj_subs = obj.subs(c2,c2_from_cons[0])

dU_dc1 = sm.diff(obj_subs,c1)
dU_dc1
```

$\displaystyle \frac{\beta \left(- r - 1\right) \left(- c_{1} r - c_{1} + m_{1} r + m_{1}\right)^{1 - \rho}}{- c_{1} r - c_{1} + m_{1} r + m_{1}} + \frac{c_{1}^{1 - \rho}}{c_{1}}$

Before we can solve for optimal consumption in period 1, we need to help `sympy` understand that the above expression is equal to

$$
\begin{align}
    \frac{1}{c_1} &= \frac{\left(\beta(1+r)\right)^\frac{1}{\rho}}{(1+r)(m_1-c_1)}
\end{align}
$$

```python
# help sympy collect terms
dU_dc1_simple = sm.Eq(1/c1,(beta*(1+r))**(1/rho)/((1+r)*(m1-c1)))
display(dU_dc1_simple)
```

$\displaystyle \frac{1}{c_{1}} = \frac{\left(\beta \left(r + 1\right)\right)^{\frac{1}{\rho}}}{\left(- c_{1} + m_{1}\right) \left(r + 1\right)}$

```python
# solve for optimal c1
c1_star_solve = sm.solve(dU_dc1_simple,c1)
c1_star = c1_star_solve[0]
c1_star
```

$\displaystyle \frac{m_{1} \left(r + 1\right)}{r + \left(\beta \left(r + 1\right)\right)^{\frac{1}{\rho}} + 1}$

From the above expression it can easily be observed that period 1 consumption is increasing in $m_1$ (more cash, more consumption) and decreasing in $\beta$ (higher patience, lower period 1 consumption), whereas the effect from the interest rate is more convoluted. This is because we have counteracting income and substitution effects that does not cancel out due to the fact that we have not assumed log-utility.

Inserting the above expression for $c_1$ we can residually compute the optimal level of second period consumption:

```python
# compute optimal c2
cons_subs = cons.subs(c1,c1_star)
c2_star_solve = sm.solve(cons_subs,c2)
c2_star = c2_star_solve[0]
c2_star
```

$\displaystyle \frac{m_{1} \left(\beta \left(r + 1\right)\right)^{\frac{1}{\rho}} \left(r + 1\right)}{r + \left(\beta \left(r + 1\right)\right)^{\frac{1}{\rho}} + 1}$

To be able to insert parameter values in the analytical solution, we turn the sympy functions into actual functions using `sm.lambdify`:

```python
# lambdify functions for c1 and c2
c1_func = sm.lambdify((m1,r,beta,rho),c1_star)
c2_func = sm.lambdify((m1,r,beta,rho),c2_star)
```

## Numerical Solution Using an Algorithm
Given some parameter values, it is also possible to solve numerically for optimal consumption in both periods. Particularly, we can set period 1 optimal consumption equal to zero to make it a root finding problem:
$$
\begin{align}
    0 &= \frac{1}{c_1} - \frac{\left(\beta(1+r)\right)^\frac{1}{\rho}}{(1+r)(m_1-c_1)}
\end{align}
$$

There are several algorithms that can solve such a problem. In the module, we define a function which uses the **bisection** method to find the root. This method is convenient in this particular setting, as we do not have to calculate derivatives. The way the algorithm works is that it compares function values over a grid, gradually closing in on the root. A necessary condition for the method to work is that the function values needs to have opposite values in the endpoints, as this ensures that a root exists between the two endpoints.

Now, we can define parameter values and use both the analitycally derived functions and the numerical algorithm to find the solution:

```python
# define parameter values
m1 = 1
r = 0.04
beta = 0.94
rho = 8

# solve analytically and numerically
c1_s, c2_s = modelproject.solve_for_consumption(m1,r,beta,rho)

print(f'analytical solution is: c1 = {c1_func(m1,r,beta,rho):.3f}, c2 = {c2_func(m1,r,beta,rho):.3f}')
print(f'numerical solution is: c1 = {c1_s:.3f}, c2 = {c2_s:.3f}\n')
print(f'analytical and numerical solution are equal with numerical precision: \nc1: {np.isclose(c1_func(m1,r,beta,rho),c1_s)} \nc2: {np.isclose(c2_func(m1,r,beta,rho),c2_s)}')
```

    analytical solution is: c1 = 0.511, c2 = 0.509
    numerical solution is: c1 = 0.511, c2 = 0.509

    analytical and numerical solution are equal with numerical precision:
    c1: True
    c2: True

## Parameter Sensitivity

Before we move on to consider an extension of the model, we examine what happens to the intertemporal allocation of consumption if we let $\rho$ vary. We consider two separate cases, namely a "high" and "low" interest rate. We show the change in consumption graphically below.

```python
# inc. effect dominates subs. effect
rho_v = np.linspace(1.01,10,100)

r1 = 0.04 # "low" interest rate
c1_l1 = np.empty(len(rho_v))
c2_l1 = np.empty(len(rho_v))

for i, rho_i in enumerate(rho_v):
    c1_l1[i],c2_l1[i] = modelproject.solve_for_consumption(m1,r1,beta,rho_i)

# subs. effect dominates inc. effect
r2 = 0.07 # "high" interest rate
c1_l2 = np.empty(len(rho_v))
c2_l2 = np.empty(len(rho_v))

for i, rho_i in enumerate(rho_v):
    c1_l2[i],c2_l2[i] = modelproject.solve_for_consumption(m1,r2,beta,rho_i)

# plot consumption for rho values
fig = plt.figure(figsize=(15,5))

ax = fig.add_subplot(1,2,1)
ax.plot(rho_v1,c1_l1,label='$c_1$')
ax.plot(rho_v1,c2_l1,label='$c_2$')
ax.set_xlabel('$rho$')
ax.set_ylabel('$c_1,c_2$')
ax.set_title('Low interest rate')
ax.legend();

ax = fig.add_subplot(1,2,2)
ax.plot(rho_v1,c1_l2,label='$c_1$')
ax.plot(rho_v1,c2_l2,label='$c_2$')
ax.set_xlabel('$rho$')
ax.set_ylabel('$c_1,c_2$')
ax.set_title('High interest rate')
ax.legend();
```

For both cases, we observe that consumption smoothing increases when $\rho$ increases. This is not surprising, as the intertemporal elasticity of substitution ($\rho^{-1}$) is decreasing in $\rho$. Thus, for a relatively low value of $\rho$, the consumer would be more inclined to distribute consumption unevenly across periods, because this is equivalent with a larger reponsiveness to (changes in) the interest rate (and the discount factor, $\beta$).

Notably, for the low interest rate, $c_1>c_2$ and vice versa. This is due to the income effect dominating the substitution effect in the low interest rate case, and the other way around for the high interest rate case.

# Model Extension

Now we introduce income in the second period as well as letting the consumer obtain utility for leaving a bequest for its heirs. Define the following additional **variables** and **parameters**:

* $y_t$ is income in period $t$
* $\nu > 0 $, $\kappa > 0$ are utility parameters

In the **second period** the household solves:

$$
\begin{aligned}
v_{2}(m_{2}) &= \max_{c_{2}}\frac{c_{2}^{1-\rho}}{1-\rho}+\nu\frac{(m_{2}-c_{2}+\kappa)^{1-\rho}}{1-\rho} \\
 \text{s.t.} \\
c_{2} & \in [0,m_{2}]
\end{aligned}
$$

In the **first period** the household solves:

$$
\begin{aligned}
v_{1}(m_{1}) & =
\max_{c_{1}}\frac{c_{1}^{1-\rho}}{1-\rho}+\beta\mathbb{E}_{1}\left[v_2(m_2)\right] \\
\text{s.t.} \\
m_2 &= (1+r)(m_{1}-c_{1})+y_{2} \\
y_{2} &= 1\\
c_{1} & \in [0,m_{1}]\\
\end{aligned}
$$

First we redefine parameter values for all **symbols**:

```python
kappa = 0.5
nu = 0.1
Delta = 0.5 # for later use
r = 0.04
beta = 0.94
rho = 8
```

# The risk free case
To solve this more elaborate dynamic optimization problem, we use the dynamic programming approach covered in Lecture 11.

## Defining value functions and solution functions

To start, we define the **basic functions** and use them to build the **value functions**:

```python
# basic functions
def utility(c,rho):
    return c**(1-rho)/(1-rho)

def bequest(m,c,nu,kappa,rho):
    return nu*(m-c+kappa)**(1-rho)/(1-rho)

# value functions
def v2(c2,m2,rho,nu,kappa):
    return utility(c2,rho) + bequest(m2,c2,nu,kappa,rho)

def v1(c1,m1,rho,beta,r,v2_interp):
    y2=1
    m2 = (1+r)*(m1-c1) + y2
    v2 = v2_interp([m2])[0]

    # total value
    return utility(c1,rho) + beta*v2
```

Next, we set up the **solution functions**, in the spirit of backwards induction beginning by period 2.

```python
def solve_period_2(rho,nu,kappa):

    # a. grids
    m2_vec = np.linspace(1e-8,5,500) # vector of potential m2 values
    v2_vec = np.empty(500)
    c2_vec = np.empty(500)

    # b. solve for each m2 in grid
    for i,m2 in enumerate(m2_vec):

        # i. objective
        obj = lambda c2: -v2(c2,m2,rho,nu,kappa)

        # ii. initial value (consume half)
        x0 = m2/2

        # iii. optimizer
        result = optimize.minimize_scalar(obj,x0,method='bounded',bounds=[1e-8,m2])

        # iv. save
        v2_vec[i] = -result.fun
        c2_vec[i] = result.x

    return m2_vec,v2_vec,c2_vec

def solve_period_1(rho,beta,r,v2_interp,v1):

    # a. grids
    m1_vec = np.linspace(1e-8,4,100)
    v1_vec = np.empty(100)
    c1_vec = np.empty(100)

    # b. solve for each m1 in grid
    for i,m1 in enumerate(m1_vec):

        # i. objective
        obj = lambda c1: -v1(c1,m1,rho,beta,r,v2_interp)

        # ii. initial guess (consume half)
        x0 = m1*1/2

        # iii. optimize
        result = optimize.minimize_scalar(obj,x0,method='bounded',bounds=[1e-8,m1])

        # iv. save
        v1_vec[i] = -result.fun
        c1_vec[i] = result.x

    return m1_vec,v1_vec,c1_vec

```

## Joint Solve Function and Plot of Consumption

```python
# joint solve function
def solve(rho,beta,r,nu,kappa,v1):

    # a. solve period 2
    m2_vec,v2_vec,c2_vec = solve_period_2(rho,nu,kappa)

    # b. construct interpolator
    v2_interp = interpolate.RegularGridInterpolator((m2_vec,), v2_vec,
                                                    bounds_error=False,fill_value=None)

    # c. solve period 1
    m1_vec,v1_vec,c1_vec = solve_period_1(rho,beta,r,v2_interp,v1)

    return m1_vec,c1_vec,m2_vec,c2_vec
```

```python
# plot risk free consumption
m1_vec,c1_vec,m2_vec,c2_vec = solve(rho,beta,r,nu,kappa,v1)

fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot(m1_vec,c1_vec)
ax.set_xlabel('$m_1$')
ax.set_ylabel('$c_1$')
ax.set_title('consumption function in period 1 without risk')
ax.set_xlim([0,4])
ax.set_ylim([0,2.5]);
```

From the figure, it can be observed that the consumer lives "hand to mouth" for $m_1$ lower than $\sim$ 0.8, whereafter the kink in the consumption function indicates that some of the cash increase is saved for period 2, where it can be used for either consumption or altruism.

# Introduce risk

Now we introduce two alternative risk structures, namely

$$
\begin{align}
y_2^{RISK} &= \begin{cases}
1-\Delta & \text{with prob. }0.5\\
1+\Delta & \text{with prob. }0.5
\end{cases}\\
\end{align}
$$

$$
\begin{align}
y_2^{TAILRISK} = \begin{cases}
1-\sqrt{\Delta} & \text{with prob. }0.1\\
1-\Delta & \text{with prob. }0.4\\
1+\Delta & \text{with prob. }0.4\\
1+\sqrt{\Delta} & \text{with prob. }0.1
\end{cases}\\
\end{align}
$$

where $\Delta \in [0,1]$ is an income shock. The latter risk structure add some low probability tail events. Neither of the risk structures are mean altering. In the local module, we implement solve functions that take into the account the respective risk structures and use these to solve for the two new scenarios. Afterwards, we plot consumption in period 1 and 2 across the three scenarios.

```python
# solve for risk scenarios
m1_vec_r,c1_vec_r,m2_vec_r,c2_vec_r = modelproject.solve_risk(rho,beta,r,Delta,nu,kappa,modelproject.v1_risk)

m1_vec_tr,c1_vec_tr,m2_vec_tr,c2_vec_tr = modelproject.solve_risk(rho,beta,r,Delta,nu,kappa,modelproject.v1_tailrisk)

# plot all scenarios
fig = plt.figure(figsize=(15,5))

ax = fig.add_subplot(1,2,1)
ax.plot(m1_vec,c1_vec,label='risk free')
ax.plot(m1_vec_r,c1_vec_r,label='risk')
ax.plot(m1_vec_tr,c1_vec_tr,label='tail risk')

ax.legend(loc='upper left')
ax.set_xlabel('$m_1$')
ax.set_ylabel('$c_1$')
ax.set_title('consumption function in period 1')
ax.set_xlim([0,4])
ax.set_ylim([0,2.5])

ax = fig.add_subplot(1,2,2)
ax.plot(m2_vec,c2_vec,label='risk free')
ax.plot(m2_vec_r,c2_vec_r,label='risk')
ax.plot(m2_vec_tr,c2_vec_tr,label='tail risk')

ax.legend(loc='upper left')
ax.set_xlabel('$m_2$')
ax.set_ylabel('$c_2$')
ax.set_title('consumption function in period 2')
ax.set_xlim([0,4])
ax.set_ylim([0,2.5]);
```

It is clear that uncertainty around future livelihood leads to precautionary saving. Also, because agents optimize for given levels of realized income in period 2, the consumption functions will be the same across risk scenarios. The precautionary savings scales with the coefficient of relative risk aversion.

# Simulation and Scenario Comparison

## Simulation

To simulate the model for a large number of consumers, we make a draw of 10 000 values of $m_1$ from a normal distribution. Then, for all three scenarios, we use interpolation methods to match the cash on hand level with the correct level of consumption.

```python
# initial draw of cash on hand
seed = np.random.seed(2020)
simN = 10000
sim_m1 = np.fmax(np.random.normal(1,0.1,size=simN),0) # "randomly" chosen distribution
```

```python
# a. simulate and plot for v1
m1_vec,c1_vec,m2_vec,c2_vec = solve(rho,beta,r,nu,kappa,v1)
c1_interp = interpolate.RegularGridInterpolator([m1_vec], c1_vec,
                                                bounds_error=False,fill_value=None)
c2_interp = interpolate.RegularGridInterpolator([m2_vec], c2_vec,
                                                bounds_error=False,fill_value=None)
# b. period 1
sim_c1 = c1_interp(sim_m1)
sim_a1 = sim_m1-sim_c1

# c. transition to period 2 with random draw
#sim_m2 = (1+r)*sim_a1+np.random.choice([0.5,1.5],p=[0.5,0.5],size=simN)

# d. period 2
sim_c2 = c2_interp(sim_m2)
```

```python
# a. simulate and plot for v1_risk
m1_vec_r,c1_vec_r,m2_vec_r,c2_vec_r = modelproject.solve_risk(rho,beta,r,Delta,nu,kappa,modelproject.v1_risk)
c1_interp_r = interpolate.RegularGridInterpolator([m1_vec_r], c1_vec_r,
                                                bounds_error=False,fill_value=None)
c2_interp_r = interpolate.RegularGridInterpolator([m2_vec_r], c2_vec_r,
                                                bounds_error=False,fill_value=None)
# b. period 1
sim_c1_r = c1_interp_r(sim_m1)
sim_a1_r = sim_m1-sim_c1_r

# c. transition to period 2 with random draw
#sim_m2_r = (1+r)*sim_a1_r+np.random.choice([0.5,1.5],p=[0.5,0.5],size=simN)

# d. period 2
sim_c2_r = c2_interp_r(sim_m2_r)
```

```python
# a. simulate and plot for v1_tailrisk
m1_vec_tr,c1_vec_tr,m2_vec_tr,c2_vec_tr = modelproject.solve_risk(rho,beta,r,Delta,nu,kappa,modelproject.v1_tailrisk)
c1_interp_tr = interpolate.RegularGridInterpolator([m1_vec_tr], c1_vec_tr,
                                                bounds_error=False,fill_value=None)
c2_interp_tr = interpolate.RegularGridInterpolator([m2_vec_tr], c2_vec_tr,
                                                bounds_error=False,fill_value=None)
# b. period 1
sim_c1_tr = c1_interp_tr(sim_m1)
sim_a1_tr = sim_m1-sim_c1_tr

# c. transition to period 2 with random draw
#sim_m2_tr = (1+r)*sim_a1_tr+np.random.choice([(1-np.sqrt(0.5)),0.5,1.5,(1+np.sqrt(0.5))],p=[0.1,0.4,0.4,0.1],size=simN)

# d. period 2
sim_c2_tr = c2_interp_tr(sim_m2_tr)
```

```python
# plot
fig = plt.figure(figsize=(10,5))
ax = fig.add_subplot(1,1,1)

ax.hist(sim_c1,bins=100,label=f'period 1 (risk free)')
ax.hist(sim_c1_r,bins=100,label=f'period 1 (risk)')
ax.hist(sim_c1_tr,bins=100,label=f'period 1 (tail risk)')

ax.legend(loc='lower right',facecolor='white',frameon=True)
ax.set_xlabel('$c_t$')
ax.set_ylabel('freq.')
ax.set_title('consumption');
```

As is evident from the figure, the precautionary savings effect carries over from the representative consumer to the heterogeneous simulation.

# Conclusion
In this project, we have applied various model analysis methods to a gradually expanding two period model of consumer behavior. initially, we performed analytical and numerical optimization of a simplified, risk free model using `sympy` and the **bisect** method. Next, we expanded the model; first with second period income and second with two types of income risk. Finally, we performed a simulation introducing heterogeneity in the initial endowment of cash on hand, showing that the precautionary savings effect carries over to the heterogeneous case.

```python

```


## Performance Indicator

It is fundamental for any algorithm to measure the performance. In a multi-objective scenario, we can not calculate the distance to the true global optimum but must consider a set of solutions. Moreover, sometimes the optimum is not even known, and other techniques must be used.

First, let us consider a scenario where the Pareto-front is known:

```python
import numpy as np
from pymoo.factory import get_problem
from pymoo.visualization.scatter import Scatter

# The pareto front of a scaled zdt1 problem
pf = get_problem("zdt1").pareto_front()

# The result found by an algorithm
A = pf[::10] * 1.1

# plot the result
Scatter(legend=True).add(pf, label="Pareto-front").add(A, label="Result").show()
```

### Generational Distance (GD)

The GD performance indicator  measure the distance from solution to the Pareto-front. Let us assume the points found by our algorithm are the objective vector set $A=\{a_1, a_2, \ldots, a_{|A|}\}$ and the reference points set (Pareto-front) is $Z=\{z_1, z_2, \ldots, z_{|Z|}\}$. Then,

\begin{align}
\begin{split}
\text{GD}(A) & = & \; \frac{1}{|A|} \; \bigg( \sum_{i=1}^{|A|} d_i^p \bigg)^{1/p}\\[2mm]
\end{split}
\end{align}

where $d_i$ represents the Euclidean distance (p=2) from $a_i$ to its nearest reference point in $Z$. Basically, this results in the average distance from any point $A$ to the closest point in the Pareto-front.

```python
from pymoo.factory import get_performance_indicator

gd = get_performance_indicator("gd", pf)
print("GD", gd.calc(A))
```

    GD 0.05497689467314528

### Generational Distance Plus (GD+)

Ishibushi et. al. proposed in  GD+:

\begin{align}
\begin{split}
\text{GD}^+(A) & = & \; \frac{1}{|A|} \; \bigg( \sum_{i=1}^{|A|} {d_i^{+}}^2 \bigg)^{1/2}\\[2mm]
\end{split}
\end{align}

where for minimization $d_i^{+} = max \{ a_i - z_i, 0\}$ represents the modified distance from $a_i$ to its nearest reference point in $Z$ with the corresponding value $z_i$.

```python
from pymoo.factory import get_performance_indicator

gd_plus = get_performance_indicator("gd+", pf)
print("GD+", gd_plus.calc(A))
```

    GD+ 0.05497689467314528

### Inverted Generational Distance (IGD)

The IGD performance indicator  inverts the generational distance and measures the distance from any point in $Z$ to the closest point in $A$.

\begin{align}
\begin{split}
\text{IGD}(A) & = & \; \frac{1}{|Z|} \; \bigg( \sum_{i=1}^{|Z|} \hat{d_i}^p \bigg)^{1/p}\\[2mm]
\end{split}
\end{align}

where $\hat{d_i}$ represents the euclidean distance (p=2) from $z_i$ to its nearest reference point in $A$.

```python
from pymoo.factory import get_performance_indicator

igd = get_performance_indicator("igd", pf)
print("IGD", igd.calc(A))
```

    IGD 0.06690908300327662

### Inverted Generational Distance Plus (IGD+)

In  Ishibushi et. al. proposed IGD+ which is weakly Pareto compliant wheres the original IGD is not.

\begin{align}
\begin{split}
\text{IGD}^{+}(A) & = & \; \frac{1}{|Z|} \; \bigg( \sum_{i=1}^{|Z|} {d_i^{+}}^2 \bigg)^{1/2}\\[2mm]
\end{split}
\end{align}

where for minimization $d_i^{+} = max \{ a_i - z_i, 0\}$ represents the modified distance from $z_i$ to the closest solution in $A$ with the corresponding value $a_i$.

```python
from pymoo.factory import get_performance_indicator

igd_plus = get_performance_indicator("igd+", pf)
print("IGD+", igd_plus.calc(A))
```

    IGD+ 0.06466828842775944

### Hypervolume

For all performance indicators showed so far, a target set needs to be known. For Hypervolume only a reference point needs to be provided. First, I would like to mention that we are using the Hypervolume implementation from [DEAP](https://deap.readthedocs.io/en/master/). It calculates the area/volume, which is dominated by the provided set of solutions with respect to a reference point.

This image is taken from  and illustrates a two objective example where the area which is dominated by a set of points is shown in grey.
Whereas for the other metrics, the goal was to minimize the distance to the Pareto-front, here, we desire to maximize the performance metric.

```python
from pymoo.factory import get_performance_indicator

hv = get_performance_indicator("hv", ref_point=np.array([1.2, 1.2]))
print("hv", hv.calc(A))
```

    hv 0.9631646448182305


## Logistic Regression

- Models the probability an object belongs to a class
- Values ranges from 0 to 1
- Can use threshold to classify into which classes a class belongs
- An S-shaped curve

$
\begin{align}
\sigma(t) = \frac{1}{1 + e^{-t}}
\end{align}
$

```python
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
import math
```

### How does the Logistic Regression function looks like

```python
from matplotlib import pyplot as plt
import math

x = [i for i in range(-10, 10)]
y = [1.0/(1+math.exp(-1*i)) for i in x]

ax = plt.figure(figsize=(8, 6))
plt.plot(x, y,  color='black')
plt.axhline(0.5)
plt.show()
```

### Read the data and plot

```python
import pandas as pd

# Read the data
df_data = pd.read_csv('../data/2d_classification.csv')

# Plot the data
plt.rcParams['figure.figsize'] = [10, 7] # Size of the plots

colors = {0:'b', 1:'g', 2:'r', 3:'c', 4:'m', 5:'y', 6:'k'}
fig, ax = plt.subplots()
grouped = df_data.groupby('label')
for key, group in grouped:
    group.plot(ax=ax, kind='scatter', x='x', y='y', label=key, color=colors[key])
plt.show()
```

### Another way to plot the data

```python
import pandas as pd
import numpy as np

# Read the data
df_data = pd.read_csv('../data/2d_classification.csv')

colors = {0:'b', 1:'g', 2:'r', 3:'c', 4:'m', 5:'y', 6:'k'}

plt.figure()
unq_labels = np.unique(df_data['label'])
for i in unq_labels:
    df = df_data.loc[df_data['label'] == i][['x','y']]
    x = df['x']
    y = df['y']
    plt.scatter(x, y, c=colors[i], alpha=1)
```

### Just using some friendly variable names

```python
data = df_data[['x','y']].values
label = df_data['label'].values
```

### Our first Logistic Regression model

```python
from sklearn.linear_model import LogisticRegression
from matplotlib import pyplot as plt

log_regr = LogisticRegression()
log_regr.fit(data, label)
predictions = log_regr.predict(data)

df_data['pred_label'] = predictions
```

```python
colors = {0:'b', 1:'g', 2:'r', 3:'c', 4:'m', 5:'y', 6:'k'}

plt.figure(figsize=(8,6))
unq_labels = np.unique(predictions)
for i in unq_labels:
    df = df_data.loc[df_data['pred_label'] == i][['x','y']]
    x = df['x']
    y = df['y']
    plt.scatter(x, y, c=colors[i], alpha=1)
```

#### Problem with the previous approach
- When we develop model, using the complete dataset will not allow us to know the actual performance metrics
- Instead always have a test data for evaluation

<font color='red'> <h1> Go to Notebook '03a - Data Split' to know about data split and return from this point </h1> </font>

## Train-Test Splits

```python
from sklearn.model_selection import train_test_split
data_train, data_test, label_train, label_test = train_test_split(data, label, test_size=0.20,
                                                                  random_state=0, stratify=None)
```

```python
df_before_pred = pd.DataFrame(data_train, columns=['x','y'])
df_before_pred['label'] = label_train

# df_before_pred['label'] = df_before_pred['labels'].apply(lambda x:x+2)
df_before_pred_test = pd.DataFrame(data_test, columns=['x','y'])
df_before_pred_test['label'] = 4

df_before_pred = pd.concat([df_before_pred, df_before_pred_test], ignore_index=True)
```

__Train-test ratio of the split data__

```python
print('Complete data:', data.shape)
print('Train Data:', data_train.shape, 'Test Data:', data_test.shape)
```

    Complete data: (100, 2)
    Train Data: (80, 2) Test Data: (20, 2)

__How the data is split__

```python
from collections import Counter
print('Training Data split', Counter(label_train))
print('Testing Data split', Counter(label_test))
```

    Training Data split Counter({0: 42, 1: 38})
    Testing Data split Counter({1: 12, 0: 8})

#### Stratified split

```python
from sklearn.model_selection import train_test_split
data_train, data_test, label_train, label_test = train_test_split(data, label, test_size=0.20,
                                                                  random_state=0, stratify=label)

print('Complete data:', data.shape)
print('Labels distribution', Counter(label))
print('Train Data:', data_train.shape, 'Test Data:', data_test.shape)

from collections import Counter
print('Training Data split', Counter(label_train))
print('Testing Data split', Counter(label_test))
```

    Complete data: (100, 2)
    Labels distribution Counter({1: 50, 0: 50})
    Train Data: (80, 2) Test Data: (20, 2)
    Training Data split Counter({1: 40, 0: 40})
    Testing Data split Counter({1: 10, 0: 10})

### Logistic regression on the train-test stratified split

```python
from sklearn.linear_model import LogisticRegression
from matplotlib import pyplot as plt

# Run Logistic Regression
log_regr = LogisticRegression()
log_regr.fit(data_train, label_train)
predictions = log_regr.predict(data_test)

data = np.concatenate((data_train, data_test))
label = np.concatenate((label_train, predictions))

df_train_test_pred = pd.DataFrame(data, columns=['x','y'])
df_train_test_pred['label'] = label
```

```python
colors = {0:'b', 1:'g', 2:'r', 3:'c', 4:'m', 5:'y', 6:'k'}

plt.figure(figsize= (16, 6))

plt.subplot(1,2,1)
unq_labels = np.unique(df_before_pred['label'])
for i in unq_labels:
    df = df_before_pred.loc[df_before_pred['label'] == i][['x','y']]
    x = df['x']
    y = df['y']
    plt.scatter(x, y, c=colors[i], alpha=1)

plt.subplot(1,2,2)
unq_labels = np.unique(df_train_test_pred['label'])
for i in unq_labels:
    df = df_train_test_pred.loc[df_train_test_pred['label'] == i][['x','y']]
    x = df['x']
    y = df['y']
    plt.scatter(x, y, c=colors[i], alpha=1)
```

## Find the accuracy of Test Data

```python
log_regr.score(data_test, label_test)
```

    0.85

## Find the coefficients

```python
log_regr.coef_
```

    array([[-1.78437808, -0.7594295 ]])

## Find the intercepts

```python
log_regr.intercept_
```

    array([-0.95934723])

## Overfitting and underfitting

### Train Data Accuracy

```python
print('Train Error', log_regr.score(data_train, label_train))
```

    Train Error 0.8875

### What is overfitting
- When you have a very high training error but very low test error

Image Source: Original Source unknown - but all over the internet

[Image Source](https://towardsdatascience.com/over-fitting-and-regularization-64d16100f45c)

[Image Source]()

```python

```

classification/notebooks/03 - Logistic Regression.ipynb


# Probability

A trial, experiment or observation is an event with an unknown outcome.  All
possible outcomes of the trial are called the sample space, and the particular
outcomes being looked for are known as events.  For example, if the trial is
flipping a coin the sample space is heads or tails.  If the trial is rolling a
six sided die, looking for an odd number, the sample space is
`{1, 2, 3, 4, 5, 6}` and the events are `{1, 3, 5}`.  Trials are considered
independent if the outcome of one doesn't affect the others, and events are
considered mutually exclusive if they cannot occur at the same time.

The probability of an event is always between 0 and 1.  Zero indicates that
there is no chance of the event occuring, whilst one means that the event is
certain to occur.  Probability statements are usually written as $P(E) = 0.25$,
meaning that the probability of event $E$ occuring is 0.25, or 25%.  The
probability that event E will occur given the occurence of event F is usually
written as $P(E|F)$.  If the two events were independent $P(E|F) = P(E)$ would
be true.

## Intersection

A compound event that occurs when *all* of its constituent events occur.

$$
\begin{align}
E & = \{1, 3\} \\
F & = \{1, 2\} \\
E \cap F & = \{1\}
\end{align}
$$

If the events are independent the probability of the intersection is the product
of the probabilities for all of the events.

$$P(E \cap F) = P(E)P(F)$$

If the events are not independent we multiply by the conditional probability.

$$P(E \cap F) = P(E)P(F|E)$$

## Union

A compound event that occurs if *at least one* of its constituent events has
occurred.

$$
\begin{align}
E & = \{1, 3\} \\
F & = \{1, 2\} \\
E \cup F & = \{1, 2, 3\}
\end{align}
$$

If the events are mutually exclusive the probability of the union is the sum of
the probabilities for all of the events.

$$P(E \cup F) = P(E) + P(F)$$

If the events aren't mutually exclusive we need to account for any
intersection.

$$P(E \cup F) = P(E) + P(F) - P(E \cap F)$$

## Complement

All outcomes in the sample space that are not part of the event.

$$
\begin{align}
\text{Sample Space} & = \{1, 2, 3, 4, 5, 6\} \\
E & = \{1, 3\} \\
E' & = \{2, 4, 5, 6\}
\end{align}
$$

The complement can also be denoted with $\bar{E}$ or $E^c$.  An event and its
complement define a Bernoulli trial.

## Permutations and Combinations

The number of permutations of subsets of size $k$, drawn from a set of size $n$:

$$nPk = \frac{n!}{(n - k)!}$$

The number of combinations:

$$nCk = \frac{n!}{k!(n-k)!} = \frac{nPk}{k!}$$

There are always fewer combinations than permutations because a different order
of the same elements is a different permutation, but not a different combination.

```python

```


# $\Sigma-\Delta$-modulator phase noise in fractional-N PLL's

The first time that I needed to design a fractional-N PLL if took me  quite some time to gain confidence over the model I was using to calculate the phase noise produced by the $\Sigma\Delta$-modulators(SDM). The reason it took me so long is that there are a couple of mathematical pitfalls that are difficult to avoid at first. Many times I tough that a reference code would have made my life much simpler.

This is one of those topics where a notebook including both theory and code seems particular appealing. This notebook is about that. This entry expose first the SDM theory in the first section, the second section shows how a mash SDM works and explain the model in Python and the last section show the results and the comparison with the theoretical model.  The SDM model is part of a more general package to design PLL's that is available in github.

## Linear phase noise analysis for the excess of phase noise at the output of a fractional divider using a $\Sigma-\Delta$ modulator

The circuit in figure 1 implements the fractional division of input $f_{in}$  by the fractional
period defined by $N+f(z)$ using a SDM. As it is difficult to design a reliable circuit that divides a input frequency by a fractional number a integer divider with a division factor that is in average the desired value is used. As this operation introduces unwanted phase changes in the output signal we are interested in finding the power spectral density of these disturbances.

 Figure 1. Topology of a fractional divider made with a SDM

```
# Prepare for ploting
import matplotlib
import matplotlib.pyplot as plt
font = {'size'   : 12}
matplotlib.rc('font', **font)
matplotlib.rc('axes',labelsize=14)

# Array fast operation
import numpy as np
from numpy import ones, pi, log10

# Signal processing routines

import scipy.signal as sig
```

The instantaneous frequency division is given by:

$$\begin{equation}
    N_{div}(z) = N+f(z)+H_{s}(z) \Delta_{q}(z),
\end{equation}$$

Where $N$ is that integer part of the division ratio, $f(z)$ is the fractional part, and
$H_s(z) \Delta_{q}(z)$ is the instantaneous error made by a SDM of order m. The instantaneous noise of the SDM has to components $\Delta_{q}(z)$ is the quantization noise and the term $H_s(z)$ is the noise shaping transfer function.

The output frequency of the SD modulator is given by:

$$\begin{equation}
    f_{div}(z) = \cfrac{f_{in}}{N+f(z)+H_s(z) \Delta_{q}(z)},
\end{equation}$$

that can be simplify, using the approximation $\cfrac{1}{1+x} \approx  1-x \: \text{for } x << 1$, as:

$$\begin{equation}
    f_{div}(z) = \cfrac{f_{in}}{N+f(z)}\left(1 - \cfrac{H_s(z) \Delta_{q}(z)}{N+f(z)}\right),
\end{equation}$$

As $\cfrac{f_{in}}{N+f(z)}$ equals the target frequency, which in a PLL is the reference frequency   $f_{ref}$, the frequency error as a function of the target frequency equals:

$$\begin{equation}
    \Delta_{f_{div}}(z) = - f_{ref} \cfrac{ H_s(z) \Delta_{q}(z)}{N+f(z)}.
\end{equation}$$

The power spectral density of the quantization noise can be approximated using a similar assumption than the one used to calculate the spectrum of the quantization noise of a ADC. In the case of a SDM the phase error increases linearly during a single period of the reference until it reaches the minimum integer division ratio of the divider. For a divider with a minimum step of one the quantization noise rms value can be approximated by:

$$\begin{equation}
    \Delta_{q,rms} = \frac{1}{12} (rad, rms),
\end{equation}$$

this noise is uniformly distributed over Nyquist bandwidth of the sampler than in the case of a PLL is $f_{ref}/2$. Then the noise power spectral density of the sampling noise is given by:

$$\begin{equation}
    \Delta_{q}^2 = \frac{1}{6 f_{ref}} (rad^2/Hz).
\end{equation}$$

The other aspect that needs to be accounted for, in order to find a close expression for the phase noise, is the shaping of the SDM, for Mash-1-1-1 SDM this is given by[1]:

$$\begin{equation}
    H_s(z)= (1-z^{-1})^m,
\end{equation}$$

where m is the SDM order.

With this elements in place it is possible to have a better expression for the error made approximating the frequency thus:

$$\begin{equation}
    \Delta_{f_{div}}(z)^2 =  \left|f_{ref} \cfrac{ (1-z^{-1})^m}{N+f(z)}\right|^2 \cfrac{1}{6 f_{ref}}.
\end{equation}$$

As it happens we are interesting in the phase, so integrating the last expression in the z-domain we have:

$$\begin{equation}
\Phi_{div}(z) = \frac{2 \pi T_{ref} \Delta_{f_{div}}(z)}{1-z^{-1}},
\end{equation}$$

that putting everything together ends being:

$$\begin{equation}
\Phi_{div}(z)^2 = \frac{(2 \pi)^2}{6 f_{ref}} \frac{|1-z^{-1}|^{2m-1}}{(N+f(z))^2}(Rad^2/Hz)
\end{equation}$$

by definition $\mathcal{L}(z)= \Phi_{div}(z)^2/2$ the phase noise is given by:

$$\begin{equation}
	\mathcal{L}(z) = \frac{(2 \pi)^2}{12 f_{ref}} \frac{|1-z^{-1}|^{2m-1}}{(N+f(z))^2}(Rad^2/Hz),
\end{equation}$$

that can be replaced by a more useful expression replacing $z$ by $e^{sT_{ref}}$ and calling the frequency $f_m$ to emphasis that this is the frequency offset due to the modulation:

$$\begin{equation}
  \mathcal{L}(f_m) = \frac{(2 \pi)^2}{12 f_{ref}} \cfrac{[2 sin(\pi f_m/ f_{ref})]^{2(m-1)}}{(N+f(z))^2}.
\end{equation}$$

As presented here this is the noise at the output of the divider, normally in previous references [1] and [2] the noise is scaled to the divider output, that is multiplying the last equation by $(N+f(z))^2$.

## Using the pnoise module and the sdmod to simulate a SDM

Within the [plldesigner](https://github.com/jfosorio/plldesigner) project two modules implement a class for adding pnoise frequency domain signals and and create sequences of SDM's.

The sigma delta modulator is implemented by an algorithm that uses a counter that add every time the input word K and overflows when the value reaches MAX value that is equivalent in to the maximum value of a register, that is: $2^N-1$. The next example shows how a first order modulator is implemented:

``` python:
k = round(Nfloat*2**N)
for j in arange(0, L):
    state = state + k[j - 1]
    if state > _maxvalue:
        over = 1
        state -= _maxvalue + 1
    else:
        over = 0
        sd[j] = over
```

The implementation of first, second and third order Sigma-delta modulators can be found in: [plldesigner](https://github.com/jfosorio/plldesigner)

```
# Load the pnoise and SDM modules
import plldesigner.pnoise as pn
import plldesigner.sdmod as sdmod
```

### Creating a SDM sequence with gen_mash

In order to create a 1-1-1 Mash sequence the gen_mash function was created in the sdmod module. The function has as inputs, the order of the modulator m, number of bits of the accumulator and the
input vector $k[n]$. The following code shows the sequence created by a SDM of third order.

```
# Parameters
NsdBits = 19
fref = 27.6e6
Tref = 1.0/fref

# Create a SDM sequency
fracnum = ((0.253232*2**NsdBits)*ones(100000)).astype(int)
sd, per = sdmod.gen_mash(3,NsdBits,fracnum)
plt.step(np.r_[0:200],sd[:200]);
plt.xlabel('steps') #x label
plt.ylabel('$\Delta N$') #y label
print("Mean value of the sequence: {:2.6f}\n".format(sd.mean()))
```

The phase error $\phi_{out}[n]$ can be calculated accumulating the error in the frequency. Then the power spectrum density is estimated with the [welch periodogram](http://docs.scipy.org/doc/scipy-dev/reference/generated/scipy.signal.welch.html).

```
# Phi_er at the output equals \sum{\DeltaN*fref}*Tref
phi_div = 2*pi*(sd-fracnum[0]/2**NsdBits).cumsum()

# Calculate the spectrum
npoints = 2**7
f, Phi2_div = sig.welch(phi_div, fref, window='blackman', nperseg=npoints)
rbw = fref/2/(len(f)-1)
ind = (f>1e5) & (f<1e9)
sim = pn.Pnoise(f[ind],10*log10(Phi2_div[ind]/2), label='simulated')

# calcualte the L teorical
Ltheory = sdmod.L_mash_dB(3,f[ind],fref)
theory = pn.Pnoise(f[ind],Ltheory, label='theoretical')

# Calculate the integral value of the two
print('''
Integrated phase noise
======================
Theory: {:2.3f} (rad,rms)
Sim   : {:2.3f} (rad,rms)'''.format(theory.integrate(),sim.integrate()))

# plot both the spectrums
sim.plot('o-')
ax = theory.plot()
plt.legend(bbox_to_anchor=(1.05, 1), loc=2)
plt.title('RBW: {:2.3f} (KHz)'.format(rbw/1e3))
plt.ylim([-140,-40])
plt.grid(True)

```

## Non-linear behavior of the $\Sigma-\Delta$ modulator

The previous theory fails to predict spurs that can occur when the input is a rational number.  This is demonstrated using the same SDM but with a input equivalent to 1/8. It is important to understand these spurs as they can fall in band after they are folded back by the non-linearity of the CP and the PFD.

```
# Fraction number
fracnum = (2/8*2**NsdBits)*ones(100000)

# Produce the SDM sequence
sd, per = sdmod.gen_mash(3,NsdBits,fracnum)

# Calculate the phase error and its PSD
phi_div = 2*pi*(sd-fracnum[0]/2**NsdBits).cumsum()
f, Phi2_div = sig.welch(phi_div, fref, window="blackman", nperseg=npoints)
sim = pn.Pnoise(f[ind],10*log10(Phi2_div[ind]/2), label='simulated')

# plot the sequence
plt.step(np.r_[0:200],sd[:200])
plt.xlabel('steps') #x label
plt.ylabel('$\Delta N$') #y label

print("Mean value of the sequence: {:2.5f}\n".format(sd.mean()))
```

```
#Plot the power spectrum density
sim.plot('o-')
theory.plot()
plt.legend(bbox_to_anchor=(1.05, 1), loc=2)
plt.title('RBW: {:2.3f} (KHz)'.format(rbw/1e3))
plt.ylim([-130,-40])
plt.grid(True)
```

The spurs appearance and amplitude depend both of the input word and the initial state of the registers of the modulator [3]. It can be that initializing the first register of the first register of the SDM creates enough dithering to reduce the spurs The gen_mash accepts a fourth argument to initialize the mash registers. This argument is a tuple with a number of elements that equals the order of the SDM.

```
sd, per = sdmod.gen_mash(3,NsdBits,fracnum,(1,0,0))

# Calculate the phase error and its PSD
phi_div = 2*pi*(sd-fracnum[0]/2**NsdBits).cumsum()
f, Phi2_div = sig.welch(phi_div, fref, window="blackman", nperseg=npoints)
sim = pn.Pnoise(f[ind],10*log10(Phi2_div[ind]/2), label='simulated')

# plot the sequence
plt.step(np.r_[0:200],sd[:200])
plt.xlabel('steps') #x label
plt.ylabel('$\Delta N$') #y label
print("Mean value of the sequence: {:2.5f}\n".format(sd.mean()))
```

```
#Plot the power spectrum density
sim.plot('o-')
theory.plot()
plt.legend(bbox_to_anchor=(1.05, 1), loc=2)
plt.title('RBW: {:2.3f} (KHz)'.format(rbw/1e3))
plt.ylim([-130,-40])
plt.grid(True)
```

## Conclution

With this entry I revisited the theory of SD modulators using a module written in Python. The fundamental aspects of Mash SDM modulators are explained and documented. To analyse the output I used the nice Scipy library.  In this way, and with the help of the Ipython notebook, the linear theory of mash SDM and its limitations are well documented and results are easily replicated.

This shows a bit what would be the intention with the plldesigner toolkit. This a project that I have been developing and aims to add specific functionality to the numpy+scipy+ipython ecosystem to simulate and model PLL's.

## References

1. B. Miller and R. J. Conley, “A multiple modulator fractional divider,” IEEE Transactions on Instrumentation and Measurement, vol. 40, no. 3, pp. 578–583, 1991.

2. T. A. D. Riley, M. A. Copeland, and T. A. Kwasniewski, “Delta-sigma modulation in fractional-N frequency synthesis,” IEEE Journal of Solid-State Circuits, vol. 28, no. 5, pp. 553–559, May 1993.

3. Kozak, M., Kale, I., "Rigorous analysis of delta-sigma modulators for fractional-N PLL frequency synthesis," IEEE Transactions on Circuits and Systems I: Regular Papers 51, 1148–1162, 2004. (doi:10.1109/TCSI.2004.829308)

## Changes

After publication some changes have been made:

- The text was corrected, although not perfect is definitely more readable.

- The pnoise class was renamed to be Pnoise, according with Python conventions, so the notebook was updated accordingly.

- The notebook imports pylab as "from pylab import *", what I consider is the best practice in a Notebook were clarity is more desired than follow software writing best practices.

- Some equations that were not displayed correctly were corrected.


```python
%matplotlib notebook

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

```python
class Quaternion:
    def __init__(self, array=[1.0, 0.0, 0.0, 0.0]):
        self.q = np.array(array)

    def __repr__(self):
        return '{:.3f}w {:.3f}i {:.3f}j {:.3f}k'.format(self.q[0], self.q[1], self.q[2], self.q[3])

    def __copy__(self):
        return Quaternion(self.q)

    def __neg__(self):
        return Quaternion(-1.0 * self.q)

    def __mul__(self, rhs):
        ''' Quaternion multiplication can be simplified to a couple forms, they're all the same mathematically. '''
        # the right-hand-side is a number (as opposed to a quaternion)
        if isinstance(rhs, int) or isinstance(rhs, float):
            return Quaternion(self.q * rhs)
        else:
            l0, l1, l2, l3 = self.q
            r0, r1, r2, r3 = rhs.q
            return Quaternion([l0 * r0 - l1 * r1 - l2 * r2 - l3 * r3,
                               l0 * r1 + l1 * r0 + l2 * r3 - l3 * r2,
                               l0 * r2 - l1 * r3 + l2 * r0 + l3 * r1,
                               l0 * r3 + l1 * r2 - l2 * r1 + l3 * r0])

    def __rmul__(self, lhs):
        # the left-hand-side is a scalar, otherwise '__mul__' would be called
        return Quaternion(lhs * self.q)

    def __truediv__(self, rhs):
        if isinstance(rhs, int) or isinstance(rhs, float):
            return Quaternion(self.q / rhs)
        else:
            return self * rhs.inv()

    def conj(self):
        return Quaternion(np.array([1.0, -1.0, -1.0, -1.0]) * self.q)

    def inv(self):
        return Quaternion(self.conj().q / self.q.dot(self.q))

    def norm(self):
        return np.linalg.norm(self.q)

    def normalize(self, inplace=False):
        if inplace:
            self.q = (self / self.norm()).q
            return self
        else:
            return self / self.norm()

    def rotate(self, vect):
        vect = np.array(vect)
        if vect.shape != (3,):
            raise TypeError("Input needs to be a 1D array with 3 elements")
        p = Quaternion([0, vect[0], vect[1], vect[2]])
        p_prime = self * p * self.conj()
        return p_prime.q[1::]
```

```python
def axis_angle_to_quat(theta, axis):
    i, j, k = np.sin(theta/2) * np.array(axis)
    return Quaternion([np.cos(theta/2), i, j, k])
```

```python
def quat_to_axis_angle(q):
    theta = 2 * np.arctan2(np.linalg.norm(q.q[1:4]), q.q[0])
    axis = q.q[1:4] / np.linalg.norm(q.q[1:4])
    return theta, axis
```

```python
q = axis_angle_to_quat(np.pi/2, [1, 0, 0])
print(q)
```

    0.707w 0.707i 0.000j 0.000k

```python
theta, vect = quat_to_axis_angle(q)
print('theta: {:.3f}    axis: {}'.format(theta, vect))
```

    theta: 1.571    axis: [1. 0. 0.]

## Quaternion multplication
A quaternion multiplication of $q_2\cdot q_1$ is the equivalent of rotation $q_1$ followed by the rotation $q_2$. Quaternion multiplication is non-commutative with one exception. Two [quaternions commute](https://en.wikipedia.org/wiki/Quaternion#Commutative_subrings) only if they lie in the same complex subplane, that is, if their vector parts are co-linear. Additionally, the product $aq = qa$ for every quaternion $q$ and every real quaternion $a$. A real quaternion is one that has all complex basis elements $i$, $j$, and $k$ equal to zero.

```python
q1 = Quaternion([3, 1, -2, 1])
q2 = Quaternion([2, -1, 2, 3])
q3 = Quaternion([3, -1, 2, 3])
q4 = Quaternion([3, 0, 0, 0])
```

```python
q1 * q2
```

    8.000w -9.000i -2.000j 11.000k

```python
q1 * 2  # __mul__ is called on 'q1'
```

    6.000w 2.000i -4.000j 2.000k

```python
2 * q1  # __rmul__ is called on 'q1'
```

    6.000w 2.000i -4.000j 2.000k

```python
q2 * q3
```

    -8.000w -5.000i 10.000j 15.000k

```python
q3 * q2
```

    -8.000w -5.000i 10.000j 15.000k

```python
q3 * q4
```

    9.000w -3.000i 6.000j 9.000k

```python
q4 * q3
```

    9.000w -3.000i 6.000j 9.000k

## Quaternion Complex Conjugate
The [complex conjuage](https://en.wikipedia.org/wiki/Norm_(mathematics)#Euclidean_norm) of $q$ is denoted as $q^*$, is defined as

$$q^* =q_0 + q_1\mathbf{i} + q_2\mathbf{j} + q_3\mathbf{k}$$

From the definintion we immediately have the property

$$(q^*)^* = q$$

```python
(q1.conj()).conj()
```

    3.000w 1.000i -2.000j 1.000k

```python
q1
```

    3.000w 1.000i -2.000j 1.000k

As well as the property

$$q^*q = qq^*$$

Additionally, if $\lVert q \rVert = 1$

$$q^*q = qq^*=1 + 0\mathbf{i} + 0\mathbf{j} + 0\mathbf{k}$$

```python
q1
```

    3.000w 1.000i -2.000j 1.000k

```python
q1.conj()*q1
```

    15.000w 0.000i 0.000j 0.000k

```python
q1*q1.conj()
```

    15.000w 0.000i 0.000j 0.000k

and given two quaternions $p$ and $q$, we can also verify that

$$(pq)^* = q^*p^*$$

```python
(q1*q2).conj()
```

    8.000w 9.000i 2.000j -11.000k

```python
q2.conj() * q1.conj()
```

    8.000w 9.000i 2.000j -11.000k

## Quaternion Norm
The length, or [norm](https://en.wikipedia.org/wiki/Quaternion#Conjugation,_the_norm,_and_reciprocal), of a quaternion is defined as the [Euclidian norm](https://en.wikipedia.org/wiki/Norm_(mathematics)#Euclidean_norm) of its components

$$\lVert q \rVert = \sqrt{qq^*}= \sqrt{q^*q} = \sqrt{q_0^2 + q_1^2 + q_2^2 + q_3^2}$$

note that this is always a **non-negative real number** (and not a quaternion).

```python
q1.norm()
```

    3.872983346207417

```python
np.sqrt((q1*q1.conj()).q[0])
```

    3.872983346207417

```python
np.sqrt(q1.q.dot(q1.q))
```

    3.872983346207417

## Inverse of a Quaternion
The inverse (reciprical) of a quaternion $q$ is essentially the normalized conjugate of the quaternion. If the quaternion is already normalized (i.e. $\lVert q \rVert = 1$) the inverse is identical to the conjugate. The inverse is defined as

$$ q^{-1}=\frac{q^*}{\rVert q \lVert^2}$$

As one would expect that the inverse of a quaternion left or right multiplied by itself will yield a unit quaternion

$$qq^{-1} = q^{-1}q = \frac{qq^*}{\lVert q \rVert^2} = \frac{\lVert q \rVert^2}{\lVert q \rVert^2} = 1 + 0\mathbf{i} + 0\mathbf{j} + 0\mathbf{k}$$

```python
q1 * q1.inv()
```

    1.000w 0.000i -0.000j 0.000k

```python
q1.inv() * q1
```

    1.000w 0.000i -0.000j 0.000k

```python
(q1.conj() / (q1.norm()**2)) * q1
```

    1.000w 0.000i -0.000j 0.000k

```python
q_tmp = q2.__copy__()
q_tmp.normalize(inplace=False)
```

    0.471w -0.236i 0.471j 0.707k

```python
q_tmp
```

    2.000w -1.000i 2.000j 3.000k

```python
q_tmp.normalize(inplace=True)
```

    0.471w -0.236i 0.471j 0.707k

```python
q_tmp
```

    0.471w -0.236i 0.471j 0.707k

For a unit quaterion where $\lVert q \rVert = 1$, the conjugate of the quaternion $q$ is the same as the inverse of the quaternion
$$q^* = q^{-1}$$

```python
(q1.normalize()).conj()
```

    0.775w -0.258i 0.516j -0.258k

```python
(q1.normalize()).inv()
```

    0.775w -0.258i 0.516j -0.258k

## Quaternion Rotations
The [quaternion rotation](https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Using_quaternion_as_rotations) of vector $p$ of the form $[0, x, y, z]$ by quaterion $q$, $p$ rotated by $q$ is
$$p^{\prime} = q p q^*$$

Assume two rotations $q_0$ followed by $q_1$
$$ \begin{align}
p^{\prime} &= q_0 \,p \,q_0^* \\
p^{\prime\prime} &= q_1 \,p \,q_1^* \\
p^{\prime\,\prime} &= q_1 \,(q_0 \,p \,q_0^*) \,q_1^* \\
p^{\prime\,\prime} &= (q_1 \,q_0) \,p \,(q_0^* \,q_1^*) \\
p^{\prime\,\prime} &= (q_1 \,q_0) \,p \,(q_1 \,q_0)^*
\end{align} $$

```python
q2
```

    2.000w -1.000i 2.000j 3.000k

```python
q1.conj() * q2.conj()
```

    8.000w -7.000i -6.000j -11.000k

```python
(q2*q1).conj()
```

    8.000w -7.000i -6.000j -11.000k

```python
qx = axis_angle_to_quat(np.pi/2, [1, 0, 0])  # rotate 90 degrees about the x-axis
qy = axis_angle_to_quat(np.pi/2, [0, 1, 0])  # rotate 90 degrees about the y-axis
qz = axis_angle_to_quat(np.pi/2, [0, 0, 1])  # rotate 90 degrees about the z-axis
p = Quaternion([0, 1, 0, 0])
p_prime = qx*p*qx.conj()
p_prime_prime = qz*qx*p*qx.conj()*qz.conj()
```

```python
f = plt.figure(figsize=(6, 6))
ax = f.add_subplot(111, projection='3d')
ax.scatter(p.q[1], p.q[2], p.q[3], color='xkcd:darkgreen', alpha=0.3)
ax.scatter(p_prime.q[1], p_prime.q[2], p_prime.q[3], color='xkcd:darkgreen', alpha=0.6)
ax.scatter(p_prime_prime.q[1], p_prime_prime.q[2], p_prime_prime.q[3], color='xkcd:darkgreen', alpha=0.9)

ax.plot([0, 1], [0, 0], [0, 0], color='xkcd:red')
ax.plot([0, 0], [0, 1], [0, 0], color='xkcd:green')
ax.plot([0, 0], [0, 0], [0, 1], color='xkcd:blue')
ax.set_xlim([-1, 1])
ax.set_ylim([-1, 1])
ax.set_zlim([-1, 1])
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
```

    <IPython.core.display.Javascript object>

    Text(0.5, 0, 'Z')

```python
qx = axis_angle_to_quat(np.pi/2, [1, 0, 0])  # rotate 90 degrees about the x-axis
qy = axis_angle_to_quat(np.pi/2, [0, 1, 0])  # rotate 90 degrees about the z-axis
qz = axis_angle_to_quat(np.pi/2, [0, 0, 1])  # rotate 90 degrees about the z-axis
p = Quaternion([0, 1, 0, 0])
p_prime = qz*p*qz.conj()
p_prime_prime = qx*qz*p*qz.conj()*qx.conj()
```

```python
f = plt.figure(figsize=(6, 6))
ax = f.add_subplot(111, projection='3d')
ax.scatter(p.q[1], p.q[2], p.q[3], color='xkcd:darkgreen', alpha=0.3)
ax.scatter(p_prime.q[1], p_prime.q[2], p_prime.q[3], color='xkcd:darkgreen', alpha=0.6)
ax.scatter(p_prime_prime.q[1], p_prime_prime.q[2], p_prime_prime.q[3], color='xkcd:darkgreen', alpha=0.9)

ax.plot([0, 1], [0, 0], [0, 0], color='xkcd:red')
ax.plot([0, 0], [0, 1], [0, 0], color='xkcd:green')
ax.plot([0, 0], [0, 0], [0, 1], color='xkcd:blue')
ax.set_xlim([-1, 1])
ax.set_ylim([-1, 1])
ax.set_zlim([-1, 1])
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
```

    <IPython.core.display.Javascript object>

    Text(0.5, 0, 'Z')

```python
qx = axis_angle_to_quat(np.pi/2, [1, 0, 0])  # rotate 90 degrees about the x-axis
qy = axis_angle_to_quat(np.pi/2, [0, 1, 0])  # rotate 90 degrees about the z-axis
qz = axis_angle_to_quat(np.pi/2, [0, 0, 1])  # rotate 90 degrees about the z-axis
p = Quaternion([0, 1, 0, 0])
p_prime = qy.conj()*p*qy
p_prime_prime = qx.conj()*qy.conj()*p*qy*qx
```

```python
f = plt.figure(figsize=(6, 6))
ax = f.add_subplot(111, projection='3d')
ax.scatter(p.q[1], p.q[2], p.q[3], color='xkcd:darkgreen', alpha=0.3)
ax.scatter(p_prime.q[1], p_prime.q[2], p_prime.q[3], color='xkcd:darkgreen', alpha=0.6)
# ax.scatter(p_prime_prime.q[1], p_prime_prime.q[2], p_prime_prime.q[3], color='xkcd:darkgreen', alpha=0.9)

ax.plot([0, 1], [0, 0], [0, 0], color='xkcd:red')
ax.plot([0, 0], [0, 1], [0, 0], color='xkcd:green')
ax.plot([0, 0], [0, 0], [0, 1], color='xkcd:blue')
ax.set_xlim([-1, 1])
ax.set_ylim([-1, 1])
ax.set_zlim([-1, 1])
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
```

    <IPython.core.display.Javascript object>

    Text(0.5, 0, 'Z')

```python
qx = axis_angle_to_quat(-np.pi/2, [1, 0, 0])  # rotate 90 degrees about the x-axis
qy = axis_angle_to_quat(-np.pi/2, [0, 1, 0])  # rotate 90 degrees about the z-axis
qz = axis_angle_to_quat(-np.pi/2, [0, 0, 1])  # rotate 90 degrees about the z-axis
p = Quaternion([0, 1, 0, 0])
p_prime = qy*p*qy.conj()
p_prime_prime = qx*qy*p*qy.conj()*qx.conj()
```

```python
f = plt.figure(figsize=(6, 6))
ax = f.add_subplot(111, projection='3d')
ax.scatter(p.q[1], p.q[2], p.q[3], color='xkcd:darkgreen', alpha=0.3)
ax.scatter(p_prime.q[1], p_prime.q[2], p_prime.q[3], color='xkcd:darkgreen', alpha=0.6)
# ax.scatter(p_prime_prime.q[1], p_prime_prime.q[2], p_prime_prime.q[3], color='xkcd:darkgreen', alpha=0.9)

ax.plot([0, 1], [0, 0], [0, 0], color='xkcd:red')
ax.plot([0, 0], [0, 1], [0, 0], color='xkcd:green')
ax.plot([0, 0], [0, 0], [0, 1], color='xkcd:blue')
ax.set_xlim([-1, 1])
ax.set_ylim([-1, 1])
ax.set_zlim([-1, 1])
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
```

    <IPython.core.display.Javascript object>

    Text(0.5, 0, 'Z')

## References
1. [https://en.wikipedia.org/wiki/Quaternion](https://en.wikipedia.org/wiki/Quaternion)
2. [http://graphics.stanford.edu/courses/cs348a-17-winter/Papers/quaternion.pdf](http://graphics.stanford.edu/courses/cs348a-17-winter/Papers/quaternion.pdf)
3. [https://www.math.brown.edu/~res/MFS/handout2.pdf](https://www.math.brown.edu/~res/MFS/handout2.pdf)
4. [http://www.cs.cmu.edu/afs/cs/academic/class/16741-s07/www/lectures/Lecture8.pdf](http://www.cs.cmu.edu/afs/cs/academic/class/16741-s07/www/lectures/Lecture8.pdf)
5. [https://mil.ufl.edu/nechyba/www/__eel6667.f2003/course_materials/t3.quaternions/quaternion_slides.pdf](https://mil.ufl.edu/nechyba/www/__eel6667.f2003/course_materials/t3.quaternions/quaternion_slides.pdf)


# First Order Initial Value Problem

The more general form of a first order Ordinary Differential Equation is:
\begin{equation}
\label{general ODE}
y^{'}=f(t,y).
\end{equation}
This can be solved analytically by integrating both sides but this is not straight forward for most problems.
Numerical methods can be used to approximate the solution at discrete points.

## Euler method

The simplest one step numerical method is the Euler Method named after the most prolific of mathematicians [Leonhard Euler](https://en.wikipedia.org/wiki/Leonhard_Euler) (15 April 1707 – 18 September 1783) .

The general Euler formula to the first order equation
$$ y^{'} = f(t,y) $$
approximates the derivative at time point $t_i$
$$y^{'}(t_i) \approx \frac{w_{i+1}-w_i}{t_{i+1}-t_{i}} $$
where $w_i$ is the approximate solution of $y$ at time $t_i$.
This substitution changes the differential equation  into a __difference__ equation of the form
$$
\frac{w_{i+1}-w_i}{t_{i+1}-t_{i}}=f(t_i,w_i) $$
Assuming uniform stepsize $t_{i+1}-t_{i}$ is replaced by $h$, re-arranging the equation gives
$$ w_{i+1}=w_i+hf(t_i,w_i),$$
 This can be read as the future $w_{i+1}$ can be approximated by the present $w_i$ and the addition of the input to the system $f(t,y)$ times the time step.

```python
## Library
import numpy as np
import math

%matplotlib inline
import matplotlib.pyplot as plt # side-stepping mpl backend
import matplotlib.gridspec as gridspec # subplots
import warnings
import pandas as pd

warnings.filterwarnings("ignore")
```

## Population growth

The general form of the non-linear population growth differential equation is:
$$ y^{'}=\alpha y-\beta y^2 $$
where $\alpha$ is the growth rate and $\beta$ is the death rate. The initial population at time $ a $ is
$$ y(a)=A $$
$$ a\leq t \leq b. $$

## Discrete Interval
The continuous time $a\leq t \leq b $ is discretised into $N$ points seperated by a constant stepsize
$$ h=\frac{b-a}{N}.$$
Here the interval is $2000\leq t \leq 2002$
$$ h=\frac{2020-2000}{200}=0.1.$$
This gives the 21 discrete points:
$$ t_0=2000, \ t_1=0.1, \ ... t_{200}=2020. $$
This is generalised to
$$ t_i=2000+i0.1, \ \ \ i=0,1,...,200.$$
The plot below shows the discrete time steps.

```python
### Setting up time
t_end=2020.0
t_start=2000.0
N=200
h=(t_end-t_start)/(N)
time=np.arange(t_start,t_end+0.01,h)
fig = plt.figure(figsize=(10,4))
plt.plot(time,0*time,'o:',color='red')
plt.title('Illustration of discrete time points for h=%s'%(h))
plt.xlim((2000,2002))
plt.plot();
```

## Initial Condition
To get a specify solution to a first order initial value problem, an __initial condition__ is required.
For our population problem the intial condition is:
$$y(2000)=6.$$
### Growth rate
Given the growth rate $$\alpha=0.2,$$ and death rate $$\beta=0.01,$$ giving the specific differential equation,
$$ y^{'}=0.2 y-0.01 y^2 $$
The initial population at time $2000$ is
$$ y(2000)=6, $$
$$ 2000\leq t \leq 2020. $$

## Numerical approximation of Population growth
The differential equation is transformed using the Euler method into a difference equation of the form
      $$ w_{i+1}=w_{i}+h (\alpha w_i-\beta w_i\times w_i). $$
This approximates a series of of values $w_0, \ w_1, \ ..., w_{N}$.
For the specific example of the population equation the difference equation is,
      $$ w_{i+1}=w_{i}+h 0.1 [0.2 w_i-0.01 w_i\times w_i], $$
where $i=0,1,2,...,199$, and $w_0=6$. From this initial condition the series is approximated.
The plot below shows the Euler approximation $w$ in blue squares.

```python
w=np.zeros(N+1)
w[0]=6
for i in range (0,N):
    w[i+1]=w[i]+h*(0.2*w[i]-0.01*w[i]*w[i])

fig = plt.figure(figsize=(10,4))
plt.plot(time,w,'s:',color='blue',label='Euler')
plt.xlim((min(time),max(time)))
plt.xlabel('time')
plt.legend(loc='best')
plt.title('Euler solution')
plt.plot();
```

### Table
The table below shows the iteration $i$, the discrete time point t[i], and the Euler approximation w[i] of the solution $y$ at time point t[i] for the non-linear population equation.

```python
d = {'time t_i': time[0:10],    'Euler (w_i) ':w[0:10]}
df = pd.DataFrame(data=d)
df
```

      time t_i
      Euler (w_i)

      0
      2000.0
      6.000000

      1
      2000.1
      6.084000

      2
      2000.2
      6.168665

      3
      2000.3
      6.253986

      4
      2000.4
      6.339953

      5
      2000.5
      6.426557

      6
      2000.6
      6.513788

      7
      2000.7
      6.601634

      8
      2000.8
      6.690085

      9
      2000.9
      6.779130

## Numerical Error
With a numerical solution there are two types of error:
* local truncation error at one time step;
* global error which is the propagation of local error.

### Derivation of  Euler Local truncation error
The left hand side of a initial value problem $\frac{dy}{dt}$ is approximated by __Taylors theorem__ expand about a point $t_0$ giving:
\begin{equation}y(t_1) = y(t_0)+(t_1-t_0)y^{'}(t_0) + \frac{(t_1-t_0)^2}{2!}y^{''}(\xi), \ \ \ \ \ \ \xi \in [t_0,t_1]. \end{equation}
Rearranging and letting $h=t_1-t_0$ the equation becomes
$$y^{'}(t_0)=\frac{y(t_1)-y(t_0)}{h}-\frac{h}{2}y^{''}(\xi). $$
From this the local truncation error is
$$\tau y^{'}(t_0)\leq \frac{h}{2}M  $$
where $y^{''}(t) \leq M $.
#### Derivation of  Euler Local truncation error for the Population Growth
As the exact solution $y$ is unknown we cannot get an exact estimate of the second derivative
$$y'(t)=0.2 y-0.01 y^2,$$
differentiate with respect to $t$,
$$y''(t)=0.2 y'-0.01 (2yy'),$$
subbing the original equation gives
$$y''(t)=0.2 (0.2 y-0.01 y^2)-0.01 \big(2y(0.2 y-0.01 y^2)\big),$$
which expresses the second derivative as a function of the exact solution $y$, this is still a problem as the value of $y$ is unknown, assuming,
$$max|y''|=M\leq 40,$$
this gives a local trucation for $h=0.1$ for our non-linear equation is
$$\tau=\frac{h}{2}40=2. $$

```python
M=40
fig = plt.figure(figsize=(10,4))
plt.plot(time[0:2],0.1*M/2*np.ones(2),'v:'
         ,color='black',label='Upper Local Truncation')
plt.xlabel('time')
plt.legend(loc='best')
plt.title('Local Truncation Error')
plt.plot();
```

### Global truncation error for the population equation
For the population equation specific values $L$ and $M$ can be calculated.

In this case $f(t,y)=\epsilon y$ is continuous and satisfies a Lipschitz Condition with constant
$$ \left|\frac{\partial f(t,y)}{\partial y}\right|\leq L $$
$$ \left|\frac{\partial (0.2 y-0.01 y^2)}{\partial y}\right|\leq |0.2-0.01(2\times y)| \leq |0.2-0.01(2\times 50)|\leq 0.8 $$

on $D=\{(t,y)|2000\leq t \leq 2015, 0 < y < 50 \}$ and that a constant $M$
exists with the property that
$$ |y^{''}(t)|\leq M\leq 6. $$

__ Specific Theorem Global Error__

Let $y(t)$ denote the unique solution of the Initial Value Problem
$$ y^{'}=0.2 y-0.01 y^2, \ \ \ 2000\leq t \leq 2020, \ \ \ y(0)=6, $$
and $w_0,w_1,...,w_N$ be the approx generated by the Euler method for some
positive integer N.  Then for $i=0,1,...,N$ the error is:
$$ |y(t_i)-w_i| \leq \frac{6 h}{2\times 0.8}|e^{0.8(t_i-2000)}-1|. $$

## Non-linear population equation with a temporal oscilation
Given the specific population differential equation with a wiggle,
$$ y^{'}=0.2 y-0.01 y^2+sin(2\pi t), $$
with the initial population at time $2000$ is
$$ y(2000)=6, $$
$$ 2000\leq t \leq 2020. $$

For the specific example of the population equation the difference equation is
      $$ w_{i+1}=w_{i}+h 0.5 (0.2 w_i-0.02 w_i\times w_i+sin(2 \pi t), $$
for $i=0,1,...,199$,
where $w_0=6$. From this initial condition the series is approximated.
The figure below shows the discrete solution.

```python
w=np.zeros(N+1)
w[0]=6
for i in range (0,N):
    w[i+1]=w[i]+h*(0.2*w[i]-0.01*w[i]*w[i]+np.sin(2*np.pi*time[i]))

fig = plt.figure(figsize=(10,4))
plt.plot(time,w,'s:',color='blue',label='Euler')
plt.xlim((min(time),max(time)))
plt.xlabel('time')
plt.legend(loc='best')
plt.title('Euler solution')
plt.plot();
```

### Table
The table below shows the iteration $i$, the discrete time point t[i], and the Euler approximation w[i] of the solution $y$ at time point t[i] for the non-linear population equation with a temporal oscilation.

```python
table = ListTable()
table.append(['i','time', 'Euler'])
for i in range (0,8):
    table.append([round(i,3),round(time[i],3), round(w[i],3)])
table
d = {'time t_i': time[0:10],    'Euler (w_i) ':w[0:10]}
df = pd.DataFrame(data=d)
df
```

Chapter 01 - Euler Methods/.ipynb_checkpoints/102_Euler_method_with_Theorems_nonlinear_Growth_function-checkpoint.ipynb


# State tomography

It is not possible to learn (or estimate) a quantum state in a single experiment due to the [no cloning theorem](https://en.wikipedia.org/wiki/No-cloning_theorem). In general one needs to re-prepare the state and measure it many times in [different bases](https://quantumcomputing.stackexchange.com/questions/1870/what-does-measurement-in-a-certain-basis-mean).

## Background

### Classical "state" tomography
The simplest classical analogy to estimating a quantum state using tomography is estimating the bias of a coin. The bias, denoted by $p$, is analogous to the quantum state in a way that will be explained shortly.

The coin toss is a random variable $R$ with outcome Heads (denoted by $1$) and tails (denoted by $0$). Given the bias $p$, the probability distribution for the random variable $R$ is

$$\Pr(R|p) = p^R(1-p)^{1-R}$$

If we have access to $n_{\rm Tot}$ independent experiments (or measurements) and observe $n_{\rm H}$ heads then the [maximum likelihood estimate](http://mathworld.wolfram.com/MaximumLikelihood.html) for the bias of the coin is

$$p_{\rm Max-Like} = \frac{n_H}{n_{\rm Tot}}$$

and the variance of this estimator is ${\rm Var}[p_{\rm Max-Like}] = p(1-p)/n_{\rm Tot}$.

The things to learn from this example are:

* it takes many measurements to estimate $p$, it can't be done in a single shot because of the [classical no-cloning theorem](https://doi.org/10.1103/PhysRevLett.88.210601)

* $p_{\rm Max-Like}$ is an estimator, there are other choices of estimators see e.g. [Beta distribution](https://en.wikipedia.org/wiki/Beta_distribution#Bayesian_inference) and [Bayes estimator](https://en.wikipedia.org/wiki/Bayes_estimator)

Let's take the analogy a little further by defining a "quantum" state

$$\begin{align}
\rho
&= \begin{pmatrix} p & 0\\ 0 & 1-p\end{pmatrix} \\
&= \frac{1}{2} (I +z Z)\\
&=\frac{1}{2} \begin{pmatrix} 1+z & 0\\ 0 & 1-z\end{pmatrix},
\end{align}$$

where $I$ and $Z$ are the identity and the Pauli-z matrix. This parameterizes the states along the z-axis of the Bloch sphere, see Figure 1.

Figure 1. A cross section through the [Bloch sphere](https://en.wikipedia.org/wiki/Bloch_sphere). The states along the z-axis are equivalent to the allowed states (biases) of a coin.

Given the state $\rho$ the probability of measuring zero and one are

$$\begin{align}
\Pr(0|\rho) &= {\rm Tr}[\rho \Pi_0]\\
\Pr(1|\rho) &= {\rm Tr}[\rho \Pi_1]
\end{align}$$

where the measurement operators $\Pi_i$ are defined by $\Pi_i = |i\rangle \langle i|$ for $i \in \{0, 1 \}$.

The relationship between the $Z$ expectation value and the coin bias is

$$z:= \langle Z \rangle = {\rm Tr}[Z \rho] = 2 p -1$$

In this analogy the pure states $|0\rangle$ and $|1\rangle$ correspond to the coin biases $p=1$, $p=0$ and z expectation values $z=+1$, $z=-1$ respectively. All other (mixed) states are convex mixtures of these extremal points e.g. the fair coin, i.e. $p=1/2$, corresponds to $z = 0$ and the state

$$\rho
= \begin{pmatrix} 0.5 & 0\\ 0 & 0.5\end{pmatrix}$$

### Quantum state tomography of a single qubit

The simplest quantum system to tomograph is a single qubit. Like before we parameterize the state with respect to a set of operators

$$\begin{align}
\rho
&= \frac{1}{2} (I+x X+ y Y +z Z)\\
&=\frac{1}{2} \begin{pmatrix} 1+z & x+iy\\ x-iy & 1-z\end{pmatrix},
\end{align}$$

where $x = \langle X \rangle$, $y = \langle Y \rangle$, and $z = \langle Z \rangle$. In the language of our classical coin we have three parameters we need to estimate that are constrained in the following way

$$0\le x \le 1,\\
0\le y \le 1,\\
0\le z \le 1,\\
x^2 + y^2 +z^2 \le 1$$

The physics of our system means that our default measurement gives us the Z basis statistics. We already constructed an estimator to go from the coin flip statistics to the Z expectation: $2p -1$.

Now we need to measure the statistics of the operators X and Y. Essentially this means we must rotate our state after we prepare it but before it is measured (or equivalently rotate our measurement basis). If we rotate the state as $\rho\mapsto U\rho U^\dagger$ and then do our usual Z-basis measurement, this is equivalent to rotating the measured observable as $Z \mapsto U^\dagger Z U$ and keeping our state $\rho$ unchanged. This is the distinction between the [Heisenberg and Schrödinger pictures](https://en.wikipedia.org/wiki/Heisenberg_picture#Summary_comparison_of_evolution_in_all_pictures). The Heisenberg picture point of view then allows us to see that if we apply a rotation such as $R_y(\alpha) = \exp(-i \alpha Y /2)$ for $\alpha = -\pi/2$ then this rotates the observable as $R_y(\pi/2)ZR_y(-\pi/2)=\cos(\pi/2) Z + \sin(\pi/2) X = X$. Similarly, we could rotate by $U=R_x(\pi/2)$ to measure the Y observable.

### Quantum state tomography in general

In this section we closely follow the reference [PBT].

When thinking about tomography it is useful to introduce the notation of "super kets" $|O\rangle \rangle = {\rm vec}(O)$  for an operator $O$.
The dual vector is the corresponding "super bra" $\langle\langle O|$ and represents $O^\dagger$. This vector space is equipped with the Hilbert-Schmidt inner product  $\langle\langle A | B \rangle\rangle = {\rm Tr}[A^\dagger B]$. For more information see [vec and unvec in superoperator_representations.md](../superoperator_representations.rst) and the [superoperator_tools ipython notebook](superoperator_tools.ipynb).

A quantum state matrix on $n$ qubits, a  $D =2^n$ dimensional Hilbert
space, can be represented by

$$\rho =  \frac{1}{D} I + \sum_{\alpha=1}^{D^2-1}x_\alpha B_\alpha$$

where the coefficients $x_\alpha$ are defined by $x_\alpha = \langle\langle B_\alpha | \rho \rangle\rangle$ for a basis of Hermitian operators
$\{ B_\alpha \}$ that is orthonormal under the Hilbert-Schmidt inner product
$\langle\langle B_\alpha | B_\beta\rangle\rangle =\delta_{\alpha,\beta}$.

For many qubits, tensor products of Pauli matrices are the natural Hermitian basis. For two qubits define $B_5 = X \otimes X$ and $B_6 = X \otimes Y$ then $\langle\langle B_5 | B_6\rangle\rangle =0$. It is typical to choose $B_0 = I / \sqrt {D}$ to be the only traceful element, where $I$ is the identity on $n$ qubits.

State tomography involves estimating or measuring the expectation values of all the operators $\{ B_\alpha \}$. If you can reconstruct all the operators $\{ B_\alpha \}$ then your measurement is said to be **tomographically complete**. To measure these operators we need to do rotations, like in the single qubit case, on many qubits. This is depicted in Figure 2.

Figure 2. This upper half of this diagram shows a simple 2-qubit quantum program consisting of both qubits initialized in the ∣0⟩ state, then transformed to some other state via a process V and finally measured in the natural qubit basis.

The operators that need to be estimated are programmatically given by `itertools.product(['I', 'X', 'Y', 'Z'], repeat=n_qubits)`. From these measurements, we can reconstruct a density matrix $\rho$ on `n_qubits`.

Most research in quantum state tomography is to do with finding estimators with desirable properties, e.g. [Minimax tomography](https://arxiv.org/abs/1503.03100), although experiment design is also considered e.g. [adaptive quantum state tomography](https://arxiv.org/abs/1303.0436).

**More information**

See the following references:

| [QTW] [Quantum tomography Wikipedia page](https://en.wikipedia.org/wiki/Quantum_tomography)

| [CWPHD] *Initialization and characterization of open quantum systems*.
|     Christopher Wood.
|     Chapter 3, PhD Thesis, University of Waterloo (2015).
|     http://hdl.handle.net/10012/9557

| [IGST] *Introduction to Quantum Gate Set Tomography*.
|     Daniel Greenbaum.
|     arXiv:1509.02921 (2015).
|     https://arxiv.org/abs/1509.02921

| [PBT] *Practical Bayesian Tomography*.
|     Christopher Granade et al.
|     New J. Phys. 18, 033024 (2016).
|     https://dx.doi.org/10.1088/1367-2630/18/3/033024
|     https://arxiv.org/abs/1509.03770

## Quantum state tomography in `forest.benchmarking`

The basic workflow is:

1. Prepare a state by specifying a pyQuil program.
2. Construct a list of observables that are needed to estimate the state; we collect this into an object called an `ObservablesExperiment`.
3. Acquire the data by running the program on a QVM or QPU.
4. Apply an estimator to the data to obtain an estimate of the state.
5. Compare the estimated state to the true state by a distance measure or visualization.

Below we break these steps down into all their ghastly glory.

## Step 1. Prepare a state with a `Program`
We'll construct a two-qubit graph state by Hadamarding all qubits and then applying a controlled-Z operation across the edges of our graph. In the two-qubit case, there's only one edge. The vector we end up preparing is

$$|\Psi\rangle = \frac{1}{2}\begin{pmatrix} 1\\ 1 \\ 1\\ -1\end{pmatrix}= {\rm CZ}(0,1){\rm H}(1){\rm H}(0)|0,0\rangle$$

which corresponds to the state matrix

$$\rho_{\rm true} = |\Psi\rangle\langle \Psi| = \frac{1}{4}\begin{pmatrix} 1 & 1 & 1 &-1\\ 1 & 1 & 1 &-1 \\ 1 & 1 & 1 &-1\\ -1 & -1 & -1 & 1\end{pmatrix}$$

```python
import numpy as np
from pyquil import Program
from pyquil.gates import *
```

```python
# numerical representation of the true state
Psi = (1/2) * np.array([1, 1, 1, -1])
rho_true = np.outer(Psi, Psi.T.conj())

rho_true
```

    array([[ 0.25,  0.25,  0.25, -0.25],
           [ 0.25,  0.25,  0.25, -0.25],
           [ 0.25,  0.25,  0.25, -0.25],
           [-0.25, -0.25, -0.25,  0.25]])

```python
# construct the state preparation program

qubits = [0, 1]
state_prep_prog = Program()

for qubit in qubits:
    state_prep_prog += H(qubit)

state_prep_prog += CZ(qubits[0], qubits[1])

print(state_prep_prog)
```

    H 0
    H 1
    CZ 0 1

## Step 2. Construct a `ObservablesExperiment` for state tomography

We use the helper function `generate_state_tomography_experiment` to construct a tomographically complete set of measurements.

We can print this out to see the 15 observables or operator measurements we will perform. Note that we could have included an additional observable `I0I1`, but since this trivially gives an expectation of 1 we instead omit this observable in experiment generation and include its contribution by hand in the estimation methods. Be mindful of this if generating your own settings.

```python
# import the generate_state_tomography_experiment function
from forest.benchmarking.tomography import generate_state_tomography_experiment

experiment = generate_state_tomography_experiment(program=state_prep_prog, qubits=qubits)

print(experiment)
```

    H 0; H 1; CZ 0 1
    0: Z+_0 * Z+_1→(1+0j)*X1
    1: Z+_0 * Z+_1→(1+0j)*Y1
    2: Z+_0 * Z+_1→(1+0j)*Z1
    3: Z+_0 * Z+_1→(1+0j)*X0
    4: Z+_0 * Z+_1→(1+0j)*X0X1
    5: Z+_0 * Z+_1→(1+0j)*X0Y1
    6: Z+_0 * Z+_1→(1+0j)*X0Z1
    7: Z+_0 * Z+_1→(1+0j)*Y0
    8: Z+_0 * Z+_1→(1+0j)*Y0X1
    9: Z+_0 * Z+_1→(1+0j)*Y0Y1
    10: Z+_0 * Z+_1→(1+0j)*Y0Z1
    11: Z+_0 * Z+_1→(1+0j)*Z0
    12: Z+_0 * Z+_1→(1+0j)*Z0X1
    13: Z+_0 * Z+_1→(1+0j)*Z0Y1
    14: Z+_0 * Z+_1→(1+0j)*Z0Z1

```python
# lets peek into the object
print('The object "experiment" is a:')
print(type(experiment),'\n')
print('It has a program attribute:')
print(experiment.program)
print('It also has a list of observables that need to be estimated:')
print(experiment.settings_string())
```

    The object "experiment" is a:
    <class 'forest.benchmarking.observable_estimation.ObservablesExperiment'>

    It has a program attribute:
    H 0
    H 1
    CZ 0 1

    It also has a list of observables that need to be estimated:
    0: Z+_0 * Z+_1→(1+0j)*X1
    1: Z+_0 * Z+_1→(1+0j)*Y1
    2: Z+_0 * Z+_1→(1+0j)*Z1
    3: Z+_0 * Z+_1→(1+0j)*X0
    4: Z+_0 * Z+_1→(1+0j)*X0X1
    5: Z+_0 * Z+_1→(1+0j)*X0Y1
    6: Z+_0 * Z+_1→(1+0j)*X0Z1
    7: Z+_0 * Z+_1→(1+0j)*Y0
    8: Z+_0 * Z+_1→(1+0j)*Y0X1
    9: Z+_0 * Z+_1→(1+0j)*Y0Y1
    10: Z+_0 * Z+_1→(1+0j)*Y0Z1
    11: Z+_0 * Z+_1→(1+0j)*Z0
    12: Z+_0 * Z+_1→(1+0j)*Z0X1
    13: Z+_0 * Z+_1→(1+0j)*Z0Y1
    14: Z+_0 * Z+_1→(1+0j)*Z0Z1

### Optional grouping
We can simultaneously estimate some of these observables, this saves on run time.

```python
from forest.benchmarking.observable_estimation import group_settings
print(group_settings(experiment))
```

    H 0; H 1; CZ 0 1
    0: Z+_0 * Z+_1→(1+0j)*X1, Z+_0 * Z+_1→(1+0j)*X0, Z+_0 * Z+_1→(1+0j)*X0X1
    1: Z+_0 * Z+_1→(1+0j)*Y1, Z+_0 * Z+_1→(1+0j)*X0Y1
    2: Z+_0 * Z+_1→(1+0j)*Z1, Z+_0 * Z+_1→(1+0j)*X0Z1
    3: Z+_0 * Z+_1→(1+0j)*Y0, Z+_0 * Z+_1→(1+0j)*Y0X1
    4: Z+_0 * Z+_1→(1+0j)*Y0Y1
    5: Z+_0 * Z+_1→(1+0j)*Y0Z1
    6: Z+_0 * Z+_1→(1+0j)*Z0, Z+_0 * Z+_1→(1+0j)*Z0X1
    7: Z+_0 * Z+_1→(1+0j)*Z0Y1
    8: Z+_0 * Z+_1→(1+0j)*Z0Z1

## Step 3. Acquire the data

PyQuil will run the tomography programs.

We will use the QVM but at this point you can use a QPU.

```python
from pyquil import get_qc
qc = get_qc('2q-qvm')
```

The next step is to over-write full `quilc` compilation with a much more simple version that *only* substitutes gates to Rigetti-native gates.

We do this because we don't want to accidentally compile away our tomography circuit or map to different qubits.

```python
from forest.benchmarking.compilation import basic_compile
qc.compiler.quil_to_native_quil = basic_compile
```

Now get the data!

```python
from forest.benchmarking.observable_estimation import estimate_observables

results = list(estimate_observables(qc, experiment))

print('ExperimentResult[(input operators)→(output operator): "mean" +- "standard error"]')
results
```

    ExperimentResult[(input operators)→(output operator): "mean" +- "standard error"]

    [ExperimentResult[Z+_0 * Z+_1→(1+0j)*X1: -0.02 +- 0.04471241438347967],
     ExperimentResult[Z+_0 * Z+_1→(1+0j)*Y1: 0.032 +- 0.04469845634918503],
     ExperimentResult[Z+_0 * Z+_1→(1+0j)*Z1: 0.068 +- 0.04461784396404649],
     ExperimentResult[Z+_0 * Z+_1→(1+0j)*X0: 0.012 +- 0.04471813949618208],
     ExperimentResult[Z+_0 * Z+_1→(1+0j)*X0X1: -0.012 +- 0.04471813949618208],
     ExperimentResult[Z+_0 * Z+_1→(1+0j)*X0Y1: -0.064 +- 0.044629676225578875],
     ExperimentResult[Z+_0 * Z+_1→(1+0j)*X0Z1: 1.0 +- 0.0],
     ExperimentResult[Z+_0 * Z+_1→(1+0j)*Y0: 0.088 +- 0.04454786190155483],
     ExperimentResult[Z+_0 * Z+_1→(1+0j)*Y0X1: -0.028 +- 0.04470382533967311],
     ExperimentResult[Z+_0 * Z+_1→(1+0j)*Y0Y1: 1.0 +- 0.0],
     ExperimentResult[Z+_0 * Z+_1→(1+0j)*Y0Z1: 0.012 +- 0.04471813949618208],
     ExperimentResult[Z+_0 * Z+_1→(1+0j)*Z0: 0.0 +- 0.044721359549995794],
     ExperimentResult[Z+_0 * Z+_1→(1+0j)*Z0X1: 1.0 +- 0.0],
     ExperimentResult[Z+_0 * Z+_1→(1+0j)*Z0Y1: -0.108 +- 0.044459779576601605],
     ExperimentResult[Z+_0 * Z+_1→(1+0j)*Z0Z1: 0.096 +- 0.044514806525469706]]

## Step 4. Apply some estimators to the data "do tomography"

### Linear inversion estimator

```python
from forest.benchmarking.tomography import linear_inv_state_estimate

rho_est_linv = linear_inv_state_estimate(results, qubits=qubits)

print(np.round(rho_est_linv, 3))
```

    [[ 0.291-0.j     0.245+0.019j  0.253-0.025j -0.253+0.023j]
     [ 0.245-0.019j  0.209-0.j     0.247-0.009j -0.247-0.019j]
     [ 0.253+0.025j  0.247+0.009j  0.243+0.j    -0.255-0.035j]
     [-0.253-0.023j -0.247+0.019j -0.255+0.035j  0.257+0.j   ]]

### Linear inversion projected to closest physical state

```python
from forest.benchmarking.operator_tools.project_state_matrix import project_state_matrix_to_physical

rho_phys = project_state_matrix_to_physical(rho_est_linv)

print(np.round(rho_phys, 3))
```

    [[ 0.277+0.j     0.238+0.001j  0.248-0.024j -0.252+0.007j]
     [ 0.238-0.001j  0.222+0.j     0.232-0.016j -0.232-0.009j]
     [ 0.248+0.024j  0.232+0.016j  0.245+0.j    -0.247-0.027j]
     [-0.252-0.007j -0.232+0.009j -0.247+0.027j  0.256+0.j   ]]

### Maximum Likelihood Estimate (MLE) via diluted iterative method

```python
from forest.benchmarking.tomography import iterative_mle_state_estimate

rho_mle = iterative_mle_state_estimate(results=results, qubits=qubits)

print(np.around(rho_mle, 3))
```

    [[ 0.264+0.j     0.248-0.003j  0.256-0.017j -0.259+0.j   ]
     [ 0.248+0.003j  0.233-0.j     0.241-0.013j -0.243-0.003j]
     [ 0.256+0.017j  0.241+0.013j  0.249+0.j    -0.251-0.017j]
     [-0.259-0.j    -0.243+0.003j -0.251+0.017j  0.254-0.j   ]]

### MLE with Max Entropy constraint

```python
rho_mle_maxent = iterative_mle_state_estimate(results=results, qubits=qubits, epsilon=0.1, entropy_penalty=0.05)

print(np.around(rho_mle_maxent, 3))
```

    [[ 0.267+0.j     0.202+0.002j  0.208-0.015j -0.21 +0.005j]
     [ 0.202-0.002j  0.232+0.j     0.198-0.009j -0.199-0.006j]
     [ 0.208+0.015j  0.198+0.009j  0.248-0.j    -0.206-0.017j]
     [-0.21 -0.005j -0.199+0.006j -0.206+0.017j  0.254-0.j   ]]

### MLE with Hedging parameter

```python
rho_mle_hedge = iterative_mle_state_estimate(results=results, qubits=qubits, epsilon=.001, beta=.61)

print(np.around(rho_mle_hedge, 3))
```

    [[ 0.264+0.j     0.247-0.003j  0.255-0.017j -0.258+0.j   ]
     [ 0.247+0.003j  0.233-0.j     0.24 -0.013j -0.242-0.003j]
     [ 0.255+0.017j  0.24 +0.013j  0.249-0.j    -0.25 -0.017j]
     [-0.258-0.j    -0.242+0.003j -0.25 +0.017j  0.254+0.j   ]]

## Step 5. Compare estimated state to the true state

```python
estimates = {
    'True State': rho_true,
    'Linear Inv': rho_est_linv,
    'ProjLinInv': rho_phys,
    'plain MLE': rho_mle,
    'MLE MaxEnt': rho_mle_maxent,
    'MLE Hedge': rho_mle_hedge}
```

### Compare fidelity and Trace distance to the true state

```python
from forest.benchmarking.distance_measures import fidelity, trace_distance

for key, rho_e in estimates.items():
    fid = np.round(fidelity(rho_true, rho_e), 3)
    print(f"Fidelity(True State, {key}) = {fid}")
```

    Fidelity(True State, True State) = 1.0
    Fidelity(True State, Linear Inv) = 1.0
    Fidelity(True State, ProjLinInv) = 0.974
    Fidelity(True State, plain MLE) = 0.999
    Fidelity(True State, MLE MaxEnt) = 0.861
    Fidelity(True State, MLE Hedge) = 0.997

```python
for key, rho_e in estimates.items():
    fid = np.round(trace_distance(rho_true, rho_e), 3)
    print(f"Tr_dist(True State, {key}) = {fid}")
```

    Tr_dist(True State, True State) = 0.0
    Tr_dist(True State, Linear Inv) = 0.055
    Tr_dist(True State, ProjLinInv) = 0.042
    Tr_dist(True State, plain MLE) = 0.025
    Tr_dist(True State, MLE MaxEnt) = 0.086
    Tr_dist(True State, MLE Hedge) = 0.025

### Compare purities of estimates

Notice how the Maximum entropy has a lower purity.

```python
from forest.benchmarking.distance_measures import purity
```

```python
for key, rho_e in estimates.items():
    p = np.round(purity(rho_e),3)
    print(f"{key} estimates a purity of {p}")
```

    True State estimates a purity of 1.0
    Linear Inv estimates a purity of 1.01
    ProjLinInv estimates a purity of 0.954
    plain MLE estimates a purity of 1.0
    MLE MaxEnt estimates a purity of 0.75
    MLE Hedge estimates a purity of 0.996

### Plot

There are many different ways to visualize states and processes in forest.benchmarking, see [state_and_process_plots.ipynb](state_and_process_plots.ipynb) for more options.

```python
import matplotlib.pyplot as plt
from forest.benchmarking.plotting.hinton import hinton

fig, (ax1, ax2) = plt.subplots(1, 2)
hinton(rho_true, ax=ax1)
hinton(rho_mle_maxent, ax=ax2)
ax1.set_title('True')
ax2.set_title('Estimated via MLE MaxEnt')
fig.tight_layout()
```

Above we can't really see any difference.

So lets try a more informative plot

```python
from forest.benchmarking.utils import n_qubit_pauli_basis
from forest.benchmarking.operator_tools.superoperator_transformations import vec, computational2pauli_basis_matrix
from forest.benchmarking.plotting.state_process import plot_pauli_rep_of_state, plot_pauli_bar_rep_of_state

# convert to pauli representation
n_qubits = 2
pl_basis = n_qubit_pauli_basis(n_qubits)
c2p = computational2pauli_basis_matrix(2*n_qubits)

rho_true_pauli = np.real(c2p @ vec(rho_true))

rho_mle_maxent_pauli = np.real(c2p @ vec(rho_mle_maxent))

fig1, (ax3, ax4) = plt.subplots(1, 2, figsize=(10,5))
plot_pauli_bar_rep_of_state(rho_true_pauli.flatten(), ax=ax3, labels=pl_basis.labels, title='True State')
plot_pauli_bar_rep_of_state(rho_mle_maxent_pauli.flatten(), ax=ax4, labels=pl_basis.labels, title='Estimated via MLE MaxEnt')
fig1.tight_layout()
print('Now we see a clear difference:')
```

## Advanced topics

### Parallel state tomography

The `ObservablesExperiment` [framework](observable_estimation.ipynb) allows for easy parallelization of experiments that operate on disjoint sets of qubits. Below we will demonstrate the simple example of tomographing two separate states, respectively prepared by `Program(X(0))` and `Program(H(1))`. To run each experiment in serial would require $3 + 3 = 6$ experimental runs, but when we run a 'parallel' experiment we need only $3$ runs.

Note that the parallel experiment is not the same as doing tomography on the two qubit state `Program(X(0), H(1))` because in the later case we need to do more data acquisition runs on the qc, and we get more information back. The `ExperimentSetting`s for that experiment are a superset of the parallel settings. We also cannot directly compare a parallel experiment with two serial experiments, because in a parallel experiment 'cross-talk' and other multi-qubit effects can impact the overall state; that is, the physics of 'parallel' experiments cannot in general be neatly factored into two serial experiments.

See the linked [notebook](observable_estimation.ipynb) for more explanation and words of caution.

```python
from forest.benchmarking.observable_estimation import ObservablesExperiment, merge_disjoint_experiments

disjoint_sets_of_qubits = [(0,),(1,)]
programs = [Program(X(0)), Program(H(1))]

expts_to_parallelize = []
for q, program in zip(disjoint_sets_of_qubits, programs):
    expt = generate_state_tomography_experiment(program, q)
    expts_to_parallelize.append(expt)

# get a merged experiment with grouped settings for parallel data acquisition
parallel_expt = merge_disjoint_experiments(expts_to_parallelize)

print(f'Original number of runs: {sum(len(expt) for expt in expts_to_parallelize)}')
print(f'Parallelized number of runs: {len(parallel_expt)}')
print(parallel_expt)
```

    Original number of runs: 6
    Parallelized number of runs: 3
    X 0; H 1
    0: Z+_0→(1+0j)*X0, Z+_1→(1+0j)*X1
    1: Z+_0→(1+0j)*Y0, Z+_1→(1+0j)*Y1
    2: Z+_0→(1+0j)*Z0, Z+_1→(1+0j)*Z1

Collect the data. Separate the results by qubit to get back estimates of each process.

```python
from forest.benchmarking.observable_estimation import get_results_by_qubit_groups

parallel_results = estimate_observables(qc, parallel_expt)
state_estimates = []

individual_results = get_results_by_qubit_groups(parallel_results, disjoint_sets_of_qubits)
for q in disjoint_sets_of_qubits:
    estimate = iterative_mle_state_estimate(individual_results[q], q, epsilon=.001, beta=.61)
    state_estimates.append(estimate)

pl_basis = n_qubit_pauli_basis(n=1)

fig, axes = plt.subplots(1, len(state_estimates), figsize=(12,5))
for idx, est in enumerate(state_estimates):
    hinton(est, ax=axes[idx])

plt.tight_layout()
```

### Lightweight Bootstrap for functionals of the state

```python
import forest.benchmarking.distance_measures as dm
from forest.benchmarking.tomography import estimate_variance
```

```python
from functools import partial
fast_tomo_est = partial(iterative_mle_state_estimate, epsilon=.0005, beta=.5, tol=1e-3)
```

**Purity**

```python
mle_est = estimate_variance(results, qubits, fast_tomo_est, dm.purity,
                            n_resamples=40, project_to_physical=True)
lin_inv_est = estimate_variance(results, qubits, linear_inv_state_estimate, dm.purity,
                                n_resamples=40, project_to_physical=True)
print("(mean, standard error)")
print(mle_est)
print(lin_inv_est)
```

    (mean, standard error)
    (0.9922772132153728, 3.958627603079607e-06)
    (0.9435900456057654, 0.00017697565779827685)

**Fidelity**

```python
mle_est = estimate_variance(results, qubits, fast_tomo_est, dm.fidelity,
                            target_state=rho_true, n_resamples=40, project_to_physical=True)
lin_inv_est = estimate_variance(results, qubits, linear_inv_state_estimate, dm.fidelity,
                                target_state=rho_true, n_resamples=40, project_to_physical=True)

print("(mean, standard error)")
print(mle_est)
print(lin_inv_est)
```

    (mean, standard error)
    (0.9936759136729794, 1.6328600346275622e-06)
    (0.968924323915877, 7.241582380286223e-05)

### Estimator accuracy for mixed state tomography

We can leverage the `PyQVM` and its `ReferenceDensitySimulator` to perform tomography on known mixed states.

```python
from pyquil.pyqvm import PyQVM
from pyquil.reference_simulator import ReferenceDensitySimulator
mixed_qvm = get_qc('1q-pyqvm')
# replace the simulator with a ReferenceDensitySimulator for mixed states
mixed_qvm.qam.wf_simulator = ReferenceDensitySimulator(n_qubits=1, rs=mixed_qvm.qam.rs)
```

```python
# we are going to supply the state ourselves, so we don't need a prep program
# we only need to indicate it is a state on qubit 0
qubits = [0]
experiment = generate_state_tomography_experiment(program=Program(), qubits=qubits)
print(experiment)
```

    0: Z+_0→(1+0j)*X0
    1: Z+_0→(1+0j)*Y0
    2: Z+_0→(1+0j)*Z0

```python
# NBVAL_SKIP
# this cell is slow, so we skip it during testing

import forest.benchmarking.operator_tools.random_operators as rand_ops
from forest.benchmarking.distance_measures import infidelity
infidn =[]
trdn =[]
n_shots_max = [400,1000,4000,16000,64000]
number_of_states = 30
for nshots in n_shots_max:
    infid = []
    trd = []
    for _ in range(0, number_of_states):
        # set the inital state of the simulator
        rho_true = rand_ops.bures_measure_state_matrix(2)
        mixed_qvm.qam.wf_simulator.set_initial_state(rho_true).reset()
        # gather data
        resultsy = list(estimate_observables(qc=mixed_qvm, obs_expt=experiment, num_shots=nshots))
        # estimate
        rho_est = iterative_mle_state_estimate(results=resultsy, qubits=qubits, maxiter=100_000)
        infid.append(infidelity(rho_true, rho_est))
        trd.append(trace_distance(rho_true, rho_est))
    infidn.append({'mean': np.mean(np.real(infid)), 'std': np.std(np.real_if_close(infid))})
    trdn.append({'mean': np.mean(np.real(trd)), 'std': np.std(np.real_if_close(trd))})
```

```python
# NBVAL_SKIP

import pandas as pd
import matplotlib.pyplot as plt

df = pd.DataFrame(infidn)
dt = pd.DataFrame(trdn)
plt.scatter(n_shots_max,df['mean'],label='1-Fidelity')
plt.scatter(n_shots_max,dt['mean'],label='Trace Dist')
plt.plot(n_shots_max,1/np.sqrt(n_shots_max),label='1/sqrt(N)')
plt.yscale('log')
plt.xscale('log')
plt.ylim([0.0000051,0.1])
plt.ylabel('Error')
plt.xlabel('Number of Shots (N)')
plt.title('Avg. error in estimating one Qubit states drawn from Bures measure')
plt.legend()
plt.show()
```

```python

```


**1.   Criar uma função que receba duas matrizes 3 x 3 e multiplique um elemento pelo outro.**

$\begin{bmatrix}
  1 & 2 & 3 \\
  4 & 5 & 6 \\
  7 & 8 & 9 \\
\end{bmatrix} *
\begin{bmatrix}
  2 & 3 & 4 \\
  2 & 3 & 4 \\
  2 & 3 & 4 \\
\end{bmatrix} =
\begin{bmatrix}
  2 & 6 & 12 \\
  8 & 15 & 24 \\
  14 & 24 & 36 \\
\end{bmatrix}$

```python
# Resposta do exercício 1
import numpy as np

def mult():
    A = np.array([[1,2,3],[4,5,6],[7,8,9]])
    B = np.array([[2,3,4],[2,3,4],[2,3,4]])

    for i in range(3):
        for j in range(3):
            M = A[i][j] * B[i][j]
            print(M)
mult()
```

    2
    6
    12
    8
    15
    24
    14
    24
    36

**2.   Criar uma função que multiplique um array 1 x 4 pelo valor 5.**

$\begin{bmatrix}
  1 & 2 & 3 & 4 \\
\end{bmatrix} *
5 =
\begin{bmatrix}
  5 & 10 & 15 & 20 \\
\end{bmatrix}$

```python
# Resposta do exercício 2
def mult():
    array = [1,2,3,4]
    for i in range(4):
        A = array[i] * 5
        print(A)
mult()
```

    5
    10
    15
    20

**3.   Criar uma rotina que receba uma martiz 5 x 5 e troque a primeira linha pela segunda **

$\begin{bmatrix}
  1 & 2 & 3 & 4 & 5 \\
  6 & 7 & 8 & 9 & 10\\
  11 & 12 & 13 & 14 & 15\\
  16 & 17 & 18 & 19 & 20\\
  21 & 22 & 23 & 24 & 25\\
\end{bmatrix} < = >
\begin{bmatrix}
  6 & 7 & 8 & 9 & 10\\
  1 & 2 & 3 & 4 & 5 \\
  11 & 12 & 13 & 14 & 15\\
  16 & 17 & 18 & 19 & 20\\
  21 & 22 & 23 & 24 & 25\\
\end{bmatrix}
$

```python
# Resposta do exercício 3
import numpy as np

def rotina(A):
    A[0], A[1] = A[1], A[0]
    return np.array(A)

A = [[1,2,3,4,5],
     [6,7,8,9,10],
     [11,12,13,14,15],
     [16,17,18,19,20],
     [21,22,23,24,25]]

print(rotina(A))
```

    [[ 6  7  8  9 10]
     [ 1  2  3  4  5]
     [11 12 13 14 15]
     [16 17 18 19 20]
     [21 22 23 24 25]]

**4.   Dada a tabela abaixo de vendas, vamos prever os 3 meses faltantes através de regressão linear. (utilizar apenas numpy e matlibplot) **

Mês	Qtde. Cliente	Total de Vendas(K)

Jan	730	9.85

Fev	880	10.20

Mar	505	6.44

Abr	529	7.07

Mai	872	9.91

Jun	907	11.20

Jul	926	11.03

Ago	679	7.24

Set	452	9.43

O objetivo desse cálculo é prever os valores de uma variável dependente (Y) com base em resultados da variável independente (X).

Gostaríamos de prever os totais de vendas para os meses de Out, Nov e Dez, caso meus clientes sejam 500, 750, 1000 respectivamente.

A fórmula da regressão linear é:

$
\begin{equation*}
Y = a + bX
\end{equation*}
$

onde,

$X$ = Qtde. de Clientes que vamos prever

$Y$ = Total de Vendas previsto

$a$ e $b$ = São os dois coeficientes de regressão, necessários para fazermos a reta de regressão.

\\

**Fórmulas:**

\\
Para calcular **a**:

$
\begin{equation*}
a = \overline{y} -b \, \overline{x}
\end{equation*}
$

Para calcular **b**:

$
\begin{align}
b = \frac{ \sum xy  -n \: \overline{xy}}{\sum x^2 - n \: \overline{x} ^2}
\end{align}
$

onde:

$x$ = Qtde. de Clientes dos meses conhecidos

$y$ = Total de Vendas dos meses conhecidos

$n$ = Qtde. de meses

$
\begin{align}
\overline{x} \end{align} $ = média de $x$

$
\begin{align}
\overline{y} \end{align} $ = média de $y$

```python
# Resposta do exercício 4
import numpy as np
import matplotlib
import matplotlib.pyplot as plt

mes = ['jan', 'fev', 'mar', 'abr', 'mai', 'jun','jul', 'ago', 'set']
client = [730,880,505,529,872,907,926,679,452]
vend = [9.85,10.20,6.44,7.07,9.91,11.20,11.03,7.24,9.43]

x = np.array(client)

y = np.array(vend)

A = np.vstack([x, np.ones(len(x))]).T

m, c = np.linalg.lstsq(A, y)[0]

print(m,c)

print(A)

plt.plot(x,y, 'o', label='Amostra')
plt.plot(x, m*x + c, 'r', label='reta')
plt.legend()
plt.show()

print('Previsão Outubro(K)-> ', m*500+c)
print('Previsão Novembro(K) -> ', m*750+c)
print('Previsão Dezembro(K) -> ', m*1000+c)
```