#### [**NICOLAS CACHANOSKY**](http://www.ncachanosky.com) | Department of Economics | Metropolitan State University of Denver | ncachano@msudenver.edu

# AD-AS MODEL
---

This note illustrates how to code a simple AD-S Model in Python. The purpose of the note is to walk through Python applications, not to offer a detailed discussion of the AD-AS Model or to show best coding practices. The note also assumes familiarity with the AD-AS model and a beginner experience with Python.

For a more complete and detailed discussion of Python applications see the material in [Quant Econ](https://quantecon.org/).

---

## TABLE OF CONTENTS
1. [The AD-AS model](#1.-THE-AD-AS-MODEL)
2. [Aggregate Demand (AD)](#2.-AGGREGATE-DEMAND-(AD))
3. [Long-Run Aggregate Supply (LRAS)](#3.-LONG-RUN-AGGREGATE-SUPPLY-(LRAS))
4. [Short-Run Aggregate Supply (SRAS)](#4.-SHORT-RUN-AGGREGATE-SUPPLY-(SRAS))
5. [Putting all the pieces together](#5.-PUTTING-ALL-THE-PIECES-TOGETHER)
6. [Shocks](#6.-SHOCKS)

# 1. THE AD-AS MODEL

The AD-AS model shows the relationship between output $(Y)$ and the price level $(P)$. Different to the IS-LM model, in this case $P$ is endogenous and varies with different levels of $Y$. The AD-AS model has three components:

1. AD: Aggregate demand
2. LRAS: Long-run aggregate supply
3. SRAR: Short-run aggregate supply

The AD-AS model is more *general* than the IS-LM model in the sense that it allows for the price level to change. Therea are two differences between these models. The first one is taht while the AD-AS model allows for the interest rate $(i)$ to change, it does not show up explicitly in the model as it does in the IS-LM framework. The second one, is that both, monetary and fiscal policy affect the same line (the $AD$), while in the IS-LM framework monetary and fiscal policy shift *different* lines.

# 2. AGGREGATE DEMAND (AD)

The $AD$ line tracks all the output and price level combinations for whith the IS-LM model is in equilibrium. Graphically, it shows how equilibrum moves in the IS-LM graph when $P$ changes. A change in $P$ shifts the LM schedule giving a new equilibrium point on the IS schedule. In simple terms: $AD = Y = C + I + G + (X - Z)$ where $C$ is the household consumption, $I$ is investment, $G$ is govegnment spending, $X$ is exports, and $Z$ is imports.

In the [IS-LM model note](https://nbviewer.jupyter.org/github/ncachanosky/Macroeconomics-with-Python/blob/master/IS-LM%20Model.ipynb)  there is consumption function, an investment function, and an imports function. The remainig variables $(G = \bar{G} \text{ and } X = \bar{X})$ are treated as exogenous. The consumption, investment, imports, and money demadn functions are:

\begin{align}
    C   &= a + b(Y-T)            \\[10pt]
    I   &= \bar{I} - d \cdot i   \\[10pt]
    Z   & = \alpha + \beta(Y-T)  \\[10pt]
    M^d &= c_1 + c_2 Y - c_3 i
\end{align}

Where $a>0$ and $b \in (0, 1)$ are the household level of autonomous consumption and the marginal propensity to consume respectively with $T$ representing the nominal value of taxes; \bar{I} is the level of investment when $i = 0$ and $d >0$ is the slope of investment with respect to $i$; $\alpha >0$ and $\beta \in (0, 1)$ are the autonomous level and the marginal propensity to import respectively; and $c_1>0, c_2>0, c_3>0$ capture the keynesian precautionary, transation, and especualtion reasons to demand money.

The $AD$ is the equilibrium level of output $(Y^*)$ from the IS-LM model, which is a function of $(P)$. From the [IS-LM model note](https://nbviewer.jupyter.org/github/ncachanosky/Macroeconomics-with-Python/blob/master/IS-LM%20Model.ipynb) (section 4):

\begin{align}
   Y^* &= \frac{\left[(a-\alpha)-(b-\beta)T+\bar{I}+\bar{G}+X\right]/d + (1/c_3) \left(M^S_0/P - c_1 \right)}{(1-b+\beta)/d - (c_2/c_3)} \\[10pt]
   Y^* &= \underbrace{\left[\frac{(a-\alpha)-(b-\beta)T+\bar{I}+\bar{G} + X}{d} - \frac{c_1}{c_3} \right] \left[\frac{1-b+\beta}{d} - \frac{c_2}{c_3} \right]^{-1}}_\text{vertical level} + \underbrace{\frac{M^S_0}{c_3} \left[\frac{1-b+\beta}{d} - \frac{c_2}{c_3} \right]^{-1}}_\text{shape} \cdot \frac{1}{P}
\end{align}

Even though the function looks complicated, note that the relationship between $Y$ and $P$ is hiperbolic. Note that an increaes in $M^S_0$ increases the level of Y but also changes the *shape* of $AD$.

## 2.1 MONEY SUPPLY AND VELOCITY

The AD-AS model has a real variable $(Y)$ and a nominal variable $(P)$. Because $PY = NGDP$ the model can be framed in terms of the equation of exchange.

\begin{equation}
    MV_{Y} = P_{Y}Y
\end{equation}

Where $M$ is money supply (shown as $M^S_0$ above), $V_Y$ is the velocity of money circulation, and $P_{Y}$ is the GDP deflator of real output $Y$. Note this simple form, $Y = \frac{MV_Y}{P_Y}$ also has the hiperbolic shape discussed above.

To add a layer of complexity, money supply can be open in base money $(B)$ times the money-multiplier $m$:

\begin{align}
   m &= \frac{1 + \lambda}{\rho + \lambda} \\[10pt]
   M &= B \cdot m                          \\[10pt]
   \left(Bm\right) V_Y &= P_Y Y
\end{align}

where $\lambda \in (0, 1)$ is the currency-drain ratio (cash-to-deposit ratio) and $\rho \in (0, 1)$ is the reserve ratio (desired plus required level of reserves).

If money demand $M^D$ is a $k$ proportion of nominal income $(P_YY)$, then, assuming equilibrium in the money market, money velocity is the inverse of money demand: $V_Y = 1/k$. As less (more) money is demanded to be hold as a cash-balance, the more (less) quickly money moves (in average) in the economy.

\begin{align}
    M^S_0V_Y &= P_YY                       \\[10pt]
    M^D      &= k \cdot \left(P_YY \right) \\[10pt]
    M^S_0    &= M^D                        \\[10pt]
    V_Y      &= \frac{1}{k}
\end{align}

---

We can code $AD$ with a `class`. A `class` allows to build our own type of objects. In this case the code constructs a class called `AD` that has (1) the money multiplier, (2) the money supply, and (3) estimates $AD$ for a given $P$.

The code follows the following structure. The first section imports the required packages. The second section builds the $AD$ `class`. The third section show the values of the money multiplier and total money supply. The fourth section plots the $AD$.

The `class` is build the following way. The first element, `__init__` collects the model (or `class`) parameters. Note that the values of these parameters are defined **inside** the `class` (this does not need to be the case) and that these parameters exist **inside** the class (they are not global values). After the parameters are defined, the `class` continues to build the three components. Note that the first two (money multiplier and money supply) can be defined with the paramters already included in the `class`. The third component, the value of $AD$, requires an exogenous value, $P$.

```python
"1|IMPORT PACKAGES"
import numpy as np               # Package for scientific computing with Python
import matplotlib.pyplot as plt  # Matplotlib is a 2D plotting library

"2|BUILD AD CLASS"
class class_AD:
    "Define the parameters of the model"
    def __init__(self, a     = 20   ,  # AD: autonomous consumption
                       b     = 0.2  ,  # AD: marginal propensity to consume
                       alpha = 5    ,  # AD: autonomous imports
                       beta  = 0.1  ,  # AD: marginal propensity to import
                       T     = 1    ,  # AD: Taxes
                       I     = 10   ,  # AD: Investment
                       G     = 8    ,  # AD: Government spending
                       X     = 2    ,  # AD: Exports
                       d     = 5    ,  # AD: Investment slope
                       c1    = 175  ,  # AD: Precautionary money demand
                       c2    = 2    ,  # AD: Transactions money demand
                       c3    = 50   ,  # AD: Speculatio money demand
                       B     = 250  ,  # AD: Base money
                       lmbda = 0.05 ,  # AD: Currency drain ratio
                       rho   = 0.10 ): # AD: Reserve requirement)

        "Assign the parameter values"
        self.a     = a
        self.b     = b
        self.alpha = alpha
        self.beta  = beta
        self.T     = T
        self.I     = I
        self.G     = G
        self.X     = X
        self.d     = d
        self.c1    = c1
        self.c2    = c2
        self.c3    = c3
        self.B     = B
        self.lmbda = lmbda
        self.rho   = rho

    "Money multiplier"
    def m(self):
        #Unpack the parameters (simplify notation)
        lmbda = self.lmbda
        rho   = self.rho
        #Calculate m
        return ((1 + lmbda)/(rho + lmbda))

    "Money supply"
    def M(self):
        #Unpack the parameters (simplify notation)
        B = self.B
        #Calculate M
        return (B*self.m())

    "AD: Aggregate demand"
    def AD(self, P):
        #Unpack the parameters (simplify notation)
        a     = self.a
        alpha = self.alpha
        b     = self.b
        beta  = self.beta
        T     = self.T
        I     = self.I
        G     = self.G
        X     = self.X
        d     = self.d
        c1    = self.c1
        c2    = self.c2
        c3    = self.c3
        #Calculate AD
        AD_level1 = (((a-alpha)-(b-beta)*T+I+G+X)/d-c1/c3)
        AD_level2 = ((1-b+beta)/d-c2/c3)**(-1)
        AD_shape  = self.M()/c3 * ((1-b+beta)/d - c2/c3)**(-1)
        return (AD_level1 * AD_level2 + AD_shape/P)

"3|SHOW RESULTS"
out = class_AD()

print("Money multiplier =", round(out.m(), 2))
print("Money supply ="    , round(out.M(), 2))

size = 50
P = np.arange(1, size)
Y = out.AD(P)

"4|PLOT AD"
y_max = np.max(P)
x_max = np.max(Y)
v = [0, x_max, 0, y_max]
fig, ax = plt.subplots(figsize=(10, 8))
ax.set(title="AGREGGATE DEMAND", xlabel="Y", ylabel="P")
ax.plot(Y, P, "k-")
ax.yaxis.set_major_locator(plt.NullLocator())  # Hide ticks
ax.xaxis.set_major_locator(plt.NullLocator())  # Hide ticks
plt.show()
```

# 3. LONG-RUN AGGREGATE SUPPLY (LRAS)

Prices are flexible in the long-run, therefore money is neutral and the $LRAS$ is a vertical line (a given value of $Y$ rather than a function). The valor of $Y$ in the long-run can be derived from the Solow model's steady-state. From the [Solow model notes](https://nbviewer.jupyter.org/github/ncachanosky/Macroeconomics-with-Python/blob/master/Solow%20Model.ipynb) (section 2) we know that there is an equilibrium level of capital $K^*$. Therefore, assuming a typical Cobb-Douglass production function with constant returns to scale, for any period of time $t$, output in the long run $(Y_{LR})$ equals:

\begin{align}
    K_{LR} &= N \cdot \left(\frac{sA}{\delta} \right)^{\frac{1}{1-\theta}}  \\
    Y_{LR} &= A \cdot \left(K_{LR}^{\theta} N^{1-\theta} \right)
\end{align}

Where $A$ is techonology or total factor productivity (TFP), $N$ is labor, and $\theta$ is the output elasticity of capital. In the long-run, the leve of output will grow at the rate of growth of population $(n)$ and technology $(\gamma)$. This is shown as a right-ward movemet in the common pictorial depiction of the AD-AS model. Also note that since th $LRAS$ is a value (a vertical line), it can only shift **left** or **right**, but not up or down.

# 4. SHORT-RUN AGGREGATE SUPPLY (SRAS)

As long as wages are sticky, then changes in $P$ do have short-run effects on the level of $Y$ thourgh changes in the real wage $(W/P)$. In the long-run $N$ is at full employment and the representative firm can change the size of $K$. But in the short-run, the firm can change $N$ but $K$ is fixed. Therefore, to construct the $SRAS$ we need the bheavior of labor supply in the short-run. For simplicity, this note takes a simple approach. It assumes a closed economy (you can try to add a foreign sector yourself) and lets the level of employment be defined by the following function with respect to the price level (you cantry to  replace with a labor market as discussed in the [labor market notes](https://nbviewer.jupyter.org/github/ncachanosky/Macroeconomics-with-Python/blob/master/Labor%20Market.ipynb)):

\begin{equation}
 N = \eta \cdot P^{\nu}; \eta, \nu > 0
\end{equation}

The function is (implicitly) assuming constant expectations by labor. Therefore, an increase in the price level reduces the real wage paid by the firms, but labor does not realize that the real wage has decreased.

# 5. PUTTING ALL THE PIECES TOGETHER

We can now put all the pieces together. In this case, instead of using a `class` the sample code uses `functions`. The model assumes a given stock of capital $K = \bar{K}$ (you can try to add a *steady-state* level of capital as discussed in the [Solow Model notes](https://nbviewer.jupyter.org/github/ncachanosky/Macroeconomics-with-Python/blob/master/Solow%20Model.ipynb). The code uses the `root` function to find the price level of equilibrium (section 4).

```python
"1|IMPORT PACKAGES"
import numpy as np               # Package for scientific computing with Python
import matplotlib.pyplot as plt  # Matplotlib is a 2D plotting library
from scipy.optimize import root  # Package to find the roots of a function

"2|DEFINE PARAMETERS"
# PRODUCTION FUNCTION
A      =   1     # Total Factor Productivity
varphi =   0.70  # Output elasticity of capital
# CAPITAL STOCK
s      =   0.25  # Savings rate
delta  =   0.20  # Depreciation rate
K      = 100     # Capital stock
# LABOR SUPPLY
v      =   0.75  # Labor supply "slope" with respect to P
# MONEY SUPPLY
B      = 100     # Base Money
lmbda  =   0.05  # Currency drain
rho    =   0.20  # Reserve ratio
# AGGREGATE DEMAND
a      =  40     # Autonomous household domestic consumption
b      =   0.3   # Marginal propensity to consume
T      =   1     # Taxes
I      =   4     # Investment with i = 0
G      =   2     # Government Spengin
d      =   2     # Slope of investment with respect to i
c1     = 200     # Money demand: Precuationary
c2     =   0.6   # Money demand: Transactions
c3     =  10     # Money demand: Speculation

"3|FUNCTIONS"
# LABOR SUPPLY
def N(P):
    N = v * P
    return N

# OUTPUT
def output(P):
    output = A * (K**(varphi)) * (N(P)**(1-varphi))
    return output

# MONEY SUPPLY
m = (1+lmbda)/(rho+lmbda)   # Money Multiplier
M = B*m                     # Money Supply

# AGGREGATE DEMAND
def AD(P):
    AD_level1 = (a-b*T + I + G)/d - (c1/c3)
    AD_level2 = ((1-b)/d - (c2/c3))**(-1)
    AD_shape  = M/c3 * AD_level2
    AD = AD_level1 * AD_level2 + AD_shape/P
    return AD

"4|EQUILIBRIUM: PRICE LEVEL"
def equation(P):
    Eq1 = AD(P) - output(N(P))
    equation = Eq1
    return equation

sol = root(equation, 10)
Pstar = sol.x
Nstar = N(Pstar)

# #Define domain of the model
size = np.round(Pstar, 0)*2
P_vector = np.linspace(1, size, 500)    # 500 dots between 1 and size

# Agregate Supply and Aggregate Demand
LRAS = output(N(Pstar))
SRAS = output(N(P_vector))
AD_vector = AD(P_vector)

"5|PLOT AD-AS MODEL"
v = [0, 80, 0, size]                            # Axis range
fig, ax = plt.subplots(figsize=(10, 8))
ax.set(title="AD-AS MODEL", ylabel=r'$P$', xlabel="Output")
ax.plot(AD_vector, P_vector, "k-", alpha = 0.7)
ax.plot(SRAS     , P_vector, "b-", alpha = 0.7)
plt.axvline(x = LRAS, color = 'r', alpha = 0.7)
plt.axhline(y = Pstar, xmax = LRAS/80, ls=':', color='k')
ax.yaxis.set_major_locator(plt.NullLocator())   # Hide ticks
ax.xaxis.set_major_locator(plt.NullLocator())   # Hide ticks
plt.text(75,  2.5, "AD")
plt.text(45, 11.0, "SRAS", color='b')
plt.text(30, 11.0, "LRAS", color='r')
plt.axis(v)
plt.show()
```

# 6. SHOCKS

We now add a real and a nominal positive shocks. In the case of the real shock, technology increases by 10-percent. In the case of the nominal shock, base money increases by 10-percent. Even though the output is not shown, the code inclides the inflation rate and the percent change in output calculations for each one of the two shocks. Note that the $SRAS$ shifts horizontally with shocks to the $LRAS$.

```python
"1|IMPORT PACKAGES"
import numpy as np               # Package for scientific computing with Python
import matplotlib.pyplot as plt  # Matplotlib is a 2D plotting library
from scipy.optimize import root  # Package to find the roots of a function

"2|DEFINE PARAMETERS"
# PRODUCTION FUNCTION
A      =   1     # Total Factor Productivity
varphi =   0.70  # Output elasticity of capital
# CAPITAL STOCK
s      =   0.25  # Savings rate
delta  =   0.20  # Depreciation rate
K      = 100     # Capital stock
# LABOR SUPPLY
v      =   0.75  # Labor supply "slope" with respect to P
# MONEY SUPPLY
B      = 100     # Base Money
lmbda  =   0.05  # Currency drain
rho    =   0.20  # Reserve ratio
# AGGREGATE DEMAND
a      =  40     # Autonomous household domestic consumption
b      =   0.3   # Marginal propensity to consume
T      =   1     # Taxes
I      =   4     # Investment with i = 0
G      =   2     # Government Spengin
d      =   2     # Slope of investment with respect to i
c1     = 200     # Money demand: Precuationary
c2     =   0.6   # Money demand: Transactions
c3     =  10     # Money demand: Speculation

"3|FUNCTIONS"
# LABOR SUPPLY
def N(P):
    N = v * P
    return N

# OUTPUT
def output(P):
    output = A * (K**(varphi)) * (N(P)**(1-varphi))
    return output

# MONEY SUPPLY
m = (1+lmbda)/(rho+lmbda)   # Money Multiplier
M = B*m                     # Money Supply

# AGGREGATE DEMAND
def AD(P):
    AD_level1 = (a-b*T + I + G)/d - (c1/c3)
    AD_level2 = ((1-b)/d - (c2/c3))**(-1)
    AD_shape  = M/c3 * AD_level2
    AD = AD_level1 * AD_level2 + AD_shape/P
    return AD

"4|EQUILIBRIUM: PRICE LEVEL"
def equation(P):
    Eq1 = AD(P) - output(N(P))
    equation = Eq1
    return equation

sol = root(equation, 10)
Pstar = sol.x
Nstar = N(Pstar)

# #Define domain of the model
size = np.round(Pstar, 0)*2
P_vector = np.linspace(1, size, 500)    # 500 dots between 1 and size

# Agregate Supply and Aggregate Demand
LRAS = output(N(Pstar))
SRAS = output(N(P_vector))
AD_vector = AD(P_vector)

"5|CALCULATE SHOCK EFFECTS"
real_shock    = 1.10
nominal_shock = 1.10

# Real shock
def output2(P):
    A2 = A * real_shock
    output2 = A2 * (K**(varphi)) * (N(P)**(1-varphi))
    return output2

def equation_real(P):
    Eq1 = AD(P) - output2(N(P))
    equation_real = Eq1
    return equation_real

sol_real = root(equation_real, 10)
Pstar2 = sol_real.x
Nstar2 = N(Pstar2)
LRAS2  = output2(N(Pstar2))
SRAS2  = output2(N(P_vector))

gP2 = Pstar2/Pstar - 1                          # Percent change in P
gY2 = LRAS2/LRAS - 1                            # Percent change in Y

# Nominal shock
M3 = (B * nominal_shock) * m

def AD3(P):
    AD_level1 = (a-b*T + I + G)/d - (c1/c3)
    AD_level2 = ((1-b)/d - (c2/c3))**(-1)
    AD_shape  = M3/c3 * AD_level2
    AD3 = AD_level1 * AD_level2 + AD_shape/P
    return AD3

def equation_nominal(P):
    Eq1 = AD3(P) - output(N(P))
    equation_nominal = Eq1
    return equation_nominal

sol_nominal = root(equation_nominal, 10)
Pstar3 = sol_nominal.x
Nstar3 = N(Pstar3)
AD_vector3 = AD3(P_vector)

gP3 = Pstar3/Pstar - 1                          # Percent change in P
gY3 = output(Pstar3)/output(Pstar) - 1          # Percent change in Y

"6|PLOT AD-AS MODEL WITH SHOCKS"
P3_stop = output(N(Pstar3))

v = [0, 80, 0, size]                            # Axis range
fig, ax = plt.subplots(nrows=2, figsize=(10, 20))
ax[0].set(title="AD-AS MODEL: REAL SHOCK", ylabel=r'$P$', xlabel="Output")
ax[0].plot(AD_vector  , P_vector, "k-", alpha = 0.7)
ax[0].plot(SRAS       , P_vector, "b-", alpha = 0.7)
ax[0].plot(SRAS2      , P_vector, "b:", alpha = 0.7)
ax[0].axvline(x = LRAS  , ls="-", color='r', alpha = 0.7)
ax[0].axvline(x = LRAS2 , ls=":", color="r", alpha = 0.7)
ax[0].axhline(y = Pstar , xmax = LRAS/80 , ls=":", color="k", alpha = 0.7)
ax[0].axhline(y = Pstar2, xmax = LRAS2/80, ls=":", color="k", alpha = 0.7)
ax[0].text(75.0,  2.5, "AD")
ax[0].text(44.5, 11.5, "SRAS" , color = "b")
ax[0].text(48.0, 11.0 , "SRAS'", color = "b")
ax[0].text(30.0, 11.0, "LRAS" , color = "r")
ax[0].text(39.0, 11.0, "LRAS'", color = "r")
ax[0].yaxis.set_major_locator(plt.NullLocator())   # Hide ticks
ax[0].xaxis.set_major_locator(plt.NullLocator())   # Hide ticks
ax[0].axis(v)
ax[1].set(title="AD-AS MODEL: NOMINAL SHOCK", ylabel=r'$P$', xlabel="Output")
ax[1].plot(AD_vector  , P_vector, "k-", alpha = 0.7)
ax[1].plot(AD_vector3 , P_vector, "k:", alpha = 0.7)
ax[1].plot(SRAS       , P_vector, "b-", alpha = 0.7)
ax[1].axvline(x = LRAS  , ls="-", color="r", alpha = 0.7)
ax[1].axhline(y = Pstar , xmax = LRAS/80, ls=":", color="k", alpha = 0.7)
ax[1].axhline(y = Pstar3, xmax = P3_stop/80, ls=":", color="k", alpha = 0.7)
ax[1].text(75,  1.7, "AD")
ax[1].text(75,  2.7, "AD'")
ax[1].text(45, 11.0, "SRAS" , color = "b")
ax[1].text(30, 11.0, "LRAS" , color = "r")
ax[1].yaxis.set_major_locator(plt.NullLocator())   # Hide ticks
ax[1].xaxis.set_major_locator(plt.NullLocator())   # Hide ticks
ax[1].axis(v)
plt.show()
```


# Reproducibility_Challenge_NeurIPS_2019

This is a blog explains method proposed in the paper Competitive gradient descent [(Schäfer et al., 2019)](https://arxiv.org/abs/1905.12103). This has been written as a supplimentary to the reproducibility report for reproducibility challenge of NeurlIPS’19. The pdf format of the report is present [here](https://gopikishan14.github.io/Reproducibility_Challenge_NeurIPS_2019/) with this github [repository](https://github.com/GopiKishan14/Reproducibility_Challenge_NeurIPS_2019) as its source.

# Paper Overview
The paper introduces a new algorithm for the numerical computation of Nash equilibria of competitive two-player games. The method is a natural generalization of gradient descent to the two-player setting where the update is given by the Nash equilibrium of a regularized bilinear local approximation of the underlying game. It avoids oscillatory and divergent behaviors seen in alternating gradient descent. Convergence and stability properties of the method are robust to strong interactions between the players, without adapting the stepsize, which is not the case with previous methods. The ability to choose larger stepsizes furthermore allows the algorithm to achieve faster convergence, as measured by the number of model evaluations (See the [report](https://gopikishan14.github.io/Reproducibility_Challenge_NeurIPS_2019/) experiments section).

## Background
The traditional optimization is concerned with a single agent trying to optimize a cost function. It
can be seen as $\min_{x \in R^m} f(x)$ . The agent has a clear objective to find (“Good local”) minimum of
f. Gradeint Descent (and its varients) are reliable Algorithmic Baseline for this purpose.

The paper talks about Competitive optimization. Competitive optimization extends this problem
to the setting of multiple agents each trying to minimize their own cost function, which in general
depends on the actions of all agents.
 The paper deals with the case of two such agents:
    \begin{align}
        &\min_{x \in R^m} f(x,y),\ \ \ \min_{y \in R^n} g(x,y)
    \end{align}
    for two functions $f,g: R^m \times R^n \longrightarrow R$.

In single agent optimization, the solution of the problem consists of the minimizer of the cost function.
In competitive optimization, the right definition of solution is less obvious, but often one is
interested in computing Nash– or strategic equilibria: Pairs of strategies, such that no player can
decrease their costs by unilaterally changing their strategies. If f and g are not convex, finding a
global Nash equilibrium is typically impossible and instead we hope to find a "good" local Nash
equilibrium

## About the problem
##### Gradient descent/ascent and the cycling problem:

For differentiable objective functions, the most naive approach to solving
\begin{align}
        \label{eqn:game}
        &\min_{x \in R^m} f(x,y),\ \ \ \min_{y \in R^n} g(x,y)
    \end{align}
is gradient descent ascent (GDA), whereby both players independently change their strategy in the direction of steepest descent of their cost function.
Unfortunately, this procedure features oscillatory or divergent behavior even in the simple case of a bilinear game ($f(x,y) = x^{\top} y = -g(x,y)$)

## Solution approach

To motivate this algorithm, authors remind us that gradient descent with stepsize $\eta$ applied to the function $f:R^m \longrightarrow R$ can be written as

\begin{equation}
        x_{k+1} = argmin_{x \in R^m} (x^T - x_{k}^T) \nabla_x f(x_k) + \frac{1}{2\eta} \|x - x_{k}\|^2.
    \end{equation}

This models a (single) player solving a local linear approximation of the (minimization) game, subject to a quadratic penalty that expresses her limited confidence in the global accuracy of the model.

```The natural generalization of this idea to the competitive case should then be given by the two players solving a local approximation of the true game, both subject to a quadratic penalty that expresses their limited confidence in the accuracy of the local approximation.```

In order to implement this idea, we need to find the appropriate way to generalize the linear approximation in the single agent setting to the competitive setting.

Authors suggest to use a **bilinear** approximation in the two-player setting.
Since the bilinear approximation is the lowest order approximation that can capture some interaction between the two players, they argue that the natural generalization of gradient descent to competitive optimization is not GDA, but rather the update rule $(x_{k+1},y_{k+1}) = (x_k,y_k) + (x,y)$, where $(x,y)$ is a Nash equilibrium of **the game**.

\begin{align}
    \begin{split}
    \label{eqn:localgame}
    \min_{x \in R^m} x^{\top} \nabla_x f &+ x^{\top} D_{xy}^2 f y + y^{\top} \nabla_y f + \frac{1}{2\eta} x^{\top} x \\
    \min_{y \in R^n}  y^{\top} \nabla_y g &+ y^{\top} D_{yx}^2 g x + x^{\top} \nabla_x g + \frac{1}{2\eta} y^{\top} y.
    \end{split}
\end{align}

Indeed, the (unique) Nash equilibrium of the above Game can be computed in closed form.

## Proposed method
**Among all (possibly randomized) strategies with finite first moment, the only Nash equilibrium of `the Game` is given by
\begin{align}
\label{eqn:nash}
&x = -\eta \left( Id - \eta^2 D_{xy}^2f D_{yx}^2 g \right)^{-1}
            \left( \nabla_{x} f - \eta D_{xy}^2f  \nabla_{y} g \right) \\
&y = -\eta \left( Id - \eta^2 D_{yx}^2g D_{xy}^2 f \right)^{-1}
            \left( \nabla_{y} g - \eta D_{yx}^2g  \nabla_{x} f \right),
\end{align}
given that the matrix inverses in the above expression exist.**

Note that the matrix inverses exist for all but one value of $\eta$, and for all $\eta$ in the case of a zero sum game.

According to the above Theorem, the Game has exactly one optimal pair of strategies, which is deter-ministic. Thus, we can use these strategies as an update rule, generalizing the idea of local optimalityfrom the single– to the multi agent setting and obtaining the following Algorithm.

`Competitive Gradient Descent (CGD)`
\begin{align}
for\ (0 <= k <= N-1)\\
&x_{k+1}  = x_{k} - \eta \left( Id - \eta^2 D_{xy}^2f D_{yx}^2 g \right)^{-1}\left( \nabla_{x} f - \eta D_{xy}^2f  \nabla_{y} g \right)\\
&y_{k+1} = y_{k} - \eta \left( Id - \eta^2 D_{yx}^2g D_{xy}^2 f \right)^{-1}
    \left( \nabla_{y} g - \eta D_{yx}^2g  \nabla_{x} f \right)\\
    return\ (x_{N},y_{N})\;
\end{align}

**What I think that they think that I think ... that they do**: Another game-theoretic interpretation of CGD follows from the observation that its update rule can be written as

\begin{equation}
\begin{pmatrix}
     \Delta x\\
     \Delta y
\end{pmatrix} = -
\begin{pmatrix}
    Id        & \eta D_{xy}^2 f \\
    \eta D_{yx}^2 g & Id
\end{pmatrix}^{-1}
\begin{pmatrix}
     \nabla_{x} f\\
     \nabla_{y} g
\end{pmatrix}.
\end{equation}

Applying the expansion $ \lambda_{\max} (A) < 1 \Rightarrow \left( Id - A \right)^{-1} = \lim_{N \rightarrow \infty} \sum_{k=0}^{N} A^k$ to the above equation, we observe that: \\

1. The first partial sum ($N = 0$) corresponds to the optimal strategy if the other player's strategy stays constant (GDA).
2. The second partial sum ($N = 1$) corresponds to the optimal strategy if the other player thinks that the other player's strategy stays constant (LCGD).
3. The third partial sum ($N = 2$) corresponds to the optimal strategy if the other player thinks that the other player thinks that the other player's strategy stays constant, and so forth, until the Nash equilibrium is recovered in the limit.

## Comparison
These six algorithms amount to different subsets of the following four terms.

\begin{align*}
       & \text{GDA: } &\Delta x =  &&&- \nabla_x f&\\
       & \text{LCGD: } &\Delta x =  &&&- \nabla_x f& &-\eta D_{xy}^2 f \nabla_y f&\\
       & \text{SGA: } &\Delta x =  &&&- \nabla_x f& &- \gamma D_{xy}^2 f \nabla_y f&  & & \\
       & \text{ConOpt: } &\Delta x =  &&&- \nabla_x f& &- \gamma D_{xy}^2 f \nabla_y f&  &- \gamma D_{xx}^2 f \nabla_x f& \\
       & \text{OGDA: } &\Delta x \approx &&&- \nabla_x f& &-\eta D_{xy}^2 f \nabla_y f&  &+\eta D_{xx}^2 f \nabla_x f& \\
       & \text{CGD: } &\Delta x = &\left(Id + \eta^2 D_{xy}^2 f D_{yx}^2 f\right)^{-1}&\bigl( &- \nabla_x f&  &-\eta D_{xy}^2 f \nabla_y f& & & \bigr)
     \end{align*}

1. The **gradient term** $-\nabla_{x}f$, $\nabla_{y}f$ which corresponds to the most immediate way in which the players can improve their cost.

2. The **competitive term** $-D_{xy}f \nabla_yf$, $D_{yx}f \nabla_x f$ which can be interpreted either as anticipating the other player to use the naive (GDA) strategy, or as decreasing the other players influence (by decreasing their gradient).

3. The **consensus term** $ \pm D_{xx}^2 \nabla_x f$, $\mp D_{yy}^2 \nabla_y f$ that determines whether the players prefer to decrease their gradient ($\pm = +$) or to increase it ($\pm = -$). The former corresponds the players seeking consensus, whereas the latter can be seen as the opposite of consensus. (It also corresponds to an approximate Newton's method. \footnote{Applying a damped and regularized Newton's method to the optimization problem of Player 1 would amount to choosing $x_{k+1} = x_{k} - \eta(Id + \eta D_{xx}^2)^{-1} f \nabla_x f \approx x_{k} - \eta( \nabla_xf - \eta D_{xx}^{2}f \nabla_x f)$, for $\|\eta D_{xx}^2f\| \ll 1$.)

4. The **equilibrium term** $(Id + \eta^2 D_{xy}^2 D_{yx}^2 f)^{-1}$, $(Id + \eta^2 D_{yx}^2 D_{xy}^2 f)^{-1}$, which arises from the players solving for the Nash equilibrium.
    This term lets each player prefer strategies that are less vulnerable to the actions of the other player.

## Code Implementation

The competitive gradeint descent algorithm contains gradient, competitive and equilibrium term. So, we need to efficiently calculat them. The equibrium term is a matrix inverse

### Computing Hessian vector products

The algorithm requires products of the mixed Hessian $v \mapsto D_{xy}f v$ and  $v \mapsto D_{yx}g v$, which we want to compute using automatic differentiation.

Many AD frameworks, like Autograd (https://github.com/HIPS/autograd) and ForwardDiff(https://github.com/JuliaDiff/ForwardDiff.jl) together with ReverseDiff(https://github.com/JuliaDiff/ReverseDiff.jl) support this procedure. While the authors used the AD frameworks from Julia, I will be using Autograd from PyTorch (https://pytorch.org/docs/stable/autograd.html)

### Matrix inversion for the equilibrium term
Authors propose to use iterative methods to approximate the inverse-matrix vector products arising in the *equilibrium term*.
Authors focus on zero-sum games, where the matrix is always symmetric positive definite, making the [conjugate gradient (CG)](https://en.wikipedia.org/wiki/Conjugate_gradient_method) algorithm the method of choice.
They also suggest terminating the iterative solver after a given relative decrease of the residual is achieved ($\| M x - y \| \leq \epsilon \|x\|$ for a small parameter $\epsilon$, when solving the system $Mx = y$).

Briefly, conjugate gradient (CG) iteratively solves the system $Mx = y$ for $x$ without calculating $M^{-1}$.

```python
import numpy as np
import matplotlib.pyplot as plt
"""
Simple python implemetation of CG tested on an example
"""

# Problem setup
A = np.matrix([[3.0, 2.0],
               [2.0, 6.0]]) # the matrix A in : Ax = b
b = np.matrix([[2.0],
               [-8.0]])  # we will use the convention that a vector is a column vector

# solution approach
x = np.matrix([[-2.0],
               [-2.0]])

steps = [(-2.0, -2.0)]  # modify according to x
i = 0
imax = 10
eps = 0.01
r = b - A * x
d = r
deltanew = r.T * r
delta0 = deltanew
while i < imax and deltanew > eps**2 * delta0:
    alpha = float(deltanew / float(d.T * (A * d)))
    x = x + alpha * d
    steps.append((x[0, 0], x[1, 0]))
    r = b - A * x
    deltaold = deltanew
    deltanew = r.T * r
    beta = float(deltanew / float(deltaold))
    d = r + beta * d
    i += 1

print("Solution vector x* for Ax = b :")
print(x)

print("And the steps taken by algorithm : ", steps)
plt.plot(steps)
```

#### Now to solve our problem of CGD, the following equation

\begin{equation}
\begin{pmatrix}
     \Delta x\\
     \Delta y
\end{pmatrix} = -
\begin{pmatrix}
    Id        & \eta D_{xy}^2 f \\
    \eta D_{yx}^2 g & Id
\end{pmatrix}^{-1}
\begin{pmatrix}
     \nabla_{x} f\\
     \nabla_{y} g
\end{pmatrix}.
\end{equation}

#### can be written as

\begin{equation}
\begin{pmatrix}
    Id        & \eta D_{xy}^2 f \\
    \eta D_{yx}^2 g & Id
\end{pmatrix}
\begin{pmatrix}
     \Delta x\\
     \Delta y
\end{pmatrix} = -
\begin{pmatrix}
     \nabla_{x} f\\
     \nabla_{y} g
\end{pmatrix}.
\end{equation}

#### so that the conjugate gradient method can used to calculate $\Delta x$ and $\Delta y$ without inverting the matrix.

## Conclusion

In the words of the authors of original paper,
`We propose a novel and natural generalization of gradient descent to competitive optimization. Besides its attractive game-theoretic interpretation, the algorithm shows improved robustness properties compared to the existing methods, which we study using a combination of theoretical analysis and computational experiments.`

Look out to the conclusion section of the [paper](https://arxiv.org/pdf/1905.12103.pdf) for extensive conclusion and future aspects.
Refers to the Experiment and Conclusion section of the reproducibility [report](https://gopikishan14.github.io/Reproducibility_Challenge_NeurIPS_2019/index.html) for details on replication.


# QG8 demo - Writing and reading a quantum graph in Python

Author: S. Whitlock (whitlock@unistra.fr)
Version 1.0: 18 July 2021

The latest version of this [IPython notebook] (http://ipython.org/ipython-doc/dev/interactive/htmlnotebook.html) is available at https://github.com/aQCess/QG8-Python.

## Introduction

QG8 is a lightweight data format for storing and exchanging numerical tensor data and data flow graphs in a binary format. It was created as way to store quantum models and data in a single file, including quantum gate and quantum circuit specifications, control pulse definitions, time independent and time-dependent model Hamiltonians, device characteristics, calibration data and noise models, user defined control flow instructions and simulation or measurement results.

In this notebook we will build a quantum graph representing a quantum gate protocol with time-dependent control pulses. The graph can be written to a QG8 file and read again. This allows for defining problem specifications in one language or one one system and executing the graph on another. In this example we show how a QG8 graph can be integrated with Python and the QuTip package in order to simulate the gate.

For more information see the QG8 project GitHub page at https://github.com/aQCess/QG8.

### Installation

To use QG8 just copy the qg8 folder to the python path or put it in your project directory.

> *For this demonstration we additionally make use of the Numpy and QuTip packages. To install QuTiP, download the latest release from https://qutip.org.*

> *For an introduction to QuTip notebooks we recommend the tutorials page of the QuTip project https://qutip.org/tutorials.html and the guide to basic operations https://qutip.org/docs/latest/guide/guide-basics.html.*

### Working principle

The core idea of the QG8 data format and graph based representations of quantum systems in general is to store quantum data and operations and as nodes of a data flow graph. Each node accepts information from its input nodes and edges represent the flow of information through the graph. The advantage of this approach is that it allows to store all the data required for a computation as well as the instructions on how to perform operations on the data in a single object which can then easily be exchanged between different systems.

In QG8 we classify nodes according to two main types: (i) *tensor nodes* which contain data. These act as input nodes or intermediate placeholder nodes for the graph. (ii) *op nodes* which do not contain data but indicate which operations should be performed on the data. In QG8 we do not provide an exhaustive set of possible operations, rather we allow the user to write their own operations which will be called by the graph when executed.

## Getting started with a QG8 graph: `QuantumGraph()`

Lets first include the necessary modules.

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

import qg8
```

The starting point for our demonstration is the `QuantumGraph` class. This is the basic object which stores all the information for the quantum gate as a list of nodes.

We can create an empty `QuantumGraph` instance by calling `QuantumGraph()`.

```python
mygraph = qg8.QuantumGraph()
```

A quantum graph is a container for QG8 nodes. For the moment we can see that it is empty.

```python
mygraph.nodes
```

    []

### QG8 node: `QG8Node`

Graph nodes are instances of the general `QG8Node` class (which in turn inherits from `qg8_chunk` in the core qg8 implementation). QG8 nodes can be either: (i) *tensor nodes*, containing input or placeholders for output data which can be represented as a scalar, vector, matrix or higher order tensors of numerical values; or (ii) *op nodes*, that describe operations that should be done on the tensors when the graph is run.

The node types that are implemented in this demo can be found in the `type_registry` dictionary. More custom node types can be defined and added in the `qg8.ops` library.

```python
qg8.node_types()
```

    {'constant',
     'expectationvalue',
     'input',
     'join',
     'ket',
     'matmul',
     'observable',
     'operator',
     'solvequtip',
     'time',
     'track'}

As a simple example, lets create two generic `input` tensor nodes and fill them with data from a numpy array using the `QuantumGraph.input` method.

```python
x = np.array([1., 2., 3., 4., 5.])
M = np.array([[0,0,1,1,0], [1,0,0,0,0], [1,0,1,0,0], [0,0,0,1,1], [1,0,0,0,1]])

vector = mygraph.input(x)
matrix = mygraph.input(M)
```

Here we have used the `np.array` method to construct a five element vector (a rank 1 tensor with shape [5]) and a 5x5 element matrix (a rank 2 tensor).

QG8 tensor data are efficiently stored in a sparse format as a list of indice arrays `(i,j...)` and values `re` and `im` corresponding to the real and imaginary parts of non-zero elements.

> *If you want to store all elements of the tensor, including zeros, you can use the argument `packing='full'` when creating the node.*

> *By default QG8 stores data in a format depending on the data type of the numpy array. If you want to specify a particular data type you can use the `dtype='name'` keyword argument when defining tensor nodes, where 'name' is a numerical type string accepted as input to the `numpy.dtype()` class.*

One can see what is inside the tensor by calling `node.indices()`, `node.values()` or the `node.full()` method

```python
matrix.indices(), matrix.values()
```

    ([array([0, 0, 1, 2, 2, 3, 3, 4, 4], dtype=uint8),
      array([2, 3, 0, 0, 2, 3, 4, 0, 4], dtype=uint8)],
     array([1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=int64))

```python
matrix.full()
```

    array([[0, 0, 1, 1, 0],
           [1, 0, 0, 0, 0],
           [1, 0, 1, 0, 0],
           [0, 0, 0, 1, 1],
           [1, 0, 0, 0, 1]])

We can now add a simple op node which performs a matrix product of two nodes. In this case the behavior is that of `numpy.matmul()`.

```python
product = mygraph.matmul(matrix, vector)
```

The functions which are called to evaluate op nodes are defined in the `qg8.ops` library.  Each op function takes a `QG8Node` as an input and processes them by setting the `.output` attribute depending on their assigned input nodes (`vector` and `matrix` in this example). These functions are evaluated according to their topological order in the graph when `QuantumGraph.run()` is called.

> *While we have assigned the nodes `vector` and `matrix`, these are just a pointers to nodes in the graph. If you must remove a node from the graph one can use the `mygraph.remove(qg8_node)` method, but this will not remove dependencies of other nodes*.

Now our quantum graph should contain three nodes.

> *Beware that running the above code several times results in adding several nodes to the graph.*

Before we save the graph we must also store the edges between graph nodes (i.e. indicating that `matrix` and `vector` are input nodes to `product`). This can be done by building an adjacency matrix for the graph. Adjacency matrices are not regular graph nodes and will be stored in the graph along with the nodes as "chunks".

```python
A = mygraph.adjacency_matrix()
mygraph.chunks
```

    [<QG8 tensor node:input, shape=(5,), length=5, itype=uint8, dtype=float64>,
     <QG8 tensor node:input, shape=(5, 5), length=9, itype=uint8, dtype=int64>,
     <QG8 op node:matmul>,
     <QG8 adjacency matrix:, shape=(3, 3), length=2>]

To save a graph to file, call `qg8.save('filename',graph)`

```python
qg8.save('mygraph.qg8',mygraph);
```

It can be read in again as a new graph.

```python
savedgraph = qg8.load('mygraph.qg8')
savedgraph.chunks
```

    [<QG8 tensor node:input, shape=(5,), length=5, itype=uint8, dtype=float64>,
     <QG8 tensor node:input, shape=(5, 5), length=9, itype=uint8, dtype=int64>,
     <QG8 op node:matmul>,
     <QG8 adjacency matrix:, shape=(3, 3), length=2>]

We can now execute the graph by calling the `run` method and verify the result. By default the graph is computed up to the last node added to the graph.

```python
result = qg8.run(savedgraph)

print(result)

all(result == np.matmul(M,x)) # verify result
```

    [7. 1. 4. 9. 6.]

    True

## Working example: iSWAP gate

We now show how to build a complete graph for simulating a quantum gate, including Hamiltonian terms, operators, observables and op nodes which connect them.

For illustration we consider a two-qubit iSWAP gate for Rydberg atom qubits, which is a special case of the XY gate family described in M. Morgardo & S. Whitlock, AVS Quantum Sci. 3, 023501 (2021) (open access link: https://arxiv.org/abs/2011.03031). This specific gate protocol works by coupling the qubit states $0$ and $1$ to two auxiliary states called $S$ and $P$ and by exploiting two-body exchange interactions $SP \leftrightarrow PS$.

### Building the Hamiltonian

```python
iSWAP = qg8.QuantumGraph()
```

To represent the Hamiltonian of the two-qubit system use a decomposition in terms of time-independent operators $\hat h_j$ and time-dependent coefficients $c_j(t)$

$$\hat H = \sum_j c_j(t) \hat h_j + h.c.$$

where $h.c.$ stands for Hermitian conjugate.

> *With QG8 it is not necessary to save the Hermitian conjugate operators, since we can use the packing argument `packing="half-hermitian"`. The hermitian conjugate terms will then be automatically added in the solver.*

#### Operators

For this demonstration we define three operators describing the single particle couplings acting on both qubits and the two-particle exchange interaction operator:
\begin{align}
\hat h_1 &= |S\rangle\langle 1|\otimes \mathbb{1} + \mathbb{1}\otimes |S\rangle\langle 1|\\
\hat h_2 &= |P\rangle\langle 0|\otimes \mathbb{1} + \mathbb{1}\otimes |P\rangle\langle 0|\\
\hat h_3 &= |P S\rangle\langle S P|
\end{align}

where the subscripts indicate which qubit the operator is acting on, and $|\alpha \beta\rangle = |\alpha\rangle\otimes|\beta\rangle$ represents a two qubit state, $\otimes$ is the tensor product and $\mathbb{1}$ is the identity operator in the single qubit basis.

To generate these operators as Numpy arrays, lets start by defining a set of single qubit basis vectors and some helper functions which return matrices representing single qubit and two qubit operators in the two-qubit basis. *We can also save a little bit of space by expressing these and the corresponding operators as 8 bit integers.*

```python
# single qubit basis vectors
zero = np.array([[1],[0],[0],[0]],dtype='uint8')
one = np.array([[0],[1],[0],[0]],dtype='uint8')
S = np.array([[0],[0],[1],[0]],dtype='uint8')
P = np.array([[0],[0],[0],[1]],dtype='uint8')
```

```python
# Helper functions for constructing single qubit and two-qubit operators as Numpy matrices
def O1(s1,s2,target=1):
    """
    Function which returns a general one-qubit operator |s1><s2| in a two-qubit basis.
    target : integer specifying the qubit that the operator is acting on
    """
    out = 1;
    for i in range(2):
        if i==target-1:
            out = np.kron(out, s1 @ np.conjugate(s2).T)
        else:
            out = np.kron(out, np.eye(len(s1)));
    return out.astype(s1.dtype)

def O2(s1,s2,s3,s4):
    """
    Function which returns a general two-qubit operator |s1,s2><s3,s4| in matrix form
    """
    out = np.kron(s1,s2) @ np.conjugate(np.kron(s3,s4)).T
    return out.astype(s1.dtype)
```

To store these operators we can use the 'operator' tensor node type.

```python
# define operators
h1 = iSWAP.operator(O1(S, one, target=1) + O1(S, one, target=2),
                    packing = "half-hermitian") # 1->S for both atoms

h2 = iSWAP.operator(O1(P, zero, target=1) + O1(P, zero, target=2),
                    packing = "half-hermitian") # 0->P for both atoms
```

Each call of the operator() method adds a new node to the graph. You can also give the operator a label using the optional `string_id` keyword argument (when written to file this will be truncated to 16 characters).

```python
h3 = iSWAP.operator(O2(P, S, S, P),
                    packing= "half-hermitian",
                    string_id= "interaction",
                    ) # SP->PS for both atoms
```

#### Time-dependent coefficients

To define the time-dependent coefficients lets first create a `time` tensor node which contains the time samples.

> *QuTip currently only accepts a single set of equidistantly spaced time points, so we will use `np.linspace` to define the time grid and to save a little bit of space we can use integer values (samples).*

```python
# define time samples to be used in the simulation
t_list = np.linspace(0,40,41, dtype ='uint8')

time = iSWAP.time(t_list) # from 0 to 1 in 41 steps
```

It is also possible to integrate custom objects with QG8 graphs. In this case one should make sure that the object has a `.shape` attribute as well as `.index()` and `.values()` methods which return numpy arrays.

We will now define two `track` tensor nodes to contain the coefficients themselves which will be associated with the first two operator terms in the Hamiltonian. For this specific example we will use a custom `Track` class which includes two methods for generating pulses. The parameters of these pulses have been optimized by hand.

```python
ch1 = qg8.Track(t_list)
ch1.add_sinepulse(start=0, duration=12, amplitude=0.2613)
ch1.add_sinepulse(start=28, duration=12, amplitude=0.2613)

ch2 = qg8.Track(t_list)
ch2.add_squarepulse(start=12, duration=8, amplitude=0.3912)
ch2.add_squarepulse(start=20, duration=8, amplitude=-0.3912)

track1 = iSWAP.track(ch1, dtype='float32')
track2 = iSWAP.track(ch2, dtype='float32')
```

We can extract the full samples as a numpy array using `node.full()` and plot them.

```python
plt.plot(track1.full());
plt.plot(track2.full());
```

The third coefficient describing the exchange interactions will be time-independent. So in this case it is simplest to add a `constant` node.

```python
track3 = iSWAP.constant(0.1988)
```

### Setting up a simulation with Op nodes

For this demonstration we will use the QuTip solver wrapped as a `solvequtip` op node.

The `solvequtip` op node takes as inputs a `ket` tensor node defining the initial state of the simulation a `time` tensor node defining the time samples used to output the simulation results and an arbitrary number of Hamiltonian terms encoded as `(operator,track)` pairs, in that order. Upon running the graph, the output of the `solvequtip` returns an array of kets evaluated at each time step (a rank 2 tensor).

The initial state for the simulation will be $|01\rangle$, expressed as a row vector (using `np.kron` to do the tensor product).

```python
psi0 = iSWAP.ket(np.kron(zero,one))
```

We can create the `(operator,track)` pairs using join nodes.

```python
op1 = iSWAP.join(h1,track1); op2 = iSWAP.join(h2,track2); op3 = iSWAP.join(h3,track3)
```

> *Reminder: these operations are not actually joining anything at this stage. It is just adding three new `join` nodes to the graph which have `operators` and `tracks` as input nodes. The process of performing the operations will only be done when `QuantumGraph.run()` is called.*

Now we can add the `solvequtip` op node to the graph.

```python
psif = iSWAP.solvequtip(psi0,time,op1,op2,op3)
```

#### Expectation values

While we could just return these wavefunctions as the solution, it may be convenient to directly compute expectation values for certain observables.

For that we define two projection operators, one for the initial state $|01\rangle$ and one for the desired target state $|10\rangle$ and add them as inputs to nodes which compute the expectation value of the time-dependent wavefunction given by the `solvequtip` node. Finally we use a `join` node to combine their outputs into one object for plotting.

```python
P_01 = iSWAP.expectationvalue(psif,iSWAP.observable(O2(zero,one,zero,one)))

P_10 = iSWAP.expectationvalue(psif,iSWAP.observable(O2(one,zero,one,zero)))

output = iSWAP.join(P_01,P_10)
```

Thats it. Now we have completed our graph. Lets take a look at the list of nodes.

```python
iSWAP.nodes
```

    [<QG8 tensor node:operator, shape=(16, 16), length=8, itype=uint8, dtype=uint8>,
     <QG8 tensor node:operator, shape=(16, 16), length=8, itype=uint8, dtype=uint8>,
     <QG8 tensor node:operator, string_id='interaction', shape=(16, 16), length=1, itype=uint8, dtype=uint8>,
     <QG8 tensor node:time, shape=(41,), length=40, itype=uint8, dtype=uint8>,
     <QG8 tensor node:track, shape=(41,), length=24, itype=uint8, dtype=float32>,
     <QG8 tensor node:track, shape=(41,), length=16, itype=uint8, dtype=float32>,
     <QG8 tensor node:constant, shape=(1,), length=1, itype=uint8, dtype=float64>,
     <QG8 tensor node:ket, shape=(16, 1), length=1, itype=uint8, dtype=uint8>,
     <QG8 op node:join>,
     <QG8 op node:join>,
     <QG8 op node:join>,
     <QG8 op node:solvequtip>,
     <QG8 tensor node:observable, shape=(16, 16), length=1, itype=uint8, dtype=uint8>,
     <QG8 op node:expectationvalue>,
     <QG8 tensor node:observable, shape=(16, 16), length=1, itype=uint8, dtype=uint8>,
     <QG8 op node:expectationvalue>,
     <QG8 op node:join>]

Finally we must build the adjacency matrix which describes the connections between nodes and add it to the graph.

```python
A = iSWAP.adjacency_matrix()
```

### Write the graph to a QG8 file

```python
qg8.save("iSWAP.qg8", iSWAP);
```

The resulting file is a bit under 1kB in size.

```python
iSWAP.datasize()  # datasize in bytes
```

    914

### Read in the graph

```python
savedgate = qg8.load("iSWAP.qg8")
savedgate.nodes
```

    [<QG8 tensor node:operator, shape=(16, 16), length=8, itype=uint8, dtype=uint8>,
     <QG8 tensor node:operator, shape=(16, 16), length=8, itype=uint8, dtype=uint8>,
     <QG8 tensor node:operator, string_id='interaction', shape=(16, 16), length=1, itype=uint8, dtype=uint8>,
     <QG8 tensor node:time, shape=(41,), length=40, itype=uint8, dtype=uint8>,
     <QG8 tensor node:track, shape=(41,), length=24, itype=uint8, dtype=float32>,
     <QG8 tensor node:track, shape=(41,), length=16, itype=uint8, dtype=float32>,
     <QG8 tensor node:constant, shape=(1,), length=1, itype=uint8, dtype=float64>,
     <QG8 tensor node:ket, shape=(16, 1), length=1, itype=uint8, dtype=uint8>,
     <QG8 op node:join>,
     <QG8 op node:join>,
     <QG8 op node:join>,
     <QG8 op node:solvequtip>,
     <QG8 tensor node:observable, shape=(16, 16), length=1, itype=uint8, dtype=uint8>,
     <QG8 op node:expectationvalue>,
     <QG8 tensor node:observable, shape=(16, 16), length=1, itype=uint8, dtype=uint8>,
     <QG8 op node:expectationvalue>,
     <QG8 op node:join>]

### Simulate the graph

```python
result = qg8.run(savedgate)
```

```python
plt.plot(result[0],label=r'P_{01}');
plt.plot(result[1],label=r'P_{10}');
plt.ylabel("populations")
plt.xlabel("time (samples)")
plt.legend();
```

This shows that starting from the $|01\rangle$ state the system evolves to the $|10\rangle$ state, thereby realizing a swap operation. At intermediate times the system evolves through auxilliary two-qubit states involving the $|S\rangle$ and $|P\rangle$ states (region where $P_{01}$ and $P_{10}$ are approximately zero), but returns back to the computational subspace at the end of the protocol.

We leave it as an exercise to confirm the full gate matrix for this protocol and to experiment with your own gate protocols or other quantum problems.


# Logbook

```python
# %load ../imports.py
"""
These is the standard setup for the notebooks.
"""

%matplotlib inline
%load_ext autoreload
%autoreload 2

#from jupyterthemes import jtplot
#jtplot.style(theme='onedork', context='notebook', ticks=True, grid=False)

import pandas as pd
pd.options.display.max_rows = 999
pd.options.display.max_columns = 999
pd.set_option("display.max_columns", None)
import numpy as np
import os
import matplotlib.pyplot as plt
#plt.style.use('paper')

#import data
import copy
from rolldecay.bis_system import BisSystem
from rolldecay import database
from mdldb.tables import Run

from sklearn.pipeline import Pipeline
from rolldecayestimators.transformers import CutTransformer, LowpassFilterDerivatorTransformer, ScaleFactorTransformer, OffsetTransformer
from rolldecayestimators.direct_estimator_cubic import EstimatorQuadraticB, EstimatorCubic
from rolldecayestimators.ikeda_estimator import IkedaQuadraticEstimator
import rolldecayestimators.equations as equations
import rolldecayestimators.lambdas as lambdas
from rolldecayestimators.substitute_dynamic_symbols import lambdify
import rolldecayestimators.symbols as symbols
import sympy as sp

from sklearn.metrics import r2_score

```

## Nomenclature
* **Ikeda** refer to the original ikeda method (require strip calculations and Lewis sections).
* **Simplified Ikeda** or **SI** refer to the simplified Ikeda method (explicit formula).

[Logbook](#logbook)
## 2020-09-22
I've been working with adding additional material to extend the paper for the [SAOC](https://www.tandfonline.com/toc/tsos20/current).

### Ikeda for one ship
I have implemented the original ikeda method and made calculations for one of the ships in the database: [01.01_ikeda_one_ship](06_ikeda/01.01_ikeda_one_ship.ipynb). I developed a python wrapper for SSPA strip calculation tool *ScoresII* here: [pyscores2](https://github.com/martinlarsalbert/pyscores2) and calculated the wave roll damping $B_W$.
The results were compared with the Simplified Ikeda (SI).
* It was found that the $B_W$ was quite different. It was however hard to say which one was the better.

* It was also found that the speed dependecy of the eddy component $B_E$ was not implemented as in .

* It was also found that the $B_{BK}$ contribution was quite different: [Ikeda vs. SI](06_ikeda/01.01_ikeda_one_ship.ipynb#ikeda_vs_si).

* For this ship $B_{BK}$ calculated with original ikeda implementation gave better results:
[Ikeda vs. SI2](06_ikeda/01.01_ikeda_one_ship.ipynb#ikeda_vs_si2).

I made a new comparison between SI and model test results where the influence of findings above were investigated ([07.2_si_compare](04_simplified_ikeda/07.2_si_compare.ipynb)). The $B_E$ speed dependency gave an improvement. Switching the $B_{BK}$ also gave an improved accuracy with maximum accuracy with fixed bilge radius $ \frac{R}{beam}=0.08$ according to:[scores](04_simplified_ikeda/07.2_si_compare.ipynb#scores).

### Validation
I also made a validation study to reproduce results from Carl-Johan for the *S175* ship: [Ikeda S175](06_ikeda/01.02_ikeda_S175.ipynb).

## 2020-09-23

### Recalculating $B_E$
[Sectional Lewis coefficients](06_ikeda/01.01_ikeda_one_ship.ipynb#lewis_coefficients) have been calculated. These are used to calculate the eddy damping: $B_E$ [here](06_ikeda/01.01_ikeda_one_ship.ipynb#eddy). This is also a translation of Carl-Johans matlab code which seams to be an implementation according to  with some exceptions where changes according to . It was a bit unclear if the $\overline{OG}$ should be possitive uppward (Journe) or into the water (Ikeda). It was also unclear if Journe or Ikeda was the best, this should be investigated!

 also proposes these formulas to estimate the bilge radius:
$$
R=\left\{\begin{array}{ll}
2 a \sqrt{\frac{a_{0}(\sigma-1)}{\pi-4}} & (R<d, R1\right) \\
B / 2 & \left(H_{0} \leq 1, R / d>H_{0}\right)
\end{array}\right.
$$

## 2020-09-24
* I found that there might be a difference in the speed dependency between Ikeda and Journe:

$$
\begin{aligned}
\frac{B_{W}}{B_{k 0}} &=0.5\left[\left(\left(A_{2}+1\right)+\left(A_{2}-1\right) \tanh (20 \tau-b)\right\}\right.\\
&\left.+\left(2 A_{1}-A_{2}-1\right) \exp \left\{-150(\tau-0.25)^{2}\right\}\right]
\end{aligned}
$$

$$
B_{44 S}=B_{44} \cdot\left\{0.5 \cdot\left(\begin{array}{l}
A_{2}+1+\left(A_{2}-1\right) \cdot \tanh [20 \cdot(\Omega-0.3)] \\
+\left(2 \cdot A_{1}-A_{2}-1\right) \cdot e^{-150(\omega-0.25)^{2}}
\end{array}\right)-1.0\right\}
$$

The difference is the $b$ coefficient which Journee has set to $20*0.3$.  call this $b$ the extinction coefficient.

* I found that the speed dependency correction for $B_W$ is not $1$ when the ship speed is $0$ which is quite strange. Is there an implementation error there?

Here is the current implementation (which is clearly wrong according to the plot below, since it is not 1 at V=0). Have I, Carl-Johan or Journée made a misstaker here???

```python
from numpy import exp, tanh
def B_W_speed_correction_factor(w, V, d, g=9.81):
    """
        Wave damping speed correction
        Parameters
        ----------
        w
            "omega" frequency of motion [rad/s]
        V
            ship speed [m/s]
        d
            ship draught [m]
        g
            gravity

        Returns
        -------
        Bw_div_Bw0
            Bw_div_Bw0 = B_W/B_W0

        """
    OMEGA = w * V / g
    zeta_d = w ** 2 * d / g
    A1 = 1 + zeta_d ** (-1.2) * exp(-2 * zeta_d)
    A2 = 0.5 + zeta_d ** (-1) * exp(-2 * zeta_d)

    Bw_div_Bw0 = 0.5 * (
            ((A1 + 1) + (A2 - 1) * tanh(20 * (OMEGA - 0.3))) + (2 * A1 - A2 - 1) * exp(-150 * (OMEGA - 0.25) ** 2))

    return Bw_div_Bw0
```

```python
w = 0.2
lpp = 175
omega_hat = 0.719
beam = 25.40
d = 9.5
g=9.81
w = lambdas.omega(beam=beam, g=g, omega_hat=omega_hat)
Fn = np.linspace(0, 0.3, 100)
V = Fn*np.sqrt(lpp*g)

factor = B_W_speed_correction_factor(w=w, V=V, d=d)

fig,ax=plt.subplots()
ax.plot(Fn,factor)
ax.grid(True)
ax.set_ylabel(r'$\frac{B_W}{B_{W0}}$')
ax.set_xlabel('Ship speed Fn [-]');
ax.set_title('$B_W$ Speed dependency factor');
```

This plot should look something as:

I would say that the Journee equation is even worse:

```python
from rolldecayestimators.ikeda_speed import B_W_speed_correction_factor_journee

factor = B_W_speed_correction_factor_journee(w=w, V=V, d=d)

fig,ax=plt.subplots()
ax.plot(Fn,factor)
ax.grid(True)
ax.set_ylabel(r'$\frac{B_W}{B_{W0}}$')
ax.set_xlabel('Ship speed Fn [-]');
ax.set_title('$B_W$ Speed dependency factor');
```

I went back to the original Ikeda paper  and this implementation is in fact much more reasonable. The Himeno equation is the same (but the value of $b$) is not written out (it is $20*0.3$ in Ikeda). I found a typo in Carl-Johans implementation which was the root cause of this error. (But Journée is simply wrong still).

```python
def B_W_speed_correction_factor_ikeda(w, V, d, b=20*0.3, g=9.81):
    """
        Wave damping speed correction
        Parameters
        ----------
        w
            "omega" frequency of motion [rad/s]
        V
            ship speed [m/s]
        d
            ship draught [m]
        g
            gravity

        Returns
        -------
        Bw_div_Bw0
            Bw_div_Bw0 = B_W/B_W0

        """
    tau=w*V/g
    zeta_d=w**2*d/g
    A1=1+zeta_d**(-1.2)*exp(-2*zeta_d);
    A2=0.5+zeta_d**(-1)*exp(-2*zeta_d);

    Bw_div_Bw0=0.5*((A2+1)+(A2-1)*tanh(20*tau-b)
                    +(2*A1-A2-1)*exp(-150*(tau-0.25)**2))

    return Bw_div_Bw0
```

```python
factor = B_W_speed_correction_factor_ikeda(w=w, V=V, d=d)

fig,ax=plt.subplots()
ax.plot(Fn,factor)
ax.grid(True)
ax.set_ylabel(r'$\frac{B_W}{B_{W0}}$')
ax.set_xlabel('Ship speed Fn [-]');
ax.set_title('$B_W$ Speed dependency factor');
```

Should the speed dependence factor be modified, here is an example where $b$ is varied:

```python
fig,ax=plt.subplots()
fig.set_size_inches(10,4)

for b in np.linspace(1,8,8):
    factor = B_W_speed_correction_factor_ikeda(w=w, V=V, d=d, b=b)

    ax.plot(Fn,factor, label='b=%0.1f'%b)

ax.grid(True)
ax.set_ylabel(r'$\frac{B_W}{B_{W0}}$')
ax.set_xlabel('Ship speed Fn [-]');
ax.set_title('$B_W$ Speed dependency factor');
ax.set_ylim(1,3)
ax.legend();

```

```python
def f(b=6.0,omega_hat=0.719):

    w = lambdas.omega(beam=beam, g=g, omega_hat=omega_hat)
    factor = B_W_speed_correction_factor_ikeda(w=w, V=V, d=d, b=b)

    fig,ax=plt.subplots()
    fig.set_size_inches(10,4)
    ax.plot(Fn,factor)
    ax.grid(True)
    ax.set_ylabel(r'$\frac{B_W}{B_{W0}}$')
    ax.set_xlabel('Ship speed Fn [-]');
    ax.set_title('$B_W$ Speed dependency factor');
    ax.set_ylim(1,4)
```

```python
from ipywidgets import interactive

interactive_plot = interactive(f, b=(0.1,8,0.5),omega_hat=(0.2,1,0.1))
output = interactive_plot.children[-1]
output.layout.height = '350px'
interactive_plot
```

## 2020-09-25
* Rerun the [04.2_maa_mdl_db_build.ipynb](rolldecay/02_roll_damping_DB/04.2_maa_mdl_db_build.ipynb).
  * With fixed $B_W$ speed dependency
  * Alternative bilge keel R/B=0.08
* Rerun the cross validation and got ([quite much better score with SI](05_new_method/05.1_maa_new_method_and_correction.ipynb#score))

* Made a new sensitivity study of the SI with some interesting plots: ([07.3_si_sensitivity2.ipynb](04_simplified_ikeda/07.3_si_sensitivity2.ipynb)).

## 2020-09-28
* Improved the plots in the ([07.3_si_sensitivity2.ipynb](04_simplified_ikeda/07.3_si_sensitivity2.ipynb)). It seems unlikely that the $\hat{\omega}$ and $F_n$ dependencies are correct in the SI.
* Started to assemble more section data ([01.03_ikeda_many_dev](06_ikeda/01.03_ikeda_many_dev.ipynb)) to make a larger comparison with the Ikeda method.

## 2020-09-30

There is equations to estimate the bilge radius according to :
$$R_{b}=\left\{\begin{array}{ll}
2 D \sqrt{\frac{H_{0}(\sigma-1)}{\pi-4}} \text { for } R_{b}<D, R<\frac{B}{2} \\
D & \text { for } H_{0} \geq 1, \frac{R_{b}}{D}>1 \\
\frac{B}{2} & \text { for } H_{0} \leq 1, \frac{R_{b}}{D}>H_{0}
\end{array}\right\}$$
This one has now been implemented (but I don't know if it is good).

* Rerun this one ([ikeda_vs_si2](06_ikeda/01.01_ikeda_one_ship.ipynb#ikeda_vs_si2)) and realized that the frequency in scoresII is not scaled correctly... This needs to be investigated!
* Also realized that I put a max constraint on the $\hat{B_{W0}}$ in the SI method, this also needs to be investigated and reconsidered. (The constraint do however increase the accuracy).

## 2020-10-01
* The scaling of ScoresII is now solved.
* Got quite good results with the Ikeda class now
* Used the estimation of bilge keel radius (above) ([ikeda_r_ikeda](06_ikeda/01.01_ikeda_one_ship.ipynb#ikeda_r_ikeda)) which gave better results than the ([ikeda_r_guess](06_ikeda/01.01_ikeda_one_ship.ipynb#ikeda_r_guess)).
* Finding a good value for bilge keel radius therefore seem to be important!
* I investigated the [R-dependency](06_ikeda/01.01_ikeda_one_ship.ipynb#R_dependency) to confirm this. It seems that the bilge keel damping can differ 2-3 times depending on the bilge radius.

## 2020-10-02
* Got very good results with Ikeda for one of the ships: ([01.03_ikeda_many_dev](06_ikeda/01.03_ikeda_many_dev.ipynb))
* I got very good results with the Ikeda method: ([01.04_ikeda_many](06_ikeda/01.04_ikeda_many.ipynb)).
  * Also made a comparison with the SI which had significantly worse results
  * The SI prediction error seem to occur mainly at high B/D.
  * It therefore seem that Ikedas method is working pretty well.
  * The SI is not, at least not outside its limits.

## 2020-10-05
* Investigated the residual between ikeda and si: [01.04_ikeda_many - residuals](06_ikeda/01.04_ikeda_many.ipynb#residuals) and it seems that the $B_W$ is the one to blame.

## 2020-10-06
* The wave damping error is not increasing with speed ([B_W-speed-factor-residual](06_ikeda/01.04_ikeda_many.ipynb#B_W_speed_factor_residual)).

## 2020-10-14
There was a restriction on damping coefficients $B_1,...,B_3 >= 0$ in ```python rolldecayestimators.estimators_cubic ``` which seems to be a false assumption (based on results from ShipFlow Motions). This is however not supported by Ikeda ([07.3_si_sensitivity2.ipynb](04_simplified_ikeda/07.3_si_sensitivity2.ipynb)) where $B_2$ is always positive. This is an interesting deviation!

* Droped this constraint and rebuild the roll damping database
* Will need to rerun the analysis to see if this changes anything.

## 2020-10-20
* Looked a bit closer at the [08.1_Be_equation](04_simplified_ikeda/08.1_Be_equation.ipynb) and found it work well for some cases, but not for all. I'm not sure if I've made a misstake or if this is something interesting..?

## 2020-10-21
Spend the day rewriting the paper, based on feedback from Jonas. I'm beginning to doubt the approach of using limited/unlimited. Does it not make more sense to just look at accuracy within/outside the input limits of the SI?

## 2020-10-22
* Investigated the SI within its limits (also using the limited approach): [10.1_si_limits.ipynb](04_simplified_ikeda/10.1_si_limits.ipynb). And found that it gave quite good results within the limits (but the remaining tests are very few)
* Also found that the **bBk/B** and **CMID** limits makes the other limits redundant for the present data
* This analysis was made for the limited approach, does that makes sense or should the other one be used instead?
* Calculated the correlation coefficient for the error, but Wengang said that it can only be used when the function is linear.
* Made a version with the unlimited approach: [10.2_si_limits.ipynb](04_simplified_ikeda/10.2_si_limits.ipynb). Found some point with **B/d** > 4.5 but with low error.
* Tried to find what other variable that is causing this behaviour, but haven't fully understood it. Hower made some 3D surface plots of the SI method, which reveals huge extrapolations!!!
* Found for instance a huge [mountain](04_simplified_ikeda/10.2_si_limits.ipynb#mountain) in the polynom!

## References

```python

```

```python

```

```python

```

```python

```

```python

```