# Brownian process in stock price dynamics

```python

```

```python

```

```python

```

Brownian Moton:

source: https://en.wikipedia.org/wiki/Brownian_motion

```python

```

```python

```

```python

```

```python

```

A **random-walk** can be seen as a **motion** resulting from a succession of discrete **random steps**.

The random-walk after the i-th steps is:
\begin{equation}
\tag{1}
X_{i} = X_{i-1} + \epsilon_{i}
\end{equation}

being $X_{i=0} = X_{0} = 0$ the starting point and $\epsilon_{i}$ a random variable

```python
# conda install -c anaconda pandas-datareader
# pip install pandas-datareader
```

```python
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()
```

```python
# Possible steps
steps = [-1,1] # backward and forward of 1 units
# Nr  of steps n_steps
n_steps = 100
```

```python
# Initialise the random walk variable X
X = np.zeros(n_steps)     #<--- numpy array of (N=n_steps) zeros
# Fill in X according to eq. 1
for i in range(1,n_steps):
    X[i]= X[i-1] + np.random.choice(steps)#<--- from 1 to fulfill Initial condition

```

```python

```

```python
# Faster alternative
def random_walk(steps,n_steps):
    random_steps = np.random.choice(steps,size=n_steps)
    X = random_steps.cumsum()
    X[0] = 0 # <--- initial position
    return X
```

```python
for i in range(4):
    plt.plot(random_walk(steps,n_steps))
```

```python

```

```python

```

```python

```

**If we repeat the experiment where does the man end up in average?**

```python
# Repeat the random walk n_trials time
# Record the last position for each trial
def monte_random_walk(n_steps,steps,n_trials):
    X_fin = np.zeros(n_trials)#<-- X_fin numpy array of (N=n_trial) zeros
    for i in range(n_trials):
        X_fin[i] =random_walk(steps,n_steps)[-1]
    return X_fin
```

```python

```

```python
n_trial = 20000
steps = [-1,1]
n_steps = 100
```

```python
X_fin = monte_random_walk(n_steps,steps,n_trial)
```

```python

```

```python

```

```python
# Plot the distribution of X_fin
width_bin = 4
n_bins = int(np.ceil((np.max(X_fin)-np.min(X_fin))/width_bin))

sns.distplot(X_fin,kde=True,bins=n_bins);
plt.xlabel('Final position');
```

```python
np.std(X_fin)
```

```python

```

We can see a Brownian process $B(t)$ as a **continuous Gaussian** random walk.

**Gaussian & continuous**: we divide the  observation time $t$  into $N$ small timestep $\Delta t$, so that $t=N\cdot\Delta t$.

For any time $t_i=i\cdot\Delta t$, the change in $B$ is normally distributed:

$$ B_{i+1}-B_i \sim \sqrt{\Delta t}\cdot N(0,1)$$

Taking time step $\Delta t$ smaller and smaller will make B a continuous random walk.

```python
def brownian_motion(T,N,n_trials,random_seed = None):
    np.random.seed(random_seed)
    dt = T/N
    random_steps = np.sqrt(dt)*np.random.normal(loc = 0,scale = 1,size = (N,n_trials))
    random_steps[0,:] = 0
    X = np.cumsum(random_steps,axis=0)

    return X
```

```python

```

```python

```

```python

```

```python

```

```python
T=7
N=100
n_trials=2000
random_seed = 1
dt=T/N
dt
```

    0.07

```python

```

```python
X= brownian_motion(T,N,n_trials,random_seed)
```

```python

```

```python
# Last step
X_fin = X[-1,:]
```

```python
plt.plot(X);
```

```python
# Plot the distribution of X_fin
width_bin = .51
n_bins = int(np.ceil((np.max(X_fin)-np.min(X_fin))/width_bin))

sns.distplot(X_fin,bins=n_bins);

```

### Connection to stock-price

The dynamics of stock-prices can be modeled by the following equation:

\begin{equation}
\tag{2}
\Delta S_{t} = \mu S_{t} \Delta t + \sigma S_{t}\Delta B_{t}
\end{equation}

being:

$S$ the stock price,

$\mu$ the drift coefficient (a.k.a the mean of returns),

$\sigma$ the diffusion coefficient (a.k.a the standard deviation of returns),

$B$ the brownian motion.

The eq. (2) admits the following solution:
\begin{equation}
\tag{3}
S(t) = S_{0} \cdot e^{[(\mu - \sigma^2/2)\cdot t + \sigma \cdot B_{t}] }
\end{equation}

```python
def stock_price(N,S0,u,sigma,T,n_trials,random_seed = None):
    """
    N: number of intervals
    S0: initial stock price
    u: mean of returns over some period
    sigma: volatility a.k.a. standard deviation of returns
    random_seed: seed for pseudorandom generator
    T: observation time
    m: number of brownian path
    """
    dt = T/N
    t = np.arange(0.,T,dt)
    t=t[:,np.newaxis]
    drift = (u - (sigma/np.sqrt(2))**2)*t
    shock = sigma * brownian_motion(T,N,n_trials,random_seed = None)
    S = S0*np.exp(drift + shock)
    return t, S
```

### Scraping from Yahoo Finance

```python
from pandas_datareader import data as scraper
import pandas as pd

symbol = 'FB' # 'FB'Facebook, 'FCA.MI' FIAT Crysler, 'AAPL' Apple
start_date = '2020-01-01'
end_date = '2020-12-31'
df = scraper.DataReader(symbol, 'yahoo', start_date, end_date)

```

```python
df.head()
```

      High
      Low
      Open
      Close
      Volume
      Adj Close

      Date

      2020-01-02
      209.789993
      206.270004
      206.750000
      209.779999
      12077100
      209.779999

      2020-01-03
      210.399994
      206.949997
      207.210007
      208.669998
      11188400
      208.669998

      2020-01-06
      212.779999
      206.520004
      206.699997
      212.600006
      17058900
      212.600006

      2020-01-07
      214.580002
      211.750000
      212.820007
      213.059998
      14912400
      213.059998

      2020-01-08
      216.240005
      212.610001
      213.000000
      215.220001
      13475000
      215.220001

```python
df.describe()
```

```python
#close price
close_price = df['Close']
close_price.plot();
plt.ylabel('Price $');
```

```python

```

```python
# Calculate the daily percentage return
daily_return= (close_price.pct_change() )
```

```python
daily_return.plot(label='Daily Return')
(close_price*.002).plot(label='Close Price');
plt.legend();
```

```python
# Plot the distribution of daily_return
width_bin = .01
n_bins = int(np.ceil((np.max(daily_return)-np.min(daily_return))/width_bin))

sns.distplot(daily_return,bins=n_bins);
plt.title("Daily returns on FB, 2020");
```

```python

```

```python
# compute the return mu and the sigma
mu = np.mean(daily_return)
sigma  = np.std(daily_return)

print(f'Mean of daily-returns μ: {round(mu,4)*100} %')
print('')
print(f'Volatility σ: {round(sigma,3)}')

```

    Mean of daily-returns μ: 0.15 %

    Volatility σ: 0.029

```python
# Parameters simulation
N = 5000 # <--- lenght of each trials
T=252    # <--- # days of a business year
S0=close_price[0]   # <--- Initial close-price
n_trials=25500  # <--- # of trials
T/N # <--- Δt about 0.05
```

    0.0504

```python
# Extracting stock price pathways and time vector from the model
t,model_S = stock_price(N,S0,mu,sigma,T,n_trials,random_seed = 42)
#model_S.shape
```

```python
# Define other two time range
t2=np.arange(0,253,1)

```

```python
# Plot simulated and actual stock-prizes
plt.plot(t,model_S);
#plt.plot(t3,close_price[-12:],linewidth=3,c='k');
plt.plot(t2,close_price[:],linewidth=3,c='k');
plt.xlabel('Days');
plt.ylabel('Stock Price');
```

```python
# Compute final predicted stock-price
S_fin = model_S[-1,:]
```

```python

```

```python

```

```python

```

```python
# Calculate mean and std from S_fin
mean = np.mean(S_fin)
median=np.median(S_fin)
std_ = np.std(S_fin)
min_ = np.min(S_fin)
max_ = np.max(S_fin)
print('*******************')
print(f'  * Statistics *')
print('*******************\n')
print(f'Min:                  {round(min_)} $')
print(f'Max:                {round(max_)} $')
print(f'Median:              {round(median)} $')
print(f'Mean:                {round(mean)} $')
print(f'Standard deviation:  {round(std_)} $')
```

    *******************
      * Statistics *
    *******************

    Min:                  47.0 $
    Max:                1705.0 $
    Median:              272.0 $
    Mean:                303.0 $
    Standard deviation:  147.0 $

```python
# Plot the simulated final stock-price
sns.distplot(S_fin);
plt.plot([median,median], [0, .02], 'k-.', lw=6,label='median')
plt.plot([mean,mean], [0, .02], 'b-.', lw=2,label='mean')
plt.plot([close_price[-1],close_price[-1]], [0, .02], 'g-', lw=2,label='actual prize')
plt.ylim(top=0.004);
plt.xlim(left=-100,right=1200)
plt.legend();
plt.title('Montecarlo Simulation on Facebook Stock-Price');
plt.xlabel('Stock price $');
```

```python

```

```python
from scipy.stats import norm,lognorm,t
```

```python

```

```python
def lognorm_fit(data_,x_min,x_max,dx):
    # Fits the datas with a log-norm distribution
    params = lognorm.fit(data_)
    shape, mean, std = params

    # Generate a log-norm probability distribution function pdf
    x = np.arange(x_min,x_max,dx)
    lnd = lognorm(s=shape,loc=mean,scale=std)# <--- initialise the log-norm distribution
    lognormal_pdf =lnd.pdf(x)

    # Calculate the mode of distribution
    index_max = np.argmax(lognormal_pdf) #np.where(lognormal_pdf == np.max(lognormal_pdf))
    mode =x[index_max]
    return lnd,lognormal_pdf, mode,x

```

```python
x_min=0
x_max=5000
dx=.1
```

```python
# Distribution and mode
lnd_S,lognormal_pdf_S,mode_S,x = lognorm_fit(S_fin,x_min,x_max,dx)
```

```python
# Plot the simulated final stock-price
sns.distplot(S_fin);
sns.lineplot(x,lognormal_pdf_S,label = 'log-normal')
plt.plot([mode_S,mode_S],[0,.02],'r-.',label= 'mode')
plt.plot([median,median], [0, .02], 'k-.', lw=6,label='median')
plt.plot([mean,mean], [0, .02], 'b-.', lw=2,label='mean')
plt.plot([close_price[-1],close_price[-1]], [0, .02], 'g-', lw=2,label='actual prize')
plt.ylim(top=0.004);
plt.xlim(left=-100,right=1200)
plt.legend();
plt.title('Montecarlo Simulation on Facebook Stock-Price');
plt.xlabel('Stock price $');
```

```python

```

```python

```

what is the probability of having a loss after one year?

```python
# Annual Return
annual_return_pct = (S_fin -S0)/S0
```

```python
# Calculate mean and std from S_fin
mean_ar = np.mean(annual_return_pct)
median_ar=np.median(annual_return_pct)
std_ar = np.std(annual_return_pct)
min_ar = np.min(annual_return_pct)
max_ar = np.max(annual_return_pct)
print('*******************')
print(f'  * Statistics *')
print('*******************\n')
print(f'Min:                {round(min_ar,2)} %')
print(f'Max:                 {round(max_ar,2)} %')
print(f'Median:               {round(median_ar,2)} %')
print(f'Mean:                {round(mean_ar,2)} %')
print(f'Standard deviation:   {round(std_ar,2)} %')
```

    *******************
      * Statistics *
    *******************

    Min:                -0.77 %
    Max:                 7.13 %
    Median:               0.3 %
    Mean:                0.44 %
    Standard deviation:   0.7 %

```python
# Plot distribution of simulated annual return
sns.distplot(annual_return_pct);

plt.ylim(top=0.8);
plt.xlim(left=-3,right=6)

plt.title('Montecarlo Simulation on Facebook Stock-Price');
plt.xlabel('Annual Return % ');
```

Analysis of underlying distribution

```python
x_min=-5
x_max=6
dx=.001
```

```python
# Distribution and mode
lnd_ar,lognormal_pdf_ar,mode_ar,x_ar = lognorm_fit(annual_return_pct,x_min,x_max,dx)
```

```python

```

```python
# Plot distribution of simulated annual return
sns.distplot(annual_return_pct);
sns.lineplot(x_ar,lognormal_pdf_ar,label = 'log-normal');
plt.plot([mode_ar,mode_ar],[0,.9],'k-.',label= 'mode');

plt.ylim(top=0.8);
plt.xlim(left=-3,right=6)
plt.legend();
plt.text(x=2,y=.5,s=f'mode @ {round(mode_ar,3)}',)
plt.title('Montecarlo Simulation on Facebook Stock-Price');
plt.xlabel('Annual Return % ');
```

```python
# Cumulative distribution Function CDF (probability of obtaining a value equal or smaller than the given value)
cdf = lnd_ar.cdf(x_ar) # <--- cumulative
```

```python

```

```python
# Plot CDF SF function
sns.lineplot(x_ar,cdf,label='CDF');
plt.plot([0,0], [0, 1], 'r-', lw=2,label='No returns');
plt.legend();
plt.xlabel('Annual Return %');

```

```python
def get_prob(value_return1,value_return2=None):

    mask_1 = (x_ar<=value_return1)

    if value_return2==None:
        prob = round(np.max(cdf[mask_1])*100,2)

    else:
        mask_2 = (x_ar<=value_return2)
        area1 = np.max(cdf[mask_1])*100
        area2 = np.max(cdf[mask_2])*100
        prob = np.round(area2 - area1,2)
    return prob
```

```python
print('**************************************')
print('             * Results *')
print('**************************************\n')
print('      Return_1   Return_2  Probability\n')
print(f'Loss                           {get_prob(-0.0001)} %')
print(f'Gain     0.1%          1%       {get_prob(0.1,1)} % ')
print(f'Gain       1%          2%      {get_prob(1,2)} % ')

```

    **************************************
                 * Results *
    **************************************

          Return_1   Return_2  Probability

    Loss                           28.48 %
    Gain     0.1%          1%       46.65 %
    Gain       1%          2%      13.91 %

```python

```


```python
def double(x):
    print(x*2)
```

```python
double(4)
```

    8

```python
def double(x):
    return x*2
```

```python
a = double(4)
print(a)
```

    8

```python
def yourFunction(x):
    for y in range(2,11):
        print(x*y)
yourFunction(5)
```

    10
    15
    20
    25
    30
    35
    40
    45
    50

```python
import sympy
p = sympy.Symbol("p")
```

```python
import sympy
import matplotlib.pyplot as plt
p = sympy.Symbol("p")
def Equilibrium(demandEquation,supplyEquation,priceStart,priceEnd):
    prices = []
    demandQ = []
    supplyQ = []
```

```python
import sympy
import matplotlib.pyplot as plt
p = sympy.Symbol("p")
def Equilibrium(demandEquation,supplyEquation,priceStart,priceEnd):
    prices = []
    demandQ = []
    supplyQ = []
    for price in range(priceStart,priceEnd+1):
        prices += [price]
        demandQ += [demandEquation.subs(p,price)]
        supplyQ += [supplyEquation.subs(p,price)]
    print(prices)
    print(demandQ)
    print(supplyQ)
```

```python
Equilibrium(10-p,p,0,10)
```

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

```python
import sympy
import matplotlib.pyplot as plt
p = sympy.Symbol("p")
def Equilibrium(demandEquation,supplyEquation,priceStart,priceEnd):
    prices = []
    demandQ = []
    supplyQ = []
    for price in range(priceStart,priceEnd+1):
        prices += [price]
        demandQ += [demandEquation.subs(p,price)]
        supplyQ += [supplyEquation.subs(p,price)]

    equilibriumP = sympy.solve(demandEquation-supplyEquation)[0]
    equilibriumQ = demandEquation.subs(p,equilibriumP)
    print(equilibriumP)
    print(equilibriumQ)
```

```python
Equilibrium(10-p,p,0,10)
```

    5
    5

```python
import sympy
import matplotlib.pyplot as plt
p = sympy.Symbol("p")
def Equilibrium(demandEquation,supplyEquation,priceStart,priceEnd):
    prices = []
    demandQ = []
    supplyQ = []
    for price in range(priceStart,priceEnd+1):
        prices += [price]
        demandQ += [demandEquation.subs(p,price)]
        supplyQ += [supplyEquation.subs(p,price)]

    equilibriumP = sympy.solve(demandEquation-supplyEquation)[0]
    equilibriumQ = demandEquation.subs(p,equilibriumP)

    plt.plot(demandQ,prices)
    plt.plot(supplyQ,prices)
    plt.legend(["Demand","Supply"])
    plt.plot(equilibriumQ,equilibriumP, 'ro')
    plt.xlabel("Supply and Demand Quantity")
    plt.ylabel("Price")
    plt.show()
    print("The equilibrium price is "+str(equilibriumP)+" and equilibrium quantity is "+str(equilibriumQ)+".")
```

```python
Equilibrium(10-p,p,0,10)
```

```python
import sympy as sy
import matplotlib.pyplot as plt
p = sy.Symbol("p")

def demandShift(demandEquation1,demandEquation2,supplyEquation,priceStart,priceEnd):
    prices = []
    demandQ1 = []
    demandQ2 = []
    supplyQ = []
    for price in range(priceStart,priceEnd+1):
        prices += [price]
        demandQ1 += [demandEquation1.subs(p,price)]
        demandQ2 += [demandEquation2.subs(p,price)]
        supplyQ += [supplyEquation.subs(p,price)]

    equilibriumP1 = sy.solve(demandEquation1-supplyEquation)[0]
    equilibriumQ1 = demandEquation1.subs(p,equilibriumP1)
    equilibriumP2 = sy.solve(demandEquation2-supplyEquation)[0]
    equilibriumQ2 = demandEquation2.subs(p,equilibriumP2)

    plt.plot(demandQ1,prices)
    plt.plot(demandQ2,prices)
    plt.plot(supplyQ,prices)
    plt.legend(["Old Demand","New Demand","Supply"])
    plt.plot(equilibriumQ1,equilibriumP1, 'ro')
    plt.plot(equilibriumQ2,equilibriumP2, 'ro')
    plt.xlabel("Supply and Demand Quantity")
    plt.ylabel("Price")
    plt.show()
    print("The old equilibrium price is "+str(equilibriumP1)+" and old equilibrium quantity is "+str(equilibriumQ1)+".")
    print("The new equilibrium price is "+str(equilibriumP2)+" and new equilibrium quantity is "+str(equilibriumQ2)+".")
    print("The price shifted "+str(equilibriumP2-equilibriumP1)+" and the quantity shifted "+str(equilibriumQ2-equilibriumQ1)+".")
```

```python
demandShift(10-p,15-p,p,0,10)
```

```python
def supplyShift(demandEquation,supplyEquation1,supplyEquation2,priceStart,priceEnd):
    prices = []
    demandQ = []
    supplyQ1 = []
    supplyQ2 = []
    for price in range(priceStart,priceEnd+1):
        prices += [price]
        demandQ += [demandEquation.subs(p,price)]
        supplyQ1 += [supplyEquation1.subs(p,price)]
        supplyQ2 += [supplyEquation2.subs(p,price)]

    equilibriumP1 = sy.solve(demandEquation-supplyEquation1)[0]
    equilibriumQ1 = demandEquation.subs(p,equilibriumP1)
    equilibriumP2 = sy.solve(demandEquation-supplyEquation2)[0]
    equilibriumQ2 = demandEquation.subs(p,equilibriumP2)

    plt.plot(demandQ,prices)
    plt.plot(supplyQ1,prices)
    plt.plot(supplyQ2,prices)
    plt.legend(["Old Demand","Old Supply","New Supply"])
    plt.plot(equilibriumQ1,equilibriumP1, 'ro')
    plt.plot(equilibriumQ2,equilibriumP2, 'ro')
    plt.xlabel("Supply and Demand Quantity")
    plt.ylabel("Price")
    plt.show()
    print("The old equilibrium price is "+str(equilibriumP1)+" and old equilibrium quantity is "+str(equilibriumQ1)+".")
    print("The new equilibrium price is "+str(equilibriumP2)+" and new equilibrium quantity is "+str(equilibriumQ2)+".")
    print("The price shifted "+str(equilibriumP2-equilibriumP1)+" and the quantity shifted "+str(equilibriumQ2-equilibriumQ1)+".")
```

```python
supplyShift(10-p,p,2*p,0,10)
```

```python
def dualShift(demandEquation1,demandEquation2,supplyEquation1,supplyEquation2,priceStart,priceEnd):
    prices = []
    demandQ1 = []
    demandQ2 = []
    supplyQ1 = []
    supplyQ2 = []
    for price in range(priceStart,priceEnd+1):
        prices += [price]
        demandQ1 += [demandEquation1.subs(p,price)]
        demandQ2 += [demandEquation2.subs(p,price)]
        supplyQ1 += [supplyEquation1.subs(p,price)]
        supplyQ2 += [supplyEquation2.subs(p,price)]

    equilibriumP1 = sy.solve(demandEquation1-supplyEquation1)[0]
    equilibriumQ1 = demandEquation1.subs(p,equilibriumP1)
    equilibriumP2 = sy.solve(demandEquation2-supplyEquation2)[0]
    equilibriumQ2 = demandEquation2.subs(p,equilibriumP2)

    plt.plot(demandQ1,prices)
    plt.plot(demandQ2,prices)
    plt.plot(supplyQ1,prices)
    plt.plot(supplyQ2,prices)
    plt.legend(["Old Demand","New Demand","Old Supply","New Supply"])
    plt.plot(equilibriumQ1,equilibriumP1, 'ro')
    plt.plot(equilibriumQ2,equilibriumP2, 'ro')
    plt.xlabel("Supply and Demand Quantity")
    plt.ylabel("Price")
    plt.show()
    print("The old equilibrium price is "+str(equilibriumP1)+" and old equilibrium quantity is "+str(equilibriumQ1)+".")
    print("The new equilibrium price is "+str(equilibriumP2)+" and new equilibrium quantity is "+str(equilibriumQ2)+".")
    print("The price shifted "+str(equilibriumP2-equilibriumP1)+" and the quantity shifted "+str(equilibriumQ2-equilibriumQ1)+".")
```

```python
dualShift(10-p,15-p,p,2*p,0,10)
```


# Random walk and diffusion

We are going to look at a 1D process: a particle of mass $m$ moves in solution (e.g., a small bead or virus in water or oil).

We will use numpy and matplotlib.

```python
import numpy as np
import matplotlib.pyplot as plt
plt.matplotlib.style.use("ggplot")
```

## Newton's equation of motion

Start with Newton's equations of motion for intertial motion:

$$
m\frac{dx^2}{dt^2} = f
$$

The force is derived from a conservative potential $U(x)$

$$
f = -\frac{dU}{dx}.
$$

## Langevin equation

But that's only the particle. Where is the water?

Model the solvent _implicitly_ by its influence on the bead
1. **friction** $-\gamma m v$ ($\gamma > 0$ is the *friction coefficient*)
2. random kicks $f_\text{random}$ from collisions

leading to the **Langevin equation**:

$$
m\frac{dx^2}{dt^2} = f - \gamma m \frac{dx}{dt} + f_\text{random}
$$

## Overdamped Langevin equation

If the friction is high then there is no inertial motion:

$$
0 = f - \gamma m \frac{dx}{dt} + f_\text{random}
$$

so the **overdamped Langevin equation** is

$$
\frac{dx}{dt} = \frac{1}{m\gamma}(f + f_\text{random})
$$

## Discretized dynamics

* Simulate over discrete time steps $\Delta t$
* $v = \Delta x/\Delta t$

$$
\Delta x = \frac{\Delta t}{m\gamma}(f + f_\text{random})
$$

$$
\Delta x = \Delta x_\text{deterministic} + \Delta x_\text{random}
$$

### Trajectories

- We are interested in *trajectories*, i.e. $x(t)$.
- Start at $x(t=0) = x_0$ (initial condition).
- Compute $\Delta x$ for subsequent times and add up steps:

  $$
  x(t) = x_0 + \Delta x(\Delta t) + \Delta x(2\Delta t) + \dots + \Delta x(t-\Delta t)
  $$
- Rewrite:

  \begin{align}
  x_j &:= x(t=j\Delta t)\\
  x_{j+1} &= x_j + \Delta x_{j+1}
  \end{align}

- Deterministic force $f = f(x)$, i.e., $f_j = f(x_j)$ so
  $$
  \Delta x_\text{deterministic} = \frac{f(x_j)}{m\gamma} \Delta t
  $$
  (taking $f$ to be approximately constant over $\Delta t$)
- Random force: zero mean Gaussian distribution

### Random force/random displacement

* Zero mean: $\langle \Delta x_\text{random} \rangle = 0$
* Gaussian distributed: $\Delta x_\text{random} \sim \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{\Delta x_\text{random}^2}{2\sigma^2}\right)$
* Variance *must* be $\sigma^2 = \frac{2 k_B T \Delta t}{m \gamma}$ (to satisfy a fluctuation-dissipation theorem)

$k_B T$ is the typical energy in thermal fluctuations where $k_B = 1.380649 × 10^{−23}\,\text{J/K}$ is the [Boltzmann constant](https://en.wikipedia.org/wiki/Boltzmann_constant) and $T$ the temperature in Kelvin.

#### Normal distribution
What does the Gaussian or **normal distribution** $\mathcal{N}$ look like?

$$
\mathcal{N}(x; \mu, \sigma) = \frac{1}{\sqrt{ 2 \pi \sigma^2 }}e^{ - \frac{ (x - \mu)^2 } {2 \sigma^2} }.
$$

Python function for the normal probability density for mean $\mu$ and standard deviation $\sigma$:

```python
def p_normal(x, mu=0, sigma=1):
    return np.exp(-0.5*((x - mu)/sigma)**2) / (np.sqrt(2*np.pi) * sigma)
```

Plot normal distribution for $\mu=0$, $\sigma=0.5$ over a range of $x$ values:

```python
X = np.linspace(-2, 2, 100)
P = p_normal(X, mu=0, sigma=0.5)
plt.plot(X, P, color="blue")
plt.xlabel("x"); plt.ylabel("p(x)");
```

## Simple diffusion

No external force:

$$
f = 0
$$

so only random movement

$$
\Delta x = \Delta x_\text{random}
$$

## Generating random steps

$$
\Delta x_\text{random} \sim \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{\Delta x_\text{random}^2}{2\sigma^2}\right)
$$

with $\sigma^2 = \frac{2 k_B T \Delta t}{m \gamma}$.

I.e. the probability distribution $p(\Delta x_\text{random})$ is the normal distribution $\mathcal{N}(\Delta x_\text{random}; 0, \sigma)$

### Generating random numbers with numpy

How to generate Gaussian random displacements, i.e. **normally distributed** random numbers?

Use [numpy's random number generation](https://numpy.org/doc/stable/reference/random/index.html) facility:

Always start like this (create a random number generator instance `rng`) at the beginning of your code:

```python
import numpy as np

rng = np.random.default_rng()
```

You then use `rng` throughout your code.

#### Samples from the normal distribution

$$
p(x) = \mathcal{N}(x; \mu, \sigma) = \frac{1}{\sqrt{ 2 \pi \sigma^2 }}e^{ - \frac{ (x - \mu)^2 } {2 \sigma^2} }
$$

Get 1000 samples for $\mu = 0$ and $\sigma = 0.5$ using the [normal()](https://numpy.org/doc/stable/reference/random/generated/numpy.random.Generator.normal.html#numpy.random.Generator.normal) method:

```python
mu = 0
sigma = 0.5

x_samples = rng.normal(loc=mu, scale=sigma, size=1000)
```

<small>(`default_rng()` is the new and recommended way to generate random numbers in numpy. Previously, one used functions in [numpy.random](https://numpy.org/doc/stable/reference/random/) such as [numpy.random.normal()](https://numpy.org/doc/stable/reference/random/generated/numpy.random.RandomState.normal.html#numpy.random.RandomState.normal) or [numpy.random.randn()](https://numpy.org/doc/stable/reference/random/generated/numpy.random.RandomState.randn.html#numpy.random.RandomState.randn). New code, however, should use the above approach that starts with a [numpy.random.Generator](https://numpy.org/doc/stable/reference/random/generator.html#simple-random-data).)</small>

### Plotting random numbers

What do the samples and the distribution look like?

#### Samples

Plot the samples $x$:

```python
plt.plot(x_samples)
plt.ylabel("x");
```

#### Probability distribution from samples

How do we get an estimate for the **distribution** $p(x)$ from our samples?

##### Histogramming
**Histogram** the data with the [numpy.histogram()](https://numpy.org/doc/stable/reference/generated/numpy.histogram.html#numpy-histogram) function (and then turn the counts into a probability density):

```python
np.histogram(x_samples, bins=10)
```

    (array([ 12,  36, 122, 226, 274, 208,  86,  31,   4,   1]),
     array([-1.55126948, -1.19586906, -0.84046863, -0.48506821, -0.12966779,
             0.22573263,  0.58113305,  0.93653347,  1.2919339 ,  1.64733432,
             2.00273474]))

Work with the `counts` (the histogram itself, related to $p(x)$) and the `edges` of the bins (related to the samples, our $x$ values)

```python
counts, edges = np.histogram(x_samples, bins=10)
```

```python
len(counts)
```

    10

```python
len(edges)
```

    11

```python
edges
```

    array([-1.55126948, -1.19586906, -0.84046863, -0.48506821, -0.12966779,
            0.22573263,  0.58113305,  0.93653347,  1.2919339 ,  1.64733432,
            2.00273474])

##### Step-plot of histogram counts

Plot the lower edge of each bin:

```python
plt.step(edges[:-1], counts, color="red", where="post")
plt.xlabel("x")
plt.ylabel("counts")
plt.plot(edges[:-1], counts, color="red", marker="o", linestyle="", alpha=0.3)
```

##### Midpoints of bins

Get the *midpoints* $x_k$ of bins $0 \le k < N$ from the edges $e_k, 0 \le k < N+1$:

$$
x_k = \frac{e_k + e_{k+1}}{2}
$$

Python implementation with a loop:

```python
x_values = [0.5*(edges[k] + edges[k+1]) for k in range(len(edges) - 1)]
np.array(x_values)
```

    array([-1.37356927, -1.01816885, -0.66276842, -0.307368  ,  0.04803242,
            0.40343284,  0.75883326,  1.11423369,  1.46963411,  1.82503453])

Using fast **numpy array operations** for $x_k = (e_k + e_{k+1})/2$

```
    +---+---+---+---+------+---+----+
e = | 0 | 1 | 2 | 3 | .... | 9 | 10 |
    +---+---+---+---+------+---+----+
```

"Thinking in arrays":

Calculate `e[k] + e[k+1]` as
```
           +---+---+---+---+------+---+
"e[k]" =   | 0 | 1 | 2 | 3 | .... | 9 |  = e[:-1]
           +---+---+---+---+------+---+
             +   +   +   +          +
           +---+---+---+---+------+---+
"e[k+1]" = | 1 | 2 | 3 | 4 | .... |10 |  = e[1:]
           +---+---+---+---+------+---+
```

```python
x_values = 0.5 * (edges[:-1] + edges[1:])
x_values
```

    array([-1.37356927, -1.01816885, -0.66276842, -0.307368  ,  0.04803242,
            0.40343284,  0.75883326,  1.11423369,  1.46963411,  1.82503453])

Plot estimate histogram at the midpoints of the bins:

```python
plt.step(x_values, counts, color="red", where="mid")
plt.xlabel("x")
plt.ylabel("counts")
plt.plot(x_values, counts, color="red", marker="o", linestyle="", alpha=0.3)
```

##### Probability distribution from samples

A probability distribution $p(x)$ over a continuous variable $x$ should be a *normalized density* , i.e.,

$$
\int_{-\infty}^{+\infty} \!\! p(x)\, dx = 1
$$

or for discrete bins with a value $p_k$ in bin $k$:

$$
\sum_{k=0}^{N-1} p_k \Delta x_k = 1
$$

where $\Delta x_k = (e_{k+1} - e_k)$ is the bin width of bin $k$.

Given counts $h_k$, one can compute the normalized probability density as

$$
p_k = \frac{h_k}{\sum_k h_k}\frac{1}{\Delta x_k}
$$

```python
Delta_x = edges[1:] - edges[:-1]   # = np.diff(edges)
p_k = counts / (counts.sum() * Delta_x)
```

Check normalization:

```python
np.sum(p_k * Delta_x)
```

    0.9999999999999999

Plot estimate for the distribution $p(x)$ of our samples:

```python
plt.step(x_values, p_k, where="mid")
plt.xlabel("x")
plt.ylabel("p(x)");
```

##### `numpy.histogram()` can generate densities

One can directly get a density (for constant bin width) with the `density=True` kwarg:

```python
density, edges = np.histogram(x_samples, bins=10, density=True)
```

Check normalization:

```python
np.sum(density * np.diff(edges))
```

    0.9999999999999999

#### Comparison to the normal distribution
$$
\mathcal{N}(x; \mu, \sigma) = \frac{1}{\sqrt{ 2 \pi \sigma^2 }}e^{ - \frac{ (x - \mu)^2 } {2 \sigma^2} }
$$

```python
X = np.linspace(x_values.min(), x_values.max(), 100)

plt.step(x_values, p_k, where="mid", label="samples")

plt.plot(x_values, p_normal(x_values, mu=mu, sigma=sigma), marker="o", color="blue", linestyle="", alpha=0.4)
plt.plot(X, p_normal(X, mu=mu, sigma=sigma), color="blue", alpha=0.4, label="Normal")

plt.xlabel("x")
plt.ylabel("p(x)")
plt.legend();
```

## Simulating random diffusion

We will simulate the diffusion of a small particle such as a [virus](http://book.bionumbers.org/how-big-are-viruses/). Specifically, we pick the harmless [Brome Mosaic Virus](https://en.wikipedia.org/wiki/Brome_mosaic_virus):

### Parameters

[Brome Mosaic virus](https://bionumbers.hms.harvard.edu/bionumber.aspx?id=105893&ver=3&trm=virus+mass&org=) particle

* mass $m=7.6\times 10^{-21} \text{kg}$
* friction coefficient $\gamma=3.4 \times 10^{10}\,\text{s}^{-1}$,
* time step $\Delta t = 1\times10^{-9}\,\text{s}$,
* temperature $T=300\,\text{K}$
* starting position $x_0 = 0$

### Algorithm (random diffusion)

1. Generate a random step
   $$
   \Delta x = \Delta x_\text{random}
   $$
   with
   $$
   \Delta x_\text{random} \sim \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{\Delta x_\text{random}^2}{2\sigma^2}\right)
   $$
   where $\sigma^2 = \frac{2 k_B T \Delta t}{m \gamma}$.
2. Save the step in `steps`.
3. If not reached max number of steps, go to 1.
4. Generate trajectory from steps
   $$
   x_{j+1} = x_j + \Delta x_{j+1}
   $$
   i.e.,
   $$
   x_{k} = x_0 + \sum_{j=1}^{k} \Delta x_j
   $$

Define parameters

```python
m = 7.6e-21      # kg
gamma = 3.4e10   # s^-1
dt = 10e-9       # s
T = 300          # K

kB = 1.380649e-23 # J/K

max_steps = 10000
x0 = 0           # m

sigma = np.sqrt(2*kB*T * dt / (m * gamma))  # m
```

```python
sigma
```

    5.662014951648707e-10

With $\Delta t = 10~\text{ns}$, the "typical" random step size $\sigma$ is on the order of 0.5 nm, i.e. with a virus particle size of ~50 nm, about 1/100 of the virus diameter.

Generate all steps:

   $$
   \Delta x = \Delta x_\text{random}
   $$
   with
   $$
   \Delta x_\text{random} \sim \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{\Delta x_\text{random}^2}{2\sigma^2}\right)
   $$

```python
steps = rng.normal(loc=0, scale=sigma, size=max_steps)
```

Generate trajectory:

   $$
   x_{k} = x_0 + \sum_{j=1}^{k} \Delta x_j
   $$

```python
x_trajectory = x0 + np.cumsum(steps)
```

```python
x_trajectory[:5]
```

    array([ 5.05723902e-10, -1.74979537e-10,  4.57238300e-10,  9.81406762e-10,
            1.26381263e-09])

Note: The first element *should* be 0: We need to include $x_0$ explicitly.
```python
x_trajectory = np.hstack([x0, x0 + np.cumsum(steps)])
```

Generate trajectory:

   $$
   x_{k} = x_0 + \sum_{j=0}^{k} \Delta x_j
   $$

with $\Delta x_0 = 0$.

```python
steps = rng.normal(loc=0, scale=sigma, size=max_steps+1)     # note size
steps[0] = 0                                                 # dx_0 = 0
```

```python
x_trajectory = x0 + np.cumsum(steps)
```

```python
x_trajectory.shape
```

    (10001,)

```python
x_trajectory[:5]
```

    array([ 0.00000000e+00,  5.52253827e-10, -9.28506712e-11, -1.33016092e-09,
           -1.56203920e-09])

### Plot random diffusion trajectory

```python
times = dt * np.arange(len(x_trajectory))
```

```python
plt.plot(times, x_trajectory)
plt.xlabel("time (s)")
plt.ylabel("position (m)");
```

### Generate multiple trajectories

Each simulation run is different (**stochastic**): What is the *average* behavior?

Run multiple repeats:

```python
N_repeats = 100
```

```python
steps = rng.normal(loc=0, scale=sigma, size=(N_repeats, max_steps+1))
steps[:, 0] = 0       # step 0 with dx=0
```

```python
steps.shape
```

    (100, 10001)

```python
x_trajectories = x0 + np.cumsum(steps, axis=1)
```

```python
x_trajectories.shape
```

    (100, 10001)

Plot the **ensemble of trajectories**

```python
times = dt * np.arange(x_trajectories.shape[1])
plt.plot(times, x_trajectories.T, alpha=0.5);
plt.xlabel("time (s)")
plt.ylabel("position (m)");
```

To show more clearly the distribution of trajectories, with the first one hightlighted:

```python
times = dt * np.arange(x_trajectories.shape[1])
plt.plot(times, x_trajectories.T, color="black", alpha=0.1);
plt.plot(times, x_trajectories[0], color="red", linewidth=3);
plt.xlabel("time (s)")
plt.ylabel("position (m)")
plt.savefig("diffusion_trajectories.png")
```

## Analysing stochastic trajectories

Given the **trajectory ensemble**, what is the **average** behavior (what are the underlying physical laws that are obscured by the stochasticity?)

#### Mean position

Average (mean) position over $N$ trajectories $x_j(t)$

$$
\bar x(t) = \langle x(t) \rangle = \frac{1}{N} \sum_{j=1}^N x_j(t)
$$

where the average is performed *at each time step* $k$ (for $t = k \Delta t$):

$$
\bar x_k = \langle x_k \rangle = \frac{1}{N} \sum_{j=1}^N x_{j,k}
$$

```python
x_mean = x_trajectories.mean(axis=0)
x_mean.shape
```

    (10001,)

Plot the mean position $\bar{x}(t)$:

```python
plt.plot(times, x_mean)
plt.xlabel("time (s)")
plt.ylabel("average position (m)");
```

To put it into perspective: the mean position $\langle x(t) \rangle$ is close to 0 when compared to the trajectories themselves:

```python
plt.plot(times, x_trajectories.T, alpha=0.1, color="black")
plt.plot(times, x_mean, linewidth=3)
plt.xlabel("time (s)")
plt.ylabel("average position (m)");
```

This should not be surprising: the normal distribution has mean 0.

#### Distance from starting point

Distance from origin $x_0$ is

$$
d(t) = \sqrt{(x(t) - x_0)^2}
$$

with the squared distance

$$
d^2(t) = (x(t) - x_0)^2
$$

being the important quantity that measures how far the particle got away from $x_0$.

Averaging over many trajectories we obtain the **mean squared distance**

$$
\text{MSD}(t) = \langle d^2(t) \rangle = \langle (x(t) - x_0)^2 \rangle
$$

The **root mean square distance** is then simply
$$
\text{RMSD}(t) = \sqrt{\text{MSD}(t)} = \sqrt{\langle(x(t) - x_0)^2\rangle}
$$

For a single trajectory, the $d^2(t)$ is noisy:

```python
D2 = (x_trajectory - x0)**2
```

```python
plt.plot(times, D2)
plt.xlabel("time (s)")
plt.ylabel(r"$d^2(t)$ (m$^2$)");
```

Averaging $d^2(t)$ over many trajectories gives the MSD:

```python
MSD = np.mean((x_trajectories - x0)**2, axis=0)
```

```python
plt.plot(times, MSD)
plt.xlabel("time (s)")
plt.ylabel(r"$\mathrm{MSD}(t)$ (m$^2$)");
```

The MSD is approximately linear with time, as given by the Einstein diffusion equation (in 1D)

$$
\lim_{t\rightarrow\infty} \text{MSD}(t) =  2 D t
$$

With the  [Einstein relation](https://en.wikipedia.org/wiki/Einstein_relation_(kinetic_theory)) (also: a fluctation-dissipation theorem for diffusion coefficient $D$ and drag coefficient $\zeta$)

$$
D = \frac{kT}{\zeta} \quad\text{and}\quad \zeta = m\gamma
$$
we can plot the theoretical MSD.

An equivalent form uses the mobility $\mu = \zeta^{-1}$ ($\mathbf{v} = \mu \mathbf{F}$)

$$
D = \mu kT
$$

```python
D = kB * T / (m * gamma)
print(D)
```

    1.6029206656346748e-11

```python
MSD_theory = 2 * D * times
```

Simulation and theoretical MSD plotted together:

```python
plt.plot(times, MSD, label="simulation")
plt.plot(times, MSD_theory, label="theory")
plt.xlabel("time (s)")
plt.ylabel(r"$\mathrm{MSD}(t)$ (m$^2$)")
plt.legend();
```

# Further exploration

- Increase the number of trajectories: Does the MSD get closer to the theorectical value?
- Increase the length of trajectories: Does the MSD get closer to the theorectical value?
- Estimate the distribution $p(x(t))$ (hint: use the histogram over the ensemble at fixed times $t$ and fix the bins; this distribution evolves over time); compare to the theoretical distribution (which is a Gaussian — why?)
- Obtain an estimate of $D$ from the trajectories.
- Have the particles move in a potential $U(x)$ and include the deterministic force,
- Extend the code to 2D. (Note: in $d$ dimensions, $\lim_{t\rightarrow+\infty} \langle (\mathbf{x}(t) - \mathbf{x}_0)^2\rangle = 2dDt$)

# Appendix

## Parameters for the Brome Mosaic Virus

Data for the Brome Mosaic Virus
* [mass](https://bionumbers.hms.harvard.edu/search.aspx?task=searchbytrmorg&log=y&trm=Brome%20Mosaic%20Virus) 4.6e3 kDa
* [D](https://bionumbers.hms.harvard.edu/bionumber.aspx?id=105894&ver=2&trm=Brome+Mosaic+Virus&org=) = 15.5 µm^2 s^-1 (T = 20ºC)

```python
Da = 1.660539066601e-27 # kg
T = 20 + 273.15 # K
kB = 1.380649e-23 # J/K

m_BMV = 4.6e3 * 1e3 * Da # kg
D_BMV = 15.5 * (1e-6)**2 # m^2/s
```

```python
print(f"mass = {m_BMV:.2g} kg")
print(f"T = {T:.1f} K")
```

    mass = 7.6e-21 kg
    T = 293.1 K

$\gamma$ from [Einstein relation](https://en.wikipedia.org/wiki/Einstein_relation_(kinetic_theory)) (fluctuation-dissipation relationship between diffusion coefficient $D$ and drag coefficient $\zeta = m\gamma$)
\begin{align}
D &= \frac{kT}{\zeta} = \frac{kT}{m\gamma}\\
\gamma &= \frac{kT}{mD}
\end{align}

```python
gamma_BMV = kB*T / (m_BMV * D_BMV)
print(f"gamma = {gamma_BMV:.2g} s^-1")
```

    gamma = 3.4e+10 s^-1

## Finding $D$ by linear fit

We can use [scipy.optimize.curve_fit()](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html#scipy.optimize.curve_fit) as a "black box" fitting function (it minimizes the RMSD between fit function and data):

```python
# with scipy
from scipy import optimize

def diff_fit(t, D, x0=0):
    return 2 * D * t  + x0
```

```python
params, params_covariance = optimize.curve_fit(diff_fit, times, MSD, p0=[1])
D_fit, = params

print(f"D = {D:.2g}   D_fit = {D_fit:.2g}")
```

    D = 1.6e-11   D_fit = 1.7e-11

```python
MSD_fit = diff_fit(times, D_fit)

plt.plot(times, MSD, label="simulation")
plt.plot(times, MSD_theory, label=r"theory $D={0:.3g}\,\mathrm{{m}}^2\cdot\mathrm{{s}}^{{-1}}$".format(D))
plt.plot(times, MSD_fit, linestyle="--", linewidth=2,
         label=r"fit $D_\mathrm{{fit}}={0:.3g}\,\mathrm{{m}}^2\cdot\mathrm{{s}}^{{-1}}$".format(D_fit))
plt.xlabel("time (s)")
plt.ylabel(r"$\mathrm{MSD}(t)$ (m$^2$)")
plt.legend()
plt.savefig("diffusion_sim_theory_fit.png")
```

```python

```


# Home Assignment -- 1

In this part of the homework you are asked implement some "interesting" functions
usinig Numpy. You are advised to **avoid using python loops**.

At the same time you are **encouraged** to google anything and read relevant
documentation, e.g. for [Numpy](https://docs.scipy.org/doc/numpy/reference/)
[matlpotlib](https://matplotlib.org/) and [python](https://docs.python.org/3/).

Please, write your solutions within the designated blocks:
```python
...
### BEGIN Solution

# >>> your solution here <<<

### END Solution
...
```

# Part 1 (28 + 7 pt.): Numpy and (some) Matplotlib

Let's import numpy, fix the randomness, and then import matplotlib.

```python
import numpy as np
np.random.seed(42)

%matplotlib inline
import matplotlib.pyplot as plt
```

## Task 1 (1 pt.)

Write a function, that returns an $n \times m$ integer matrix with
a specified value on the border and another specified value inside.

You might want to study numpy's indexing and [array creation](https://docs.scipy.org/doc/numpy/reference/routines.array-creation.html).

```python
def make_bordered_matrix(n_rows, n_cols, fill_value=1, border_value=0):
    """Constructs a bordered (n x m) matrix.

    Parameters
    ----------
    n_rows : int
        Nonnegative integer, spcifying the number of rows.
    n_cols : int
        Nonnegative integer, spcifying the number of columns.
    fill_value : float
        The value to fill the matrix with.
    border_value : float
        The value to set the border of the matrix to.

    Returns
    -------
    arr : array, 2 dimensional, shape = (n_rows, n_cols)
        The final bordered matrix.
    """
    ### BEGIN Solution

    ### END Solution

    return out
```

## Task 2 (1 pt.)

Implement a routine, which creates a binary checkerboard matrix with the specified
shape.

Example 4x4:
```python
[[0, 1, 0, 1],
 [1, 0, 1, 0],
 [0, 1, 0, 1],
 [1, 0, 1, 0]]
```

```python
def make_binary_checkerboard_matrix(n_rows, n_cols):
    """Construct an n x m matrix with checkerboard pattern of 0 and 1.

    Parameters
    ----------
    n_rows : int
        Nonnegative integer, spcifying the number of rows.
    n_cols : int
        Nonnegative integer, spcifying the number of columns.

    Returns
    -------
    arr : array, 2 dimensional, shape = (n_rows, n_cols)
        The final checkerboard matrix.
    """
    ### BEGIN Solution

    ### END Solution

    return out
```

## Task 3 (1 pt.)

Write a function that shifts and scales a given array `arr` so that its values
lie within $[0, 1]$.

```python
def normalize(arr):
    """Shift and scale the array to fit it within [0, 1].

    Parameters
    ----------
    arr : array, multidimensional
        The array to be normalized.

    Returns
    -------
    out : array
        The normalized array.
    """
    ### BEGIN Solution

    ### END Solution

    return out
```

## Task 4 (1 pt.)

Write a function, which takes a matrix `arr` and centers each of its rows
by the mean within that row. Check [this](https://docs.scipy.org/doc/numpy/reference/routines.statistics.html)
out for documentation.

```python
def center(arr):
    """Center each row of a matrix by the mean value in that row.

    Parameters
    ----------
    arr : array, shape = (n_rows, n_cols)
        The matrix, the rows of which to center.

    Returns
    ----------
    out : array, shape = (n_rows, n_cols)
        The final row-centered matrix.
    """
    assert arr.ndim == 2
    n_rows, n_cols = arr.shape

    ### BEGIN Solution

    ### END Solution

    return out
```

## Task 5 (2 pt.)

Implement the following fancy function:
\begin{equation}
    f(x) = \sigma\bigl(
                \max\{x + 5, 0\} + \max\{5 - x, 0\}
                + \max\{\min\{\cos(2 x \pi), \tfrac12\}, -\tfrac14\}
            \bigr)
         \,,
\end{equation}
where $\sigma(x) = (1+e^{-x})^{-1}$ is the sigmoid function.

```python
def fancy_function(x):
    """Compute some fancy function.

    Parameters
    ----------
    X : array, 1 dimendional, shape=(n_samples,)
        The array argument values.

    Returns
    -------
    y : array, 1 dimendional, shape=(n_samples,)
        The values of the fancy function.
    """
    ### BEGIN Solution

    ### END Solution

    return out
```

Plot the function

```python
x = np.linspace(-12.5, 12.5, num=1001)
fig, ax = plt.subplots(figsize=(5, 5))
ax.plot(x, fancy_function(x))
plt.show()
```

## Task 6 (2 pt.)

Write a function that sorts the rows of a given matrix by the values of a
specified column in **descending order**.

```python
def sort_by_column_desc(arr, col):
    """Sort the rows of a matrix in descending order of values in a specified column.

    Parameters
    ----------
    arr : array, 2 dimensional
        The array to sort.

    col : int
        The index of the column to sort the rows by.

    Returns
    ----------
    out : array, 2 dimensional
        The sorted array.

    """
    ### BEGIN Solution

    ### END Solution

    return out
```

## Task 7 (4 pt.)

Write a function that takes two real valued matrices $X$ and $Y$ with different
number of rows and equal number of columns and outputs the nearest neighbours'
indices. Specifically, each row $i$ in the output matrix indexes the rows of $Y$
in order **from the closest to the farthest** from the $i$-th row of $X$.

**Remeber** to avoid explicit loops!

You might like to have a look at *np.linalg.norm* and *np.argsort*.
Example:
```python
>>> X = [[0, 1], [3, 1]]
>>> Y = [[1, 1], [2, 2], [-20, -20]]
>>> index_of_nearest(X, Y, norm=2.0)
array([[0, 1, 2],
       [1, 0, 2]])
```

```python
def nearest_neighbours(X, Y, norm=2.0):
    """Index the rows of `Y` in increasing order of distance from each row in `X`.

    Parameters
    ----------
    X : array, 2 dimensional, shape = (n_samples, n_dim)
        The matrix, for the rows of which to find the closest row in `Y`.

    Y : array, 2 dimensional, shape = (n_reference, n_dim)
        The matrix, among the rows of which to seek the closest one for `X`.

    norm : float, positive
        The order of the l^p norm to use for computing the pairwise distances.

    Returns
    -------
    index : 2 dimensional
        The matrix of indices of the rows of `Y` for each vector in `X`
        in increasing order of distnace with respect to the l^p norm.

    """
    assert X.ndim == Y.ndim and X.ndim == 2

    ### BEGIN Solution

    ### END Solution

    return out
```

## Task 8 (4+1 pt.)

Plot the level sets of the $l^p$ norm
\begin{equation}
    \|z\|_p = \biggl(\sum_i \lvert x_i\rvert^p\biggr)^\tfrac1{p}
        \,.
\end{equation}
and make the contour of the unit ball in $l^p$ norm stand out. Draw plots
for $p \in \{0, \tfrac1{25}, \tfrac12, 1, 1.5, 2, 7, \infty\}$.

Study plotting examples on [this](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.contourf.html#matplotlib.pyplot.contourf) and [this](http://scikit-learn.org/stable/auto_examples/svm/plot_oneclass.html) pages (**especially the last one**) and have a look at these functions: *np.meshgrid*, *np.linspace* in numpy's documentation. You
will get extra points if you use *np.linalg.norm* (which is more numerically stable).

Try to produce a plot that **looks like the one below**:

```python
p_values = [0., 0.04, 0.5, 1, 1.5, 2, 7, np.inf]
xx, yy = np.meshgrid(np.linspace(-3, 3, num=101),
                     np.linspace(-3, 3, num=101))

fig, axes = plt.subplots(ncols=(len(p_values) + 1)// 2,
                         nrows=2, figsize=(14, 7))

for p, ax in zip(p_values, axes.flat):
    ### BEGIN Solution

    ### END Solution

plt.show()
```

## Task 9 (4 pt.)

Write a function, that accepts an array of real values, a set of splitting points
and returns the sample mean of all values that end up within **each bin**, defined
by two consecutive splitting points.

Example for $\mathtt{splits} = [-1, 0, 1]$ the bins are
\begin{equation}
    \mathtt{bins} = \bigl[[-\infty, -1), [-1, 0), [0, +1), [+1, +\infty)\bigr]\,.
\end{equation}

Look up functions *np.digitize* and *np.bincount*, and **pay close attention** to what they
return and how to correctly use them.

```python
def means_in_bins(bin_splits, arr):
    """Compute the sample mean of the values within bins.

    Parametrs
    ---------
    bin_splits : array, 1 dimension, shape=(n_splits,)
        1d array of values in ascending order.

    arr : arraylike, 1 dimension
        1d array of values to be put in bins.

    Returns
    -------
    means : arrat, 1 dimensional, shape = (n_splits + 1,)
        The sample mean within each bin defined by `bin_splits`.

    Details
    -------
    The flat array `bin_splits` of size `n_splits` defines the following bins:
    (-inf, B[0]), ..., [B[i-1], B[i]), ..., [B[n_splits], +inf), where B denotes
    `bin_splits`. The vlaues in `arr` are assigned to each bin and then averages
    within each bin are computed.
    """
    bins = np.r_[-np.inf, bin_splits, +np.inf]

    ### BEGIN Solution

    ### END Solution

    return out
```

## Task 10 (2 pt.)

Write a function that takes a multidimentional real valued array with
**missing values** (represented by *NaN*) and returns its copy with the
missing values filled with the sample mean of all nonmissing values. If
the input array **contains nothing but missing values**, then fill it
with zero.

```python
def fill_missing(arr):
    """Fill the missing values in an array with mean of nonmissing values.

    Parameters
    ---------
    arr : arraylike, multidimensional
        The real-valued array with missing values to be filled.

    Returns
    ----------
    out : arraylike, multidimensional
        A copy of the input array with filled missing values.
    """

    ### BEGIN Solution

    ### END Solution

    return out
```

## Task 11 (2 pt.)

[pandas](https://pandas.pydata.org/pandas-docs/stable/) is great, no doubt about it,
but in some cases you might need a bit simpler solution. In this task you are asked
to load some text data with **missing** values using [Numpy's API](https://docs.scipy.org/doc/numpy/reference/routines.io.html) and apply `fill_missing`
to it. You will have to **tweak some parameters** the data loader call.

P.S.: You might want to look up what **io.StringIO** does in Python.
P.P.S: Generally pandas's data loaders are by far more versatile than numpy's,
take, for example, [pd.read_html](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_html.html), [pd.read_excel](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_excel.html), [pd.read_json](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_json.html) and
others.

```python
from io import StringIO

data = """version 8.0.0815
    Some irrelevant garbage before the data block
    @first line of data block is a comment
    1.||65.21|4|5.87||||9|5.4||
    @this is another pertinent comment
    |3.1|4|||8.7|45|1||9|-3.654|15
    """

### BEGIN Solution

### END Solution
```

## Task 12 (4 pt.)

Write a function that solves the following optimization problem for any
$X \in \mathbb{R}^{N\times d}$, $y\in \mathbb{R}^{N}$ and $C > 0$:
\begin{equation}
    \begin{aligned}
        & \underset{\beta, \beta_0}{\text{minimize}}
          & & \tfrac12 \|X \beta + \mathbf{1}\beta_0 - y\|^2 + \tfrac{C}2\|\beta\|^2
              \,,
    \end{aligned}
\end{equation}
where $\|\cdot\|^2$ is the squared $l^2$ norm, and $\mathbf{1}$ is a column vector of ones.
You will have to derive the explicit solution and implement it using numpy.

### Solution

1. Write out the FOC for $\beta_0$ and solve it for $\beta_0$.

2. Plug $\beta_0$ into the objective and write out the new optimization problem. Observe how $X$ and $y$ are augmented.

3. Get the FOC for $\beta$ for the new problem and solve it.

4. Recover $\beta_0$ from the known solution for $\beta$.

Implement the function.

```python
def ridge_leastsq(X, y, alpha=1.0):
    """Find the Ridge regression solution for the given dataset (X, y).

    Parameters
    ----------
    X : array, 2 dimensional, shape = (n_samples, n_features)
        The input feature matrix.

    y : array, 1 dimensional, shape = (n_samples,)
        The target values for the regression task.

    alpha : float
        The value of the ridge regularization coefficient.

    Returns
    -------
    intercept : float
        The optimal intercept term.
    beta : array, shape = (n_features,)
        The optimal ridge regression coefficients.
    """
    assert X.ndim == 2

    n_samples, n_features = X.shape
    assert y.ndim == 1 and len(y) == n_samples

    ### BEGIN Solution

    ### END Solution

    return beta_0, beta

```

## Task 13 (6 pt., optional)

In this exercise you are asked to remember and use the
[Singular Value Decomposition](https://en.wikipedia.org/wiki/Singular-value_decomposition)
to solve the previous task not for just one value of $C$,
but for an arbitrary flat array of positive regularization
coefficients.

**Hint** Carefully study the capabilities of [np.einsum](https://docs.scipy.org/doc/numpy/reference/generated/numpy.einsum.html)
and the parameters of [np.svd](https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.svd.html)
Then figure out a way to use just one call to SVD instead of multiple
matrix inversions.

**Remember**: avoid using python loops!

```python
def ridge_path(X, y, alpha=(1.0,)):
    """Find the Ridge regression solution for the given dataset (X, y).

    Parameters
    ----------
    X : array, 2 dimensional, shape = (n_samples, n_features)
        The input feature matrix.

    y : array, 1 dimensional, shape = (n_samples,)
        The target values for the regression task.

    alpha : arraylike, 1 dimensional, shape = (n_coefficients,)
        The value of the ridge regularization coefficient.

    Returns
    -------
    intercept : array, 1 dimensional, shape = (n_coefficients,)
        The optimal intercept term for each value in `C`.
    beta : array, shape = (n_features, n_coefficients)
        The optimal ridge regression coefficients for each value in `C`.
    """
    assert X.ndim == 2

    n_samples, n_features = X.shape
    assert y.ndim == 1 and len(y) == n_samples

    alpha = np.asarray(alpha, dtype=float).ravel()
    assert np.all(alpha > 0)

    ### BEGIN Solution

    ### END Solution

    return beta_0, beta
```


### Constrained optimization: linear programming
Luca Magri (lm547@cam.ac.uk)

(With many thanks to Professor Gábor Csányi.)

##  List of contents

1. Linear programming
1. Fundamental theorem
1. Graphical solution
1. Solution by hand
1. Solution with simplex algorithm
1. Solution with the tableau
1. Slack variables
1. Phase 1

## Linear Programming (LP)

Linear programming is an optimization problem in which _both_ the objective _and_ constraints are linear.

- Efficient algorithm exists to find exact solution (Simplex method)
- Large problems can be solved with computers
- Strong connection to linear algebra
- Historical importance (George Dantzig, 1946-47, US Army logistics and planning)
 - Start of numerical optimization as a formal field
 - The notion of _program_ originally referred to military schedules
- Includes _network optimization_ (e.g. optimal transport) as a subclass, although specialised algorithms for this allow larger problems to be solved than general LP

## Standard form of a linear program

Minimize

$$f(x) = c^T x, \qquad x = (x_1,\ldots x_n)^T$$

subject to $m$ equality constraints

$$ Ax = b, \qquad A\;\;\text{ is an }\;\;m\times n\;\;\text{ matrix}$$

and bounds $$x_i \ge 0, \qquad i=1,\ldots, n$$

- In this lecture, $\textrm{rank}(A)=m<n$

```python
from pylab import *
import numpy as np
```

## Graphical solution with an example

Minimize $$ f(x) = x_1  + 2 x_2 $$

subject to $$ x_1+x_2 = 2$$

and $$x_i \ge 0$$

```python
figure(figsize=(12,8))
x1 = np.array([-1, 6])
for fx in [1, 2, 3, 4, 5, 6]:
    plot(x1, (fx-x1)/2.0, "b-")
plot(x1, 2-x1, "k", linewidth=2)
plot([0,5], [0,0], "k", linewidth=2)
plot([0,0], [0,4], "k", linewidth=2)
plot([0,2], [2,0], "g", linewidth=3)
plot(2, 0, color='red', marker='s', linestyle='dashed',linewidth=2, markersize=10)
plot(0, 2, color='red', marker='s', linestyle='dashed',linewidth=2, markersize=10)
annotate(s='feasible region', xy=(0.5,1.5), xytext=(1,3), arrowprops=dict(arrowstyle='->',color="g"), color="g", fontsize=24)
annotate(s='optimum ($f=2$)', xy=(2,0), xytext=(0.5,-0.8), arrowprops=dict(arrowstyle='->',color="r"), color="r", fontsize=24)
annotate(s='$f=4$', xy=(0,2), xytext=(-2,1), arrowprops=dict(arrowstyle='->',color="r"), color="r", fontsize=24)
annotate(s='', xy=(4,3), xytext=(3,1), arrowprops=dict(arrowstyle='->',color="b"), fontsize=24)
text(3.7, 2, "Isocontours: $f(x) = x_1  + 2 x_2=c $", fontsize=24, color="b")
xlabel("$x_1$", fontsize=24); ylabel("$x_2$", fontsize=24)
axis('tight')
axis('equal')
axis([-1,5,-1,4])
show()
```

- The equality constraint and the bounds define the feasible region.
- The linear objective function defines a series of parallel lines as the contours of constant $f$ (also known as isocontours)
- The minimum value of $f$ is the first isocontour to intersect the feasible region as $f$ increases.

In general, in the multi-dimensional case it is best to use linear algebra instead of graphical solutions:

- The $m$ constraints $Ax = b$ define a _subspace_ of $\mathbb{R}^n$.
 - Typically $m < n$, otherwise the  feasible set is either empty or consists of only the point $A^{-1} b$
- The bounds $x_i \ge 0$ cut out a portion of this subspace
  - This defines the __feasible region__, which is geometrically a convex polyhedron
- The feasible region has a number of corners
 - These are called __extremal points__, which are called __basic__ solutions
 - $n-m$ control variables are zero at these points. These are called __free__ variables
- The isocontours of $f$ are a series of parallel planes (of dimensionality $n-1$).
 - As $f$ increases, the first isocontour to intersect the feasible region will do so at one of the extremal points
  - This corresponds to the minimum.
- In this lecture, we assume that the problem is non-degenerate, therefore there are $m$ non-zero basic variables
 - If the problem is degenerate, cycling may occur. There exist perturbation methods to avoid this, but we will not study them here.

- Finding the minimum is equivalent to finding the $m$ control variables that
  - satisfy $Ax=b$ with positive solutions
  - give the lowest value of $f$.
 <- Solving a linear system of $m$ equations to satisfy $Ax=b$ is not the hard part>
 <- The hard part is finding the right set of control variables>
<- All of this is interesting in the case of large $m$ and $n$>

## Fundamental theorem of linear programming (only enunciation)

Given a linear program in the standard form where A is an $m\times n$ matrix of rank $m$
- If there is  a feasible solution, there is a _basic_ feasible solution
 - Geometrically, a basic feasible solution is a vertex
- If there is an optimal feasible solution, there  is an optimal _basic_ feasible solution

(Optional, see the proof: Sections 2.4, 2.5. of Luenberger, Ye, Linear and Nonlinear Programming)

Implications:
- We can solve a linear  program by inspecting basic feasible solutions until we find the optimum
 <- There are $\frac{n!}{m!(n-m)!}$ possible vertices->
- This is what the simplex method does systematically

## An example of linear programming with three variables (optimal allocation)

- Elena is a decision-making university administrator, who faces an unexpected budget deficit
  (The story, all names, characters, and incidents portrayed are fictitious!)
- She decides to open new fee-paying places on existing and new graduate courses.
- To do so, she needs to hire teaching staff of three different types

 - lecturers, readers and professors
 <- who can all teach laboratories and give lectures. >
- 80 hours of labs and 40 hours of lectures must be convered every week

- The weekly rates and the corresponding teaching hours are:

| Temp staff | Weekly rate | Labs (hours) | Lectures (hours) |
|---|
|Lecturer | 400 | 7 | 3 |
|Reader | 680 | 5 | 5 |
| Professor | 800 | 3 | 7 |

- In order to minimise the costs, how many lecturers, readers and professors will Elena hire?
(Elena took 3M1, therefore she will make the optimal choice.)

1. We formalize the problem.
 - $x_1$ = number of lecturers
 - $x_2$ = number of readers
 - $x_3$ = number of professors
 - $x = (x_1,x_2,x_3)^T$.

- The teaching hours to be delivered define the equality constraints
\begin{array}
~7 x_1 + 5 x_2 + 3 x_3 &= 80\\
3 x_1 + 5 x_2 + 7 x_3 &= 40\\
\end{array}
- The objective function to be minimized is the payroll cost
\begin{align}
f(x) &= c^Tx \\
& = \begin{bmatrix}400 & 680 & 800\end{bmatrix} \begin{bmatrix} x_1\\x_2\\x_3\end{bmatrix}
\end{align}

## Solution by hand

- The optimum must be an extremal point
- There are $m=2$ equality constraints, therefore $n-m=3-2=1$ control variable must be zero at an extremal point
- There are three possible cases

1. Case $x_1 = 0$
\begin{array}
~&5 x_2 + 3 x_3 &= 80\\
&5 x_2 + 7 x_3 &= 40\\
\Rightarrow&\hfil 4 x_3 &= -40\\
&&\therefore \text{infeasible}
\end{array}

2. Case $x_2 = 0$
\begin{array}
~&7 x_1 + 3 x_3 &= 80\\
&3 x_1 + 7 x_3 &= 40\\
\Rightarrow&\hfil 40 x_3 &= 40\\
&\hfil  x_3 &= 1\\
&\hfil x_1 &= 11\\
&\hfil f(x) &= 5200
\end{array}

 3. Case $x_3 = 0$
\begin{array}
~&7 x_1 + 5 x_2 &= 80\\
&3 x_1 + 5 x_2 &= 40\\
\Rightarrow&\hfil 4 x_1 &= 40\\
&\hfil  x_1 &= 10\\
&\hfil x_2 &= 2\\
&\hfil f(x) &= 5360
\end{array}

- Case 2 is the minimum
 - To minimize the payroll cost Elena should hire 11 lecturers,  0 readers and 1 professor.

## The simplex algorithm
- Larger linear programs cannot be easily solved by hand.
- The simplex algorithm provides a procedure for solving linear programs.

1. **Phase 1. Start the algorithm**
 - Start with an extremal feasible solution, which is a vertex of the feasible region with $m$ nonzero control variables
   (We will see how to do this after learning how to do Phase 2)

1. **Phase 2. Move from one vertex to another until the minimum is found**
 - Move along an edge of the feasible region to another vertex where $f$ is smaller
 - Continue until you find a vertex where every edge leading away would increase $f$
 - This is the minimum

## Solution with the simplex algorithm

- To complicate the previous problem, Elena can hire _postgraduate students_ (variable $x_0$) as well

| Temp staff | Weekly rate | Labs (hours) | Lectures (hours) |
|---|
|Postdocs | 200 | 3 | 0 |
|Lecturer | 400 | 7 | 3 |
|Reader | 680 | 5 | 5 |
| Professor | 800 | 3 | 7 |

### Step 0: Formalize the optimization problem
- The problem now has an additional variable $x_0$

\begin{array}
\text{minimize}&c^Tx = 200x_0 + 400x_1+680x_2+800x_3\\
\text{subject to}& Ax=b\\
&=\begin{bmatrix}
3 & 7&5&3\\0 & 3&5&7\\
\end{bmatrix} \begin{bmatrix} x_0\\x_1\\x_2\\x_3 \end{bmatrix} = \begin{bmatrix}80\\40\end{bmatrix}\\
\text{and}&x_i\ge0
\end{array}

- The first and second rows of $Ax=b$ will be denoted $\boxed{1}$ and $\boxed{2}$, respectively

### Step 1: Start from a basic solution (= vertex)
- A starting vertex is a __particular solution__ of $Ax=b$
- In this case, we start with $$x = \begin{bmatrix} 0\\ 10\\2\\0 \end{bmatrix}$$ (we shall see in Phase 1 how to calculate this)
- $x_1$ and $x_2$ are the __basic variables__ corresponding to this solution
- The remaining variables, which are zero, are the __free variables__ ($x_0$ and $x_3$ in this case)
- The objective function at this point is
\begin{align} c^T x & = 0x_0 + 400\cdot 10+680\cdot 2+0x_3 \\
&=5360\end{align}

- It is convenient to rearrange the constraint equations so that the coefficients of the basic variables form the identity matrix
- By _Gaussian elimination_

\begin{align}
\begin{matrix}
\frac{1}{4}\left(\boxed{1} - \boxed{2}\right) & \rightarrow\\
\frac{1}{20}\left(7\times\boxed{2} - 3\times\boxed{1}\right) &\rightarrow
\end{matrix}
\;\;\;
\begin{bmatrix}
\frac 3 4 & \bf{1} & \bf{0} & -1 \\
-\frac 9 {20}&\bf{0} & \bf{1} & 2 \\
\end{bmatrix}
\begin{bmatrix}
0\\ x_1
\\x_2\\ 0
\end{bmatrix}=
\begin{bmatrix}
10\\2
\end{bmatrix}
\end{align}

Notice that the right hand side contains the vertex coordinates.

### Step 2: Identify the edge to follow
To move away from this vertex, one of the free variables, $x_0$ or $x_3$, has to increase away from zero.
- Let's try increasing $x_0$ while leaving $x_3$ zero.

  - The constraint equations for $x_0\neq 0$ and $x_3=0$ are

\begin{array}
~x_1 &= 10-0.75 x_0\\
x_2 &= 2+0.45 x_0
\end{array}

 - Substituting this into the objective function provides

\begin{align}
c^Tx & = 200x_0 + 400\cdot\underbrace{(10-0.75 x_0)}_{x_1} + 680\cdot\underbrace{(2+0.45 x_0)}_{x_2}  \\
&= 5360 +206x_0
\end{align}

 - The coefficient of $x_0$ is the __reduced cost__
 - If we increase $x_0$, the objective function would increase because the reduced cost is positive

- Now, let's try increasing $x_3$ while keeping $x_0$ zero
   - The constraint equations for $x_0\neq 0$ and $x_3=0$ tell us what the basic variables do

\begin{array}
~x_1 &= 10+ x_3\\
x_2 &= 2-2x_3
\end{array}

  - Substituting this into the objective function $c^Tx$ provides

$$
400\cdot\underbrace{(10+x_3)}_{x_1}+680\cdot\underbrace{(2-2x_3)}_{x_2}+800x_3 = 5360-160x_3
$$

 - The coefficient (reduced cost) of $x_3$ is $-160$
 - Thus, increasing $x_3$ away from zero would decrease the objective function
 - We follow this edge
 - When we have a choice as to which variable to change, calculate all the reduced costs, and pick the most negative one.

### Step 3: Calculate the next vertex

The next question is how much to increase $x_3$ by?

 - We follow this edge until we reach the next vertex, where one of the basic variables (here $x_1$ or $x_2$) becomes zero.

 - As $x_3$ increases, $x_1$ increases, but $x_2$ decreases
   - Therefore, $x_2$  will reach zero, when $x_3 = 1$ (and thus $x_1=11$).
  - So the next vertex we reach is $x = (0, 11, 0, 1)^T$, for which $c^Tx = 5200$.

- The basic variables are now $x_1$ and $x_3$.
  - $x_3$ has become a new basic variable, so it is the __entering variable__
  - $x_2$ is no longer a basic variable, so it is the __leaving variable__.

### Repeat step 2 for the new vertex

- The reduced costs need to be calculated
- Express the constraint equations into __canonical form__, so that the coefficients of the basic variables form the identity matrix
\begin{align}
\begin{matrix}
\frac{1}{40}\left(7\times\boxed{1} - 3\times\boxed{2}\right) & \rightarrow\\
\frac{1}{40}\left(7\times\boxed{2} - 3\times\boxed{1}\right) &\rightarrow
\end{matrix}
\;\;\;
\begin{bmatrix}
0.525 & \bf{1} & 0.5 & \bf{0} \\
-0.225&\bf{0} & 0.5 & \bf{1} \\
\end{bmatrix} \begin{bmatrix} 0\\ x_1\\ 0\\ x_3\end{bmatrix} =
\begin{bmatrix}
11\\1
\end{bmatrix}
\end{align}

- Note that we  have just eliminated $x_2$ as a basic variable
  - Therefore, its reduced cost must be positive. We shall not follow it because we would increase the objective function
- Now, we need to check if increasing  $x_0$ increases or decreases the cost function

- The constraint equations tell us that

   \begin{array}
   ~x_1 &= 11-0.525x_0\\
   x_3 &=1+0.225 x_0
   \end{array}

    The objective function is

$$200x_0 + 400\cdot\underbrace{(11-0.525x_0)}_{x_1} + 800\cdot\underbrace{(1+0.225x_0)}_{x_3}  = 5200+170x_0
$$

 - So the reduced cost of $x_0$ is positive.
 - Therefore, the objective function increases along all edges away from this vertex
 - We have reached the minimum
  - To minimize the payroll cost Elena should hire 11 lecturers,  0 readers and 1 professor as before

## Solution with the Tableau

- All of the previous calculations can be conveniently and systematically performed by using the tableau

### Step 0: Formalize the optimization problem with the Tableau
1. We collect the constraint equations and the objective function coefficients into a single __tableau__, which is an $(m+1)\times(n+1)$ matrix

    $$
    \begin{bmatrix}
    A & b\\
        c^T & 0
    \end{bmatrix}
    =
    \begin{bmatrix}
    3 & 7 & 5 & 3 & 80\\
    0 & 3&5&7&40\\
    200&400&680&800 & 0
    \end{bmatrix}
    $$

- The first $n$ columns of the tableau correspond to the variables
- The first $m$ rows correspond to the constraints
- The last row is the cost function

### Step 1: Start from a basic solution (= vertex)

- Our initial vertex had $x_1$ and $x_2$ as basic variables, so we perform Gaussian elimination to transform the tableau into canonical form

$$
\begin{bmatrix}
0.75 & \bf{1} & \bf{0} & -1 & 10\\
-0.45 & 0&1&2&2\\
200&400&680&800 & 0
\end{bmatrix}
$$

- We eliminate the coefficients of the basic variables in the last line (this is equivalent to expressing the cost function as a function of the free variables only)

$$
\begin{bmatrix}
0.75 & 1 & 0 & -1 & 10\\
-0.45 & 0&1&2&2\\
206& 0& 0&-160 & \bf{-5360}
\end{bmatrix}
$$
- We obtain the _negative_ value of the objective function at the bottom right

- We also obtained the reduced costs on the bottom line

### Step 2: Identify the edge to follow

- Because the reduced cost is negative, we pick $x_3$ as the entering variable
- $x_0$ remains a free variable
- But which is the leaving variable? We can  use the tableu entries to decide.
- The rates of increase/decrease of the basic variables is given by the coefficients in the corresponding row of the entering variable column:

$$
\begin{bmatrix}
0.75 & 1 & 0 & \bf{-1} & 10\\
-0.45 & 0&1&\bf{2}&2\\
206& 0& 0&-160 & -5360
\end{bmatrix}
$$

- As $x_3$ changes
\begin{array}
~x_1=0 &\text{ when }& x_3 = \frac{10}{-1} &= -10\\
x_2 =0 &\text{ when }& x_3 = \frac 2 2 &= 1
\end{array}

- Variables cannot be negative
- The variable that is closer to zero is chosen to be the leaving variable
  - It is the variable with the lowest nonegative ratio. In this case, $x_2$.
  (If all ratios are negative, it can be shown that the problem is unbounded. Hence, $f\rightarrow-\infty$)

### Step 3: Calculate the next vertex

- Transform the tableau into canonical form by using Gaussian elimination with $x_1$ and $x_3$ as the basic variables

$$
\begin{bmatrix}
0.525 & 1 & 0.5 & 0 & 11\\
-0.225 & 0&0.5& 1 &1\\
\bf{170}& 0& \bf{80}&0 & \bf{-5200}
\end{bmatrix}
$$

-  All reduced costs are positive
- Therefore, we are at the optimum solution $x_0=0$, $x_1=11$, $x_2=0$, $x_3=1$.
- The negative of the cost function is the number at the bottom right.

## Slack variables to tackle inequality constraints

The tableau contains __equality constraints__ and zero bounds, this is called __standard form__ of an LP problem.

If there are __inequality constraints__ of the form

$$a_j^T x \ge b_j$$

We can convert them into an equality constraints by introducing a new __slack variable__, $s_j$,

$$ s_j = a_j^T x - b_j $$

with the constraint $s_j \ge 0$, this is equivalent to the original constraint, but now it is back in standard form.

## Simplex method Phase 1

To start the Simplex Algorithm, we need an initial feasible solution, i.e. $m$ basic variables that satisfy the constraints.

How do we find an initial feasible solution, _if there is not an obvious one_ ?

The answer is elegant:

- Construct an _auxiliary_ optimization problem, with the objective function being the "error" that measures by how much the constraints are violated
- The optimal solution of this problem is that with zero error, which provides the feasible solution.

- Create $m$ slack variables, add one to each of the constraint equations
- The new objective function is the sum of the slack variables
- The initial feasible solution has the slack variables ($s_i$) as basic, and the actual variables of the problem ($x_i$) as zero

## Example of Phase 1

- In the previous example, introduce $s_1$ and $s_2$ as slack variables
- The constraints of this auxiliary problem are

\begin{array}
~7 x_1 + 5 x_2 + 3 x_3 +s_1 + 0&= 80\\
3 x_1 + 5 x_2 + 7 x_3 +0+s_2 &= 40\\
\end{array}

 - The auxiliary objective function is $$\begin{bmatrix} 0& 1\end{bmatrix}\begin{bmatrix}x\\ s\end{bmatrix} = s_1+s_2$$ So the auxiliary tableu is

\begin{bmatrix}
3 & 7 & 5 & 3 & \bf{1} & \bf{0} & 80\\
0 & 3&5&7&\bf{0} & \bf{1} & 40\\
0&0&0&0 & 1 &1 & 0
\end{bmatrix}

Solve this using the Simplex Algorithm
- if $s_1=s_2=0$, the actual variables are a feasible solution of the original problem.
- If $s_1>0$ and $s_2>0$, there is no feasible solution to the original problem

Eliminating the coefficients of the basic variables on the last line:

\begin{bmatrix}
3 & 7 & 5 & 3 & \bf{1} & \bf{0} & 80\\
0 & 3&5&7&\bf{0} & \bf{1} & 40\\
-3&-10&-10&-10 &0 &0 & -120
\end{bmatrix}

the reduced costs of $x_1,x_2,x_3$ are all the same, so we pick one, e.g. $x_1$ to be the entering variable.

Looking at the ratios of the second and last columns, $80\div 7 < 40\div 3$, so $s_1$ is the leaving variable.

Continue with the Tableau as before.

## Online tool

Check your solutions and tableaus here:

https://www.zweigmedia.com/simplex/simplex.php?lang=en