```python
import iris
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
```

\begin{equation}
\int_{0}^{t} FA \; dt = \bar{F} \int_{0}^{t} A' \; dt \; + \; \bar{A} \int_{0}^{t} F' \; dt
\end{equation}

```python
evap_area_file = '/g/data/r87/dbi599/CMIP6/DAMIP/CSIRO/ACCESS-ESM1-5/hist-GHG/r1i1p1f1/Ayr/evspsbl/gn/v20200615/evspsbl-pe-region-sum-anomaly_Ayr_ACCESS-ESM1-5_hist-GHG_r1i1p1f1_gn_185001-202012-cumsum.nc'
evap_bar_file = '/g/data/r87/dbi599/CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/piControl/r1i1p1f1/Ayr/evspsbl/gn/v20191214/evspsbl-pe-region-mean_Ayr_ACCESS-ESM1-5_piControl_r1i1p1f1_gn_010101-100012.nc'
evap_dashed_file = '/g/data/r87/dbi599/CMIP6/DAMIP/CSIRO/ACCESS-ESM1-5/hist-GHG/r1i1p1f1/Ayr/evspsbl/gn/v20200615/evspsbl-pe-region-mean-anomaly_Ayr_ACCESS-ESM1-5_hist-GHG_r1i1p1f1_gn_185001-202012-cumsum.nc'
area_bar_file = '/g/data/r87/dbi599/CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/piControl/r1i1p1f1/Ayr/areacella/gn/v20191214/areacella-pe-region-sum_Ayr_ACCESS-ESM1-5_piControl_r1i1p1f1_gn_010101-100012.nc'
area_dashed_file = '/g/data/r87/dbi599/CMIP6/DAMIP/CSIRO/ACCESS-ESM1-5/hist-GHG/r1i1p1f1/Ayr/areacella/gn/v20200615/areacella-pe-region-sum-anomaly_Ayr_ACCESS-ESM1-5_hist-GHG_r1i1p1f1_gn_185001-202012-cumsum.nc'
```

```python
evap_area_cube = iris.load_cube(evap_area_file, 'water_evapotranspiration_flux')
evap_bar_cube = iris.load_cube(evap_bar_file, 'water_evapotranspiration_flux')
evap_dashed_cube = iris.load_cube(evap_dashed_file, 'water_evapotranspiration_flux')
area_bar_cube = iris.load_cube(area_bar_file, 'cell_area')
area_dashed_cube = iris.load_cube(area_dashed_file, 'cell_area')
```

```python
def get_time_integral(cube, variable):
    """Get time integral"""

    start = cube[0, 0:5, -1].data
    value = cube[-1, 0:5, -1].data - start

    return value

def get_average(cube, variable, ntime):
    """Get average"""

    data = cube.data[0:ntime, 0:5, -1]
    average = np.mean(data, axis=0)

    return average
```

```python
evap_area_integral = get_time_integral(evap_area_cube, 'water_evapotranspiration_flux')
evap_dashed_integral = get_time_integral(evap_dashed_cube, 'water_evapotranspiration_flux')
area_dashed_integral = get_time_integral(area_dashed_cube, 'cell_area')
```

```python
evap_bar = get_average(evap_bar_cube, 'water_evapotranspiration_flux', evap_area_cube.shape[0])
area_bar = get_average(area_bar_cube, 'cell_area', evap_area_cube.shape[0])
```

```python
evap_area_integral.data
```

    array([-8.97865292e+16,  4.66662497e+17,  1.17329324e+17,  2.89853212e+17,
            6.27760929e+16])

```python
(evap_bar * area_dashed_integral) + (area_bar * evap_dashed_integral)
```

    masked_array(data=[-8.95757164e+16,  4.64583727e+17,  1.19091390e+17,
                        2.91554073e+17,  6.29280691e+16],
                 mask=False,
           fill_value=1e+20)

```python
area_component = evap_bar * area_dashed_integral
intensity_component = area_bar * evap_dashed_integral
```

```python
data = [['total', 'SH precip', evap_area_integral[0]],
        ['total', 'SH evap', evap_area_integral[1]],
        ['total', 'tropical precip', evap_area_integral[2]],
        ['total', 'NH evap', evap_area_integral[3]],
        ['total', 'NH precip', evap_area_integral[4]],
        ['area', 'SH precip', area_component[0]],
        ['area', 'SH evap', area_component[1]],
        ['area', 'tropical precip', area_component[2]],
        ['area', 'NH evap', area_component[3]],
        ['area', 'NH precip', area_component[4]],
        ['intensity', 'SH precip', intensity_component[0]],
        ['intensity', 'SH evap', intensity_component[1]],
        ['intensity', 'tropical precip', intensity_component[2]],
        ['intensity', 'NH evap', intensity_component[3]],
        ['intensity', 'NH precip', intensity_component[4]],
       ]
```

```python
df = pd.DataFrame(data, columns = ['component', 'P-E region', 'time-integrated anomaly (kg)'])
```

```python
df
```

      component
      P-E region
      time-integrated anomaly (kg)

      0
      total
      SH precip
      -8.978653e+16

      1
      total
      SH evap
      4.666625e+17

      2
      total
      tropical precip
      1.173293e+17

      3
      total
      NH evap
      2.898532e+17

      4
      total
      NH precip
      6.277609e+16

      5
      area
      SH precip
      -6.177479e+16

      6
      area
      SH evap
      2.475651e+17

      7
      area
      tropical precip
      -6.613969e+16

      8
      area
      NH evap
      6.413645e+16

      9
      area
      NH precip
      -3.762233e+16

      10
      intensity
      SH precip
      -2.780093e+16

      11
      intensity
      SH evap
      2.170186e+17

      12
      intensity
      tropical precip
      1.852311e+17

      13
      intensity
      NH evap
      2.274176e+17

      14
      intensity
      NH precip
      1.005504e+17

```python
#plt.figure(figsize=[12, 12])
sns.catplot(data=df, kind="bar", x="P-E region", y="time-integrated anomaly (kg)", hue="component")
```

```python

```


# US Production Data for RBC Modeling

```python
import pandas as pd
import numpy as np
import fredpy as fp
import matplotlib.pyplot as plt
plt.style.use('classic')
%matplotlib inline
pd.plotting.register_matplotlib_converters()
```

```python
# Load API key
fp.api_key = fp.load_api_key('fred_api_key.txt')

# Download nominal GDP, nominal personal consumption expenditures, nominal
# gross private domestic investment, the GDP deflator, and an index of hours
# worked in the nonfarm business sector produced by the BLS. All data are
# from FRED and are quarterly.
gdp = fp.series('GDP')
cons = fp.series('PCEC')
invest = fp.series('GPDI')
hours = fp.series('HOANBS')
defl = fp.series('GDPDEF')

# Make sure that all of the downloaded series have the same data ranges
gdp,cons,invest,hours,defl = fp.window_equalize([gdp,cons,invest,hours,defl])

# Compute real GDP, real consumption, real investment
gdp.data = gdp.data/defl.data*100
cons.data = cons.data/defl.data*100
invest.data = invest.data/defl.data*100

# Print units
print('Hours units:   ',hours.units)
print('Deflator units:',defl.units)
```

    Hours units:    Index 2012=100
    Deflator units: Index 2012=100

Next, compute the quarterly capital stock series for the US using the perpetual inventory method. The discrete-time Solow growth model is given by:

\begin{align}
Y_t & = A_tK_t^{\alpha}L_t^{1-\alpha} \tag{1}\\
C_t & = (1-s)Y_t \tag{2}\\
Y_t & = C_t + I_t \tag{3}\\
K_{t+1} & = I_t + (1-\delta)K_t \tag{4}\\
A_{t+1} & = (1+g)A_t \tag{5}\\
L_{t+1} & = (1+n)L_t \tag{6}.
\end{align}

Here the model is assumed to be quarterly so $n$ is the *quarterly* growth rate of labor hours, $g$ is the *quarterly* growth rate of TFP, and $\delta$ is the *quarterly* rate of depreciation of the capital stock. Given a value of the quarterly depreciation rate $\delta$, an investment series $I_t$, and an initial capital stock $K_0$, the law of motion for the capital stock, Equation (4), can be used to compute an implied capital series. But we don't know $K_0$ or $\delta$ so we'll have to *calibrate* these values using statistics computed from the data that we've already obtained.

Let lowercase letters denote a variable that's been divided by $A_t^{1/(1-\alpha)}L_t$. E.g.,

\begin{align}
y_t = \frac{Y_t}{A_t^{1/(1-\alpha)}L_t}\tag{7}
\end{align}

Then (after substituting consumption from the model), the scaled version of the model can be written as:

\begin{align}
y_t & = k_t^{\alpha} \tag{8}\\
i_t & = sy_t \tag{9}\\
k_{t+1} & = i_t + (1-\delta-n-g')k_t,\tag{10}
\end{align}

where $g' = g/(1-\alpha)$ is the growth rate of $A_t^{1/(1-\alpha)}$. In the steady state:

\begin{align}
k & = \left(\frac{s}{\delta+n+g'}\right)^{\frac{1}{1-\alpha}} \tag{11}
\end{align}

which means that the ratio of capital to output is constant:

\begin{align}
\frac{k}{y} & = \frac{s}{\delta+n+g'} \tag{12}
\end{align}

and therefore the steady state ratio of depreciation to output is:

\begin{align}
\overline{\delta K/ Y} & = \frac{\delta s}{\delta + n + g'} \tag{13}
\end{align}

where $\overline{\delta K/ Y}$ is the long-run average ratio of depreciation to output. We can use Equation (13) to calibrate $\delta$ given $\overline{\delta K/ Y}$, $s$, $n$, and $g'$.

Furthermore, in the steady state, the growth rate of output is constant:

\begin{align}
\frac{\Delta Y}{Y} & = n + g' \tag{14}
\end{align}

1. Assume $\alpha = 0.35$.
2. Calibrate $s$ as the average of ratio of investment to GDP.
3. Calibrate $n$ as the average quarterly growth rate of labor hours.
4. Calibrate $g'$ as the average quarterly growth rate of real GDP minus n.
5. Calculate the average ratio of depreciation to GDP $\overline{\delta K/ Y}$ and use the result to calibrate $\delta$. That is, find the average ratio of Current-Cost Depreciation of Fixed Assets (FRED series ID: M1TTOTL1ES000) to GDP (FRED series ID: GDPA). Then calibrate $\delta$ from the following steady state relationship:
\begin{align}
\delta & = \frac{\left( \overline{\delta K/ Y} \right)\left(n + g' \right)}{s - \left( \overline{\delta K/ Y} \right)} \tag{15}
\end{align}
6. Calibrate $K_0$ by asusming that the capital stock is initially equal to its steady state value:
\begin{align}
K_0 & = \left(\frac{s}{\delta + n + g'}\right) Y_0 \tag{16}
\end{align}

Then, armed with calibrated values for $K_0$ and $\delta$, compute $K_1, K_2, \ldots$ recursively. See Timothy Kehoe's notes for more information on the perpetual inventory method:

http://users.econ.umn.edu/~tkehoe/classes/GrowthAccountingNotes.pdf

```python
# Set the capital share of income
alpha = 0.35

# Average saving rate
s = np.mean(invest.data/gdp.data)

# Average quarterly labor hours growth rate
n = (hours.data[-1]/hours.data[0])**(1/(len(hours.data)-1)) - 1

# Average quarterly real GDP growth rate
g = ((gdp.data[-1]/gdp.data[0])**(1/(len(gdp.data)-1)) - 1) - n

# Compute annual depreciation rate
depA = fp.series('M1TTOTL1ES000')
gdpA = fp.series('gdpa')

gdpA = gdpA.window([gdp.data.index[0],gdp.data.index[-1]])
gdpA,depA = fp.window_equalize([gdpA,depA])

deltaKY = np.mean(depA.data/gdpA.data)
delta = (n+g)*deltaKY/(s-deltaKY)

# print calibrated values:
print('Avg saving rate:        ',round(s,5))
print('Avg annual labor growth:',round(4*n,5))
print('Avg annual gdp growth:  ',round(4*g,5))
print('Avg annual dep rate:    ',round(4*delta,5))

# Construct the capital series. Note that the GPD and investment data are reported on an annualized basis
# so divide by 4 to get quarterly data.
capital = np.zeros(len(gdp.data))
capital[0] = gdp.data[0]/4*s/(n+g+delta)

for t in range(len(gdp.data)-1):
    capital[t+1] = invest.data[t]/4 + (1-delta)*capital[t]

# Save in a fredpy series
capital = fp.to_fred_series(data = capital,dates =gdp.data.index,units = gdp.units,title='Capital stock of the US',frequency='Quarterly')

# plot the computed capital series
plt.plot(capital.data.index,capital.data,'-',lw=3,alpha = 0.7)
plt.ylabel(capital.units)
plt.title(capital.title)
plt.grid()
```

```python
# Compute TFP
tfp = gdp.data/capital.data**alpha/hours.data**(1-alpha)
tfp = fp.to_fred_series(data = tfp,dates =gdp.data.index,units = gdp.units,title='TFP of the US',frequency='Quarterly')

# Plot the computed capital series
plt.plot(tfp.data.index,tfp.data,'-',lw=3,alpha = 0.7)
plt.ylabel(tfp.units)
plt.title(tfp.title)
plt.grid()
```

```python
# Convert each series into per capita using civilian pop 16 and over
gdp = gdp.per_capita(civ_pop=True)
cons = cons.per_capita(civ_pop=True)
invest = invest.per_capita(civ_pop=True)
hours = hours.per_capita(civ_pop=True)
capital = capital.per_capita(civ_pop=True)

# Put GDP, consumption, and investment in units of thousands of dollars per person
gdp.data = gdp.data*1000
cons.data = cons.data*1000
invest.data = invest.data*1000
capital.data = capital.data*1000

# Scale hours per person to equal 100 on October (Quarter III) of 2012
hours.data = hours.data/hours.data.loc['2012-10-01']*100

# Make sure TFP series has same length as the rest (since the .per_capita() function may affect the date range.
tfp,gdp = fp.window_equalize([tfp,gdp])

# Compute and plot log real GDP, log consumption, log investment, log hours
gdp_log = gdp.log()
cons_log = cons.log()
invest_log = invest.log()
hours_log = hours.log()
capital_log = capital.log()
tfp_log = tfp.log()

# HP filter to isolate trend and cyclical components
gdp_log_cycle,gdp_log_trend = gdp_log.hp_filter()
cons_log_cycle,cons_log_trend = cons_log.hp_filter()
invest_log_cycle,invest_log_trend = invest_log.hp_filter()
hours_log_cycle,hours_log_trend = hours_log.hp_filter()
capital_log_cycle,capital_log_trend = capital_log.hp_filter()
tfp_log_cycle,tfp_log_trend = tfp_log.hp_filter()
```

```python
# Create a DataFrame with actual and trend data
data = pd.DataFrame({
        'gdp':gdp.data,
        'gdp_trend':np.exp(gdp_log_trend.data),
        'gdp_cycle':gdp_log_cycle.data,
        'consumption':cons.data,
        'consumption_trend':np.exp(cons_log_trend.data),
        'consumption_cycle':cons_log_cycle.data,
        'investment':invest.data,
        'investment_trend':np.exp(invest_log_trend.data),
        'investment_cycle':invest_log_cycle.data,
        'hours':hours.data,
        'hours_trend':np.exp(hours_log_trend.data),
        'hours_cycle':hours_log_cycle.data,
        'capital':capital.data,
        'capital_trend':np.exp(capital_log_trend.data),
        'capital_cycle':capital_log_cycle.data,
        'tfp':tfp.data,
        'tfp_trend':np.exp(tfp_log_trend.data),
        'tfp_cycle':tfp_log_cycle.data,
    },index = gdp.data.index)

columns_ordered =[]
names = ['gdp','consumption','investment','hours','capital','tfp']
for name in names:
    columns_ordered.append(name)
    columns_ordered.append(name+'_trend')

data[columns_ordered].to_csv('../Csv/rbc_data_actual_trend.csv')

# Create a DataFrame with actual, trend, and cycle data
columns_ordered =[]
names = ['gdp','consumption','investment','hours','capital','tfp']
for name in names:
    columns_ordered.append(name)
    columns_ordered.append(name+'_trend')
    columns_ordered.append(name+'_cycle')

data[columns_ordered].to_csv('../Csv/rbc_data_actual_trend_cycle.csv')
```


## ph237 Caltech 2018
### Gravitational Radiation
#### Assignment #1

```python
# standard preamble for ph237 notebooks
%matplotlib notebook
from __future__ import division, print_function
import numpy as np

#from mpl_toolkits.mplot3d import Axes3D

import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib import cm
plt.style.use('seaborn-paper')
#plt.style.use('fivethirtyeight')

import scipy.constants as const
from astropy.constants import M_sun

mpl.rcParams.update({'text.usetex': False,
                     'lines.linewidth': 2.5,
                     'font.size': 18,
                     'xtick.labelsize': 'medium',
                     'ytick.labelsize': 'medium',
                     'axes.labelsize': 'small',
                     'axes.grid': True,
                     'grid.alpha': 0.73,
                     'lines.markersize': 12,
                     'legend.borderpad': 0.2,
                     'legend.fancybox': True,
                     'legend.fontsize': 13,
                     'legend.framealpha': 0.7,
                     'legend.handletextpad': 0.1,
                     'legend.labelspacing': 0.2,
                     'legend.loc': 'best',
                     'savefig.dpi': 100,
                     'figure.figsize': (9,6),
                     'pdf.compression': 9})
```

## Problem 1: Fabry-Perot Cavity

**There exists a Fabry-Perot cavity with a length of 4000\,m. The power transmission, $T_I$, of the input mirror is 1% and the transmission of the end mirror, $T_E$, is 10 ppm. The cavity is illuminated from the input side with a 100 W laser having a wavelength of 437 nm.**

### Part (a)

** Using the steady state fields approach, solve for the transmitted power as a function of cavity length.**

The Fabry-Perot Cavity is schematically drawn below

To use the steady state fields approach, we make the ansatz that the field is a superposition of a left-going wave and a right going wave to the left of the cavity (with amplitudes $E_{\rm in}$ and $E_{\rm R}$ in the figure), a superposition of a left-going wave and a right going wave inside the cavity  (with amplitudes $E_{1}$ and $E_{2}$ in the figure) and a right-going wave to the right of the cavity ( (with amplitude $E_{\rm T}$).

To use the steady state field approach we have to make sure the ansatz is consistent with the reflectivity and transmissivity at each mirror. This means we solve the system of equations (for $E_1$, $E_2$, $E_{\rm R}$ and $E_{\rm T}$ as a function of L , $\omega$, and $E_{\rm In}$)
\begin{align}
E_{\rm R}&=-r_I E_{\rm In} \\
E_1&=t_I E_{\rm In}-r_I E_2 \\
E_2& =-r_E e^{-2i\phi}E_1 \\
E_{\rm T}&=t_E e^{-i\phi}E_1,
\end{align}
where the phase $\phi =\omega_0L/c$ and the lower case $t$ and $r$ are the amplitude transmissivity and reflectivity (which are the square roots of the energy transmissivity and reflectivity).
This solution for the transmitted field is
\begin{align}
E_{\rm T}=\frac{t_Et_I e^{-i\phi}}{1-r_Ir_E e^{-2i\phi}}E_{\rm In}
\end{align}

The power is proportional to the field modulus squared $P\propto |E|^2$; Hence the transmitted power is
\begin{align}
P_T&=\left |\frac{t_Et_I e^{-i\phi}}{1-r_Ir_E e^{-2i\phi}} \right |^2P_{\rm In} \nonumber \\
&=\frac{T_ET_I}{1+R_IR_E-2r_Ir_E\cos(2\phi)}
,
\end{align}
where $P_{\rm In}$ is the laser power,

### Part b

Draw a diagram of the cavity, label each of the nodes, and write down the Adjacency Matrix, $A$, for the Fabry-Perot cavity. Solve for the System Matrix, $G$ using Mathematica or Python or otherwise, and show how one of the elements of $G$ can be used to find the solution in part a)

We translate the optical system into a directed graph

and then write down the adjacency matrix, defined as

$A_{ij}=\text{value of connection connecting node j to node i}$

as well as the system matrix $G=(1-A)^{-1}$

```python
# Define adjacency matrix for FP
def A(w):
    M = np.zeros([7,7],complex)
    prop = np.exp(-1j*ph(w))
    M[1,0] =  tI
    M[1,5] = -rI
    M[2,1] =  prop
    M[3,2] =  tE
    M[4,2] = -rE
    M[5,4] =  prop
    M[6,0] =  rI
    M[6,5] =  tI
    return M

# function that calculates system matrix G = (1-A)^(-1) for FP
def G(w):
    return np.linalg.inv(np.identity(7,complex) - A(w))

```

Recall that the vector of fields $\vec E =(E_0, \dots, E_6)^T$ obeys
\begin{align}
\vec E =A \vec E +\vec E_{\rm Inj},
\end{align}
where  the vector of injected fields is (in this case) $\vec E_{\rm Inj}=(E_{\rm In},\dots)^T$. This means that we can use the system matrix $G=(1-A)^{-1}$ to find the field at any of the node locations via
\begin{align}
\vec E =G \vec E_{\rm Inj} .
\end{align}
Performing the matrix multiplication, we see that
\begin{align}
E_i=G_{i0}E_{\rm In}.
\end{align}

# Problem 2: Frequency Respons

**In this problem we will compute the frequency response of a LIGO-like interferometer to gravitational waves. Assume that the results from above still hold.**

## Part (a)

**Assume that we now drive the end mirror with a sinusoidal modulation having an amplitude of $x_0$ and a frequency, $\omega$. Write down an expression for the fields reflected from the mirror, utilizing the Jacobi-Anger expansion.**

As is shown below,

suppose the central location of the mirror is $x=L$ and it is driven so that is position becomes $x=L+\delta L$, with $\delta L =x_0 \cos \omega t$. We calculate the values of the reflected field $E_{\rm R}$ (referred to at $x=0$) in terms of the ingoing field $E_{\rm In}$ (also referred to at $x=0$).

At the mirror location $x=L+\delta L$ the reflected field is simply $-r$ times the ingoing field evaluated at the mirror. The propagation to the mirror from $x=0$ contributes a phase factor $e^{-i\phi}$ and the propagation from the mirror to $x=0$, also contributes a phase factor of $e^{-i\phi}$, where $\phi=\omega_0(L+\delta L)/c=\omega_0(L+x_0\cos\omega t)/c$ and $\omega_0$ is the light frequency. Hence the reflected field is
\begin{align}
E_R&=-r_E e^{-2i\phi}E_{\rm In}
=-r_E e^{-2i\omega_0L/c}e^{-2i x_0 \cos (\omega t)/c} E_{\rm In}
\end{align}
Taylor expanding in small $x_0$ (at fancy restaurants they call this utilizing the Jacobi-Anger expansion)
\begin{align}
E_R=-r_E e^{-2i\omega_0 L/c}\left[1-i\frac{x_0\omega_0}{c}(e^{i\omega t}+e^{-i\omega t})\right]E_{\rm In} \label{eq:side}
\end{align}

Note if we restore the time dependent factor $e^{i\omega_0t}$ that the first term has a time dependence $e^{i\omega_0t}$ (and is simply the reflected field when there is no modulation) while the second two terms are sideband fields with the time dependence $e^{i(\omega_0\pm\omega)t}$.

## Part b

**Use your knowledge of the frequency dependent System Matrix derived above to compute an expression for the transmitted power. Make a plot of the transfer function of the transmitted power as a function of modulation frequency (the y-axis should be in units of Watts/meter).
Hint Remember that the transmitted field will be the sum of the DC fields (computed above) and the AC fields}**

Now consider the Fabry Perot Cavity of problem one and imagine that we modulate the end mirror with $\delta L =x_0 \cos\omega t$. From part (a), we know that this produces sideband fields at node 4, i.e
\begin{align}
E_4=-r_E E_2+ir_E\frac{x_0\omega_0}{c}(e^{i\omega t}+e^{-i\omega t})E_{2},
\end{align}
As we are working to first order in the amplitude modulation, we can take $x_0=0$ when we evaluate $E_2$ the second term. From problem 1, when $x_0=0$, we know how to evaluate $E_2$ in terms of $E_{\rm In}$ and the system matrix $E_2=G_{20}(\omega_0)E_{\rm in}$. Hence
\begin{align}
E_4=-r_E E_2+ir_E\frac{x_0\omega_0}{c}(e^{i\omega t}+e^{-i\omega t})G_{20}(\omega_0)E_{\rm in}.
\end{align}
Here we are now writing the system matrix $G(\omega)$ as a function of frequency, with the frequency dependence coming from the frequency dependent phase $\phi(\omega)=\omega L/c$. Thus we see that we are effectively injecting fields at the sideband frequencies $\omega_0\pm \omega$ at node 4. The system matrix evaluated at the sideband frequencies also governs how light at the sideband frequencies propagates through the optical system. Thus we can write the complete field (with it's time dependence) as
\begin{align}
\vec E(t)&=G(\omega_0)
\begin{bmatrix}
E_{\rm In} \\ 0 \\0 \\0 \\0 \\0 \\0
\end{bmatrix}
e^{i\omega_0 t} \nonumber \\
&+G(\omega_0 +\omega)
\begin{bmatrix}
0 \\ 0 \\ 0 \\0 \\ ir_EG_{20}(\omega_0)E_{\rm In}x_0\omega_0/c \\ 0 \\0
\end{bmatrix}
e^{i(\omega_0+\omega)t}
+G(\omega_0 -\omega)
\begin{bmatrix}
0 \\ 0 \\ 0 \\0 \\ ir_EG_{20}(\omega_0)E_{\rm In}x_0\omega_0/c \\ 0 \\0
\end{bmatrix}
e^{i(\omega_0-\omega)t}
\end{align}

Performing the matrix multiplication yields a transmitted field (including all of the time dependence) of
\begin{align}
E_T(t)&=G_{30}(\omega_0)E_{\rm In}e^{i\omega_0 t} \nonumber \\
&+G_{34}(\omega_0+\omega)ir_EG_{20}(\omega_0)x_0\frac{\omega_0}{c}E_{\rm In}e^{i(\omega+\omega_0)t}
+G_{34}(\omega_0-\omega)ir_EG_{20}(\omega_0)x_0\frac{\omega_0}{c}E_{\rm In}e^{i(-\omega+\omega_0)t} \label{eq:ETmod}
\end{align}

Thus we see that the transmitted field also has components at the carrier frequency $\omega_0$ and and the sideband frequencies $\omega_0\pm \omega$.

This produces an output power with a DC component  (computed in problem 1) and a slowly varying (relative to the carrier frequency) modulation at $\omega$. Namely, anytime the complex electric field is of the form
\begin{align}
E(t)=E_0e^{i\omega_0 t}+E_+e^{i(\omega_0+\omega)t}+E_-e^{i(\omega_0-\omega)t},
\end{align}
with $E_\pm\ll E_0$, then
the power is
\begin{align}
P&\propto |E|^2 \nonumber \\
&=|E_0|^2+E_0 e^{i\omega_0 t}(E_+^*e^{-i(\omega_0+\omega)t}+E_-^*e^{i(\omega_0+\omega)t}) +E_0^* e^{-i\omega_0 t}(E_+e^{i(\omega_0+\omega)t}+E_-e^{-i(\omega_0+\omega)t}) +\mathcal{O}(E_\pm^2) \nonumber \\
&=|E_0|^2+e^{i\omega t}\left[E_0^*E_++E_0E_-^*\right]+e^{-i\omega t}\left[E_0E_+^*+E_0^*E_-\right] \nonumber \\
&=|E_0|^2 +2A\cos(\omega t+\delta),
\end{align}
where $A$ and $\delta$ are the amplitude and phase of $E_0^*E_++E_0E_-^*=Ae^{i\delta}$.

The transmitted field is of this form with
\begin{align}
&E_0=G_{30}(\omega_0)E_{\rm In},& &E_{\pm}=ir_Ex_0\frac{\omega_0}{c}G_{34}(\omega_0\pm \omega)G_{20}(\omega_0)E_{\rm In}&
\end{align}
Note both $E_0$ and $E_{\pm}$ are proportional to $E_{\rm in}$. Note that both $A$ and $|E_0|^2$ are proportional to $|E_{\rm In}|^2$ or equivalently the input power $P_{\rm In}$. Hence, we can write
\begin{align}
P_T=|G_{30}(\omega_0)|^2P_{\rm In}+\Delta P\cos(\omega t+\Phi),
\end{align}
where
\begin{align}
\Delta P e^{i\Phi}&=2G^*_{30}(\omega_0)ir_Ex_0\frac{\omega_0}{c}G_{34}(\omega_0+\omega)G_{20}(\omega_0)+2G_{30}(\omega_0)(ir_Ex_0\frac{\omega_0}{c}G_{34}(\omega_0-\omega)G_{20}(\omega_0))^*P_{\rm In} \nonumber \\
&=2ir_Ex_0\frac{\omega_0}{c}\left[G^*_{30}(\omega_0)G_{34}(\omega_0+\omega)G_{20}(\omega_0)-G_{30}(\omega_0)G^*_{34}(\omega_0-\omega)G^*_{20}(\omega_0)\right]P_{\rm in},
\end{align}
We consider the transfer function from modulation amplitude to power to be $\Delta P/x_0$.

The constants characterizing the Fabry-Perot Cavity in this probelm are below. Note that the length of the cavity is micro-tuned to be slightly off resonance.

```python
#Defining constants
c      = const.c    # speed of light
lam    = 437e-9           # laser wavelength
w0     = 2*np.pi*c/lam    # laser frequency
L0     = 4000             # initial length guess
L      = round(L0/lam)*lam # length of Fabry Perot cavity (tuned to int # of waves)
L     += 10e-12            # add small offset so that there is a linear readout

TI     = 0.014             # power transmissivity of input mirror
TE     = 1e-5            # power transmissivity of end mirror
tI     = np.sqrt(TI)      # amplitude transmissivity of input mirror
tE     = np.sqrt(TE)      # amplitdue transmissivity of end mirror
RI     = 1-TI             # energy reflectivity of input mirror
RE     = 1-TE             # energy reflectivity of end mirror
rI     = np.sqrt(RI)      # amplitude reflectivity of input mirror
rE     = np.sqrt(RE)      # amplitude reflectivity of end mirror
Pin    = 1                # laser power
def ph(w):                # phase accumaled over a half round trip in the FP cavity
        return w*L/c
```

A function that computes $\frac{\Delta P}{x_0} e^{i\Phi}$. The transfer function for the modulation amplitude is the absolute value of this function and the transfer function for the phase is the arguement.

```python
# define a function that computes Delta P/ x_0 in eq. 22 for FP
def P_trans(w):
    wc = w0
    z = 2j*rE*(2*np.pi/lam)*Pin * (np.conj(G(wc)[3,0]) *        (G(wc+w)[3,4] * G(wc)[2,0]) -
                                           G(wc)[3,0]  * np.conj(G(wc-w)[3,4] * G(wc)[2,0]))

    return z
```

Bode plots for magnitude of the tranfer function

```python
#plot (zoomed out) for FP
f = np.logspace(0, 5, 1300)

omega = 2*np.pi*f
#y = list(map(Transfer, omega))
y = np.zeros_like(omega, complex)
for i in range(len(omega)):
    y[i] = P_trans(omega[i])
```

```python
fig1,ax = plt.subplots(2,1, sharex=True, figsize=(8,7))
ax[0].loglog(f, np.abs(y),
            rasterized=True)
ax[0].set_title(r'Single Fabry-Perot transfer function')
ax[0].set_ylabel(r'$\Delta P/x_0 [W/m]$')

ax[1].semilogx(f, np.angle(y, deg=True),
            rasterized=True)
ax[1].set_ylabel(r'Phase [deg]')
ax[1].set_xlabel(r'Frequency [Hz]')
ax[1].set_yticks(np.arange(-180,181,45))

plt.savefig("Figures/2bwide.pdf", bbox_inches='tight')
```

A close up of the resonances. The yellow lines denote the resonances frequency of the static cavity

```python
# plot (zoomed in) for FP
f = np.linspace(0, 1e5, 1000)

y = np.zeros_like(f, complex)
for i in range(len(f)):
    y[i] = P_trans(2*np.pi*f[i])

plt.figure(221)
plt.semilogy(f/1000, np.abs(y),
            rasterized=True)
plt.axvline(c/2/L/1000, color='xkcd:tangerine', alpha=0.5, lw=5)
plt.axvline(c/1/L/1000, color='xkcd:shit', alpha=0.5, lw=5)

plt.xlabel(r'Frequency [kHz]')
plt.ylabel(r'$\Delta P/x_0 [W/m]$')
plt.savefig("Figures/2bclose.pdf", bbox_inches='tight')

```

## Part c

**Now write down a larger Adjacency Matrix which represents a Michelson interferometer with Fabry-Perot cavities in place of the usual end mirrors. Assume that there is a small asymmetry in the Michelson, such that the distance from the beamsplitter to one of the FP cavities is 100 pm larger than the distance to the other cavity.
Make a Bode plot of the transfer function as in part b), but instead of the transmission of the FP cavity, use the anti-symmetric (detection) port as the readout.**

The optical layout of the Michelson interferometer is

The corresponding directed graph is

We take the mirrors in each Fabry-Perot cavity to be identical. We assume the y-axis FP cavity is located a distance d from the beam splitter\footnote{The field at the antisymmetric port depends on d when $\Delta \neq 0$, but that the power does not. Hence we will set $d=0$ in our numerical computations of the power.} and the the x-cavity is located a distance $d+\Delta$ from the beam splitter with $\Delta =100\,pm$. We take the transmissivity and reflectivity of the beam splitter to be $t_{\rm BS}=r_{\rm BS}=1/\sqrt{2}$.

```python
# part 2c
# extra parameters for 2c
TBS = 0.5
tBS = np.sqrt(TBS) #beam splitter tranmissivity
rBS = np.sqrt(1 - TBS) #beam splitter reflectivity

# distance to the y FP cavity.
# The field at the antisymmetric port depends on d when Del is not zero, but the power doesn't
d = 0

Del = 1e-10 #difference between distance to x cavity and the distance to the y cavity
```

We now imagine that the x-axis end mirror is shaken about its central location with $\delta L= x_0\cos\omega t$. Using the results of problem 2 (a) and the logic of 2 (b), this means that the field at the anti-symmetric port is now the field at the carrier frequency plus the result of injecting the field (including the full time dependence)
\begin{align}
E_{side}(t)=ir_E\frac{x_0\omega_0}{c}(e^{i(\omega_0 +\omega)t}+e^{+i(\omega_0-\omega )t})G_{20}(\omega_0)E_{\rm in}
\end{align}
in node 3. Again using the system matrix to propagate the fields, the field at the antisymmetric port is
\begin{align}
E_{AS}(t)&=G_{12,0}(\omega_0)E_{\rm In}e^{i\omega_0 t} \nonumber \\
&+G_{12,3}(\omega_0+\omega)ir_EG_{20}(\omega_0)x_0\frac{\omega_0}{c}E_{\rm In}e^{i(\omega+\omega_0)t}
+G_{12,3}(\omega_0-\omega)ir_EG_{20}(\omega_0)x_0\frac{\omega_0}{c}E_{\rm In}e^{i(-\omega+\omega_0)t}, \label{eq:EASmod}
\end{align}
which is exactly of the same form of the field as the transmitted field from the modulated Fabry-Perot Cavity, with the exception that we have relabeled the elements of the system matrix to correspond to the correct nodes. Hence, the same logic as above reveals that the power at the antisymmetric port is
\begin{align}
P_T=|G_{12,0}(\omega_0)|^2P_{\rm In}+\Delta P\cos(\omega t+\Phi),
\end{align}
where
\begin{align}
\Delta P e^{i\Phi}&=2G^*_{12,0}(\omega_0)ir_Ex_0\frac{\omega_0}{c}G_{12,3}(\omega_0+\omega)G_{20}(\omega_0)+2G_{12,0}(\omega_0)(ir_Ex_0\frac{\omega_0}{c}G_{12,3}(\omega_0-\omega)G_{20}(\omega_0))^*P_{\rm In} \nonumber \\
&=2ir_Ex_0\frac{\omega_0}{c}\left[G^*_{12,0}(\omega_0)G_{12,3}(\omega_0+\omega)G_{20}(\omega_0)-G_{12,0}(\omega_0)G^*_{12,3}(\omega_0-\omega)G^*_{20}(\omega_0)\right]P_{\rm in},
\end{align}

The nonzero components of the adjacency matrix A are
\begin{align}
&A_{10}=t_{BS}e^{-i\phi_x} & \nonumber \\
&A_{21}=t_I, & &A_{23}=-r_Ie^{-i\phi}&  \nonumber \\
&A_{32}=-r_Ee^{-i\phi}& \nonumber  \\
&A_{42}=t_Ee^{-i\phi}& \nonumber \\
&A_{51}=r_I, & &A_{53}=t_I e^{-i\phi}& \nonumber \\
&A_{60}=-r_{BS}e^{-i\phi_y}& \nonumber \\
&A_{76}=t_I,& &A_{7,9}=-r_Ie^{-i\phi}& \nonumber \\
&A_{87}=t_E e^{-i\phi},& \nonumber \\
&A_{97}=-r_E e^{-i\phi},& \nonumber \\
&A_{10,6}=r_I,&
&A_{10,9}=t_I e^{-i\phi},& \nonumber \\
&A_{11,5}=t_{BS}e^{-i\phi_x},&
&A_{11,10}=-r_{BS}e^{-i\phi_y}& \nonumber \\
&A_{12,5}=r_{BS}e^{-i\phi_x},&
&A_{12,10}=t_{BS}e^{-i\phi_y}&,
\end{align}
where the phases are
\begin{align}
\phi(\omega)&=\omega L/c \nonumber \\
\phi_y(\omega)&=\omega d/c \nonumber  \\
\phi_x(\omega) &=\omega(d+\Delta)/c
\end{align}

We compute the adjacency matrix, the system matrix and the power transfer funciton $\frac{\Delta P}{x_0}e^{i\Phi}$

```python
def phx(w): #phase accumulated travelling to the x FP cavity
        return w*(d+Del)/c

def phy(w): #phase accumulated travelling to the y FP cavity
        return w*(d)/c#plot (zoomed out) for FP

# make x be a list of f rather than \omega, so we can plot transmitted power versus f
f = np.logspace(0, 5, 1000)

#y = list(map(Transfer, 2*np.pi*f))
```

```python
#Define adjacency matrix for Michelson
def A(w):
    M = np.zeros([13,13],complex)
    M[1,0]   = tBS*np.exp(-1j*phx(w))
    M[2,1]   = tI
    M[2,3]   = -rI*np.exp(-1j*ph(w))
    M[3,2]   = -rE*np.exp(-1j*ph(w))
    M[4,2]   = tE*np.exp(-1j*phx(w))
    M[5,1]   = +rI
    M[5,3]   = tI*np.exp(-1j*ph(w))
    M[6,0]   = -rBS*np.exp(-1j*phy(w))
    M[7,6]   = tI
    M[7,9]   = -rI*np.exp(-1j*ph(w))
    M[8,7]   = tE*np.exp(-1j*ph(w))
    M[9,7]   = -rE*np.exp(-1j*ph(w))
    M[10,6]  = +rI
    M[10,9]  = tI*np.exp(-1j*ph(w))
    M[11,5]  = tBS*np.exp(-1j*phx(w))
    M[11,10] = -rBS*np.exp(-1j*phy(w))
    M[12,5]  = rBS*np.exp(-1j*phx(w))
    M[12,10] = tBS*np.exp(-1j*phy(w))
    return M

# function that calculates system matrix G = (1-A)^(-1) for Michelson
def G(w):
    return np.linalg.inv(np.identity(13,complex) - A(w))

#define a function that computes Delta P/ x_0 in eq. 26 for Michelson
def P_dark(w):
    z = 2j * rE * Pin* (w0/c) * (np.conj(G(w0)[12,0])*         G(w0+w)[12,3] * G(w0)[2,0] -
                                         G(w0)[12,0] * np.conj(G(w0-w)[12,3] * G(w0)[2,0]))
    return z
```

Bode plots

```python
#plot (zoomed out) for Fabry-Perot Michelson
f = np.logspace(0, 5, 1000)

#y = list(map(Transfer, omega))
y = np.zeros_like(f, complex)
for i in range(len(f)):
    y[i] = P_dark(2*np.pi*f[i])
```

```python
fig23, ax = plt.subplots(2,1, sharex=True, figsize=(8,7))
ax[0].loglog(f, np.abs(y),
            rasterized=True, c='xkcd:Burple')
ax[0].set_title(r'Michelson w/ Fabry-Perot arms')
ax[0].set_ylabel(r'$\Delta P/x_0 [W/m]$')

ax[1].semilogx(f, np.angle(y, deg=True),
            rasterized=True, c='xkcd:primary blue')
ax[1].set_ylabel(r'Phase [deg]')
ax[1].set_xlabel(r'Frequency [Hz]')
ax[1].set_yticks(np.arange(-180,181,45))

plt.savefig("Figures/2cwide.pdf")
```

Close up of the resonances. The yellow lines denote the resonances frequency of the static Fabry Perot cavity

```python
#plot (zoomed in) for Michelson
# make x be a list of f rather than \omega, so we can plot transmitted power versus f
f = np.linspace(0, 1e5, 1000)

y = np.zeros_like(f, complex)
for i in range(len(f)):
    y[i] = P_dark(2*np.pi*f[i])

plt.figure(227)
plt.semilogy(f/1000, np.abs(y),
            rasterized=True)
plt.axvline(c/2/L/1000, color='xkcd:tangerine', alpha=0.5, lw=5)
plt.axvline(c/1/L/1000, color='xkcd:shit',      alpha=0.5, lw=5)

plt.xlabel(r'Frequency [kHz]')
plt.ylabel(r'$\Delta P/x_0 [W/m]$')

plt.savefig("Figures/2cclose.pdf")
```

```python

```


```
from sympy import init_printing
init_printing()

from numpy import roots
```

```
from tabulate import tabulate
from IPython.display import Latex
from numpy import ceil, zeros

def routh_hurwitz(polinomio):
    """
    Este función toma como entrada un arreglo con los coeficientes
    del polinomio caracteristico del sistema para analizar su
    estabilidad, por medio de Routh - Hurwitz.
    """
    grado = len(polinomio)
    linea_cero = False
    long_linea = ceil((grado + 1)/2.0)

    def fila(tabla, i):
        """
        Se calcula la fila 'i' de una tabla de Routh - Hurwitz.
        """
        temp = []
        long_linea = ceil(grado + 1)/2.0
        for j in range(len(tabla[0]) - 1):
            # Se calcula por la formula (a1 b2 - a2 b1) / a1
            temp.append((tabla[i - 1][0]*tabla[i - 2][j + 1] -
                         tabla[i - 1][j + 1]*tabla[i - 2][0]) /
                         tabla[i - 1][0])
        # Se agrega un 0 en cada fila, por los que no se pueden calcular
        temp.append(0)
        # Se revisa el caso en que una linea se hace 0 en su totalidad
        # lo cual pasa porque la fila es linealmente dependiente con la
        # anterior.
        if (temp == zeros(long_linea)).all():
            linea_cero = True
            linea = grado - i - 1
            exponentes = [linea - j for j in range(0, linea + 1, 2)]
            while long_linea > len(exponentes):
                exponentes.append(0)
            temp =[]
            for j in range(len(tabla[0])):
                temp.append(tabla[i - 1][j]*exponentes[j])
        return temp

    # Se crean 2 arreglos y se introducen los coeficientes de las potencias
    # pares en uno y las impares en otro
    temp1 = []
    temp2 = []
    for i, coef in enumerate(polinomio):
        if i%2 == 0:
            temp1.append(coef)
        else:
            temp2.append(coef)

    # Si tiene un numero impar de potencias la longitud de los arreglos
    # será diferente, por lo que se agregará un 0 al final del segundo
    # para igualarlos
    if len(temp1) != len(temp2):
        temp2.append(0)

    # Se agregan los dos arreglos en una tabla
    tabla = []
    tabla.append(temp1)
    tabla.append(temp2)

    # Se itera atraves de las filas de la tabla y se calculan
    for i in range(2, len(polinomio)):
        tabla.append(fila(tabla, i))

    # Se agrega al inicio de cada fila el grado del polinomio asociado
    n = len(tabla)
    for i in range(n):
        tabla[i] = ["s**" + str(n - i - 1)] + tabla[i]

    # Se imprimen cada una de las lineas de la tabla
    if linea_cero:
        print "Se saco la derivada de una linea para sacar la siguiente,"
        print "debido a que es linealmente dependiente de la anterior."

    print tabulate(tabla)
```

```
A = [1, 2, 1, 2]
for r in roots(A):
    print r
routh_hurwitz(A)
```

    (-2+0j)
    (-1.38777878078e-16+1j)
    (-1.38777878078e-16-1j)
    ----  -  -
    s**3  1  1
    s**2  2  2
    s**1  2  0
    s**0  2  0
    ----  -  -

```
A = [1, 0, -3, 2]
for r in roots(A):
    print r
routh_hurwitz(A)
```

```
A = [1, 2, 24, 48, -25, -50]
for r in roots(A):
    print r
routh_hurwitz(A)
```

    (-1.11022302463e-16+5j)
    (-1.11022302463e-16-5j)
    (1+0j)
    (-2+0j)
    (-1+0j)
    ----  ---  ---  ---
    s**5    1   24  -25
    s**4    2   48  -50
    s**3    6   48    0
    s**2   32  -50    0
    s**1   57    0    0
    s**0  -50    0    0
    ----  ---  ---  ---

```
A = [1, 2, 3, 4, 5]
for r in roots(A):
    print r
routh_hurwitz(A)
```

    (0.287815479558+1.41609308017j)
    (0.287815479558-1.41609308017j)
    (-1.28781547956+0.857896758328j)
    (-1.28781547956-0.857896758328j)
    ----  --  -  -
    s**4   1  3  5
    s**3   2  4  0
    s**2   1  5  0
    s**1  -6  0  0
    s**0   5  0  0
    ----  --  -  -

```
A = [1, 3, 1, 5]
for r in roots(A):
    print r
routh_hurwitz(A)
```

    (-3.17998107216+0j)
    (0.0899905360791+1.25069504925j)
    (0.0899905360791-1.25069504925j)
    ----  --  -
    s**3   1  1
    s**2   3  5
    s**1  -1  0
    s**0   5  0
    ----  --  -

```

```


This notebook is part of https://github.com/AudioSceneDescriptionFormat/splines, see also https://splines.readthedocs.io/.

[back to overview](bezier.ipynb)

# De Casteljau's Algorithm

There are several ways that lead to Bézier curves, one was already shown in
[the notebook about Hermite curves](hermite-uniform.ipynb#Relation-to-Bézier-Splines)
(but only for cubic curves).
In this notebook,
we will derive Bézier curves of arbitrary polynomial degree utilizing
[De Casteljau's algorithm](https://en.wikipedia.org/wiki/De_Casteljau's_algorithm).

### Preparations

Before we continue, here are are few preparations for the following calculations:

```python
%config InlineBackend.print_figure_kwargs = {'bbox_inches': None}
import matplotlib.pyplot as plt
import numpy as np
import sympy as sp
sp.init_printing()
```

We import a few utilities and helpers from the files
[utility.py](utility.py)
and
[helper.py](helper.py).

```python
from utility import NamedExpression, NamedMatrix
from helper import plot_basis
```

Let's prepare a few symbols for later use ...

```python
t, x0, x1, x2, x3, x4 = sp.symbols('t, xbm:5')
```

... and a helper function for plotting:

```python
def plot_curve(func, points, dots=30, ax=None):
    if ax is None:
        ax = plt.gca()
    times = np.linspace(0, 1, dots)
    ax.plot(*func(points, times).T, '.')
    ax.plot(
        *np.asarray(points).T,
        color='lightgrey',
        linestyle=':',
        marker='x',
        markeredgecolor='black',
    )
    ax.scatter(*np.asarray(points).T, marker='x', c='black')
    ax.set_title(func.__name__ + ' Bézier curve')
    ax.axis('equal')
```

We also need to prepare for the animations we will see below.
This is using code from the file [casteljau.py](casteljau.py):

```python
from casteljau import create_animation
from IPython.display import display, HTML

def show_casteljau_animation(points, frames=30, interval=200):
    ani = create_animation(points, frames=frames, interval=interval)
    display({
        'text/html': ani.to_jshtml(default_mode='reflect'),
        'text/plain': 'Animations can only be shown in HTML output, sorry!',
    }, raw=True)
    plt.close()  # avoid spurious figure display
```

### Degree 1 (Linear)

After all those preparations,
let's start with the trivial case:
A Bézier spline of degree 1 is just a piecewise linear curve connecting all the control points.
There are no "off-curve" control points that could bend the curve segments.

Assuming that we have two control points,
$\boldsymbol{x}_0$ and $\boldsymbol{x}_1$,
we can set up a linear equation:

\begin{equation*}
\boldsymbol{p}_{0,1}(t) = \boldsymbol{x}_0 + t (\boldsymbol{x}_1 - \boldsymbol{x}_0).
\end{equation*}

Another way to write the same thing it like this:

\begin{equation*}
\boldsymbol{p}_{0,1}(t) = (1 - t) \boldsymbol{x}_0 + t \boldsymbol{x}_1,
\end{equation*}
where in both cases $t \in [0, 1]$.
Since we will be needing quite a few of those linear interpolations,
let's create a helper function:

```python
def lerp(one, two):
    """Linear interpolation.

    The parameter *t* is expected to be between 0 and 1.

    """
    return (1 - t) * one + t * two
```

Now we can define the equation in SymPy:

```python
p01 = NamedExpression('pbm_0,1', lerp(x0, x1))
p01
```

```python
b1 = [p01.expr.expand().coeff(x.name).factor() for x in (x0, x1)]
b1
```

Doesn't look like much, but those are the
[Bernstein bases](https://en.wikipedia.org/wiki/Bernstein_polynomial)
for degree 1.
It doesn't get much more interesting if we plot them:

```python
plot_basis(*b1)
```

If you want to convert this to coefficients for the
[monomial basis](polynomials.ipynb) $[t, 1]$
instead of the Bernstein basis functions, you can use this matrix:

```python
M_B1 = NamedMatrix(
    r'{M_\text{B}^{(1)}}',
    sp.Matrix([[c.coeff(x) for x in (x0, x1)]
               for c in p01.expr.as_poly(t).all_coeffs()]))
M_B1
```

Applying this matrix leads to the coefficients of the linear equation mentioned in the beginning of this section
($\boldsymbol{p}_{0,1}(t) = t (\boldsymbol{x}_1 - \boldsymbol{x}_0) + \boldsymbol{x}_0$):

```python
sp.MatMul(M_B1.expr, sp.Matrix([x0, x1]))
```

```python
_.doit()
```

In case you ever need that, here's the inverse:

```python
M_B1.I
```

Anywho, let's calculate points on the curve by using the Bernstein basis functions:

```python
def linear(points, times):
    """Evaluate linear Bézier curve (given by two points) at given times."""
    return np.column_stack(sp.lambdify(t, b1)(times)) @ points
```

```python
points = [
    (0, 0),
    (1, 0.5),
]
```

```python
plot_curve(linear, points)
```

```python
show_casteljau_animation(points)
```

I know, not very exciting. But it gets better!

### Degree 2 (Quadratic)

Now we consider three control points,
$\boldsymbol{x}_0$, $\boldsymbol{x}_1$ and $\boldsymbol{x}_2$.
We use the linear interpolation of the first two points from above ...

```python
p01
```

... and we do the same thing for the second and third point:

```python
p12 = NamedExpression('pbm_1,2', lerp(x1, x2))
p12
```

Finally, we make another linear interpolation between those two results:

```python
p02 = NamedExpression('pbm_0,2', lerp(p01.expr, p12.expr))
p02
```

From this,
we can get the Bernstein basis functions of degree 2:

```python
b2 = [p02.expr.expand().coeff(x.name).factor() for x in (x0, x1, x2)]
b2
```

```python
plot_basis(*b2)
```

```python
M_B2 = NamedMatrix(
    r'{M_\text{B}^{(2)}}',
    sp.Matrix([[c.coeff(x) for x in (x0, x1, x2)]
               for c in p02.expr.as_poly(t).all_coeffs()]))
M_B2
```

```python
M_B2.I
```

```python
def quadratic(points, times):
    """Evaluate quadratic Bézier curve (given by three points) at given times."""
    return np.column_stack(sp.lambdify(t, b2)(times)) @ points
```

```python
points = [
    (0, 0),
    (0.2, 0.5),
    (1, -0.3),
]
```

```python
plot_curve(quadratic, points)
```

```python
show_casteljau_animation(points)
```

#### Quadratic Tangent Vectors

For some more insight,
let's look at the first derivative of the curve (i.e. the tangent vector) ...

```python
v02 = p02.diff(t)
```

... at the beginning and the end of the curve:

```python
v02.evaluated_at(t, 0)
```

```python
v02.evaluated_at(t, 1)
```

This shows that the tangent vector
at the beginning and end of the curve
is parallel to the line
from $\boldsymbol{x}_0$ to $\boldsymbol{x}_1$ and
from $\boldsymbol{x}_1$ to $\boldsymbol{x}_2$, respectively.
The length of the tangent vectors is twice the length of those lines.

You might have already seen that coming, but it turns out that the last line in de Casteljau's algorithm ($\boldsymbol{p}_{1,2}(t) - \boldsymbol{p}_{0,1}(t)$ in our case) is exactly half of the tangent vector (at any given $t \in [0, 1]$).

```python
assert (v02.expr - 2 * (p12.expr - p01.expr)).simplify() == 0
```

In case you are wondering, the factor 2 comes from the degree 2 of our quadratic curve.

### Degree 3 (Cubic)

Let's now consider four control points,
$\boldsymbol{x}_0$,
$\boldsymbol{x}_1$,
$\boldsymbol{x}_2$ and
$\boldsymbol{x}_3$.

By now, the pattern should be clear:
We take the result from the first three points from above
and linearly interpolate it with the result for the three points
$\boldsymbol{x}_1$, $\boldsymbol{x}_2$ and $\boldsymbol{x}_3$.

Combination of $\boldsymbol{x}_2$ and $\boldsymbol{x}_3$:

```python
p23 = NamedExpression('pbm_2,3', lerp(x2, x3))
p23
```

Combination of $\boldsymbol{x}_1$, $\boldsymbol{x}_2$ and $\boldsymbol{x}_3$:

```python
p13 = NamedExpression('pbm_1,3', lerp(p12.expr, p23.expr))
p13
```

Combination of $\boldsymbol{x}_0$, $\boldsymbol{x}_1$, $\boldsymbol{x}_2$ and $\boldsymbol{x}_3$:

```python
p03 = NamedExpression('pbm_0,3', lerp(p02.expr, p13.expr))
p03
```

This leads to the cubic Bernstein bases:

```python
b3 = [p03.expr.expand().coeff(x.name).factor() for x in (x0, x1, x2, x3)]
b3
```

Those are of course the same Bernstein bases
as we found in
[the notebook about Hermite splines](hermite-uniform.ipynb#Relation-to-Bézier-Splines).

```python
plot_basis(*b3)
```

```python
M_B3 = NamedMatrix(
    r'{M_\text{B}^{(3)}}',
    sp.Matrix([[c.coeff(x) for x in (x0, x1, x2, x3)]
               for c in p03.expr.as_poly(t).all_coeffs()]))
M_B3
```

```python
M_B3.I
```

```python
def cubic(points, times):
    """Evaluate cubic Bézier curve (given by four points) at given times."""
    return np.column_stack(sp.lambdify(t, b3)(times)) @ points
```

```python
points = [
    (0, 0.3),
    (0.2, 0.5),
    (0.1, 0),
    (1, 0.2),
]
```

```python
plot_curve(cubic, points)
```

```python
show_casteljau_animation(points)
```

#### Cubic Tangent Vectors

As before,
let's look at the derivative (i.e. the tangent vector) of the curve ...

```python
v03 = p03.diff(t)
```

... at the beginning and the end of the curve:

```python
v03.evaluated_at(t, 0)
```

```python
v03.evaluated_at(t, 1)
```

This shows that the tangent vector at the beginning and end of the curve is parallel to the line
from $\boldsymbol{x}_0$ to $\boldsymbol{x}_1$ and
from $\boldsymbol{x}_2$ to $\boldsymbol{x}_3$, respectively.
The length of the tangent vectors is three times the length of those lines.
This also means that if the begin and end positions
$\boldsymbol{x}_0$ and $\boldsymbol{x}_3$
as well as the corresponding tangent vectors
$\boldsymbol{\dot{x}}_0$ and $\boldsymbol{\dot{x}}_3$
are given,
it's easy to calculate the two missing control points:

\begin{align*}
\boldsymbol{x}_1 &= \boldsymbol{x}_0 + \frac{\boldsymbol{\dot{x}}_0}{3}\\
\boldsymbol{x}_2 &= \boldsymbol{x}_3 - \frac{\boldsymbol{\dot{x}}_3}{3}
\end{align*}

This can be used to
[turn uniform Hermite splines into Bézier splines](hermite-uniform.ipynb#Relation-to-Bézier-Splines)
and to
[construct uniform Catmull--Rom splines using Bézier segments](catmull-rom-uniform.ipynb#Using-Bézier-Segments).

We can now also see that the last linear segment in de Casteljau's algorithm
($\boldsymbol{p}_{1,3}(t) - \boldsymbol{p}_{0,2}(t)$ in this case)
is exactly a third of the tangent vector (at any given $t \in [0, 1]$):

```python
assert (v03.expr - 3 * (p13.expr - p02.expr)).simplify() == 0
```

Again, the factor 3 comes from the degree 3 of our curve.

#### Cubic Bézier to Hermite Segments

We now know the tangent vectors at the beginning and the end of the curve, and obviously we know the values of the curve at the beginning and the end:

```python
p03.evaluated_at(t, 0)
```

```python
p03.evaluated_at(t, 1)
```

With these four pieces of information,
we can find a transformation from the four Bézier control points
to the two control points and two tangent vectors
of a Hermite spline segment:

```python
M_BtoH = NamedMatrix(
    r'{M_\text{B$\to$H}}',
    sp.Matrix([[expr.coeff(cv) for cv in [x0, x1, x2, x3]]
               for expr in [
                   x0,
                   x3,
                   v03.evaluated_at(t, 0).expr,
                   v03.evaluated_at(t, 1).expr]]))
M_BtoH
```

And we can simply invert this if we want to go in the other direction, from Hermite to Bézier:

```python
M_BtoH.I.pull_out(sp.S.One / 3)
```

Of course, those are the same matrices as shown in the
[notebook about uniform cubic Hermite splines](hermite-uniform.ipynb#Relation-to-Bézier-Splines).

### Degree 4 (Quartic)

By now you know the drill,
we consider five control points,
$\boldsymbol{x}_0$,
$\boldsymbol{x}_1$,
$\boldsymbol{x}_2$,
$\boldsymbol{x}_3$ and $\boldsymbol{x}_4$,
which lead to more linear interpolations:

```python
p34 = NamedExpression('pbm_3,4', lerp(x3, x4))
p24 = NamedExpression('pbm_2,4', lerp(p23.expr, p34.expr))
p14 = NamedExpression('pbm_1,4', lerp(p13.expr, p24.expr))
p04 = NamedExpression('pbm_0,4', lerp(p03.expr, p14.expr))
p04
```

Kinda long, but anyway, let's try to extract the Bernstein bases:

```python
b4 = [p04.expr.expand().coeff(x.name).factor() for x in (x0, x1, x2, x3, x4)]
b4
```

```python
plot_basis(*b4)
```

```python
M_B4 = NamedMatrix(
    '{M_B^{(4)}}',
    sp.Matrix([[c.coeff(x) for x in (x0, x1, x2, x3, x4)]
               for c in p04.expr.as_poly(t).all_coeffs()]))
M_B4
```

```python
M_B4.I
```

```python
def quartic(points, times):
    """Evaluate quartic Bézier curve (given by five points) at given times."""
    return np.column_stack(sp.lambdify(t, b4)(times)) @ points
```

```python
points = [
    (0, 0),
    (0.5, 0),
    (0.7, 1),
    (1, 1.5),
    (-1, 1),
]
```

```python
plot_curve(quartic, points)
```

```python
show_casteljau_animation(points)
```

#### Quartic Tangent Vectors

For completeness' sake,
let's look at the derivative (i.e. the tangent vector) of the curve ...

```python
v04 = p04.diff(t)
```

... at the beginning and the end of the curve:

```python
v04.evaluated_at(t, 0)
```

```python
v04.evaluated_at(t, 1)
```

By now it shouldn't be surprising that the tangent vector at the beginning and end of the curve is parallel to the line
from $\boldsymbol{x}_0$ to $\boldsymbol{x}_1$ and
from $\boldsymbol{x}_3$ to $\boldsymbol{x}_4$, respectively.
The length of the tangent vectors is four times the length of those lines.
The last line in de Casteljau's algorithm ($\boldsymbol{p}_{1,4}(t) - \boldsymbol{p}_{0,3}(t)$ in this case) is exactly a fourth of the tangent vector (at any given $t \in [0, 1]$):

```python
assert (v04.expr - 4 * (p14.expr - p03.expr)).simplify() == 0
```

Again, the factor 4 comes from the degree 4 of our curve.

### Arbitrary Degree

We could go on doing this for higher and higher degrees,
but this would get more and more annoying.
Luckily,
there is a closed formula available to calculate Bernstein polynomials
for an arbitrary degree $n$
(using the
[binomial coefficient](https://en.wikipedia.org/wiki/Binomial_coefficient)
${n \choose i} = \frac{n!}{i!(n - i)!}$):

\begin{equation*}
b_{i,n}(x) = {n \choose i} x^i \left( 1 - x \right)^{n - i}, \quad i = 0, \ldots, n.
\end{equation*}

This is used in the Python class
[splines.Bernstein](../python-module/splines.rst#splines.Bernstein).

```python
show_casteljau_animation([
    (0, 0),
    (-1, 1),
    (-0.5, 2),
    (1, 2.5),
    (2, 2),
    (2, 1.5),
    (0.5, 0.5),
    (1, -0.5),
])
```


```python
import numpy as np
import math
import matplotlib.pyplot as plt
import matplotlib as mpl
from mpl_toolkits.mplot3d import Axes3D
from IPython.display import display
from IPython.display import display_latex
import sympy
import sympy as sp
from sympy import Eq, symbols, simplify, trigsimp, pi
from sympy.functions.special.tensor_functions import KroneckerDelta
from sympy.vector import Vector, CoordSys3D, AxisOrienter, BodyOrienter, Del, curl, divergence, gradient, is_conservative, is_solenoidal, scalar_potential, Point, scalar_potential_difference, Del, express, matrix_to_vector
from sympy import latex, Array, Matrix, transpose, zeros, diff, Function, Derivative, cos, sin, sqrt, solve, linsolve, acos, atan, asin

from sympy.plotting import plot, plot_parametric
```

```python
# Usage: display_equation('u_x', x)
def display_equation(idx, symObj):
    if(isinstance(idx, str)):
        eqn = '\\[' + idx + ' = ' + latex(symObj) + '\\]'
        display_latex(eqn, raw=True)
    else:
        eqn = '\\[' + latex(idx) + ' = ' + latex(symObj) + '\\]'
        display_latex(eqn, raw=True)
    return
```

```python
import unittest

class TestNotebook(unittest.TestCase):

    def test_add(self):
        self.assertEqual(2 + 2, 4)

unittest.main(argv=[''], verbosity=2, exit=False)
```

    test_add (__main__.TestNotebook) ... ok

    ----------------------------------------------------------------------
    Ran 1 test in 0.002s

    OK

    <unittest.main.TestProgram at 0x179a09e29a0>

$v(t) = V_msin(wt + \phi)$
where
$V_m$ = the amplitude
$w$ = the angular frequency in radians/s
$\omega t + \phi$ = the argument of the sinusoid
$\phi$ = the phase

$T = \frac{2 \pi}{\omega}$

```python
V_m, omega, m, t, T, phi, theta = symbols('V_m omega m t T phi theta')
```

```python
T = 2*pi/omega
V_m = sin(omega * (t + T))
display_equation('V_m', V_m)
V_m = trigsimp(simplify(V_m))
display_equation('V_m', V_m)
display_equation('T', T)
display_equation('V_m', V_m)
#N(V_m, 5)
#V_m.evalf()
#V_m.subs(t, 2).subs(omega, 30)
```

\[V_m = \sin{\left(\omega \left(t + \frac{2 \pi}{\omega}\right) \right)}\]

\[V_m = \sin{\left(\omega t \right)}\]

\[T = \frac{2 \pi}{\omega}\]

\[V_m = \sin{\left(\omega t \right)}\]

Periodic Function

$f = \frac{1}{T}$
$\omega = 2 \pi f$

```python
wt = symbols('wt')
V_m = sin(wt + phi)
aplot = plot(V_m.subs(phi, 0), V_m.subs(phi, pi / 2), (wt, 0, 2*pi))
aplot[0].line_color = 'b'
aplot[1].line_color = 'r'
aplot.show()
```

Note: Graphical method for phase shift is a blessing

$z = x + jy$ Rectangular form
$z = r \angle\phi$ Polar form
$z = re^{j \phi }$ Exponential form
$z = x + jy = r\angle\phi = re^{\pm j \phi} = r(cos\phi + j sin\phi)$

Addition and subtraction of complex numbers are better performed
in rectangular form; multiplication and division are better done in polar
form.

$e^{\pm j \phi} = cos\phi \pm jsin\phi$
$cos\phi = Re(e^{j\phi})$
$sin\phi = Im(e^{j\phi})$

$v(t) = V_mcos(\omega t + \phi) = Re(V_m e^{j(\omega t + \phi)}) = Re(V_m e^{j(\omega t + \phi)}) = Re(V_m e^{j\omega t} e^{j\phi)})$

Thus,

```python

```


IBM Quantum Challenge Africa 2021

Introduction and the Crop-Yield Problem

Quantum Computing has the potential to revolutionize computing, as it can solve problems that are not possible to solve on a classical computer. This extra ability that quantum computers have is called quantum advantage. To achieve this goal, the world needs passionate and competent end-users: those who know how to apply the technology to their field.

In this challenge you will be exposed, at a high-level, to quantum computing through Qiskit. As current or future users of quantum computers, you need to know what problems are appropriate for quantum computation, how to structure the problem model/inputs so that they are compatible with your chosen algorithm, and how to execute a given algorithm and quantum solution to solve the problem.

This is the first notebook for the IBM Quantum Challenge Africa. Before starting here, ensure you have completed the Week 0 content in preparation for the following exercises.

## Initialization

To ensure the demonstrations and exercises have the required Python modules and libraries, run the following cell before continuing.

```python
# Import auxiliary libraries
import numpy as np

# Import Qiskit
from qiskit import IBMQ, Aer
from qiskit.algorithms import QAOA, VQE, NumPyMinimumEigensolver
from qiskit.algorithms.optimizers import COBYLA
from qiskit.utils import QuantumInstance, algorithm_globals
from qiskit.providers.aer.noise.noise_model import NoiseModel

from qiskit_optimization import QuadraticProgram
from qiskit_optimization.algorithms import MinimumEigenOptimizer
from qiskit_optimization.converters import QuadraticProgramToQubo

import qiskit.test.mock as Fake
```

## Table of Contents

The notebook is structured as follows:

1. Initialization
2. Table of Contents
3. Qiskit and its parts
4. Setting up the Qiskit Environment
5. Quadratic Problems
6. Crop-Yield Problem as a Quadratic Problem
7. Solving the Crop-Yield Problem using Quantum Computing
8. Simulating a Real Quantum Computer for the Crop-Yield Problem

## Qiskit and its parts

Qiskit is divided into multiple modules for different purposes, with Terra at the core of the Qiskit ecosystem. It helps to be familiar with what each module can do and whether you will need to utilize the software contained within them for your specific problem. There are four modules that deal with the application of quantum computing; with tools and algorithms built specifically for their fields.

Have a quick look at the [Qiskit documentation](https://qiskit.org/overview) and this IBM Research [blog post](https://www.ibm.com/blogs/research/2021/04/qiskit-application-modules/) to see what these modules are called. Once you have done so, you can complete the first exercise of the challenge: replace the ficticious module names, in the python cell below, with the correct module names. Though it would be fun to have a Qiskit Gaming module, it has not yet been developed. If you are interested in contributing to the open-source Qiskit community, have a look at the [contribution guide](https://qiskit.org/documentation/contributing_to_qiskit.html).

### Exercise 1a: Qiskit Applications Modules

```python
# Definitely real Qiskit module names
qiskit_module_names = [
    "Qiskit Gaming",
    "Qiskit Cooking",
    "Qiskit VR",
    "Qiskit Word Processing",
]
```

Run the following python cell to check if you have the correct module names.

```python
from qc_grader import grade_ex1a

grade_ex1a(qiskit_module_names)
```

### Qiskit Applications Modules

In this notebook, we will use the `Qiskit Optimization` applications module for a specific problem to illustrate how

1. classical and mathematical problem definitions are represented in Qiskit,
2. some quantum algorithms are defined,
3. to execute a quantum algorithm for a given problem definition,
4. and algorithms and Qiskit problems are executed on real and simulated quantum computers.

We will also be using Qiskit Terra and Aer as they provide the foundation of Qiskit and high-performance quantum simulators respectively.

## Quadratic Problems

Some computational problems can be formulated into quadratic equations such that the minimum of the quadratic equations is the optimal solution, if any exist. These problems are encountered in finance, agriculture, operations and production management, and economics.

Quadratic programming is also used to identify an optimal financial portfolio with minimum risk and optimizing the layout of production components in a factory to minimize the travel distance of resources. This notebook focuses on agriculture as it is a relevant application of quantum computing to problems facing the African continent. However, all of these applications share two common characteristics: the system can be modelled as a quadratic equation and the system variables may be constrained, with their values limited to within a given range.

---

Quadratic problems take on the following structure. Given a vector of $n$ variables $x\in\mathbb{R}^n$, the quadratic function to minimize is as follows.

$$
\begin{align}
\text{minimize}\quad & f\left(x\right)=\frac{1}{2}x^\top{}\mathbf{Q}x + c^\top{}x &\\
\text{subject to}\quad & \mathbf{A}x\leq{}b&\\
& x^\top{}\mathbf{Q}_ix + c_{i}^\top{}x\leq{}r_i,\quad&\forall{}i\in[1,k_q]\\
& l_i\leq{}x_i\leq{}u_i,\quad&\forall{}i\in[1,k_l]\\
\end{align}
$$

$\mathbf{Q}$, $\mathbf{Q}_i$, and $\mathbf{A}$ are $n\times{}n$ symmetric matrices. $c$ and $c_i$ are $n\times{}1$ column vectors. $\mathbf{Q}_i$, $\mathbf{A}$, $c_i$, $l_i$, and $u_i$ define constraints on the variables in $x$. The quadratic equation at the core of the quadratic problem is found by multiplying out the matrices in the minimization function. Though '$\leq{}$' is used in the constraint equations above, any identity relationship may be used for any number of constraints: i.e. "$<$", "$=$", "$>$", "$\geq$", or "$\leq$".

A valid solution to the quadratic must satisfy all conditions for the problem. Examples of some constraints are given below. The first two are linear constraints whereas the third example is a quadratic constraint.

$$ x_1 + x_4 \leq{} 10$$

$$ x_2 - 3x_6 = 10$$

$$x_1x_2 - 4x_3x_4 + x_5 \leq{} 15 $$

Qiskit has Python code that allows you to implement a quadratic problem as a `QuadraticProgram` instance. Though our definition above used matrices to define the coefficients, `QuadraticProgram` allows you to define the objective (function overwhich to minimize) directly. To illustrate how to use `QuadraticProgram`, we will use the following quadratic problem definition, with three integer variables.

$$\begin{align}
\text{minimize}\quad{} & f(x)=(x_1)^2 + (x_2)^2 - x_1x_2 - 6x_3 \\
\text{subject to}\quad{} & x_1 + x_2 = 2             \\
                         & x_2x_3 \geq{} 1           \\
                         & -2 \leq{} x_2 \leq{} 2    \\
                         & -2 \leq{} x_3 \leq{} 4    \\
\end{align}$$

The figure below shows the constraints on $x_1$ and $x_3$, with some simplifcation. The shaded area denotes valid values for $x_1$ and $x_3$, within which $f(x)$ must be minimized.

In the following code, the above quadratic problem is defined as a `QuadraticProgram` instance. Have a look at the [Qiskit documentation for `QuadraticProgram`](https://qiskit.org/documentation/stubs/qiskit.optimization.QuadraticProgram.html), as it can be very useful in helping your understanding of its interface.

The quadratic to minimize, called an objective, is implemented using dictionaries. This allows you, the developer, to explicitly define coefficients for specific variables and terms. The keys in the dictionaries are the variable names identifying a term in $f(x)$. For example, `("x_1","x_2")` is for $x_1x_2$. The values for each item are the coefficients for said terms. Terms that are subtracted in $f(x)$ must have a negative coefficient.

```python
quadprog = QuadraticProgram(name="example 1")
quadprog.integer_var(name="x_1", lowerbound=0, upperbound=4)
quadprog.integer_var(name="x_2", lowerbound=-2, upperbound=2)
quadprog.integer_var(name="x_3", lowerbound=-2, upperbound=4)
quadprog.minimize(
    linear={"x_3": -6},
    quadratic={("x_1", "x_1"): 1, ("x_2", "x_2"): 1, ("x_1", "x_2"): -1},
)
quadprog.linear_constraint(linear={"x_1": 1, "x_2": 1}, sense="=", rhs=2)
quadprog.quadratic_constraint(quadratic={("x_2", "x_3"): 1}, sense=">=", rhs=1)
```

A `QuadraticProgram` can have three types of variables: binary, integer, and continuous. The Qiskit implementation of the algorithms we are going to use currently only support binary and integer variables. There are other algorithms that allow for the simulation of continuous variables, but they are not covered in this notebook. If you want to know more about them though, have a look at this Qiskit tutorial on [algorithms to solve mixed-variable quadratic problems](https://qiskit.org/documentation/tutorials/optimization/5_admm_optimizer.html).

We can visualize the `QuadraticProgram` as an LP string, a portable text-based format used representating the model as a **L**inear **P**rogramming problem.

```python
print(quadprog.export_as_lp_string())
```

Any optimization problem that can be represented as a single second-order equation, in that the greatest exponent of any term is 2, can be transformed into a quadratic problem or program of the form given above. The above example is arbitrary and does not necessarily represent a given real-world problem. The main problem of this notebook focuses on optimizing the yield of a farm, though only the problem definition need be changed to apply this technique to other quadratic problem applications.

## Crop-Yield Problem as a Quadratic Problem

To show how to solve your quadratic program using a quantum computer, we will use two algorithms to solve the crop-yield problem. It is a common need to optimize the crops and management of a farm to reduce risk while increasing profits. One of the big challenges facing Africa and the whole world is how to produce enough food for everyone. The problem here focuses not on profits but on the tonnage of crops harvested. Imagine you have a farm with three hectares of land suitable for farming. You need to choose which crops to plant from a selection of four. Furthermore, you also need to determine how many hectares of each you should plant. The four crops you can plant are wheat, soybeans, maize, and a push-pull crop. The fourth cannot be sold once harvested but it can help increase the yield of the other crops.

            Our beautiful three hectare farm

            Wheat

            Soybeans

            Maize

            Push-Pull

There are three types of farming methods we can use: monocropping, intercropping, and push-pull farming. These are shown below. Monocropping is where only one crop is farmed. This is can make the farm susceptible to disease and pests as the entire yield would be affected. In some instances, growing two different plants nearby each other will increase the yield of both, though sometimes it can decrease the yield. Intercropping is the process where different plants are chosen to _increase_ the yield. Push-Pull crops are pairs of plants that repel pests and attract pests respectively. Integrating them into a larger farm increases the yield of harvested food but with the cost of not necessarily being able to use the harvest of Push-Pull crops as part of the total yield. This is because the Push-Pull crop may not be usable or even edible.

            Monocropping

            Intercropping

            Push-Pull farming

---
Only in certain cases can quadratic programming problems be solved easily using classical problems. In their general sense, they are NP-Hard; a class of problems that is difficult to solve using classical computational methods. In fact, the best classical method to solve these problems involves heuristics, a technique that finds an approximate solution. Quantum Computers have been shown to provide significant speed-up and better scaling for some heuristic problems. The crop-yield problem is a combinatorial problem, in that the solution is a specific combination of input parameters. Though the problem shown here is small enough to solve classically, larger problems become intractable on a classical computer owing to the number of combinations of which to optimize.

Solving the above problem using quantum computing involves three components:

1. Defining the problem
2. Defining the algorithm
3. Executing the algorithm on a backend

Many problems in Qiskit follow this structure as the algorithm you use can typically be swapped for another without significantly redefining your problem. Execution on different backends is the easiest, as long as the device has sufficient resource. The first component is given below, with the second and third in sections 1.5 and 1.6.

### Define the Crop-Yield problem

The following problem is defined for you but the `QuadraticProgram` is not implemented. Your task at the end of this section is to implement the `QuadraticProgram` for the given crop-yield model.

Your farm has three hectares available, $3~ha$, with each crop taking up $0~ha$ or $1~ha$. We define the yield of the farm as a quadratic function where the influence of each crop on eachother is represented by the quadratic coefficients. The variables in this quadratic are the number of hectares of the crop to be planted and the objective function to maximize is the yield of usable crops in tons. Here is the mathematical model for the problem. In this scenario, all crops increase the yield of other crops. However, the problem to solve is which crops to use to achieve the maximum yield.

The farm yield, in tons, is modelled as a quadratic equation, given below, with constraints on the hectares used by each crop and the total hectares available. Each crop is shown using a different symbol, as shown above, representing the number of hectares to be planted of said plant. Note that we can only plant up to 1 hectare of each crop and that our farm is constrained to 3 hectares.

----
#### Non-graphical notation
Here is a non-graphical representation of the above model, if you are struggling to interpret the above graphic.

$$
\begin{align}
    \text{maximize} \quad & 2(\operatorname{Wheat}) + \operatorname{Soybeans} + 4(\operatorname{Maize}) \\
    & + 2.4(\operatorname{Wheat}\times\operatorname{Soybeans}) \\ & + 4(\operatorname{Wheat}\times\operatorname{Maize})\\
    &+ 4(\operatorname{Wheat}\times\operatorname{PushPull}) \\ & + 2(\operatorname{Soybeans}\times\operatorname{Maize}) \\
                          & + (\operatorname{Soybeans}\times\operatorname{PushPull}) \\ & + 5(\operatorname{Maize}\times\operatorname{PushPull})
\end{align}
$$

$$
\begin{align}
\text{subject to} \quad & \operatorname{Wheat} + \operatorname{Soybeans} + \operatorname{Maize} + \operatorname{PushPull} \leq{} 3\\
& 0\leq{}\operatorname{Wheat}\leq{}1\\
& 0\leq{}\operatorname{Soybeans}\leq{}1\\
& 0\leq{}\operatorname{Maize}\leq{}1\\
& 0\leq{}\operatorname{PushPull}\leq{}1
\end{align}
$$

### Exercise 1b: Create Quadratic Program from crop-yield variables

Your first exercise is to create a `QuadraticProgram` that represents the above model. Write your implementation in the `cropyield_quadratic_program` function below. Remember to use the example as a guide, and to look at the [QuadraticProgram documentation](https://qiskit.org/documentation/tutorials/optimization/1_quadratic_program.html?highlight=quadraticprogram) and [Qiskit reference](https://qiskit.org/documentation/stubs/qiskit.optimization.QuadraticProgram.html?highlight=quadraticprogram#qiskit.optimization.QuadraticProgram).

**Note:** Ensure your variables are named `Wheat`, `Soybeans`, `Maize,` and `PushPull`. This is necessary for the grader to work.

```python
def cropyield_quadratic_program():
    cropyield = QuadraticProgram(name="Crop Yield")
    ##############################
    # Put your implementation here

    #
    ##############################
    return cropyield
```

Once you feel your implementation is correct, you can grade your solution in the following cell.

```python
# Execute this cell to grade your solution
from qc_grader import grade_ex1b

cropyield = cropyield_quadratic_program()
grade_ex1b(cropyield)
```

### Converting QuadraticPrograms

If we want to estimate how many qubits this quadratic program requires, we can convert it to an Ising Model and print the `num_qubits` parameter. An [ising model](https://qiskit.org/documentation/apidoc/qiskit.optimization.applications.ising.html?highlight=ising) is a special system model type that is well suited for quantum computing. Though we will not be using an ising model explicitly, the algorithms and Qiskit classes we are using do this conversion internally.

```python
# Estimate the number of qubits required
ising_operations, _ = (
    QuadraticProgramToQubo()
    .convert(
        cropyield,
    )
    .to_ising()
)
print(f"Number of qubits required is {ising_operations.num_qubits}")
```

Even though quadratic programs are widely used in Qiskit, the algorithms we are going to use require binary variables. Qiskit provides an automated method for converting our integer variables into binary variables. The binary-only form is called a _Quadratic Unconstrained Binary Optimization_ problem, or `QUBO`. The conversion is done using `QuadraticProgramToQUBO` from the Qiskit optimization module. Every integer variable, and their associated constraints, are transformed into binary variables.

Run the following code to see how the QUBO version of the cropyield problem looks. Notice how the quadratic becomes longer and more variables are added. This is to account for the bits in each variable, including the constraints. When we run our quantum algorithm to solve this QuadraticProgram, it is converted to a QUBO instance within the Qiskit algorith implementation, implicitly.

```python
QuadraticProgramToQubo().convert(cropyield)
```

## Solving the Crop-Yield Problem using a Quantum Computer

There are three ways to _run_ a quantum algorithm using Qiskit:
1. on a simulator locally on your own machine
2. on a simulator hosted in the cloud by IBM
3. on an actual quantum computer accessible through IBM Quantum.

All of these are called backends. In all cases, the _backend_ can easily be swapped for another as long as the simulator or device has appropriate resources (number of qubits etc.). In the code below, we show how to access different backends. We demonstrate this using the local Aer QASM simulator from Qiskit. The Aer QASM simulator models the physical properties of a real quantum computer so you, researchers, and developers can test their quantum computing code and algorithms before running on real devices.

```python
# We will use the Aer provided QASM simulator
backend = Aer.get_backend("qasm_simulator")

# Given we are using a simulator, we will fix the algorithm seed to ensure our results are reproducible
algorithm_globals.random_seed = 271828
```

We would like to compare our quantum solution to that obtained classically. Secondly, we also want to try different algorithms. The following three subsections show how these different methods for solving the Crop-Yield problem are implemented in Qiskit. The two algorithms used are the [_Quantum Approximate Optimization Algorithm_](https://qiskit.org/documentation/stubs/qiskit.algorithms.QAOA.html?highlight=qaoa#qiskit.algorithms.QAOA) `QAOA` and the [_Variational Quantum Eigensolver_](https://qiskit.org/documentation/stubs/qiskit.algorithms.VQE.html?highlight=vqe#qiskit.algorithms.VQE) `VQE`.

Both of these algorithms are hybrid, in that they use a classical _optimizer_ to alter parameters that affect the quantum computation. The VQE algorithm is used to find the lowest eigenvalue of a matrix, which can describe a system to optimize. The QAOA also finds the lowest eigenvalue, but achieves this is a different way to VQE. Both are very popular algorithms, with varying applications and strengths.

### Classical Solution

The classical solution to the crop-yield problem can easily be found using Numpy and Qiskit. The QUBO problem can be solved by finding the minimum eigenvalue of its underlying matrix representation. Fortunately, we don't have to know what this matrix looks like. We only need to pass it to a `MinimumEigensolver` and `MinimumEigenOptimizer`.

The optimizer translates the provided problem into a parameterised representation which is then passed to the solver. By optimizing the paramters, the solver will eventually give the minimum eigenvalue for the parameterized representation and thus the solution to the original problem. Here we use a classical solver from NumPy, the `NumPyMinimumEigensolver`.

```python
def get_classical_solution_for(quadprog: QuadraticProgram):
    # Create solver
    solver = NumPyMinimumEigensolver()

    # Create optimizer for solver
    optimizer = MinimumEigenOptimizer(solver)

    # Return result from optimizer
    return optimizer.solve(quadprog)
```

If we execute the classical method for our crop-yield problem, we get a valid solution that maximises the yield.

```python
# Get classical result
classical_result = get_classical_solution_for(cropyield)

# Format and print result
print("Solution found using the classical method:\n")
print(f"Maximum crop-yield is {classical_result.fval} tons")
print(f"Crops used are: ")

_crops = [v.name for v in cropyield.variables]
for cropIndex, cropHectares in enumerate(classical_result.x):
    print(f"\t{cropHectares} ha of {_crops[cropIndex]}")
```

### QAOA Solution

To solve our problem using QAOA, we need only replace the classical_solver with a `QAOA` class instance. Now that we are running a quantum algorithm, we need to tell the solver where to execute the quantum component. We use a `QuantumInstance` to store the backend information. The QAOA is an iterative algorithm, and thus is run multiple times with different internal parameters. The parameters are tuned classically during the optimization step of the computation by `optimizer`. If we leave `optimizer` as `None`, our algorithms will use the default optimization algorithm. To determine how many iterations there are, we define a callback function that runs for each iteration and stores the number of evaluations thus far. At the end of our algorithm execution, we return the result and the number of iterations.

```python
def get_QAOA_solution_for(
    quadprog: QuadraticProgram, quantumInstance: QuantumInstance, optimizer=None,
):
    _eval_count = 0

    def callback(eval_count, parameters, mean, std):
        nonlocal _eval_count
        _eval_count = eval_count

    # Create solver
    solver = QAOA(
        optimizer=optimizer, quantum_instance=quantumInstance, callback=callback,
    )

    # Create optimizer for solver
    optimizer = MinimumEigenOptimizer(solver)

    # Get result from optimizer
    result = optimizer.solve(quadprog)
    return result, _eval_count
```

If we execute the QAOA method for our crop-yield problem, we get the same result as the classical method, showing that 1) the quantum solution is correct and 2) that you now know how to use a quantum algorithm! 🌟

```python
# Create a QuantumInstance
simulator_instance = QuantumInstance(
    backend=backend,
    seed_simulator=algorithm_globals.random_seed,
    seed_transpiler=algorithm_globals.random_seed,
)

# Get QAOA result
qaoa_result, qaoa_eval_count = get_QAOA_solution_for(cropyield, simulator_instance)

# Format and print result
print("Solution found using the QAOA method:\n")
print(f"Maximum crop-yield is {qaoa_result.fval} tons")
print(f"Crops used are: ")
for cropHectares, cropName in zip(qaoa_result.x, qaoa_result.variable_names):
    print(f"\t{cropHectares} ha of {cropName}")

print(f"\nThe solution was found within {qaoa_eval_count} evaluations of QAOA.")
```

### VQE Solution

The `VQE` algorithm works in a very similar way to the `QAOA`. Not only in a mathematical modelling and algorithmic perspective, but also programmatically. There is a quantum solver and a classical optimizer. The `VQE` instance is also iterative, and so we can measure how many iterations are needed to find a solution to the Crop-Yield problem.

```python
def get_VQE_solution_for(
    quadprog: QuadraticProgram, quantumInstance: QuantumInstance, optimizer=None,
):
    _eval_count = 0

    def callback(eval_count, parameters, mean, std):
        nonlocal _eval_count
        _eval_count = eval_count

    # Create solver and optimizer
    solver = VQE(
        optimizer=optimizer, quantum_instance=quantumInstance, callback=callback
    )

    # Create optimizer for solver
    optimizer = MinimumEigenOptimizer(solver)

    # Get result from optimizer
    result = optimizer.solve(quadprog)
    return result, _eval_count
```

And we should get the exact same answer as before.

```python
# Create a QuantumInstance
simulator_instance = QuantumInstance(
    backend=backend,
    seed_simulator=algorithm_globals.random_seed,
    seed_transpiler=algorithm_globals.random_seed,
)

# Get VQE result
vqe_result, vqe_eval_count = get_VQE_solution_for(cropyield, simulator_instance)

# Format and print result
print("Solution found using the VQE method:\n")
print(f"Maximum crop-yield is {vqe_result.fval} tons")
print(f"Crops used are: ")
for cropHectares, cropName in zip(vqe_result.x, vqe_result.variable_names):
    print(f"\t{cropHectares} ha of {cropName}")

print(f"\nThe solution was found within {vqe_eval_count} evaluations of VQE")
```

### Exercise 1c: Classical and Quantum Computational Results

From the above computations you received six results, the maximum crop-yield and the number of evaluations for three different methods. The maximum yield values should be the same. If your yield values aren't all the same, rerun the algorithms. Sometimes the optimization process can miss the correct answer because of the randomness used to initialize the algorithm parameters.

Run the code cell below to see if the maximum yields you computed are correct.

```python
from qc_grader import grade_ex1c

max_yield_qaoa = qaoa_result.fval
max_yield_vqe = vqe_result.fval

grade_ex1c(tonnage_qaoa=max_yield_qaoa, tonnage_vqe=max_yield_vqe)
```

_You could always verify your result with the classical method, though this is only possible here given the size of the problem. Larger problems become more difficult to verify._

## Simulating a Real Quantum Computer for the Crop-Yield Problem

Sometimes one would want to _simulate_ a real quantum computer to see how the actual hardware may impact the performance of the algorithm. All quantum computers have an underlying architecture, different noise characeristics, and error rates. These three aspects impact how well the algorithm can perform on a given deivce. To test the impact a given quantum computer has on the QAOA instance, we can utilize a _fake_ instance of the device in Qiskit to tell our simulator what parameters to use. In this example we will be simulating `ibmq_johannesburg`, a device named after the city of Johannesburg in South Africa.

```python
fake_device = Fake.FakeJohannesburg()
```

We can inspect what this device _looks_ like using the Qiskit Jupyter tools, shown below. You do not need to know about this structure to execute quantum programs on a device, but it is useful to visualize the parameters.

```python
import qiskit.tools.jupyter

fake_device
```

The three aforementioned components of a quantum computer are represented as a noise model, coupling map, and the basis gate set. The noise model is a representation of how the noise and errors in the computer behave. The coupling map and basis gate set are core to the architecture of the device. The coupling map represents how the physical qubits can interact whereas the basis gate set is analogous to the set of fundamental computatonal instructions we can use. You can see the coupling map in the above widget as the lines connecting each qubit in the architecture diagram.

To simulate `ibmq_johannesburg`, we must pass these three components to our Aer simulator.

```python
# Create the noise model, which contains the basis gate set
noise_model = NoiseModel.from_backend(fake_device)

# Get the coupling map
coupling_map = fake_device.configuration().coupling_map
```

Next we create a new `QuantumInstance` with these parameters

```python
fake_instance = QuantumInstance(
    backend=backend,
    basis_gates=noise_model.basis_gates,
    coupling_map=coupling_map,
    noise_model=noise_model,
    seed_simulator=algorithm_globals.random_seed,
    seed_transpiler=algorithm_globals.random_seed,
)
```

We can then execute the `QAOA` from before on this new _fake_ quantum device.

```python
# Get QAOA result
qaoa_result, qaoa_eval_count = get_QAOA_solution_for(cropyield, fake_instance)

# Format and print result
print("Solution found using the QAOA method:\n")
print(f"Maximum crop-yield is {qaoa_result.fval} tons")
print(f"Crops used are: ")
for cropHectares, cropName in zip(qaoa_result.x, qaoa_result.variable_names):
    print(f"\t{cropHectares} ha of {cropName}")

print(f"\nThe solution was found within {qaoa_eval_count} evaluations of QAOA.")
```

### Scaling of the Quantum Solution vs Classical

When we created our quadratic program for the crop-yield problem, we saw that the Ising Model required 6 qubits. We had constrained our problem such that we could only plant up to 1 hectare per crop. However, we could change the model so that we can plot 3 hectares per crop, upto our maximum available farm area of 3 hectares.

How many qubits would this ising model require?

---

Furthermore, what if we had more land to farm? We know that this problem is NP-Hard and thus classical solutions are mostly found using heuristics. This is the core reason why quantum computers are promising to solve these kinds of problems. But what are the resource requirements for the quantum solution, with a larger farm and crops that can be planted in more hectares?

To illustrate this, we've provided a function that returns the number of qubits required by the underlying Ising Model for the Crop-Yield Problem. We then see the estimated number of qubits needed for different problem parameters. Feel free to modify the variables being used to see how the qubit resource requirements change.

```python
# Function to estimate the number of qubits required
def estimate_number_of_qubits_required_for(max_hectares_per_crop, hectares_available):
    return int(
        4 * np.ceil(np.log2(max_hectares_per_crop + 1))
        + np.ceil(np.log2(hectares_available + 1))
    )

# Our new problem parameters
hectares_available = 10
max_hectares_per_crop = 10

# Retrieving the number of qubits required
number_of_qubits_required = estimate_number_of_qubits_required_for(
    max_hectares_per_crop=max_hectares_per_crop, hectares_available=hectares_available
)

print(
    f"Optimizing a {hectares_available} ha farm with each crop taking up to {max_hectares_per_crop} ha each,",
    f"the computation is estimated to require {number_of_qubits_required} qubits.",
)
```

The number of qubits required is related to the constraints in the quadratic program and how the integer variables are converted to binary variables. In fact, the scaling of the number of qubits, as a function of the hectares available, is logarithmic in nature; owing to this conversion.

## Running on real quantum hardware

To use the IBM Quantum platform is easy. First you need to load the account you enabled in the week 0 content. If you didn't complete this, follow this [quick guide](https://quantum-computing.ibm.com/lab/docs/iql/manage/account/ibmq) on connecting your IBM Quantum account with Qiskit in python and Jupyter.

```python
IBMQ.load_account()
```

IBM Quantum backends are accessed through a provider, which manages the devices to which you have access. For this challenge, you have access to the new `ibm_perth` quantum computer! Typically, you would find your provider details under your [IBM Quantum account details](https://quantum-computing.ibm.com/account). Under your account you can see the different hubs, groups, and projects you are a part of. Qiskit allows us to retrieve a provider using just the hub, group, and project as follows:

```python
provider = IBMQ.get_provider(hub="ibm-q", group="open", project="main")
```

However, because we have given you special access for this challenge, we are going to retrive the provider using a different method. Execute the code cell below to retrieve the correct provider.

```python
provider = None
for prov in IBMQ.providers():
    if (
        "iqc-africa-21" in prov.credentials.hub
        and "q-challenge" in prov.credentials.group
        and "ex1" in prov.credentials.project
    ):
        # Correct provider found
        provider = prov

if provider == None:
    print("ERROR: The expected provider was not found!")
else:
    print("Yay! The expected provider was found!")
```

If the above code cell returned an error, you may not yet have access to the real quantum computer. The list of participants is updated daily, so you may have to wait some time before the correct provider appears. If you need assistance, send a message to the challenge Slack channel [#challenge-africa-2021](https://qiskit.slack.com/archives/C02C8MKP153) and make sure to tag the admin team with [@africa_admin](#).

------

To retrieve a backend from the provider, one needs only request it by name. For example, we can request `ibm_perth` as follows.

```python
backend_real = provider.get_backend("ibm_perth")
```

We can also list all backends available through a given backend. In this example we use the _open_ provider as it has access to all open devices and simulators, instead of the limited few for the challenge.

```python
for _backend in IBMQ.get_provider(hub='ibm-q', group='open', project='main').backends():
    print(_backend.name())
```

Qiskit provides visual tools to view backend information in a jupyter notebook. To accomplish this, one needs to import the `jupyter` submodule and call the appropriate _magic comment_. With `qiskit_backend_overview` you can view all devices accessible by the current IBMQ account. Notice how it does not include simulators. Furthermore, you should see that all devices available through the _open group_ have at most 5 qubits. This is a problem for solving the crop-yield problem we created earlier, as we showed it requires 6 qubits.

To demonstrate how a real quantum device is used, a smaller `QuadraticProgram` is provided, requiring a maximum of 4 qubits.

```python
%qiskit_backend_overview
```

If you want access to larger and more sophisticated quantum computers through IBM, see if your university or company is part of the [IBM Quantum Network](https://www.ibm.com/quantum-computing/network/members/). Researchers are institutions that are part of the [African Research Universities Alliance (ARUA)](https://arua.org.za/) can also apply for access through the University of the Witwatersrand, in South Africa; which is a member of the IBM Quantum Network. If you are a researcher, you can also apply for access through the [IBM Quantum Researchers Program](https://www.ibm.com/quantum-computing/researchers-program/). If you're a student at a highschool or university, you can ask your teachers or lecturers to apply for access through the [IBM Quantum Educators Program](https://www.ibm.com/quantum-computing/educators-program/).

Given that we have imported `qiskit.tools.jupyter`, Jupyter will now display a helpful widget when an IBMQ backend is displayed. We do not have to use a _magic comment_ here as the jupyter submodule defines how some variables are displayed in a jupyter notebook, without requiring a _magic comment_.

```python
backend_real
```

We can create a new `QuantumInstance` object to contain our real quantum computer backend, similar to how we created one to manage our simulator. For real devices, there is an extra parameter we can set: `shots`. The output of a quantum computing algorithm is probabilistic. Therefore, we must execute the quantum computation multiple times, sampling the outputs to estimate their probabilities. The number of `shots` is the number of executions of the quantum computation. Here we set out `QuantumInstance` to use the least busy backend with 2048 shots.

```python
quantum_instance_real = QuantumInstance(backend_real, shots=2048)
```

The VQE algorithm and QAOA are iterative, meaning that they incorporate a classical-quantum loop which repeats certain computations, _hopefully_ converging to a valid solution. In each iteration, or evaluation, the quantum backend will execute the quantum operations 2048 times. Each shot is quite fast, so we do not have to worry about a significant increase in processing time by using more shots.

----
We now define our small crop-yield problem, which only requires 4 qubits. In this example, only Wheat and Maize are used. The model is altered to illustrate the impact of growing too much of a single crop, with the yield decreasing as the number of hectares of a single crop is increased. However, utilizing both wheat and maize increases yield, showing the benefits of intercropping.

**NB:** The maximum number of hectares available is 4, but given that the model would never exceed this limit, the linear constraint defining the maximum number of hectares is not included. This reduces the number of qubits required from 6 to 4.

```python
# Create a small crop-yield example quadratic program
cropyield_small = QuadraticProgram(name="Small Crop-Yield")

# Add two variables, indicating whether we grow 0, 1, or 2 hectares for two different crops
cropyield_small.integer_var(lowerbound=0, upperbound=2, name="Wheat")
cropyield_small.integer_var(lowerbound=0, upperbound=2, name="Maize")

# Add the objective function defining the yield in tonnes
cropyield_small.maximize(
    linear={"Wheat": 3, "Maize": 3},
    quadratic={("Maize", "Wheat"): 1, ("Maize", "Maize"): -2, ("Wheat", "Wheat"): -2},
)

# This linear constraint is not used as the model never reaches this. This is because the
# sum of the upperbounds on both variables is 4 already. If this constraint is applied, the
# model would require 6 qubits instead of 4.
# cropyield_small.linear_constraint(linear={"Wheat": 1, "Maize": 1}, sense="<=", rhs=4)

print(cropyield_small)
```

Here we verify that our small crop-yield problem requires only 4 qubits.

```python
# Estimate the number of qubits required
ising_operations_small, _ = (
    QuadraticProgramToQubo()
    .convert(
        cropyield_small,
    )
    .to_ising()
)
print(f"Number of qubits required is {ising_operations_small.num_qubits}")
```

### Exercise 1d: Submitting a job to a real quantum computer

Now that we know the problem can be run on our chosen device, we can execute the VQE algorithm. In this case we will set the optimizer, with a maximum number of iterations of 1, so that we do not occupy the device for too long. Our answer will be incorrect, but we only want to see how to send a quantum program to a real quantum computer.

```python
# Create our optimizer
optimizer = COBYLA(maxiter=1)

## Get result from real device with VQE
vqe_result_real, vqe_eval_count_real = get_VQE_solution_for(
    cropyield_small, quantum_instance_real, optimizer=optimizer
)
```

Qiskit uses `jobs` to track computations and their results on remote devices and simulators. We can query the backend object for the jobs it received, which would be those created by the VQE algorithm.

```python
# Retrieve the VQE job sent
job_real = backend_real.jobs()[0]

print(f"VQE job created at {job_real.creation_date()} and has a job id of {job_real.job_id()}")
```

Put the job id for your the above job into the cell below and execute the code cell.

```python
from qc_grader import grade_ex1d

job_id = 'put your job id here'

grade_ex1d(job_id)
```

You have now completed the first lab of the IBM Quantum Challenge Africa 2021! Make sure that you are on the [Qiskit Slack channel](https://ibm.co/Africa_Slack) so you can ask questions and talk to other participants. There are two more labs left in the challenge, which are more difficult than this introductory lab, covering quantum computing for finance and HIV.

## References

[1] A. A. Nel, ‘Crop rotation in the summer rainfall area of South Africa’, South African Journal of Plant and Soil, vol. 22, no. 4, pp. 274–278, Jan. 2005, doi: 10.1080/02571862.2005.10634721.

[2] H. Ritchie and M. Roser, ‘Crop yields’, Our World in Data, 2013, [Online]. Available: https://ourworldindata.org/crop-yields.

[3] G. Brion, ‘Controlling Pests with Plants: The power of intercropping’, UVM Food Feed, Jan. 09, 2014. https://learn.uvm.edu/foodsystemsblog/2014/01/09/controlling-pests-with-plants-the-power-of-intercropping/ (accessed Feb. 15, 2021).

[4] N. O. Ogot, J. O. Pittchar, C. A. O. Midega, and Z. R. Khan, ‘Attributes of push-pull technology in enhancing food and nutrition security’, African Journal of Agriculture and Food Security, vol. 6, pp. 229–242, Mar. 2018.

```python
import qiskit.tools.jupyter

%qiskit_version_table
%qiskit_copyright
```

Version Information

Qiskit Software	Version

qiskit-terra	0.18.1

qiskit-aer	0.8.2

qiskit-ignis	0.6.0

qiskit-ibmq-provider	0.16.0

qiskit-aqua	0.9.4

qiskit	0.29.0

qiskit-nature	0.1.5

qiskit-finance	0.2.0

qiskit-optimization	0.2.1

qiskit-machine-learning	0.2.0

System information

Python	3.8.10 | packaged by conda-forge | (default, May 11 2021, 07:01:05)
[GCC 9.3.0]

OS	Linux

CPUs	8

Memory (Gb)	31.400043487548828

Wed Sep 08 20:29:02 2021 UTC

This code is a part of Qiskit

© Copyright IBM 2017, 2021.

This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
 of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.