```python
import cirq
from cirq.contrib.svg import SVGCircuit
import sympy
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
```

# The Portfolio Rebalancing Optimization using QAOA with soft constraints

In this notebook, we will apply the QAOA algorithm to solve the portfolio optimization problem using a soft constraint

$$
C_{RR}({\bf z}) = \sum_i (1-\lambda) r_i z_i - \lambda\sum_{ij} \sigma_{ij} z_i z_j
$$

$$
C_{TC}({\bf z}) = \sum_i T (1- \delta(z-y))
$$

The soft constraint that we impose will be given

$$
H =C_{RR}({\bf z})+C_{TC}({\bf z})+A\left(D-\sum_i z_i \right)^2
$$

# Generate the inital ground state wave function of the reference Hamiltonian

```python
# Choose the number of portfolios to optimize
N = 2

# Define the transaction cost
T = sympy.symbols("T")
D = sympy.symbols("D")
A = sympy.symbols("A")

# Define our previous holdings
y = np.zeros(N)

# Define qubits on the grid.
qubits = [cirq.LineQubit(i) for i in range(2*N)]

# Define the indices that we will use
index_grid_pair = [(2*i,2*i+1) for i in range(0,N) ]

print(index_grid_pair)
print()

# Next we take these quibits which are in states |0> and apply a Hadamard gate to each one.
# this generates the eigenvectors of the reference Hamiltonian

print(qubits)

hadamard_operators = [cirq.H(qubits[i]) for i in range(len(qubits))]
measurements = cirq.Moment([cirq.measure(qubits[i],key=str(i)) for i in range(len(qubits))])
#measurements = cirq.Moment( [cirq.measure(qubits[2],qubits[0],qubits[1],qubits[3],key='x') ])

# Create the circuit from this
circuit = cirq.Circuit((hadamard_operators,measurements))

#print(circuit)
SVGCircuit(circuit)

```

    [(0, 1), (2, 3)]

    [cirq.LineQubit(0), cirq.LineQubit(1), cirq.LineQubit(2), cirq.LineQubit(3)]

# Implementation of the mixing operator

We have generated the ground state wave function of the reference Hamiltonian. Now, we need to construc the Unitary operator $U({\beta})$ that represents the time evolution operator

$$
\begin{align}
U({\bf \beta}) &= e^{-i\beta B}, \\
B &= \sum_{i} X_i.
\end{align}
$$

In the `cirq` api, this operator is implemented as

$$
\text{cirq.rx} \equiv e^{-i X \beta / 2}.
$$

```python
# Generate a list of symbolic arguments
beta_list = ["b_"+str(i) for i in range(1)]

# Convert them to symbolic numpy objects
betas = sympy.symbols(beta_list)

mixer_operators = [cirq.rx(2*betas[0]).on(qubits[i]) for i in range(len(qubits))]

# Create the circuit from this
circuit = cirq.Circuit((hadamard_operators,mixer_operators,measurements))

#print(circuit)

SVGCircuit(circuit)
```

# Implementation of the exponential cost-function

For the portfolio optimization problem we have that the operator $Z_i$ is the difference between the Pauli-$z$ matrices
$$
\begin{align}
Z_i = \frac{1}{2}\left(z^{+}_{i}-z^{-}_{i} \right),
\end{align}
$$

where the $z^+_i$ and $z^-_i$ operate on the $i$-th qubit pair $|q_+,q_- \rangle$ and $q_{\pm}=\lbrace 0,1\rbrace$

$$
C_{RR}({\bf z}) = \sum_i (1-\lambda) r_i z_i - \lambda\sum_{ij} \sigma_{ij} z_i z_j
$$

$$
\begin{align}
e^{-i \gamma C_{RR}(z) } &=   \rm{Exp}\left(  i \gamma (1-\lambda) \sum_{k=1} \mu_k Z_k \right)  \rm{Exp}\left( - i \lambda \gamma  \sum_{ij} \sigma_{ij} Z_i Z_j \right) \\
&=   \prod_{k=1} \rm{Exp}\left( i \gamma (1-\lambda) \mu_k Z_k \right) \prod_{ij} \rm{Exp}\left( - i \lambda \gamma  \sigma_{ij} Z_i Z_j \right) \\
&=   \prod_{k=1} \rm{Exp}\left( i \gamma (1-\lambda) \frac{1}{2} \mu_k z^{+}_{k} \right)\left( i \gamma (1-\lambda) \frac{1}{2} \mu_k (-z^{-}_{k}) \right) \prod_{ij} \rm{Exp}\left( - i \lambda \frac{1}{4} \gamma  \sigma_{ij} \left(z^{+}_{i}z^{+}_{j}-z^{+}_{i}z^{-}_{j}-z^{-}_{i}z^{+}_{j}+z^{-}_{i}z^{-}_{j} \right) \right)
\end{align}
$$

```python
# Returns
mu = sympy.symbols(["mu_"+str(k) for k in range(N)])

# Convert them to symbolic numpy objects
lam,gamma = sympy.symbols("lam, gamma")

cost_returns_operators = [cirq.rz((-1)**int(i)*(1-lam)*gamma*mu[int(i/2)]).on(qubits[i]) for i in range(len(qubits))]

# Create the circuit from this
circuit = cirq.Circuit((hadamard_operators,mixer_operators,cost_returns_operators,measurements))

#print(circuit)

SVGCircuit(circuit)
```

 ## Implementation of $e^{ i \gamma z_i z_j}$

This operator only contributes if $i \neq j$. If $i=j$ then the operator is simply an overall constant phase that can be ignored. First, we will implement $e^{ i \gamma \pi z_i z_j}$ by rescaling the $\gamma$ parameter. Furthermore, we note that an overall phase factor does not not change the final result when taking expectation values with wavefunctions, so we will remove an $e^{i\pi\gamma}$ phase from the implementation of this operator to simplify its decomposition.

 $$
 \begin{align}
  \text{Exp}\left(i \pi \gamma z_i z_j \right) &=
 \begin{bmatrix}
   e^{\left(i \pi \gamma   \right)} & 0 & 0 & 0 \\
  0 & e^{-\left(i \pi \gamma   \right)} & 0 & 0 \\
  0 & 0 & e^{-\left(i \pi \gamma \right)} & 0 \\
  0 & 0 & 0 & e^{\left(i \pi \gamma  \right)}
  \end{bmatrix},\\
  &=
 e^{i \pi \gamma}
 \begin{bmatrix}
   1 & 0 & 0 & 0 \\
  0 & e^{-2\left(i \pi \gamma   \right)} & 0 & 0 \\
  0 & 0 & e^{-2\left(i \pi \gamma \right)} & 0 \\
  0 & 0 & 0 &1
  \end{bmatrix},
 \end{align}
 $$

 Now, we note that in `cirq`, the above matrix is implemented as

 $$
 \text{cirq.ZZPowGate}(t) =
   \begin{bmatrix}
   1 & 0 & 0 & 0 \\
  0 & e^{i\pi t} & 0 & 0 \\
  0 & 0 &  e^{i\pi t} & 0 \\
  0 & 0 & 0 &  1
  \end{bmatrix}.
 $$

 For the purposes of implementation also note that

 $$
 X_2  \equiv \left( I \otimes X \right)  = \begin{bmatrix}
  X & {\bf 0}\\
  {\bf 0} & X
  \end{bmatrix}
   = \begin{bmatrix}
  0 & 1 & 0& 0\\
  1 & 0 & 0& 0\\
  0 & 0 & 0& 1\\
  0 & 0 & 1& 0\\
  \end{bmatrix},
 $$

  $$
 X_1  \equiv \left( X \otimes I \right)  = \begin{bmatrix}
  {\bf 0} & I\\
  I & {\bf 0}
  \end{bmatrix}
   = \begin{bmatrix}
  0 & 0 & 1& 0\\
  0 & 0 & 0& 1\\
  1 & 0 & 0& 0\\
  0 & 1 & 0& 0\\
  \end{bmatrix},
 $$

   $$
 X_1  X_2 = X_2  X_1 = \begin{bmatrix}
  {\bf 0} & X\\
  X & {\bf 0}
  \end{bmatrix}
   = \begin{bmatrix}
  0 & 0 & 0& 1\\
  0 & 0 & 1& 0\\
  0 & 1 & 0& 0\\
  1 & 0 & 0& 0\\
  \end{bmatrix}.
 $$

Finally, we arrive at the decomposition of our operator of interest in terms of gate operations

$$
\text{Exp}\left(i \pi \gamma z_i z_j \right) = e^{i\gamma \pi}\text{cirq.ZZPowGate}(-2\gamma)
$$

```python
def exp_zz(gamma,qubit1,qubit2):
    """
    Exponentiates the Exp[]
    """

    #D1 = [ cirq.X.on(qubit2), cirq.CZPowGate(exponent= -2*gamma).on(qubit1,qubit2), cirq.X.on(qubit2)]
    #D2 = [ cirq.X.on(qubit1),cirq.X.on(qubit2), cirq.CZPowGate(exponent= -2*gamma).on(qubit1,qubit2),cirq.X.on(qubit1),cirq.X.on(qubit2)]

    #decomposition = D2.append(D1)

    return [cirq.ZZPowGate(exponent= -2*gamma).on(qubit1,qubit2)]

def remove_digonal_combinations(combination_list):
    """
    Simple utility function to remove the diagonal combinations from a list
    of possible connections
    """

    purged_list = []

    for i in range(len(index_grid_pp)):
        p1,p2 = combination_list[i]

        if(p1!=p2):
            purged_list.append((p1,p2))

    return purged_list

def map_combinations_to_portfolio_index(combination_list):

    portfolio_index= [ (int(combination_list[i][0]/2), int(combination_list[i][1]/2)) for i in range(len(combination_list))]

    return portfolio_index

# Covariance matrix
sigma = sympy.symbols([["sigma_"+str(i)+"_"+str(j) for i in range(N)] for j in range(N)])

print(qubits)
print()
# Define the s^+ , s^- permutations that we will use for summations
index_grid_p = [2*i for i in range(0,N)]
index_grid_m = [2*i+1 for i in range(0,N)]
index_grid_pm = [(2*i,2*j+1) for i in range(0,N) for j in range(0,N) ]
index_grid_pm_ii = [(2*i,2*i+1) for i in range(0,N)]
index_grid_mp = [(2*i+1,2*j) for i in range(0,N) for j in range(0,N) ]
index_grid_pp = [(2*i,2*j) for i in range(0,N) for j in range(0,N)  ]
index_grid_mm = [(2*i+1,2*j+1) for i in range(0,N) for j in range(0,N) ]

index_grid_pp_offdiagonal  =  remove_digonal_combinations(index_grid_pp)
index_grid_mm_offdiagonal  =  remove_digonal_combinations(index_grid_mm)
index_grid_pm_offdiagonal  =  remove_digonal_combinations(index_grid_pm)
index_grid_mp_offdiagonal  =  remove_digonal_combinations(index_grid_mp)

index_grid_pp_portfolio = map_combinations_to_portfolio_index(index_grid_pp_offdiagonal)
index_grid_mm_portfolio = map_combinations_to_portfolio_index(index_grid_mm_offdiagonal)
index_grid_pm_portfolio = map_combinations_to_portfolio_index(index_grid_pm_offdiagonal)
index_grid_mp_portfolio = map_combinations_to_portfolio_index(index_grid_mp_offdiagonal)

print("++ Combinations")
print(index_grid_pp_offdiagonal)
print()
print("-- Combinations")
print(index_grid_mm_offdiagonal)
print()
print("+- Combinations")
print(index_grid_pm_offdiagonal)
print()
print("-+ Combinations")
print(index_grid_mp_offdiagonal)
print()
print("+- Combinations| Same portfolio")
print(index_grid_pm_ii)

k=0
print(sigma[int(index_grid_pp[k][0]/2)][int(index_grid_pp[k][1]/2)])

pp1,pp2 = index_grid_pp_offdiagonal[0]
pp1_portfolio, pp2_portfolio =index_grid_pp_portfolio[0]
circuit = cirq.Circuit(exp_zz(gamma*lam/4*sigma[pp1_portfolio][pp2_portfolio],qubit1=qubits[pp1],qubit2=qubits[pp2]))

print("")
print("Example circut for application of Exp(zi zj) \n")
#print(circuit)
SVGCircuit(circuit)
#print("")

```

    [cirq.LineQubit(0), cirq.LineQubit(1), cirq.LineQubit(2), cirq.LineQubit(3)]

    ++ Combinations
    [(0, 2), (2, 0)]

    -- Combinations
    [(1, 3), (3, 1)]

    +- Combinations
    [(0, 1), (0, 3), (2, 1), (2, 3)]

    -+ Combinations
    [(1, 0), (1, 2), (3, 0), (3, 2)]

    +- Combinations| Same portfolio
    [(0, 1), (2, 3)]
    sigma_0_0

    Example circut for application of Exp(zi zj)

# Implementation of of full volatility term

$$
\text{Exp}\left( \sum_{ij} \lambda \sigma_{ij}Z_i Z_j \right) = \text{Exp}\left( \sum_{ij} \frac{\lambda}{4}\sigma_{ij}\left(z^{+}_{i} z^{+}_{j}-z^{+}_{i} z^{-}_{j}-z^{-}_{i} z^{+}_{j}+z^{-}_{i} z^{-}_{j} \right) \right)
$$

```python
# Now let us build the entire circuit up to now

cost_volatility_operators_pp = []
cost_volatility_operators_mm = []
cost_volatility_operators_pm = []
cost_volatility_operators_mp = []

# Loop over all ++ combinations
for k in range(len(index_grid_pp_offdiagonal)):
    i_qubit,j_qubit = index_grid_pp_offdiagonal[k]
    i_portfolio, j_portfolio =index_grid_pp_portfolio[k]
    cost_volatility_operators_pp.append(exp_zz(gamma*lam/4*sigma[i_portfolio][j_portfolio],qubit1=qubits[i_qubit],qubit2=qubits[j_qubit]))

# Loop over all +- combinations
for k in range(len(index_grid_pm_offdiagonal)):
    i_qubit,j_qubit = index_grid_pm_offdiagonal[k]
    i_portfolio, j_portfolio =index_grid_pm_portfolio[k]
    cost_volatility_operators_pm.append(exp_zz((-1)*gamma*lam/4*sigma[i_portfolio][j_portfolio],qubit1=qubits[i_qubit],qubit2=qubits[j_qubit]))

    # Loop over all -+ combinations
for k in range(len(index_grid_mp_offdiagonal)):
    i_qubit,j_qubit = index_grid_mp_offdiagonal[k]
    i_portfolio, j_portfolio =index_grid_mp_portfolio[k]
    cost_volatility_operators_mp.append(exp_zz((-1)*gamma*lam/4*sigma[i_portfolio][j_portfolio],qubit1=qubits[i_qubit],qubit2=qubits[j_qubit]))

    # Loop over all -- combinations
for k in range(len(index_grid_mm_offdiagonal)):
    i_qubit,j_qubit = index_grid_mm_offdiagonal[k]
    i_portfolio, j_portfolio =index_grid_mm_portfolio[k]
    cost_volatility_operators_mm.append(exp_zz(gamma*lam/4*sigma[i_portfolio][j_portfolio],qubit1=qubits[i_qubit],qubit2=qubits[j_qubit]))

#cost_returns_operators = [cirq.rz((-1)**int(i)*(1-lam)*gamma*mu[int(i/2)]).on(qubits[i]) for i in range(len(qubits))]

# Create the circuit from this
circuit = cirq.Circuit((cost_volatility_operators_pp,cost_volatility_operators_pm,cost_volatility_operators_mp,cost_volatility_operators_mm))
SVGCircuit(circuit)
```

# Implement the exponential operator of the transaction cost

$$
C_{TC}(z) = \frac{1}{4}T\left( 3+(1-y^2_i-y_i)z^{+}_{i}+(1-y^2_i+y_i)z^{-}_{i}+(2y^2_i-1)z^{+}_{i}z^{-}_{i} \right)
$$

$$
\begin{align}
\text{Exp}\left( i\pi \gamma C_{TC}(z) \right) = \prod_{j=1} \text{Exp}\left(  i\pi \gamma \frac{1}{4}T\left(1-y^2_j-y_j\right)z^{+}_{j}\right)\text{Exp}\left(  i\pi \gamma \frac{1}{4}T\left(1-y^2_j+y_j\right)z^{-}_{j} \right)\text{Exp}\left(  i\pi \gamma \frac{1}{4}T\left(2y^2_j-1\right)z^{+}_{j}z^{-}_{j} \right) \\
= \prod_{j=1} \text{Exp}\left(  i\pi \gamma \frac{1}{4}T\left(1-y^2_j-y_j\right)z^{+}_{j}\right) \prod_{j=1} \text{Exp}\left(  i\pi \gamma \frac{1}{4}T\left(1-y^2_j+y_j\right)z^{-}_{j} \right) \prod_{j=1}\text{Exp}\left(  i\pi \gamma \frac{1}{4}T\left(2y^2_j-1\right)z^{+}_{j}z^{-}_{j} \right)
\end{align}
$$

```python
transaction_operators_p_i = []
transaction_operators_m_i = []
transaction_operators_pm_ii = []

print(index_grid_p)

for k in range(len(index_grid_p)):
    angle_i = (1/4)*T*(1-y[k]**2-y[k])*gamma
    i_qubit = index_grid_p[k]
    transaction_operators_p_i.append(cirq.rz(angle_i).on(qubits[i_qubit]) )

for k in range(len(index_grid_pm_ii)):
    angle_k = (1/4)*T*(2*y[k]**2-1)*gamma
    i_qubit, j_qubit = index_grid_pm_ii[k]
    i_portfolio, j_portfolio =index_grid_pm_ii[k]
    transaction_operators_pm_ii.append(exp_zz( angle_k,qubit1=qubits[i_qubit],qubit2=qubits[j_qubit]))

for k in range(len(index_grid_m)):
    angle_i = (1/4)*T*(1-y[k]**2+y[k])*gamma
    i_qubit = index_grid_m[k]
    transaction_operators_m_i.append(cirq.rz(angle_i).on(qubits[i_qubit]) )

circuit = cirq.Circuit((transaction_operators_p_i,transaction_operators_pm_ii,transaction_operators_m_i))
SVGCircuit(circuit)
```

    [0, 2]

# Now we implement the soft constraint Term

$$
P_{\rm INV}(z) = A\left(\sum^N_i z^2_i -D \right)^2
$$

which can be factored into

$$
\begin{align}
P_{\rm INV}(z) = \frac{A}{4} \left( z^+_i z^+_j-z^+_i z^-_j - z^+_i z^+_j+ z^-_i z^-_j \right) -AD(z^+_j-z^-_j)+AD^2
\end{align}
$$

```python

# We implement the AD terms of the penalty function just like we did for the returns
penalty_AD_operators = [cirq.rz( (-1)**i * gamma*A*D).on(qubits[i]) for i in range(len(qubits))]

# Create the circuit from this
circuit = cirq.Circuit((penalty_AD_operators))

#print(circuit)

SVGCircuit(circuit)
```

```python
# Now let us build the entire circuit up to now

penalty_A_operators_pp = []
penalty_A_operators_mm = []
penalty_A_operators_pm = []
penalty_A_operators_mp = []

# Loop over all ++ combinations
for k in range(len(index_grid_pp_offdiagonal)):
    i_qubit,j_qubit = index_grid_pp_offdiagonal[k]
    i_portfolio, j_portfolio =index_grid_pp_portfolio[k]
    penalty_A_operators_pp.append(exp_zz(gamma*A/4,qubit1=qubits[i_qubit],qubit2=qubits[j_qubit]))

# Loop over all +- combinations
for k in range(len(index_grid_pm_offdiagonal)):
    i_qubit,j_qubit = index_grid_pm_offdiagonal[k]
    i_portfolio, j_portfolio =index_grid_pm_portfolio[k]
    penalty_A_operators_pm.append(exp_zz((-1)*A/4*gamma,qubit1=qubits[i_qubit],qubit2=qubits[j_qubit]))

    # Loop over all -+ combinations
for k in range(len(index_grid_mp_offdiagonal)):
    i_qubit,j_qubit = index_grid_mp_offdiagonal[k]
    i_portfolio, j_portfolio =index_grid_mp_portfolio[k]
    penalty_A_operators_mp.append(exp_zz((-1)*A/4*gamma,qubit1=qubits[i_qubit],qubit2=qubits[j_qubit]))

    # Loop over all -- combinations
for k in range(len(index_grid_mm_offdiagonal)):
    i_qubit,j_qubit = index_grid_mm_offdiagonal[k]
    i_portfolio, j_portfolio =index_grid_mm_portfolio[k]
    penalty_A_operators_mm.append(exp_zz(A/4*gamma,qubit1=qubits[i_qubit],qubit2=qubits[j_qubit]))

# Create the circuit from this
circuit = cirq.Circuit((penalty_A_operators_pp ,penalty_A_operators_pm,penalty_A_operators_mp,penalty_A_operators_mm))
#circuit = cirq.Circuit((penalty_A_operators_pm))
SVGCircuit(circuit)
```

# Construct the final circuit and run the simulator for a single instance of the parameters

```python
penalty = (penalty_AD_operators,penalty_A_operators_pp ,penalty_A_operators_pm,penalty_A_operators_mp,penalty_A_operators_mm)
portfolio_lagrangian = (cost_returns_operators,cost_volatility_operators_pp,cost_volatility_operators_pm,cost_volatility_operators_mp,cost_volatility_operators_mm)
transaction_costs = (transaction_operators_p_i,transaction_operators_pm_ii,transaction_operators_m_i)

circuit = cirq.Circuit(hadamard_operators,mixer_operators,portfolio_lagrangian,transaction_costs,penalty,measurements)
SVGCircuit(circuit)
```

```python
def convert_to_y(x):
    '''
    Converts the bitstring x to the vector y={-1,0,1}
    for the portfolio
    '''

    # Rescales the bit string to [-1 or 1]
    s = 2.0*np.array(x)-1.0

    y = np.zeros(N)

    for i in range(N):
        y[i] = (s[2*i]-s[2*i+1])/2

    return y

def portfolio_penalty_cost(y,A,D):
    penalty = 0.0

    penalty = A*(np.sum(y)-D)**2

    return penalty

def portfolio_cost_function(y):
    '''
    The cost function for a portfolio

    y-- a bitstring array giving the results of the portfolio {-1,0,1}
    '''
    cost = 0

    for i in range(N):
        Zi = x[2*i]+x[2*i+1]

    return cost
```

```python
def convert_measurements_to_array(results,N,n_repetitions):
    '''

    results --
    '''

    # Instantiate the array
    X = np.zeros((2*N,n_repetitions))

    # Retrieve all of the measurements for each Qbit and store the result in the tensor
    for i in range(len(result.measurements.keys())):
        key = list(result.measurements.keys())[i]
        X[i,:] = np.array(result.measurements[key]).flatten()

    return X

def convert_measurements_to_Z(X,N,n_repetitions):
    '''
    Converts collection of simulations in the computational basis to
    the Z-values

    Z_i = (1/2)(z^+_i - z^-_i)

    where z^{+/-} = {-1,1}
    '''

    Z = np.zeros((N,n_repetitions))

    for i in range(0,n_repetitions):
        x_bitstring = X[:,i]

        # Converts the bitstring to portfolio
        Z[:,i] = convert_to_y(x_bitstring)

    return Z

def count_instances(N,results,n_repetitions):
    '''

    '''

    X = convert_measurements_to_array(results,N,n_repetitions)
    Z = convert_measurements_to_Z(X,N,n_repetitions)

    # The dictionary to store the results
    portfolio_results = {}

    # The values that can be taken by Zi
    portfolio_index = [-1,0,1]

    portfolio_keys = itertools.product(portfolio_index,repeat=N)

    labels = []
    label_counts = []
    label_penalty = []

    for state in list(portfolio_keys):

        # convert the tuple to numpy integer vector
        Z_state =  np.array([state[i] for i in range(len(state))]).astype(int)

        # convert the Z_state to a string
        state_key = ''.join(Z_state.astype('str'))

        state_count = 0

        for i in range(n_repetitions):

            # retrieve a sample
            Zi = Z[:,i].flatten()

            label_penalty.append(portfolio_penalty_cost(Zi,A_val,D_val))

            # check if Zi sample matches the Z-label
            if(np.array_equal(Z_state,Zi)==True):
                state_count+=1

        # Save all of the values
        labels.append(state_key)
        label_counts.append(state_count)

        portfolio_results[state_key] = state_count

    portfolio_results['penalty_cost_samples'] = label_penalty
    portfolio_results['labels']= labels
    portfolio_results['counts']= label_counts

    return portfolio_results

```

```python
print(mu,sigma,lam,gamma,betas)

resolved_params = {}

A_val = 1.0
D_val = 0.0

for k in range(len(mu)):

    mu_k = mu[k]

    if(k==0):
        resolved_params[mu_k] = 100.0
    else:
        resolved_params[mu_k] = -10.0

for i in range(len(mu)):
    for j in range(len(mu)):

        if(i==j):
            resolved_params[sigma[i][j]] = 0.0
        else:
            resolved_params[sigma[i][j]] = 0.0

for beta in betas:
    resolved_params[beta] =  2.3

resolved_params[gamma] = 0.0
resolved_params[lam] = 0

resolved_params[A] = A_val
resolved_params[D] = D_val

print(resolved_params)

# Resolve all of the parameters
resolver = cirq.ParamResolver(resolved_params)
```

    [mu_0, mu_1] [[sigma_0_0, sigma_1_0], [sigma_0_1, sigma_1_1]] lam gamma [b_0]
    {mu_0: 100.0, mu_1: -10.0, sigma_0_0: 0.0, sigma_1_0: 0.0, sigma_0_1: 0.0, sigma_1_1: 0.0, b_0: 2.3, gamma: 0.0, lam: 0, A: 1.0, D: 0.0}

```python
import itertools

n_repetitions = 5000

#circuit = cirq.Circuit(hadamard_operators,mixer_operators,portfolio_lagrangian,penalty)
#circuit = cirq.Circuit(hadamard_operators,mixer_operators,penalty)
#penalty_AD_operators
#circuit = cirq.Circuit(hadamard_operators,mixer_operators,penalty_AD_operators)
circuit = cirq.Circuit(hadamard_operators,mixer_operators,penalty_AD_operators)
circuit.append(measurements)

simulator = cirq.Simulator()
result = simulator.run(circuit, resolver,repetitions=n_repetitions)

portfolio_results = count_instances(N,result,n_repetitions)

plt.figure(figsize=(10,10))
plt.title('D-value: '+str(D_val))
plt.bar(np.arange(len(portfolio_results['labels'])), portfolio_results['counts'], tick_label=portfolio_results['labels'])
plt.xticks(rotation=90)
plt.show()
#cirq.plot_state_histogram(result)
# # Generate a grid for the parameters
# sweep_b0 = cirq.Linspace(key=betas[0], start=0.0, stop=1.0, length=10)
# angles_b0 = [angle for angle in sweep_b0]
# #sweep_gamma = cirq.Linspace(key=gamma, start=0.0, stop=1.0, length=10)
# #sweeps = (sweep_b0*sweep_gamma)

# result_sweep = simulator.run_sweep(circuit,params=sweep_b0, repetitions=10)

# #
# X = np.zeros((len(qubits),10))

# for i in range(len(result.measurements.keys())):
#     key = list(result.measurements.keys())[i]
#     X[i,:] = np.array(result.measurements[key]).flatten()

# print(X)

plt.figure(figsize=(10,10))
plt.hist(portfolio_results['penalty_cost_samples'])
plt.show()

plt.clf()
cirq.plot_state_histogram(result)

SVGCircuit(circuit)
```

```python
print(result_sweep[1].measurements['0'].flatten())
print(result_sweep[1].measurements['1'].flatten())
print(result_sweep[1].measurements['2'].flatten())
print(result_sweep[1].measurements['3'].flatten())
```

```python
plt.hist(label_penalty)
```

```python

```


# The IS-LM model

Imports and set magics:

```python
%matplotlib inline
import numpy as np
from scipy import optimize
import sympy as sp
from sympy import symbols
import matplotlib.pyplot as plt
sp.init_printing()

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

# local modules
import modelproject
```

# Model description

For this project we will analyze the famous IS-LM model, identifying a mutual equilibrium for the goods and the loans markets. The equilibrium is expressed in terms of a combination of the income and the interest rate. We will compute an IS schedule and a LM schedule where each point of the curves represents an equilibrium in the respective market.

In order to derive the Investment-Saving schedule, we use the following 3 equations describing the output market:
$$Y = C + I + \overline G$$
where Y is the aggregate output demand, C is private consumption, I is private fixed capital investments, and G is public spending on goods and services. G is considered an exogenous variable, determined by the policy makers and thus represented as $\overline G$.

Private consumption is given by the keynesian consumption function:
$$C = \alpha + \beta(Y - T)$$
where $\alpha$ ≥ 0 and $\beta \in (0,1)$ represents the level of consumption independent of income and the marginal propensity to consume, respectively. T represents taxes.

Lastly, the output market is also described by the linear relationship between investment, I, and interest rate, i:
$$I = \overline I - d * i$$
where $\overline I$ is the level of I when i = 0, and d is the rate at which an increase in i leads to a fall in I.

Next, we derive the Liquidity Preference-Money Supply (LM) schedule.

The money supply is exogenous, decided upon by the central bank and thus independent of the interest rate. It is given by:
$$\frac {M^S} P = \frac {M^S_0} P$$

---------What is $M^S_0$?-----------

The money demand, however, depends on the amount of money needed for precautionary savings, transactions and investments, given by the following equation:

$$\frac {M^D} P = c_1 + c_2*Y - c_3*i$$

where $c_1$, $c_2$, and $c_3$ represents precautionary reasons, transactional reasons, and speculative reasons, respectively.

In both cases, P is the current price level, which is treated as an exogenous variable. In equilibrium, $M^S = M^D$.

## Analytical solution

In this part we will solve the IS-LM relation analytically. The idea is to isolate for the interrest rate for both the IS and LM curve. In equilibrium, the interest rate should be equal in both the output and asset markets. Thus, by setting them equal to each other, we can find the repective level of outcome Y that matches the interest rate.

### The IS curve

We will start by considering the IS relation in a closed economy:

$$Y = C + I + \overline G$$

If we replace the equations given above for C and I, we get the following expression:

$$Y = \alpha + \beta(Y - T) + \overline I - d * i + \overline G$$

In order to solve the model analytically, we first have to define all the variables as sympy symbols. Once that is done, we can express the above equation using sympy.

```python
# Defining known parameters of IS relation
alpha = sp.symbols('alpha', real=True)
beta = sp.symbols('beta', real=True)
G = sp.symbols('\overline{G}', real=True)
T = sp.symbols('T', real=True)
d = sp.symbols('d', real=True)
i = sp.symbols('i', real=True)
I = sp.symbols('I', real=True)
I_bar = sp.symbols('\overline{I}', real=True)
Y = sp.symbols('Y', real=True)

# Defining output and the IS equation
output = alpha + beta*(Y-T) + I_bar - (d*i) + G
IS = sp.Eq(Y, output)
display(IS)
```

Since we know that in equilibrium, the interest rate will be the same in both markets, we want to isolate for i. We can use the sympy solve function to do that:

```python
# Solving for interest rate in the IS curve
sol_IS = sp.solve(IS, i)[0]

display(sp.Eq(i, sol_IS))
```

### The LM curve

Now we will do the same for the LM curve that is solve for the interest rate. Recall that in equilbrium $M^S = M^D$. Thus, we want to isolate for i in the following equation:

$$ \frac {M^S_0} P = c_1 + c_2*Y - c_3*i$$

Again, we start by defining the variables as sympy symbols and displaying the above equation using sympy.

```python
#Defning known parameters of LM relation
Money = sp.symbols('M^S_0', real=True)
P = sp.symbols('P', real=True)
c_1 = sp.symbols('c_1', real=True)
c_2 = sp.symbols('c_2', real=True)
c_3 = sp.symbols('c_3', real=True)
i = sp.symbols('i', real=True)
Y = sp.symbols('Y', real=True)

#Showing money demand and the LM equation
M_demand = c_1+c_2*Y-c_3*i
LM = sp.Eq(Money/P, M_demand)
display(LM)
```

Now, solving for i we get the following:

```python
# Solving for interest rate in the LM curve
sol_LM = sp.solve(LM, i)[0]

display(sp.Eq(i, sol_LM))
```

### Finding the equilibrium

Now we have solved for the interest rate in both the IS and LM schedules. Thus, we can set them equal to each other and solve for the equilibrium output, $Y^*$.

```python
# Defining equilibrium parameters
Y_star = sp.symbols('Y^*', real=True)
i_star = sp.symbols('i^*', real=True)

# Setting IS=LM
ISLM = sp.Eq(sol_IS, sol_LM)

display(ISLM)
```

To solve for Y, we then use the sympy solve function.

```python
# Solving for Y
ISLM_Y = sp.solve(ISLM, Y)[0]

display(sp.Eq(Y_star, ISLM_Y))
```

The equilbrium $Y^*$ can then be plugged in to either of the equations for the equilibrium interest rate that we found above. That way we will obtain the general equilibrium $i^*$ which completes the analytical solution of the IS-LM model.

```python
# Replacing Y with the variable Y*
sol_LM_star1 = sol_LM.subs(Y, Y_star)

display(sp.Eq(i_star, sol_LM_star1))
```

Plugging in the above equation we found for $Y^*$ into the equation for $i^*$, we get the equilibrium expression for the interest rate:

```python
# Replacing the variable Y* with the equation for Y*
sol_LM_star2 = sol_LM_star1.subs(Y_star, ISLM_Y)

display(sp.Eq(i_star, sol_LM_star2))
```

The pair ($Y^*, i^*$) represents the analytical general equilibrium of the IS-LM model.

## Numerical solution

Now that we have solved the IS-LM model analytically, we will proceed to solving it numerically. We start by looking at the IS schedule.

### The IS curve

The IS curve is defined by the output, consumption and investment. Thus, in order to solve the model numerically, we start by assigning specific numeric values to the other paramters in the IS schedule.

```python
# Assigning numerical values to parameters
Y_size = 100
Y = np.arange(Y_size)
alpha = 100
beta = 0.2
T = 1
d = 2
G_bar = 20
I_bar = 10
M_S0 = 23500
c_1 = 2500
c_2 = 0.75
c_3 = 5
P = 10
```

In the .py file, we have defined the function for C and for I, as stated in the model description. We vectorize these two variables by storing them inside the vector X1. Since the IS curve has Y on the x-axis and i on the y-axis, we need to define both of those variables. This is also done in the .py file, and this is where the vectorization of C and I comes in handy: by vectorizing, we are able to run both variables inside the function for Y_IS and i_IS.

```python
def IS(T, beta, Y, G_bar, I_bar, alpha, d):
    return (-T*beta + Y*beta - Y + G_bar + I_bar + alpha) / d
```

Once this is done, we can plot the IS schedule in a graph to illustrate what it looks like.

```python
iIS = IS(T, beta, Y, G_bar, I_bar, alpha, d)

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)

ax.plot(Y, iIS)
ax.set_xlabel('Output (Y)')
ax.set_ylabel('Interest rate (i)')
ax.set_title('IS schedule')

ax.axes.xaxis.set_ticklabels([])
ax.axes.yaxis.set_ticklabels([])
plt.show()

```

```python
def LM(M_S0, P, Y, c_1, c_2, c_3):
    return (- M_S0 + P*(Y*c_2 + c_1))/(P*c_3)

```

```python
iLM = LM(M_S0, P, Y, c_1, c_2, c_3)

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)

ax.plot(Y, iLM)
ax.set_xlabel('Output (Y)')
ax.set_ylabel('Interest rate (i)')
ax.set_title('LM schedule')

ax.axes.xaxis.set_ticklabels([])
ax.axes.yaxis.set_ticklabels([])
plt.show()
```

```python
def obj(Y):
    return IS(T, beta, Y, G_bar, I_bar, alpha, d) - LM(M_S0, P, Y, c_1, c_2, c_3)

Y_guess = 10
res = optimize.root(obj, Y_guess, method = 'broyden1')

res.x

#lecture 11 does numerical optimization, avsnitt 7.1
```

    array(63.45454545)

```python
iLM = LM(M_S0, P, c_1, c_2, c_3,  Y=res.x)
```

```python
import ipywidgets as widgets

def plot_islm( )

# instructions on how to make interactive plot lecture 7, avsnitt 3.2.1
#want to be able to change tax rate T
```

# Further analysis

Make detailed vizualizations of how your model changes with parameter values.

Try to make an extension of the model.

The extension of this model will feature net export, which means we will be modelling an open economy from now on.

# Conclusion

Add concise conclusion.


Machine Learning with Python

Laboratory on Numpy / Matplotlib / Scikit-learn

***

***

## Introduction

In the past few years, Python has become the de-facto standard programming language for data analytics. Python's success is due to several factors, but one major reason has been the availability of powerful, open-source libraries for scientific computation such as Numpy, Scipy and Matplotlib. Python is also the most popular programming language for machine learning, thanks to libraries such as Scikit-learn and TensorFlow.

In this lecture we will explore the basics of Numpy, Matplotlib and Scikit-learn. The first is a library for data manipulation through the powerfull `numpy.ndarray` data structure; the second is useful for graphical visualization and plotting; the third is a general purpose library for machine learning, containing dozens of algorithms for classification, regression and clustering.

In this lecture we assume familiarity with the Python programming language. If you are not familiar with the language, we advise you to look it up before carrying over to the next sections. Here are some useful links to learn about Python:
- https://docs.python.org/3/tutorial/introduction.html
- https://www.learnpython.org/
- http://www.scipy-lectures.org/

If you have never seen a page like this, it is a **Jupyther Notebook**. Here one can easily embed Python code and run it on the fly. You can run the code in a cell by selecting the cell and clicking the *Run* button (top). You can do the same using the **SHIFT+Enter** shortcut. You can modify the existing cells, run them and finally save your changes.

## Requirements

1. Python (preferably version > 3.3): https://www.python.org/downloads/
2. Numpy, Scipy and Matplotlib: https://www.scipy.org/install.html
3. Scikit-learn: http://scikit-learn.org/stable/install.html

## References

- https://docs.scipy.org/doc/numpy/
- https://docs.scipy.org/doc/scipy/reference/
- https://matplotlib.org/users/index.html
- http://scikit-learn.org/stable/documentation.html

# Numpy

Numpy provides high-performance data structures for data manipulation and numeric computation. In particular, we will look at the `numpy.ndarray`, a data structure for manipulating vectors, matrices and tensors. Let's start by importing `numpy`:

```python
# the np alias is very common
import numpy as np
```

We can initialize a Numpy array from a Python list using the `numpy.array` function:

```python
# if the argument is a list of numbers, the array will be a 1-dimensional vector
a = np.array([1, 2, 3, 4, 5, 6])

a
```

    array([1, 2, 3, 4, 5, 6])

```python
# if the argument is a list of lists, the array will be a 2-dimensional matrix
M = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]])

M
```

    array([[ 1,  2,  3,  4],
           [ 5,  6,  7,  8],
           [ 9, 10, 11, 12],
           [13, 14, 15, 16]])

Given a Numpy array, we can check its `shape`, a tuple containing the number of elements for each dimension:

```python
a.shape
```

    (6,)

```python
M.shape
```

    (4, 4)

The size of an array is its total number of elements:

```python
a.size
```

    6

```python
M.size
```

    16

We can do quite some nice things with Numpy arrays that are not possible with standard Python lists.

### Indexing
Numpy array allow us to index arrays in quite advanced ways.

```python
# A 1d vector can be indexed in all the common ways
a[0]
```

    1

```python
a[1:3]
```

    array([2, 3])

```python
a[0:5:2]
```

    array([1, 3, 5])

```python
# Use a boolean mask
mask = [True, False, False, True, True, False]
a[mask]
```

    array([1, 4, 5])

```python
# Access specific elements by passing a list of index
a[[1, 4, 5]]
```

    array([2, 5, 6])

The power of Numpy indexing capabilities starts showing up with 2d arrays:

```python
# Access a single element of the matrix
M[0, 1]
```

    2

```python
# Access an entire row
M[1]
```

    array([5, 6, 7, 8])

```python
# Access an entire column
M[:,2]
```

    array([ 3,  7, 11, 15])

```python
# Extract a sub-matrix
M[1:3, 0:2]
```

    array([[ 5,  6],
           [ 9, 10]])

### Data manipulation
We can manipulate data in several ways.

```python
# Flatten a matrix
M.flatten()
```

    array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16])

```python
# Reshaping a matrix
M.reshape(2, 8)
```

    array([[ 1,  2,  3,  4,  5,  6,  7,  8],
           [ 9, 10, 11, 12, 13, 14, 15, 16]])

```python
# The last index can be automatically inferred using -1
M.reshape(2, -1)
```

    array([[ 1,  2,  3,  4,  5,  6,  7,  8],
           [ 9, 10, 11, 12, 13, 14, 15, 16]])

```python
# Computing the max and the min
M.max(), M.min()
```

    (16, 1)

```python
# Computing the mean and standard deviation
M.mean(), M.std()
```

    (8.5, 4.6097722286464435)

```python
# Computing the sum along the rows
M.sum(axis=1)
```

    array([10, 26, 42, 58])

### Linear algebra
Numpy is very useful to all sort of numeric computation, especially linear algebra:

```python
# Transpose
M.T
```

    array([[ 1,  5,  9, 13],
           [ 2,  6, 10, 14],
           [ 3,  7, 11, 15],
           [ 4,  8, 12, 16]])

```python
# Adding and multiplying a constant
10 * M + 5
```

    array([[ 15,  25,  35,  45],
           [ 55,  65,  75,  85],
           [ 95, 105, 115, 125],
           [135, 145, 155, 165]])

```python
# Element wise product
b = np.array([-1, -2, 4, 6, 8, -4])

a * b
```

    array([ -1,  -4,  12,  24,  40, -24])

```python
# Dot product
a.dot(b)
```

    47

```python
# More linear algebra in the package numpy.linalg
# Determinant
np.linalg.det(M)
```

    4.7331654313261276e-30

```python
# Eigenvalues
np.linalg.eigvals(M)
```

    array([  3.62093727e+01,  -2.20937271e+00,  -3.18863232e-15,
            -1.34840081e-16])

### Vector generation and sampling
Numpy allows us to generate or randomly sample vectors:

```python
# Generate an array with 0.5 spacing
x = np.arange(-10, 10, 0.5)

x
```

    array([-10. ,  -9.5,  -9. ,  -8.5,  -8. ,  -7.5,  -7. ,  -6.5,  -6. ,
            -5.5,  -5. ,  -4.5,  -4. ,  -3.5,  -3. ,  -2.5,  -2. ,  -1.5,
            -1. ,  -0.5,   0. ,   0.5,   1. ,   1.5,   2. ,   2.5,   3. ,
             3.5,   4. ,   4.5,   5. ,   5.5,   6. ,   6.5,   7. ,   7.5,
             8. ,   8.5,   9. ,   9.5])

```python
# Generate an array with 20 equally spaced points
x = np.linspace(-10, 10, 20)

x
```

    array([-10.        ,  -8.94736842,  -7.89473684,  -6.84210526,
            -5.78947368,  -4.73684211,  -3.68421053,  -2.63157895,
            -1.57894737,  -0.52631579,   0.52631579,   1.57894737,
             2.63157895,   3.68421053,   4.73684211,   5.78947368,
             6.84210526,   7.89473684,   8.94736842,  10.        ])

```python
# Sample a vector from a standardize normal distribution
np.random.normal(size=(10,))
```

    array([-1.45737898,  0.23555453,  0.24578509, -2.07977299,  1.08726802,
           -0.41107403,  0.12253856,  1.47129648,  0.5223578 , -0.29633517])

### Functions
Numpy provides all sorts of mathematical functions we can apply to arrays

```python
# Exponential function
np.exp(x)
```

    array([  4.53999298e-05,   1.30079023e-04,   3.72699966e-04,
             1.06785292e-03,   3.05959206e-03,   8.76628553e-03,
             2.51169961e-02,   7.19647439e-02,   2.06192028e-01,
             5.90777514e-01,   1.69268460e+00,   4.84984802e+00,
             1.38956932e+01,   3.98136782e+01,   1.14073401e+02,
             3.26840958e+02,   9.36458553e+02,   2.68312340e+03,
             7.68763460e+03,   2.20264658e+04])

```python
# Sine
np.sin(x)
```

    array([ 0.54402111, -0.4594799 , -0.99916962, -0.53027082,  0.47389753,
            0.99970104,  0.5163796 , -0.48818921, -0.99996678, -0.50235115,
            0.50235115,  0.99996678,  0.48818921, -0.5163796 , -0.99970104,
           -0.47389753,  0.53027082,  0.99916962,  0.4594799 , -0.54402111])

```python
# A gaussian function
y = np.exp(-(x ** 2)/2)

y
```

    array([  1.92874985e-22,   4.13228632e-18,   2.92342653e-14,
             6.82937941e-11,   5.26814324e-08,   1.34190319e-05,
             1.12868324e-03,   3.13480292e-02,   2.87498569e-01,
             8.70659634e-01,   8.70659634e-01,   2.87498569e-01,
             3.13480292e-02,   1.12868324e-03,   1.34190319e-05,
             5.26814324e-08,   6.82937941e-11,   2.92342653e-14,
             4.13228632e-18,   1.92874985e-22])

# Matplotlib
The above matrices provide little insight without the possibility of visualizing them properly. Matplotlib is a powerful library for data visualization. Let's plot the above function.

```python
# the following line is only needed to show plots in the notebook
%matplotlib inline
import matplotlib.pyplot as plt

x = np.linspace(-10, 10, 200)    # get a sample of the x axis
y = np.exp(-(x**2)/(2*1))        # compute the function for all points in the sample
plt.plot(x, y)                   # add the curve to the plot
plt.show()                       # show the plot
```

We can also plot more than one line in the same figure and add a grid to the plot.

```python
z = np.exp(-(x**2)/(2*10))
plt.grid()                 # add the grid under the curves
plt.plot(x, y)             # add the first curve to the plot
plt.plot(x, z)             # add the second curve to the plot
plt.show()                 # show the plot
```

We can also set several properties of the plot in this way:

```python
plt.grid()
plt.xlabel('x')                     # add a label to the x axis
plt.ylabel('y')                     # add a label to the y axis
plt.xticks(np.arange(-10, 11, 2))   # specify in which point to place a tick on the x axis
plt.yticks(np.arange(0, 2.2, 0.2))  # and on the y axis

# rs- stands for red, squared markers, solid line
# yd-- stands for yellow, diamond markers, dashed line
plt.plot(x, y, 'rs-', markevery=10, label='sigma=1')    # add a style and a label and specify the gap
plt.plot(x, z, 'yd--', markevery=10, label='sigma=10')  # between markers for both curves

plt.legend()  # add the legend (displays the labels of the curves)
plt.show()    # show the plot
```

Finally, we can save the plot into a png file in this way:

```python
plt.grid()
plt.xlabel('x')
plt.ylabel('y')
plt.xticks(np.arange(-10, 11, 2))
plt.yticks(np.arange(0, 2.2, 0.2))
plt.plot(x, y, 'rs-', markevery=10, label='sigma=1')
plt.plot(x, z, 'yd--', markevery=10, label='sigma=10')
plt.legend()
plt.savefig('plot.png', dpi=300)  # saves the plot into the file plot.png with 300 dpi
                                  # will not work on lion0b because directory is read-only
```

# Scikit-learn
Let's now dive into the real **Machine Learning** part. *Scikit-learn* is perhaps the most wide-spread library for Machine Learning in use nowadays, and most of its fame is due to its extreme simplicity. With Scikit-learn it is possible to easily manage datasets, and train a wide range of classifiers out-of-the-box. It is also useful for several other Machine Learning tasks such as regression, clustering, dimensionality reduction, and model selection.

In the following we will see how to use Scikit-learn to load a dataset, train a classifier and perform validation and model selection.

Scikit-learn comes with a range of popular reference datasets. Let's load and use the *Digits* dataset:

```python
from sklearn.datasets import load_digits

digits = load_digits()

print(digits.DESCR)     # print a description of the digits dataset
```

    Optical Recognition of Handwritten Digits Data Set
    ===================================================

    Notes
    -----
    Data Set Characteristics:
        :Number of Instances: 5620
        :Number of Attributes: 64
        :Attribute Information: 8x8 image of integer pixels in the range 0..16.
        :Missing Attribute Values: None
        :Creator: E. Alpaydin (alpaydin '@' boun.edu.tr)
        :Date: July; 1998

    This is a copy of the test set of the UCI ML hand-written digits datasets
    http://archive.ics.uci.edu/ml/datasets/Optical+Recognition+of+Handwritten+Digits

    The data set contains images of hand-written digits: 10 classes where
    each class refers to a digit.

    Preprocessing programs made available by NIST were used to extract
    normalized bitmaps of handwritten digits from a preprinted form. From a
    total of 43 people, 30 contributed to the training set and different 13
    to the test set. 32x32 bitmaps are divided into nonoverlapping blocks of
    4x4 and the number of on pixels are counted in each block. This generates
    an input matrix of 8x8 where each element is an integer in the range
    0..16. This reduces dimensionality and gives invariance to small
    distortions.

    For info on NIST preprocessing routines, see M. D. Garris, J. L. Blue, G.
    T. Candela, D. L. Dimmick, J. Geist, P. J. Grother, S. A. Janet, and C.
    L. Wilson, NIST Form-Based Handprint Recognition System, NISTIR 5469,
    1994.

    References
    ----------
      - C. Kaynak (1995) Methods of Combining Multiple Classifiers and Their
        Applications to Handwritten Digit Recognition, MSc Thesis, Institute of
        Graduate Studies in Science and Engineering, Bogazici University.
      - E. Alpaydin, C. Kaynak (1998) Cascading Classifiers, Kybernetika.
      - Ken Tang and Ponnuthurai N. Suganthan and Xi Yao and A. Kai Qin.
        Linear dimensionalityreduction using relevance weighted LDA. School of
        Electrical and Electronic Engineering Nanyang Technological University.
        2005.
      - Claudio Gentile. A New Approximate Maximal Margin Classification
        Algorithm. NIPS. 2000.

Let's take a look at the data:

```python
X, y = digits.data, digits.target

# The attributes of the first instance (notice it is a Numpy array)
X[0]
```

    array([  0.,   0.,   5.,  13.,   9.,   1.,   0.,   0.,   0.,   0.,  13.,
            15.,  10.,  15.,   5.,   0.,   0.,   3.,  15.,   2.,   0.,  11.,
             8.,   0.,   0.,   4.,  12.,   0.,   0.,   8.,   8.,   0.,   0.,
             5.,   8.,   0.,   0.,   9.,   8.,   0.,   0.,   4.,  11.,   0.,
             1.,  12.,   7.,   0.,   0.,   2.,  14.,   5.,  10.,  12.,   0.,
             0.,   0.,   0.,   6.,  13.,  10.,   0.,   0.,   0.])

```python
# The label of the first instance
y[0]
```

    0

Being a Numpy array, we can actually take a look at this image. We first need to reshape it into an 8x8 matrix and then use matplotlib.

```python
x = X[0].reshape((8, 8))

plt.gray()      # use a grayscale
plt.matshow(x)  # display a matrix of values
plt.show()      # show the figure
```

Now we want to train a classifier to recognize the digits from the images and then we want to evaluate it. In order to make a proper evaluation, we first need to split the dataset in two sets, one for training and one for testing. Scikit-learn helps us with that:

```python
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Let's check the length of the two sets
len(X_train), len(X_test)
```

    (1437, 360)

Now we need a classifier. Let's use an **SVM**. A reminder:

\begin{align}
   \min_{\boldsymbol{w}} \quad & \frac{1}{2}\|\boldsymbol{w}\|^2 + C \sum_{i\in|\mathcal{D}|} \xi_i \\
   \forall (x_i, y_i) \in \mathcal{D} \quad & y_i ( \boldsymbol{w}^T x_i + b ) \ge 1 - \xi_i
\end{align}

```python
from sklearn.svm import SVC

# Specify the parameters in the constructor.
# C is the parameter of the primal problem of the SVM;
# The rbf kernel is the Radial Basis Function;
# The rbf kernel takes one parameter: gamma
clf = SVC(C=10, kernel='rbf', gamma=0.02)
```

Now the classifier can be trained and then used to predict unseen instances.

```python
# Training
clf.fit(X_train, y_train)

# Prediction
y_pred = clf.predict(X_test)

y_pred
```

    array([3, 3, 3, 7, 3, 1, 3, 3, 3, 3, 3, 3, 3, 0, 3, 3, 3, 3, 8, 3, 3, 3, 3,
           3, 3, 3, 3, 3, 6, 3, 3, 9, 1, 3, 3, 6, 3, 4, 3, 6, 6, 3, 1, 3, 3, 3,
           3, 3, 6, 3, 3, 3, 3, 3, 3, 0, 3, 3, 0, 1, 3, 3, 3, 3, 3, 5, 3, 3, 3,
           3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 0, 3, 4, 3, 3, 3, 3,
           8, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 7, 7, 3, 3, 3,
           3, 3, 3, 3, 7, 2, 6, 3, 3, 3, 3, 3, 7, 3, 3, 3, 3, 3, 3, 3, 6, 3, 4,
           3, 3, 3, 3, 3, 3, 3, 3, 6, 3, 3, 3, 3, 3, 6, 3, 3, 3, 3, 3, 3, 3, 3,
           2, 3, 3, 4, 3, 3, 3, 3, 3, 3, 3, 4, 3, 1, 3, 7, 3, 2, 2, 3, 3, 8, 3,
           3, 2, 3, 3, 6, 9, 3, 3, 1, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4,
           3, 1, 3, 3, 3, 3, 6, 1, 3, 6, 0, 4, 3, 2, 7, 3, 6, 3, 3, 3, 3, 3, 2,
           3, 6, 3, 1, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
           3, 2, 3, 3, 3, 3, 3, 3, 0, 3, 3, 3, 0, 1, 3, 4, 3, 1, 3, 3, 6, 0, 3,
           3, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 9, 3, 3, 3, 3, 3, 3, 0, 3, 3, 3, 3,
           0, 3, 3, 6, 3, 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1,
           3, 3, 7, 0, 6, 3, 3, 3, 3, 1, 3, 4, 3, 3, 7, 3, 3, 3, 3, 3, 3, 0, 7,
           3, 3, 3, 3, 2, 7, 3, 1, 3, 7, 3, 3, 3, 3, 3])

Now we want to evaluate the performance of our classifier. A reminder:

\begin{align}
    \text{Accuracy } &= \frac{\text{true-positive} + \text{true-negative}}{\text{all examples}} \\
    \text{Precision } &= \frac{\text{true-positive}}{\text{true-positive} + \text{false-positive}} \\
    \text{Recall } &= \frac{\text{true-positive}}{\text{true-positive} + \text{false-negative}} \\
    F_1 &= \frac{2 \times \text{precision} \times \text{recall}}{\text{precision} + \text{recall}} \\
\end{align}

In a multiclass classification Precision, Recall and $F_1$ are computed per class, considering the given class as positive and all others as negative.

We can use Scikit-learn to compute and show these measures for all classes.

```python
from sklearn import metrics

report = metrics.classification_report(y_test, y_pred)

# the support is the number of instances having the given label in y_test
print(report)
```

                 precision    recall  f1-score   support

              0       1.00      0.39      0.57        33
              1       1.00      0.61      0.76        28
              2       1.00      0.36      0.53        33
              3       0.12      1.00      0.22        34
              4       1.00      0.20      0.33        46
              5       1.00      0.02      0.04        47
              6       1.00      0.49      0.65        35
              7       1.00      0.35      0.52        34
              8       1.00      0.10      0.18        30
              9       1.00      0.07      0.14        40

    avg / total       0.92      0.34      0.37       360

Finally we can compute the accuracy of our classifier:

```python
metrics.accuracy_score(y_test, y_pred)
```

    0.33611111111111114

Apparently our classifier performs a bit poorly out-of-sample. This is probably due to the random choice of the parameters for the classifier. We can do much better! We need to perform model selection, that is we need to search for better parameters for our classifier.

In particular, we are going to perform a **cross-validation** on the training set and see how the classifier performs with different values of *gamma*.

A $k$-fold cross-validation works like this:
- Split the dataset $D$ in $k$ equally sized disjoint subsets $D_i$
- For $i \in [1, k]$
 - Train the classifier on $T_i = D \setminus D_i$
 - Compute the score (accuracy, precision, ...) on $D_i$
- Return the list of scores, one for each fold

Scikit-learn helps us with this as well. We compute the cross-validated accuracy for all the possible values of *gamma* and select the *gamma* with the best average accuracy.

```python
from sklearn.model_selection import KFold, cross_val_score

# 3-fold cross-validation
# random_state ensures same split for each value of gamma
kf = KFold(n_splits=3, shuffle=True, random_state=42)

gamma_values = [0.1, 0.05, 0.02, 0.01]
accuracy_scores = []

# Do model selection over all the possible values of gamma
for gamma in gamma_values:

    # Train a classifier with current gamma
    clf = SVC(C=10, kernel='rbf', gamma=gamma)

    # Compute cross-validated accuracy scores
    scores = cross_val_score(clf, X_train, y_train, cv=kf.split(X_train), scoring='accuracy')

    # Compute the mean accuracy and keep track of it
    accuracy_score = scores.mean()
    accuracy_scores.append(accuracy_score)

# Get the gamma with highest mean accuracy
best_index = np.array(accuracy_scores).argmax()
best_gamma = gamma_values[best_index]

# Train over the full training set with the best gamma
clf = SVC(C=10, kernel='rbf', gamma=best_gamma)
clf.fit(X_train, y_train)

# Evaluate on the test set
y_pred = clf.predict(X_test)
accuracy = metrics.accuracy_score(y_test, y_pred)

accuracy
```

    0.81388888888888888

Much better! Model selection allows us to fine-tune the parameters of a lerning algorithm to get the best performance.

Let's now look at the **Learnig curve** of our classifier, in which we plot the training accuracy and the cross-validated accuracy for increasing number of examples.

```python
from sklearn.model_selection import learning_curve

plt.figure()
plt.title("Learning curve")
plt.xlabel("Training examples")
plt.ylabel("Score")
plt.grid()

clf = SVC(C=10, kernel='rbf', gamma=best_gamma)

# Compute the scores of the learning curve
# by default the (relative) dataset sizes are: 10%, 32.5%, 55%, 77.5%, 100%
train_sizes, train_scores, test_scores = learning_curve(clf, X_train, y_train, scoring='accuracy')

# Get the mean and std of train and test scores along the varying dataset sizes
train_scores_mean = np.mean(train_scores, axis=1)
train_scores_std = np.std(train_scores, axis=1)
test_scores_mean = np.mean(test_scores, axis=1)
test_scores_std = np.std(test_scores, axis=1)

# Plot the mean and std for the training scores
plt.plot(train_sizes, train_scores_mean, 'o-', color="r", label="Training score")

plt.fill_between(train_sizes, train_scores_mean - train_scores_std,
                 train_scores_mean + train_scores_std, alpha=0.1, color="r")

# Plot the mean and std for the cross-validation scores
plt.plot(train_sizes, test_scores_mean, 'o-', color="g", label="Cross-validation score")

plt.fill_between(train_sizes, test_scores_mean - test_scores_std,
                 test_scores_mean + test_scores_std, alpha=0.1, color="g")

plt.legend()
plt.show()
```

Now we want to go even further. We can perform the above model selection procedure considering the *C* parameter as well. In general, this process over several parameters is called **grid search**, and Scikit-learn has an automated procedure to perform cross-validated grid search for any classifier.

```python
from sklearn.model_selection import GridSearchCV

possible_parameters = {
    'C': [1e0, 1e1, 1e2, 1e3],
    'gamma': [1e-1, 1e-2, 1e-3, 1e-4]
}

svc = SVC(kernel='rbf')

# The GridSearchCV is itself a classifier
# we fit the GridSearchCV with the training data
# and then we use it to predict on the test set
clf = GridSearchCV(svc, possible_parameters, n_jobs=4) # n_jobs=4 means we parallelize the search over 4 threads
clf.fit(X_train, y_train)

y_pred = clf.predict(X_test)
accuracy = metrics.accuracy_score(y_test, y_pred)

accuracy
```

    0.98888888888888893

Nice! Now we have a classifier with a quite competitive accuracy. The state-of-the-art (on a very similar task) has accuracy around $0.9979$, achieved by using Neural Networks, which we will see in the next Lab. Stay tuned!


[Pyro](https://pyro.ai/) is a probabilistic programming system built on top of PyTorch. It supports posterior inference based on MCMC and stochastic variational inference; discrete latent variables can be marginalized out exactly using dynamic programmming.

```python
!pip install pyro-ppl
```

    Collecting pyro-ppl
      Downloading https://files.pythonhosted.org/packages/aa/7a/fbab572fd385154a0c07b0fa138683aa52e14603bb83d37b198e5f9269b1/pyro_ppl-1.6.0-py3-none-any.whl (634kB)
         |████████████████████████████████| 634kB 5.4MB/s
    Requirement already satisfied: torch>=1.8.0 in /usr/local/lib/python3.7/dist-packages (from pyro-ppl) (1.8.1+cu101)
    Collecting pyro-api>=0.1.1
      Downloading https://files.pythonhosted.org/packages/fc/81/957ae78e6398460a7230b0eb9b8f1cb954c5e913e868e48d89324c68cec7/pyro_api-0.1.2-py3-none-any.whl
    Requirement already satisfied: numpy>=1.7 in /usr/local/lib/python3.7/dist-packages (from pyro-ppl) (1.19.5)
    Requirement already satisfied: tqdm>=4.36 in /usr/local/lib/python3.7/dist-packages (from pyro-ppl) (4.41.1)
    Requirement already satisfied: opt-einsum>=2.3.2 in /usr/local/lib/python3.7/dist-packages (from pyro-ppl) (3.3.0)
    Requirement already satisfied: typing-extensions in /usr/local/lib/python3.7/dist-packages (from torch>=1.8.0->pyro-ppl) (3.7.4.3)
    Installing collected packages: pyro-api, pyro-ppl
    Successfully installed pyro-api-0.1.2 pyro-ppl-1.6.0

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

import pyro
import pyro.infer
import pyro.optim
import pyro.distributions as dist
from torch.distributions import constraints
from pyro.infer import MCMC, NUTS, Predictive, HMC
from pyro.infer import SVI, Trace_ELBO
from pyro.infer import EmpiricalMarginal

from pyro.distributions import Beta, Binomial, HalfCauchy, Normal, Pareto, Uniform
from pyro.distributions.util import scalar_like
from pyro.infer.mcmc.util import initialize_model, summary
from pyro.util import ignore_experimental_warning

pyro.set_rng_seed(101)
```

# Example: inferring mean of 1d Gaussian .

We use the simple example from the [Pyro intro](https://pyro.ai/examples/intro_part_ii.html#A-Simple-Example). The goal is to infer the weight $\theta$ of an object, given noisy measurements $y$. We assume the following model:
$$
\begin{align}
\theta &\sim N(\mu=8.5, \tau^2=1.0)\\
y \sim &N(\theta, \sigma^2=0.75^2)
\end{align}
$$

Where $\mu=8.5$ is the initial guess.

```python
def model(hparams, data=None):
    prior_mean, prior_sd, obs_sd = hparams
    theta = pyro.sample("theta", dist.Normal(prior_mean, prior_sd))
    y = pyro.sample("y", dist.Normal(theta, obs_sd), obs=data)
    return y
```

## Exact inference

By Bayes rule for Gaussians, we know that the exact posterior,
given a single observation $y=9.5$, is given by

$$
\begin{align}
\theta|y &\sim N(m, s^s) \\
m &=\frac{\sigma^2 \mu + \tau^2 y}{\sigma^2 + \tau^2}
  = \frac{0.75^2 \times 8.5 + 1 \times 9.5}{0.75^2 + 1^2}
  = 9.14 \\
s^2 &= \frac{\sigma^2 \tau^2}{\sigma^2  + \tau^2}
= \frac{0.75^2 \times 1^2}{0.75^2 + 1^2}= 0.6^2
\end{align}
$$

```python
mu = 8.5
tau = 1.0
sigma = 0.75
hparams = (mu, tau, sigma)

y = 9.5
m = (sigma**2 * mu + tau**2 * y) / (sigma**2 + tau**2)  # posterior mean
s2 = (sigma**2 * tau**2) / (sigma**2 + tau**2)  # posterior variance
s = np.sqrt(s2)
print(m)
print(s)
```

    9.14
    0.6

## Ancestral sampling

```python
def model2(hparams, data=None):
    prior_mean, prior_sd, obs_sd = hparams
    theta = pyro.sample("theta", dist.Normal(prior_mean, prior_sd))
    y = pyro.sample("y", dist.Normal(theta, obs_sd), obs=data)
    return theta, y

for i in range(5):
    theta, y = model2(hparams)
    print([theta, y])
```

    [tensor(9.1529), tensor(8.7116)]
    [tensor(8.7306), tensor(9.3978)]
    [tensor(9.0740), tensor(9.4240)]
    [tensor(7.3040), tensor(7.8569)]
    [tensor(7.8939), tensor(8.0257)]

## MCMC

See [the documentation](http://docs.pyro.ai/en/stable/mcmc.html)

```python
nuts_kernel = NUTS(model)
obs = torch.tensor(y)
mcmc = MCMC(nuts_kernel, num_samples=1000, warmup_steps=50)
mcmc.run(hparams, obs)
print(type(mcmc))
```

    Sample: 100%|██████████| 1050/1050 [00:03, 326.67it/s, step size=1.30e+00, acc. prob=0.880]

    <class 'pyro.infer.mcmc.api.MCMC'>

```python
samples = mcmc.get_samples()
print(type(samples))
print(samples.keys())
print(samples["theta"].shape)
```

    <class 'dict'>
    dict_keys(['theta'])
    torch.Size([1000])

```python
mcmc.diagnostics()
```

    {'acceptance rate': {'chain 0': 0.924},
     'divergences': {'chain 0': []},
     'theta': OrderedDict([('n_eff', tensor(500.2368)),
                  ('r_hat', tensor(1.0050))])}

```python
thetas = samples["theta"].numpy()
print(np.mean(thetas))
print(np.std(thetas))
```

    9.152181
    0.625822

##  Variational Inference

See [the documentation](http://docs.pyro.ai/en/stable/inference_algos.html)

For the guide (approximate posterior), we use a [pytorch.distributions.normal](https://pytorch.org/docs/master/distributions.html#torch.distributions.normal.Normal).

```python
# the guide must have the same signature as the model
def guide(hparams, data):
    y = data
    prior_mean, prior_sd, obs_sd = hparams
    m = pyro.param("m", torch.tensor(y))  # location
    s = pyro.param("s", torch.tensor(prior_sd), constraint=constraints.positive)  # scale
    return pyro.sample("theta", dist.Normal(m, s))

# initialize variational parameters
pyro.clear_param_store()

# set up the optimizer
# optimizer = pyro.optim.Adam({"lr": 0.001, "betas": (0.90, 0.999)})
optimizer = pyro.optim.SGD({"lr": 0.001, "momentum": 0.1})

# setup the inference algorithm
svi = SVI(model, guide, optimizer, loss=Trace_ELBO())

n_steps = 2000
# do gradient steps
obs = torch.tensor(y)
loss_history, m_history, s_history = [], [], []
for t in range(num_steps):
    loss_history.append(svi.step(hparams, obs))
    m_history.append(pyro.param("m").item())
    s_history.append(pyro.param("s").item())

plt.plot(loss_history)
plt.title("ELBO")
plt.xlabel("step")
plt.ylabel("loss")

post_mean = pyro.param("m").item()
post_std = pyro.param("s").item()
print([post_mean, post_std])
```

# Example: beta-bernoulli model

Example is from [SVI tutorial](https://pyro.ai/examples/svi_part_i.html)

The model is
$$
\begin{align}
\theta &\sim \text{Beta}(\alpha, \beta) \\
x_i &\sim \text{Ber}(\theta)
\end{align}
$$
where $\alpha=\beta=10$.

```python
alpha0 = 10.0
beta0 = 10.0

def model(data):
    alpha0_tt = torch.tensor(alpha0)
    beta0_tt = torch.tensor(beta0)
    f = pyro.sample("theta", dist.Beta(alpha0_tt, beta0_tt))
    # loop over the observed data
    for i in range(len(data)):
        pyro.sample("obs_{}".format(i), dist.Bernoulli(f), obs=data[i])

def model_binom(data):
    alpha0_tt = torch.tensor(alpha0)
    beta0_tt = torch.tensor(beta0)
    theta = pyro.sample("theta", dist.Beta(alpha0_tt, beta0_tt))
    data_np = [x.item() for x in data]
    N = len(data_np)
    N1 = np.sum(data_np)
    N0 = N - N1
    pyro.sample("obs", dist.Binomial(N, theta))
```

```python
# create some data with 6 observed heads and 4 observed tails
data = []
for _ in range(6):
    data.append(torch.tensor(1.0))
for _ in range(4):
    data.append(torch.tensor(0.0))

data_np = [x.item() for x in data]
print(data)
print(data_np)

N = len(data_np)
N1 = np.sum(data_np)
N0 = N - N1
print([N1, N0])
```

    [tensor(1.), tensor(1.), tensor(1.), tensor(1.), tensor(1.), tensor(1.), tensor(0.), tensor(0.), tensor(0.), tensor(0.)]
    [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0]
    [6.0, 4.0]

```python

```

    [tensor(1.), tensor(1.), tensor(1.), tensor(1.), tensor(1.), tensor(1.), tensor(0.), tensor(0.), tensor(0.), tensor(0.)]
    [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0]

## Exact inference

The posterior is given by
$$
\begin{align}
\theta &\sim \text{Ber}(\alpha + N_1, \beta  + N_0) \\
N_1 &= \sum_{i=1}^N [x_i=1] \\
N_0 &= \sum_{i=1}^N [x_i=0]
\end{align}
$$

```python
alpha1 = alpha0 + N1
beta1 = beta0 + N0
print("exact posterior: alpha={:0.3f}, beta={:0.3f}".format(alpha1, beta1))
post_mean = alpha1 / (alpha1 + beta1)
post_var = (post_mean * beta1) / ((alpha1 + beta1) * (alpha1 + beta1 + 1))
post_std = np.sqrt(post_var)
print([post_mean, post_std])
```

    exact posterior: alpha=16.000, beta=14.000
    [0.5333333333333333, 0.08960286733763294]

## MCMC

```python
nuts_kernel = NUTS(model)
mcmc = MCMC(nuts_kernel, num_samples=1000, warmup_steps=50)
mcmc.run(data)
print(mcmc.diagnostics())
samples = mcmc.get_samples()
print(samples["theta"].shape)
```

    Sample: 100%|██████████| 1050/1050 [00:09, 111.12it/s, step size=1.50e+00, acc. prob=0.803]

    {'theta': OrderedDict([('n_eff', tensor(443.8569)), ('r_hat', tensor(0.9992))]), 'divergences': {'chain 0': []}, 'acceptance rate': {'chain 0': 0.864}}
    torch.Size([1000])

```python
thetas = samples["theta"].numpy()
print(np.mean(thetas))
print(np.std(thetas))
```

    0.5330437
    0.09079484

```python
nuts_kernel = NUTS(model_binom)
mcmc = MCMC(nuts_kernel, num_samples=1000, warmup_steps=50)
mcmc.run(data)
print(mcmc.diagnostics())
samples = mcmc.get_samples()
print(samples["theta"].shape)
```

    Sample: 100%|██████████| 1050/1050 [00:08, 117.55it/s, step size=9.57e-01, acc. prob=0.919]

    {'theta': OrderedDict([('n_eff', tensor(269.4737)), ('r_hat', tensor(0.9990))]), 'divergences': {'chain 0': []}, 'acceptance rate': {'chain 0': 0.951}}
    torch.Size([1000])

```python
thetas = samples["theta"].numpy()
print(np.mean(thetas))
print(np.std(thetas))
```

    0.48617417
    0.112258926

## Variational inference

```python
def guide(data):
    alpha_q = pyro.param("alpha_q", torch.tensor(15.0), constraint=constraints.positive)
    beta_q = pyro.param("beta_q", torch.tensor(15.0), constraint=constraints.positive)
    pyro.sample("theta", dist.Beta(alpha_q, beta_q))
```

```python
# optimizer = pyro.optim.Adam({"lr": 0.0005, "betas": (0.90, 0.999)})
optimizer = pyro.optim.SGD({"lr": 0.001, "momentum": 0.1})

svi = SVI(model, guide, optimizer, loss=Trace_ELBO())

n_steps = 2000
loss_history = []
for step in range(n_steps):
    loss_history.append(svi.step(data))

plt.plot(loss_history)
plt.title("ELBO")
plt.xlabel("step")
plt.ylabel("loss");
```

```python
# grab the learned variational parameters
alpha_q = pyro.param("alpha_q").item()
beta_q = pyro.param("beta_q").item()
print("variational posterior: alpha={:0.3f}, beta={:0.3f}".format(alpha_q, beta_q))

post_mean = alpha_q / (alpha_q + beta_q)
post_var = (post_mean * beta_q) / ((alpha_q + beta_q) * (alpha_q + beta_q + 1))
post_std = np.sqrt(post_var)
print([post_mean, post_std])
```

    variational posterior: alpha=15.414, beta=14.094
    [0.5223745147578196, 0.09043264875842827]

```python

```


# Chapter 2: Iterated Function System (IFS)

Chapter 2 of the book [PL] Fraktale i chaos (eng.: "Fractals and Chaos") written by Jacek Kudrewicz is a basic description of a specific class of fractals. The example of the IFS is a spiral presented as the last example in the Chapter 1 notebook.

I want to honor authors of books, papers and code examples which are used by me during my learning. That's why every new notebook will start from the list of literature.

## Bibliography:

#### Base:
* Fraktale i chaos (eng.: "Fractals and Chaos") by Jacek Kudrewicz.

#### Additional:

* Affine Transformation - Wikipedia: https://en.wikipedia.org/wiki/Affine_transformation
* Contraction Mapping description and example - http://www.math.uconn.edu/~kconrad/blurbs/analysis/contraction.pdf
* Contraction Mapping Practical Considerations - https://en.wikipedia.org/wiki/Newton%27s_method#Practical_considerations
* Hausdorff distance for object mapping in image processing - http://cgm.cs.mcgill.ca/~godfried/teaching/cg-projects/98/normand/main.html

## 2.1 Christmas tree and other images

* Affine transformations:

- translation,
- scaling,
- homothety,
- similarity transformation,
- reflection,
- rotation,
- shear mapping,
- ... and combinations of them.

Mathematical description is given on the Wikipedia: https://en.wikipedia.org/wiki/Affine_transformation#Mathematical_definition
but the general formula is a simple linear function in the form:

$$ f(\bar{x}) = A\bar{x} + \bar{b} $$

where:
* $ A $ is a linear mapping matrix,
* $ \bar{b} $ is a translation vector.

#### but before I go with the topic...
I want to try program some transformations in Numpy to observe how this works in practice (by the way, I know many of these from my mechanics classes and from image processing but it is always good to repeat some things to understand it better).

```python
# Import Python libraries
import random
import numpy as np
import matplotlib.pyplot as plt
% matplotlib inline
```

```python
# Prepare data for transformations

def draw_circle(canvas, center_coordinates, radius):
    x = -radius
    y = 0
    err = 2 - 2 * radius

    while x < 0:
        first_quadrant_coord = [center_coordinates[0] - x,
                                center_coordinates[1] + y]
        second_quadrant_coord = [center_coordinates[0] - y,
                                center_coordinates[1] - x]
        third_quadrant_coord = [center_coordinates[0] + x,
                               center_coordinates[1] - y]
        fourth_quadrant_coord = [center_coordinates[0] + y,
                                center_coordinates[1] + x]
        coordinates = [first_quadrant_coord,
                      second_quadrant_coord,
                      third_quadrant_coord,
                      fourth_quadrant_coord]
        for coord in coordinates:
            if (coord[0] > 0) and (coord[1] > 0):
                try:
                    canvas[coord[1], coord[0]] = 255
                except IndexError:
                    pass
            else:
                pass
        radius = err
        if radius <= y:
            y = y + 1
            err = err + y * 2 + 1
        if (radius > x) or (err > y):
            x = x + 1
            err = err + x * 2 + 1
    return canvas

mtx = np.zeros(shape=(100, 80))

circ = draw_circle(mtx, [13, 80], 33)
circ = draw_circle(circ, [54, 55], 8)
```

```python
plt.imshow(circ)
```

```python
# Identity transformation = A * x

identity_matrix_A = np.eye(100)
output = np.matmul(identity_matrix_A, circ)

plt.imshow(output)
```

```python
# Reflection (vertically)

output = np.flip(circ, 0)

plt.imshow(output)
```

```python
# Reflection (horizontally)

output = np.flip(circ, 1)

plt.imshow(output)
```

```python
# Reflection (vertically and horizontally)

output_a = np.flip(circ, 0)
output_z = np.flip(output_a, 1)

plt.imshow(output_z)
```

```python
# Upscale (x-axis)

def calculate_value(step, unit_size, base_array, indexes):
    distances = np.array([-1, 0])
    value = step * unit_size
    output = 0
    if np.any(indexes == value):
        output = output + base_array[int(value)]
    else:
        idxs = indexes.astype(np.float)
        array = np.append(indexes, value)
        array = np.sort(array)
        for i in distances:
            try:
                # find the place of value in array
                value_index = np.where(array==value)

                # calculate weight
                weight = np.absolute((indexes[value_index + i][0][0] - value))

                # calculate value
                if weight != 0:
                    output = output + base_array[indexes[value_index + i]][0][0] * weight
                else:
                    output = output + base_array[indexes[value_index + i]][0][0]
                output = output
            except IndexError:
                output = output + 0
    return output

def interpolate_values(x_array, scale_factor):
    base_array_length = np.shape(x_array)[0]
    base_array_indexes = np.arange(0, base_array_length)
    upscaled_array_length = int(base_array_length * scale_factor + base_array_length)
    upscaled_array_values = np.zeros(shape=(1, upscaled_array_length))
    step_size = base_array_length / upscaled_array_length
    for idx, i in enumerate(upscaled_array_values[0]):
        upscaled_array_values[:, idx] = calculate_value(idx, step_size, x_array, base_array_indexes)
    return upscaled_array_values[0]

upscaled_array = []
for i in range(0, np.shape(circ)[0]):
    z = interpolate_values(circ[i, :], 0.75)
    z[z > 0] = 255
    upscaled_array.append(z)

plt.imshow(upscaled_array)
np.shape(upscaled_array)
```

### Base equations

$$\begin{eqnarray}
x' = a_{11}x + a_{12}y + c_{1} \\
y' = a_{21}x + a_{22}y + c_{2} \\
\end{eqnarray}$$

Example values:

No. | $ a_{11} $ | $ a_{12} $ | $ a_{21} $ | $ a_{22} $ | $ c_{1} $ | $ c_{2} $ |
--- | ---------- | ---------- | ---------- | ---------- | --------- | --------- |
1 | -0.67 | -0.02 | -0.18 | 0.81 | 0.00 | 1.02 |
2 | 0.40 | 0.40 | -0.10 | 0.40 | -0.04 | 0.06 |
3 | -0.40 | -0.40 | -0.10 | 0.40 | 0.04 | 0.06 |
4 | -0.10 | 0.00 | 0.44 | 0.44 | 0.00 | -0.14 |

```python
def prepare_christmas_tree(iterations=1000000):
    choices = [1, 2, 3, 4]
    parameters = {
        1: [-0.67, -0.02, -0.18, 0.81, 0.00, 1.02],
        2: [0.40, 0.40, -0.10, 0.40, -0.04, 0.06],
        3: [-0.40, -0.40, -0.10, 0.40, 0.04, 0.06],
        4: [-0.10, 0.00, 0.44, 0.44, 0.00, -0.14]
    }
    start_point = [np.random.rand(), np.random.rand()]
    points = [start_point]
    for i in range(0, iterations):
        no = random.choice(choices)
        x = points[-1][0] * parameters[no][0] + points[-1][1] * parameters[no][1] + parameters[no][4]
        y = points[-1][0] * parameters[no][2] + points[-1][1] * parameters[no][3] + parameters[no][5]
        points.append([x, y])
    return np.asarray(points)
```

```python
my_tree = prepare_christmas_tree()
```

```python
plt.figure(figsize=(10,10))
plt.scatter(my_tree[:, 0], my_tree[:, 1], s=0.1, c='g')
```

### Contraction mapping

http://www.math.uconn.edu/~kconrad/blurbs/analysis/contraction.pdf

```python
def find_square_root(value, number_of_iterations):
    print('You are looking for the square root of:', value)
    last_val = 10
    for i in range(number_of_iterations):
        new_val = 0.5 * (last_val + value/last_val)
        if (new_val == last_val):
            break
        else:
            print('Iteration:', i)
            last_val = new_val
            print(last_val)
    return new_val
```

```python
for i in range(0, 2):
    find_square_root(i, 10)
    print('\n')
```

    You are looking for the square root of: 0
    Iteration: 0
    5.0
    Iteration: 1
    2.5
    Iteration: 2
    1.25
    Iteration: 3
    0.625
    Iteration: 4
    0.3125
    Iteration: 5
    0.15625
    Iteration: 6
    0.078125
    Iteration: 7
    0.0390625
    Iteration: 8
    0.01953125
    Iteration: 9
    0.009765625

    You are looking for the square root of: 1
    Iteration: 0
    5.05
    Iteration: 1
    2.624009900990099
    Iteration: 2
    1.5025530119986812
    Iteration: 3
    1.0840434673026924
    Iteration: 4
    1.0032578510960606
    Iteration: 5
    1.0000052895642693
    Iteration: 6
    1.0000000000139897
    Iteration: 7
    1.0

```python
def find_log(value, logarithm_base, number_of_iterations):
    if value==0:
        raise ValueError('Value cannot be zero!')
    print('You are looking for the logarithm of base {} of {}.'.format(logarithm_base,
                                                                      value))
    last_val = 10
    base_log = np.log(logarithm_base)
    for i in range(number_of_iterations):
        new_val = last_val - ((logarithm_base**last_val - value) / (logarithm_base**last_val * base_log))
        if (new_val == last_val):
            break
        else:
            last_val = new_val
    print(new_val)
    return new_val
```

```python
for i in range(1, 4):
    find_log(i, 2, 100)
    print('\n')
find_log(0.4, 2, 100)
```

    You are looking for the logarithm of base 2 of 1.
    3.4860243383e-17

    You are looking for the logarithm of base 2 of 2.
    1.0

    You are looking for the logarithm of base 2 of 3.
    1.58496250072

    You are looking for the logarithm of base 2 of 0.4.
    -1.32192809489

    -1.3219280948873622

-> This method is not good for the logarithms. More in the Wikipedia article: https://en.wikipedia.org/wiki/Newton%27s_method#Practical_considerations

### Hausdorff metrics

Example 1.3 from the Chapter 1 shows Sierpinski's carpet:

```python
class SierpinskiCarpet:

    def __init__(self, number_of_iterations=3):
        # Calculate size of np.array
        self.steps = number_of_iterations
        self.size = 3**number_of_iterations
        self.canvas = []

    def create_canvas(self):
        self.canvas = np.ones((self.size, self.size), dtype=int)

    def show_canvas(self):
        fig = plt.figure(figsize=(10,10))
        plt.imshow(self.canvas, cmap='gray')
        plt.show()

    @staticmethod
    def find_satellite_points(point, distance):
        # find new distance and store it
        if distance == 1:
            new_distance = 0
        else:
            new_distance = int((3**(distance-1)) / 2)

        # find all satellite points
        d = 3**distance
        list_of_points = []
        for p in point:
            pA = [p[0] - d, p[0], p[0] + d]
            pB = [p[1] - d, p[1], p[1] + d]
            for x in pA:
                for y in pB:
                    if [x, y] != [p[0], p[1]]:
                        list_of_points.append([x, y])
        list_of_points.append(new_distance)
        return list_of_points

    @staticmethod
    def prepare_neighbours(pts, dist):
        full_array = []
        for p in pts:
            for i in range(p[0] - dist, p[0] + dist + 1):
                for j in range(p[1] - dist, p[1] + dist + 1):
                    full_array.append([i, j])
        return full_array

    def prepare_points(self):
        # The idea behind algorithm is explained after the code - it will be not clear if I write it here
        i = self.steps

        # Define first point
        middle = int(np.ceil(self.size/2))
        first_point = [middle, middle]
        first_n_distance = int((3**(i-1)) / 2)
        points = {i: [first_point, first_n_distance]}

        # Define the rest of points
        x = points[i]
        i = i - 1
        while i > 0:
            x = self.find_satellite_points(x[:-1], i)
            points[i] = x
            i = i - 1
        return points

    def fill_matrix(self, points_dict):
        for key in points_dict:
            array_of_vals = points_dict[key][:-1]
            neighbours = points_dict[key][-1]
            if neighbours > 0:
                array_of_vals = self.prepare_neighbours(array_of_vals, neighbours)
            for val in array_of_vals:
                self.canvas[val[0]-1, val[1]-1] = 0

    def create_carpet(self):
        # The first part is to prepare dict with points to fill
        filled_points = self.prepare_points()
        # The second part is to fill matrix of ones by zeros
        self.fill_matrix(filled_points)
        return filled_points
```

```python
my_carpet = SierpinskiCarpet(6)
my_carpet.create_canvas()
f = my_carpet.create_carpet()
```

```python
my_carpet.show_canvas()
```

It is an example of the set described by the Hausdorff metric. Very intersting is usage of Hausdorff distance in image processing (object matching): http://cgm.cs.mcgill.ca/~godfried/teaching/cg-projects/98/normand/main.html. Many known fractals may be described by this metric.

Sierpinski Carpet has his limit inside this metric beacuse each new square is inside the base square at the equal distance to his "parent" edges. The same is true for the Sierpinsky Triangle:

```python
class SierpinskiTraingle:

    def __init__(self, triangle_edge_length=100):
        self.base_length = triangle_edge_length
        self.base_points = self.calc_base([0, 0], triangle_edge_length)
        self.edge_points = []
        self.inner_triangles = []

    def calc_base(self, pt_a, length):
        pt_b = [pt_a[0] + length, pt_a[1]]
        pt_c = [(pt_a[0] + length) / 2, (pt_a[1] + 0.866 * length)]
        return [pt_a, pt_b, pt_c]

    @staticmethod
    def calc_values(points):
        bl = points[0]
        br = points[1]
        u = points[2]

        upper_left_x = bl[0] + (br[0] - bl[0]) * 0.25
        upper_left_y = bl[1] + 0.866 * (br[0] - bl[0]) * 0.5
        upper_left = [upper_left_x, upper_left_y]

        bottom_middle_x = bl[0] + (br[0] - bl[0]) * 0.5
        bottom_middle_y = bl[1]
        bottom_middle = [bottom_middle_x, bottom_middle_y]

        upper_right_x = bl[0] + (br[0] - bl[0]) * 0.75
        upper_right_y = upper_left_y
        upper_right = [upper_right_x, upper_right_y]
        return ([upper_left, bottom_middle, upper_right],
                [bl, bottom_middle, upper_left],
                [bottom_middle, br, upper_right],
                [upper_left, upper_right, u])

    def calc_inner_triangle(self):
        if len(self.edge_points) == 0:
            a = self.base_points[0]
            b = self.base_points[1]

            upper_left_x = (b[0] - a[0]) * 0.25
            upper_left_y = 0.866 * (b[0] - a[0]) * 0.5
            upper_left = [upper_left_x, upper_left_y]

            bottom_middle_x = (b[0] - a[0]) * 0.5
            bottom_middle_y = a[1]
            bottom_middle = [bottom_middle_x, bottom_middle_y]

            upper_right_x = (b[0] - a[0]) * 0.75
            upper_right_y = upper_left_y
            upper_right = [upper_right_x, upper_right_y]
            self.edge_points.append([upper_left, bottom_middle, upper_right])
            self.inner_triangles.append([[self.base_points[0], bottom_middle, upper_left],
                                         [bottom_middle, self.base_points[1], upper_right],
                                         [upper_left, upper_right, self.base_points[2]]])
            self.print_info()
        else:
            # Last level triangles
            last_level = self.inner_triangles[-1]
            new_level = []
            for i in last_level:
                x = self.calc_values(i)
                self.edge_points.append(x[0])
                new_level.append(x[1])
                new_level.append(x[2])
                new_level.append(x[3])
            self.inner_triangles.append(new_level)
            self.print_info()

    def show_triangle(self):
        fig = plt.figure(figsize=(6,6))
        # plot base
        plt.plot([self.base_points[0][0], self.base_points[1][0]],
                [self.base_points[0][1], self.base_points[1][1]], color='red')
        plt.plot([self.base_points[1][0], self.base_points[2][0]],
                [self.base_points[1][1], self.base_points[2][1]], color='red')
        plt.plot([self.base_points[0][0], self.base_points[2][0]],
                [self.base_points[0][1], self.base_points[2][1]], color='red')
        # plot smaller edges
        if len(self.edge_points) > 0:
            for record in self.edge_points:
                upper_left = record[0]
                bottom_middle = record[1]
                upper_right = record[2]
                # upper left and bottom middle
                plt.plot([upper_left[0], bottom_middle[0]], [upper_left[1], bottom_middle[1]],
                        color='blue')
                # upper left and upper right
                plt.plot([upper_left[0], upper_right[0]], [upper_left[1], upper_right[1]],
                        color='blue')
                # bottom middle and upper right
                plt.plot([bottom_middle[0], upper_right[0]], [bottom_middle[1], upper_right[1]],
                        color='blue')
        plt.show()

    def print_info(self):
        triangles = 'You generated {} triangles'.format(len(self.edge_points))
        depth = 'You are at the level {}'.format(len(self.inner_triangles))
        print(triangles)
        print(depth)
        print("")
```

```python
st = SierpinskiTraingle(triangle_edge_length=10)
```

```python
st.show_triangle()
```

```python
st.calc_inner_triangle()
```

    You generated 1 triangles
    You are at the level 1

```python
st.show_triangle()
```

```python
st.calc_inner_triangle()
```

    You generated 4 triangles
    You are at the level 2

```python
st.show_triangle()
```

```python
st.calc_inner_triangle()
st.show_triangle()
```

```python
st.calc_inner_triangle()
st.show_triangle()
```

```python
st.calc_inner_triangle()
st.show_triangle()
```

```python
st.calc_inner_triangle()
st.show_triangle()
```

## IFS examples from parametric equations

### Example 2.1: IFS described by three functions:

$$\begin{equation}
w_{1}(z) = \frac{z}{2} \\
w_{2}(z) = \frac{z + 1}{2} \\
w_{3}(z) = \frac{2z + 1 + i \sqrt(3)}{4} \\
\end{equation}$$

where:

$$\begin{equation}
z = x + iy
\end{equation}$$

```python
def run_example_2_1(x, y, iterations):
    # Random point allocation
    z = complex(x, y)
    points_list = [[x, y]]
    for i in range(0, iterations):
        test = np.random.choice(['w1', 'w2', 'w3'])
        if test == 'w1':
            z = z / 2
        elif test == 'w2':
            z = (z + 1) / 2
        else:
            z = (2 * z + 1 + 1j * np.sqrt(3)) / 4
        points_list.append([z.real, z.imag])
    return np.asarray(points_list)

example21 = run_example_2_1(0.5, 0.5, 50000)
plt.figure(figsize=(10,10))
plt.scatter(example21[:, 0], example21[:, 1], s=0.4, c='g')
```

```python
def calculate_z(p):
    z = complex(p[0], p[1])
    z1 = z / 2
    z2 = (z + 1) / 2
    z3 = (2 * z + 1 + 1j * np.sqrt(3)) / 4
    return [[z1.real, z1.imag], [z2.real, z2.imag], [z3.real, z3.imag]]

def run_example_2_1_all(x, y, iterations):
    z = complex(x, y)
    points_list = []
    z1 = z / 2
    z2 = (z + 1) / 2
    z3 = (2 * z + 1 + 1j * np.sqrt(3)) / 4
    points_list.append([[z1.real, z1.imag], [z2.real, z2.imag], [z3.real, z3.imag]])

    for i in range(0, iterations):
        new_set_of_points = []
        for record in points_list[-1]:
            zzz = calculate_z(record)
            new_set_of_points.append(zzz[0])
            new_set_of_points.append(zzz[1])
            new_set_of_points.append(zzz[2])
        points_list.append(new_set_of_points)

    new_array = []
    for dim in points_list:
        for rec in dim:
            new_array.append(rec)

    return np.asarray(new_array)

example21 = run_example_2_1_all(1, 1, 10)
plt.figure(figsize=(10,10))
plt.scatter(example21[:, 0], example21[:, 1], s=0.4, c='pink')
```

```python
def run_example_2_1(x, y, iterations):
    # Random point allocation
    z = complex(x, y)
    points_list = [[x, y]]
    for i in range(0, iterations):
        test = np.random.choice(['w1', 'w2', 'w3'])
        if test == 'w1':
            z = z / 2
        elif test == 'w2':
            z = (z + 1) / 2
        else:
            z = (2 * z + 1 + 1j * np.sqrt(3)) / 4
        points_list.append([z.real, z.imag])
    return np.asarray(points_list)

example21 = run_example_2_1(-0.1, 1.98, 50000)
plt.figure(figsize=(10,10))
plt.scatter(example21[:, 0], example21[:, 1], s=1.8, c='black')
```

### Example 2.2: IFS described by two functions - spiral:

$$\begin{equation}
w_{1}(z) = -0.4z - (1 - 0.1i) \\
w_{2}(z) = (0.76 + 0.4i)*z \\
\end{equation}$$

where:

$$\begin{equation}
z = x + iy
\end{equation}$$

```python
def run_example_2_2(x, y, iterations):
    # Random point allocation
    z = complex(x, y)
    points_list = [[x, y]]
    for i in range(0, iterations):
        test = np.random.choice(['w1', 'w2'])
        if test == 'w1':
            z = -0.4 * z - (1 -0.1j)
        else:
            z = (0.76 + 0.4j) * z
        points_list.append([z.real, z.imag])
    return np.asarray(points_list)

example22 = run_example_2_2(0, 0, 500000)
plt.figure(figsize=(10,10))
plt.scatter(example22[:, 0], example22[:, 1], s=0.4, c='magenta')
```

### Example 2.3: IFS described by two functions - line:

$$\begin{equation}
w_{1}(x) = \frac{x}{3} \\
w_{2}(x) = \frac{x + 2}{3} \\
\end{equation}$$

where:

$$\begin{equation}
0 <= x <= 1
\end{equation}$$

```python
def run_example_2_3(x, iterations):
    # Random point allocation
    points_list = [[x, 0]]
    for i in range(0, iterations):
        test = np.random.choice(['w1', 'w2'])
        if test == 'w1':
            x = x / 3
        else:
            x = (x + 2) / 3
        points_list.append([x, 0])
    return np.asarray(points_list)

example23 = run_example_2_3(0, 50)
plt.figure(figsize=(10,10))
plt.scatter(example23[:, 0], example23[:, 1], c='magenta')
```

#### Summary:

In this chapter I've learned about Iterated Function Systems and the Hausdorff metrics.

* Hausdorff metrics describes fractals.
* IFS evolves into attractor  - as example it could be seen with example 2.1 the last traingle (where points are moving from the initial position into the triangle).
* Memory management is very painful when the fractal points are stored in arrays. (Depth limit).
* Contraction mapping is a method for generating fractals but it could be used to find square root of a number. It is not so well suited for the logarithms :)

> The next chapter (3) will be covered in the new notebook. And it covers Probabilistic IFS algorithms. They are used to generate fractals and save memory.

-----

Things to correct in this notebook:
- Actually nothing :)

```python

```


# Table of Contents
* [1c. Fixed flux spinodal decomposition on a T shaped domain](#1c.-Fixed-flux-spinodal-decomposition-on-a-T-shaped-domain)
	* [Use Binder For Live Examples](#Use-Binder-For-Live-Examples)
	* [Define $f_0$](#Define-$f_0$)
	* [Define the Equation](#Define-the-Equation)
	* [Solve the Equation](#Solve-the-Equation)
	* [Run the Example Locally](#Run-the-Example-Locally)
	* [Movie of Evolution](#Movie-of-Evolution)

# 1c. Fixed flux spinodal decomposition on a T shaped domain

## Use Binder For Live Examples

[](http://mybinder.org/repo/wd15/fipy-hackathon1)

The free energy is given by,

$$ f_0\left[ c \left( \vec{r} \right) \right] =
   - \frac{A}{2} \left(c - c_m\right)^2
   + \frac{B}{4} \left(c - c_m\right)^4
   + \frac{c_{\alpha}}{4} \left(c - c_{\alpha} \right)^4
   + \frac{c_{\beta}}{4} \left(c - c_{\beta} \right)^4 $$

In FiPy we write the evolution equation as

$$ \frac{\partial c}{\partial t} = \nabla \cdot  \left[
       D \left( c \right) \left( \frac{ \partial^2 f_0 }{ \partial c^2} \nabla c - \kappa \nabla \nabla^2 c \right)
   \right] $$

Let's start by calculating $ \frac{ \partial^2 f_0 }{ \partial c^2} $ using sympy. It's easy for this case, but useful in the general case for taking care of difficult book keeping in phase field problems.

```python
%matplotlib inline

import sympy
import fipy as fp
import numpy as np
```

```python
A, c, c_m, B, c_alpha, c_beta = sympy.symbols("A c_var c_m B c_alpha c_beta")
```

```python
f_0 = - A / 2 * (c - c_m)**2 + B / 4 * (c - c_m)**4 + c_alpha / 4 * (c - c_alpha)**4 + c_beta / 4 * (c - c_beta)**4
```

```python
print f_0
```

    -A*(-c_m + c_var)**2/2 + B*(-c_m + c_var)**4/4 + c_alpha*(-c_alpha + c_var)**4/4 + c_beta*(-c_beta + c_var)**4/4

```python
sympy.diff(f_0, c, 2)
```

    -A + 3*B*(c_m - c_var)**2 + 3*c_alpha*(c_alpha - c_var)**2 + 3*c_beta*(c_beta - c_var)**2

The first step in implementing any problem in FiPy is to define the mesh. For [Problem 1a]({{ site.baseurl }}/hackathon1/#a.-Square-Periodic) the solution domain is just a square domain, but the boundary conditions are periodic, so a `PeriodicGrid2D` object is used. No other boundary conditions are required.

```python
mesh = fp.Grid2D(dx=0.5, dy=0.5, nx=40, ny=200) + (fp.Grid2D(dx=0.5, dy=0.5, nx=200, ny=40) + [[-40],[100]])
```

The next step is to define the parameters and create a solution variable.

```python
c_alpha = 0.05
c_beta = 0.95
A = 2.0
kappa = 2.0
c_m = (c_alpha + c_beta) / 2.
B = A / (c_alpha - c_m)**2
D = D_alpha = D_beta = 2. / (c_beta - c_alpha)
c_0 = 0.45
q = np.sqrt((2., 3.))
epsilon = 0.01

c_var = fp.CellVariable(mesh=mesh, name=r"$c$", hasOld=True)
```

Now we need to define the initial conditions given by,

Set $c\left(\vec{r}, t\right)$ such that

$$ c\left(\vec{r}, 0\right) = \bar{c}_0 + \epsilon \cos \left( \vec{q} \cdot \vec{r} \right) $$

```python
r = np.array((mesh.x, mesh.y))
c_var[:] = c_0 + epsilon * np.cos((q[:, None] * r).sum(0))
viewer = fp.Viewer(c_var)
```

## Define $f_0$

To define the equation with FiPy first define `f_0` in terms of FiPy. Recall `f_0` from above calculated using Sympy. Here we use the string representation and set it equal to `f_0_var` using the `exec` command.

```python
out = sympy.diff(f_0, c, 2)
```

```python
exec "f_0_var = " + repr(out)
```

```python
#f_0_var = -A + 3*B*(c_var - c_m)**2 + 3*c_alpha*(c_var - c_alpha)**2 + 3*c_beta*(c_var - c_beta)**2
f_0_var
```

    (((((pow((0.5 - $c$), 2)) * 29.6296296296) + -2.0) + ((pow((0.05 - $c$), 2)) * 0.15)) + ((pow((0.95 - $c$), 2)) * 2.85))

## Define the Equation

```python
eqn = fp.TransientTerm(coeff=1.) == fp.DiffusionTerm(D * f_0_var) - fp.DiffusionTerm((D, kappa))
eqn
```

    (TransientTerm(coeff=1.0) + (DiffusionTerm(coeff=[-(((((((pow((0.5 - $c$), 2)) * 29.6296296296) + -2.0) + ((pow((0.05 - $c$), 2)) * 0.15)) + ((pow((0.95 - $c$), 2)) * 2.85)) * 2.22222222222))]) + DiffusionTerm(coeff=[2.2222222222222223, 2.0])))

## Solve the Equation

To solve the equation a simple time stepping scheme is used which is decreased or increased based on whether the residual decreases or increases. A time step is recalculated if the required tolerance is not reached.

```python
elapsed = 0.0
steps = 0
dt = 0.01
total_sweeps = 2
tolerance = 1e-1
total_steps = 10
```

```python
c_var[:] = c_0 + epsilon * np.cos((q[:, None] * r).sum(0))
c_var.updateOld()
from fipy.solvers.pysparse import LinearLUSolver as Solver
solver = Solver()

while steps < total_steps:
    res0 = eqn.sweep(c_var, dt=dt, solver=solver)

    for sweeps in range(total_sweeps):
        res = eqn.sweep(c_var, dt=dt, solver=solver)

    if res < res0 * tolerance:
        steps += 1
        elapsed += dt
        dt *= 1.1
        c_var.updateOld()
    else:
        dt *= 0.8
        c_var[:] = c_var.old

viewer.plot()
print 'elapsed_time:',elapsed
```

## Run the Example Locally

The following cell will dumpy a file called `fipy_hackathon1c.py` to the local file system to be run. The images are saved out at each time step.

```python
%%writefile fipy_hackathon_1c.py

import fipy as fp
import numpy as np

mesh = fp.Grid2D(dx=0.5, dy=0.5, nx=40, ny=200) + (fp.Grid2D(dx=0.5, dy=0.5, nx=200, ny=40) + [[-40],[100]])

c_alpha = 0.05
c_beta = 0.95
A = 2.0
kappa = 2.0
c_m = (c_alpha + c_beta) / 2.
B = A / (c_alpha - c_m)**2
D = D_alpha = D_beta = 2. / (c_beta - c_alpha)
c_0 = 0.45
q = np.sqrt((2., 3.))
epsilon = 0.01

c_var = fp.CellVariable(mesh=mesh, name=r"$c$", hasOld=True)

r = np.array((mesh.x, mesh.y))
c_var[:] = c_0 + epsilon * np.cos((q[:, None] * r).sum(0))

f_0_var = -A + 3*B*(c_var - c_m)**2 + 3*c_alpha*(c_var - c_alpha)**2 + 3*c_beta*(c_var - c_beta)**2

eqn = fp.TransientTerm(coeff=1.) == fp.DiffusionTerm(D * f_0_var) - fp.DiffusionTerm((D, kappa))

elapsed = 0.0
steps = 0
dt = 0.01
total_sweeps = 2
tolerance = 1e-1
total_steps = 600

c_var[:] = c_0 + epsilon * np.cos((q[:, None] * r).sum(0))

c_var.updateOld()

from fipy.solvers.pysparse import LinearLUSolver as Solver

solver = Solver()

viewer = fp.Viewer(c_var)
while steps < total_steps:
    res0 = eqn.sweep(c_var, dt=dt, solver=solver)

    for sweeps in range(total_sweeps):
        res = eqn.sweep(c_var, dt=dt, solver=solver)

        print ' '
        print 'steps',steps
        print 'res',res
        print 'sweeps',sweeps
        print 'dt',dt

    if res < res0 * tolerance:
        steps += 1
        elapsed += dt
        dt *= 1.1
        if steps % 1 == 0:
             viewer.plot('image{0}.png'.format(steps))
        c_var.updateOld()
    else:
        dt *= 0.8
        c_var[:] = c_var.old
```

    Overwriting fipy_hackathon_1c.py

## Movie of Evolution

The movie of the evolution for 600 steps.

The movie was generated with the output files of the form `image*.png` using the following commands,

    $ rename 's/\d+/sprintf("%05d",$&)/e' image*
    $ ffmpeg -f image2 -r 6 -i 'image%05d.png' output.mp4

```python
from IPython.display import YouTubeVideo
scale = 1.5
YouTubeVideo('aZk38E7OxcQ', width=420 * scale, height=315 * scale, rel=0)
```

```python

```