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

def eqSolve(eq1,eq2,tax):
    demandP = sympy.solve(eq1-q,p)[0]
    supplyP = sympy.solve(eq2-q,p)[0]
    print(demandP)
    print(supplyP)
eqSolve(10-p,2*p,2)
```

    -q + 10
    q/2

```python
import sympy
p = sympy.Symbol("p")
q = sympy.Symbol("q")
cTax = sympy.Symbol("cTax")
pTax = sympy.Symbol("pTax")

def eqSolve(eq1,eq2,tax):
    demandP = sympy.solve(eq1-q,p)[0]
    supplyP = sympy.solve(eq2-q,p)[0]
    demandP = demandP-cTax
    supplyP = supplyP+pTax
    print(demandP)
    print(supplyP)
eqSolve(10-p,2*p,2)
```

    -cTax - q + 10
    pTax + q/2

```python
import sympy
p = sympy.Symbol("p")
q = sympy.Symbol("q")
cTax = sympy.Symbol("cTax")
pTax = sympy.Symbol("pTax")

def eqSolve(eq1,eq2,tax):
    demandP = sympy.solve(eq1-q,p)[0]
    supplyP = sympy.solve(eq2-q,p)[0]
    demandP = demandP-cTax
    supplyP = supplyP+pTax

    demandQ = sympy.solve(demandP-p,q)[0]
    supplyQ = sympy.solve(supplyP-p,q)[0]
    print(demandQ)
    print(supplyQ)

eqSolve(10-p,2*p,2)
```

    -cTax - p + 10
    2*p - 2*pTax

```python
import sympy
p = sympy.Symbol("p")
q = sympy.Symbol("q")
cTax = sympy.Symbol("cTax")
pTax = sympy.Symbol("pTax")

def eqSolve(eq1,eq2,tax):
    demandP = sympy.solve(eq1-q,p)[0]
    supplyP = sympy.solve(eq2-q,p)[0]
    demandP = demandP-cTax
    supplyP = supplyP+pTax

    demandQ = sympy.solve(demandP-p,q)[0]
    supplyQ = sympy.solve(supplyP-p,q)[0]

    return sympy.solve((demandP-supplyP, demandQ-supplyQ,tax-cTax-pTax), q,p,cTax,pTax)
eqSolve(10-p,2*p,2)
```

    {cTax: -pTax + 2, p: pTax + 8/3, q: 16/3}

```python
music = {"Genre":"Rap","Artists":["Kanye West","Kendrick Lamar","J. Cole"],"Albums":10}
print(music)
```

    {'Genre': 'Rap', 'Artists': ['Kanye West', 'Kendrick Lamar', 'J. Cole'], 'Albums': 10}

```python
print(music["Genre"])
```

    Rap

```python
music["Albums"]+=1
print(music["Albums"])
```

    11

```python
import sympy
p = sympy.Symbol("p")
q = sympy.Symbol("q")
cTax = sympy.Symbol("cTax")
pTax = sympy.Symbol("pTax")

def eqSolve(eq1,eq2,tax):
    demandP = sympy.solve(eq1-q,p)[0]
    supplyP = sympy.solve(eq2-q,p)[0]
    demandP = demandP-cTax
    supplyP = supplyP+pTax

    demandQ = sympy.solve(demandP-p,q)[0]
    supplyQ = sympy.solve(supplyP-p,q)[0]

    return sympy.solve((demandP-supplyP, demandQ-supplyQ,tax-cTax-pTax), q,p,cTax,pTax)[q]
eqSolve(10-p,2*p,2)
```

    16/3

```python
import sympy
import matplotlib.pyplot as plt
p = sympy.Symbol("p")
q = sympy.Symbol("q")
cTax = sympy.Symbol("cTax")
pTax = sympy.Symbol("pTax")
def EquilibriumTax(demandEquation,supplyEquation,priceStart,priceEnd,tax):
    prices = []
    demand = []
    supply = []
    for price in range(priceStart,priceEnd+1):
        prices += [price]
        demand += [demandEquation.subs(p,price)]
        supply += [supplyEquation.subs(p,price)]

    equilibriumQ = eqSolve(demandEquation,supplyEquation,tax)
    equilibriumP1 = sympy.solve(demandEquation-equilibriumQ)[0]
    equilibriumP2 = sympy.solve(supplyEquation-equilibriumQ)[0]

    plt.plot(demand,prices)
    plt.plot(supply,prices)
    plt.legend(["Demand","Supply"])
    plt.plot(equilibriumQ,equilibriumP1, 'ro')
    plt.plot(equilibriumQ,equilibriumP2, 'ro')
    plt.xlabel("Supply and Demand Quantity")
    plt.ylabel("Price")
    plt.show()
    print("The equilibrium prices are "+str(equilibriumP1)+" and "+str(equilibriumP2)+" and equilibrium quantity is "+str(equilibriumQ)+".")
```

```python
EquilibriumTax(10-p,p,0,10,4)
```

```python
import sympy
import matplotlib.pyplot as plt
import matplotlib.patches as patches
p = sympy.Symbol("p")
q = sympy.Symbol("q")
cTax = sympy.Symbol("cTax")
pTax = sympy.Symbol("pTax")

def EquilibriumTax(demandEquation,supplyEquation,priceStart,priceEnd,tax):
    prices = []
    demand = []
    supply = []
    for price in range(priceStart,priceEnd+1):
        prices += [price]
        demand += [demandEquation.subs(p,price)]
        supply += [supplyEquation.subs(p,price)]

    nonTaxPrice = sympy.solve(demandEquation-supplyEquation)[0]
    nonTaxQ = demandEquation.subs(p,nonTaxPrice)

    equilibriumQ = eqSolve(demandEquation,supplyEquation,tax)
    equilibriumP1 = sympy.solve(demandEquation-equilibriumQ)[0]
    equilibriumP2 = sympy.solve(supplyEquation-equilibriumQ)[0]

    triangle1 = patches.Polygon([[nonTaxQ,nonTaxPrice],[equilibriumQ,nonTaxPrice],[equilibriumQ,equilibriumP1]],True,color="green")
    triangle2 = patches.Polygon([[nonTaxQ,nonTaxPrice],[equilibriumQ,nonTaxPrice],[equilibriumQ,equilibriumP2]],True)
    currentAxis = plt.gca()
    currentAxis.add_patch(triangle1)
    currentAxis.add_patch(triangle2)

    plt.plot(demand,prices)
    plt.plot(supply,prices)
    plt.legend(["Demand","Supply"])
    plt.plot(equilibriumQ,equilibriumP1, 'ro')
    plt.plot(equilibriumQ,equilibriumP2, 'ro')
    plt.xlabel("Supply and Demand Quantity")
    plt.ylabel("Price")
    plt.show()
    print("The equilibrium prices are "+str(equilibriumP1)+" and "+str(equilibriumP2)+" and equilibrium quantity is "+str(equilibriumQ)+".")
EquilibriumTax(10-p,p,0,10,4)
```

```python
import sympy
import matplotlib.pyplot as plt
import matplotlib.patches as patches
p = sympy.Symbol("p")
q = sympy.Symbol("q")
cTax = sympy.Symbol("cTax")
pTax = sympy.Symbol("pTax")

def EquilibriumTax(demandEquation,supplyEquation,priceStart,priceEnd,tax):
    prices = []
    demand = []
    supply = []
    for price in range(priceStart,priceEnd+1):
        prices += [price]
        demand += [demandEquation.subs(p,price)]
        supply += [supplyEquation.subs(p,price)]

    nonTaxPrice = sympy.solve(demandEquation-supplyEquation)[0]
    nonTaxQ = demandEquation.subs(p,nonTaxPrice)

    equilibriumQ = eqSolve(demandEquation,supplyEquation,tax)
    equilibriumP1 = sympy.solve(demandEquation-equilibriumQ)[0]
    equilibriumP2 = sympy.solve(supplyEquation-equilibriumQ)[0]

    triangle1 = patches.Polygon([[nonTaxQ,nonTaxPrice],[equilibriumQ,nonTaxPrice],[equilibriumQ,equilibriumP1]],True,color="green")
    triangle2 = patches.Polygon([[nonTaxQ,nonTaxPrice],[equilibriumQ,nonTaxPrice],[equilibriumQ,equilibriumP2]],True)
    currentAxis = plt.gca()
    currentAxis.add_patch(triangle1)
    currentAxis.add_patch(triangle2)

    rect1 = patches.Rectangle((0,nonTaxPrice),equilibriumQ,equilibriumP1-nonTaxPrice,linewidth=1,facecolor="red")
    rect2 = patches.Rectangle((0,nonTaxPrice),equilibriumQ,equilibriumP2-nonTaxPrice,linewidth=1,facecolor="yellow")
    currentAxis.add_patch(rect1)
    currentAxis.add_patch(rect2)

    plt.plot(demand,prices)
    plt.plot(supply,prices)

    plt.legend([rect1,rect2,triangle1,triangle2], ["Consumer Tax","Producer Tax","Consumer Deadweight Loss","Producer Deadweight Loss"])
    plt.plot(equilibriumQ,equilibriumP1, 'ro')
    plt.plot(equilibriumQ,equilibriumP2, 'ro')
    plt.xlabel("Supply and Demand Quantity")
    plt.ylabel("Price")
    plt.show()
    print("The equilibrium prices are "+str(equilibriumP1)+" and "+str(equilibriumP2)+" and equilibrium quantity is "+str(equilibriumQ)+".")
    print("Taxes raised from consumers equals "+str(equilibriumQ*(equilibriumP1-nonTaxPrice)))
    print("Taxes raised from producers equals "+str(equilibriumQ*(nonTaxPrice-equilibriumP2)))
    print("Total taxes raised equals "+str(equilibriumQ*tax))
EquilibriumTax(10-p,p,0,10,4)
```

```python
EquilibriumTax(10-p,p*2,0,10,4)
```

```python
EquilibriumTax(10-p*2,p,0,5,4)
```

```python
def taxRevenue(demandEquation,supplyEquation,priceStart,priceEnd,tax):
    equilibriumQ = eqSolve(demandEquation,supplyEquation,tax)
    return tax*equilibriumQ
taxs = []
moneyRaised = []
for x in range(0,11):
    taxs+=[x]
    moneyRaised+=[taxRevenue(10-p,p,0,10,x)]
plt.plot(taxs,moneyRaised)
plt.xlabel("Tax Applied")
plt.ylabel("Money Raised")
plt.title("The Laffer Cuve")
plt.show()
```

```python
def EquilibriumTax(demandEquation,supplyEquation,priceStart,tax):
    priceEnd = sympy.solve(demandEquation)[0]
    prices = []
    demand = []
    supply = []
    for price in range(priceStart,priceEnd+1):
        prices += [price]
        demand += [demandEquation.subs(p,price)]
        supply += [supplyEquation.subs(p,price)]

    nonTaxPrice = sympy.solve(demandEquation-supplyEquation)[0]
    nonTaxQ = demandEquation.subs(p,nonTaxPrice)

    equilibriumQ = eqSolve(demandEquation,supplyEquation,tax)
    equilibriumP1 = sympy.solve(demandEquation-equilibriumQ)[0]
    equilibriumP2 = sympy.solve(supplyEquation-equilibriumQ)[0]

    triangle1 = patches.Polygon([[nonTaxQ,nonTaxPrice],[equilibriumQ,nonTaxPrice],[equilibriumQ,equilibriumP1]],True,color="green")
    triangle2 = patches.Polygon([[nonTaxQ,nonTaxPrice],[equilibriumQ,nonTaxPrice],[equilibriumQ,equilibriumP2]],True)
    currentAxis = plt.gca()
    currentAxis.add_patch(triangle1)
    currentAxis.add_patch(triangle2)

    rect1 = patches.Rectangle((0,nonTaxPrice),equilibriumQ,equilibriumP1-nonTaxPrice,linewidth=1,facecolor="red")
    rect2 = patches.Rectangle((0,nonTaxPrice),equilibriumQ,equilibriumP2-nonTaxPrice,linewidth=1,facecolor="yellow")
    currentAxis.add_patch(rect1)
    currentAxis.add_patch(rect2)

    plt.plot(demand,prices)
    plt.plot(supply,prices)

    plt.legend([rect1,rect2,triangle1,triangle2], ["Consumer Tax","Producer Tax","Consumer Deadweight Loss","Producer Deadweight Loss"])
    plt.plot(equilibriumQ,equilibriumP1, 'ro')
    plt.plot(equilibriumQ,equilibriumP2, 'ro')
    plt.xlabel("Supply and Demand Quantity")
    plt.ylabel("Price")
    plt.show()
    print("The equilibrium prices are "+str(equilibriumP1)+" and "+str(equilibriumP2)+" and equilibrium quantity is "+str(equilibriumQ)+".")
    print("Taxes raised from consumers equals "+str(equilibriumQ*(equilibriumP1-nonTaxPrice)))
    print("Taxes raised from producers equals "+str(equilibriumQ*(nonTaxPrice-equilibriumP2)))
    print("Total taxes raised equals "+str(equilibriumQ*tax))
```


# Trk
Prof. dr. Janko Slavič, Univerza v Ljubljani, Fakulteta za strojništvo

## Izpeljava enačb centričnega premega trka

```python
import sympy as sym
from sympy.solvers.solveset import linsolve
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
sym.init_printing()
```

Če uporabite Google Colab, potem odkomentirajte (odstranite #) spodnje vrstice in jih sprožite:

```python
#def custom_latex_printer(exp,**options):
#    from google.colab.output._publish import javascript
#    url = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.3/latest.js?config=default"
#    javascript(url=url)
#    return sym.printing.latex(exp,**options)
#sym.init_printing(use_latex="mathjax",latex_printer=custom_latex_printer)
```

Definirajmo simbole (1-prva masa, 2-druga masa, a-začetek kompresije, b-konec kompresije, c-konec ekspanzije):

```python
v_1a, v_2a, v_12b, v_1c, v_2c, m_1, m_2, ϵ = sym.symbols('\
v_1a, v_2a, v_12b, v_1c, v_2c, m_1, m_2, varepsilon')
```

Ker ni zunanjih sil, se gibalna količina od začetka kompresije (`a`) do konca kompresije (`b`) ne spremeni:

```python
a = sym.Eq(m_1*v_1a + m_2*v_2a, (m_1*v_12b + m_2*v_12b))
a
```

Ker ni zunanjih sil, se tudi gibalna količina od začetka kompresije (`a`) do konca ekspanzije (`c`) ne spremeni:

```python
b = sym.Eq(m_1*v_1a + m_2*v_2a, (m_1*v_1c + m_2*v_2c))
b
```

Dodati je potreba že Poissonov zakon trka, ki poveže fazo kompresije in ekspanzije:

```python
I_k = - m_1*(v_12b-v_1a)
I_e = - m_1*(v_1c-v_12b)
c = sym.Eq(ϵ*I_k, I_e)
c
```

In rešimo sistem linearnih enačb:

```python
sol = linsolve([a, b, c], (v_1c, v_2c, v_12b))
sol
```

Hitrost prvega telesa po trku torej je:

```python
sol.args[0][0].expand()
```

In še hitrost drugega telesa po trku:

```python
sol.args[0][1].expand()
```


# Option-implied PDFs

http://econweb.rutgers.edu/mizrach/pubs/[42]-2010_Handbook.pdf

https://www.mathworks.com/company/newsletters/articles/estimating-option-implied-probability-distributions-for-asset-pricing.html

## Black-Scholes

$$\begin{align}
C &= N(d_1) S - N(d_2) Ke^{-rt} \\
P &= - N(-d_1)S + N(-d_2)Ke^{-rt}
\end{align}
$$

where
$$\begin{align}
d_1 &= \frac{1}{\sigma \sqrt{t}}\left[\ln \frac S K + (r + \frac{\sigma^2}{2})t\right] \\
d_2 &=  d_1 - \sigma \sqrt t
\end{align}
$$

We will use a simple Black-Scholes model in order to generate payoff diagrams.

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scipy
from scipy.stats import norm
import yfinance as yf
```

```python
def option_value(option_type, S, K, sigma, t=0, r=0):
    """
    Calculate the value of an option using the Black-Scholes model

    :param option_type: "call"/"c" or "put"/"p"
    :type option_type: str
    :param S: price of the underlying
    :type S: float
    :param K: strike price of option
    :type K: float
    :param sigma: input implied volatility
    :type sigma: float
    :param t: time to expiration
    :type t: float, optional
    :param r: risk-free rate
    :type r: float, optional
    """
    with np.errstate(divide='ignore'):
        d1 = np.divide(1, sigma * np.sqrt(t)) * (np.log(S/K) + (r+sigma**2 / 2) * t)
        d2 = d1 - sigma * np.sqrt(t)
    if option_type.lower() in {"c", "call"}:
        return np.multiply(norm.cdf(d1),S) - np.multiply(norm.cdf(d2), K * np.exp(-r * t))
    elif option_type.lower() in {"p", "put"}:
        return -np.multiply(norm.cdf(-d1), S) + np.multiply(norm.cdf(-d2), K * np.exp(-r * t))
```

```python
# Construction of a butterfly spread
S = np.linspace(50, 150, 1000)
C1 = option_value("c", S, 90, sigma=0.20)
C2 = -option_value("c", S, 100, sigma=0.20)
C3 = option_value("c", S, 110, sigma=0.20)
butterfly = C1 + 2 * C2 + C3
```

```python
# (Gross) payoff diagram
fig, (ax, ax1) = plt.subplots(1,2, figsize=(12,4), sharey=True)
ax.plot(S, C1, S, C2, S, C3)
ax.set_xlabel("Stock price at expiration")
ax.set_ylabel("Gross payoff")
ax.legend(["long call, $90 strike", "2x short call, $100 strike", "long call, $110 strike"], loc="best")

ax1.plot(S, butterfly, c="m")
ax1.legend(["long butterfly"], loc="upper left")
# plt.show();
plt.savefig("long_butterfly.png", dpi=200)
```

## Implied distributions from butterfly spreads

Using data from my broker, Firstrade, we will attempt to use the prices of butterfly spreads to infer the risk-neutral probability of a certain move in the underlying.

```python
# Read in call and put data from excel
calls = pd.read_excel("SPY_191020exp_290920.xlsx", sheet_name="call")
calls
```

      strike
      lastPrice
      change
      bid
      ask
      BxA
      volume
      openInterest

      0
      170
      0.00
      0.0
      163.70
      164.06
      146 x 146
      0
      0

      1
      175
      0.00
      0.0
      158.70
      159.06
      146 x 146
      0
      0

      2
      180
      0.00
      0.0
      153.69
      154.05
      146 x 102
      0
      0

      3
      185
      0.00
      0.0
      148.70
      149.04
      146 x 102
      0
      0

      4
      190
      0.00
      0.0
      143.70
      144.06
      146 x 146
      0
      0

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      110
      405
      0.01
      0.0
      0.00
      0.01
      0 x 11065
      0
      28

      111
      410
      0.02
      0.0
      0.00
      0.01
      0 x 8620
      0
      200

      112
      415
      0.01
      0.0
      0.00
      0.01
      0 x 8630
      0
      5

      113
      420
      0.02
      0.0
      0.00
      0.01
      0 x 8641
      0
      55

      114
      425
      0.05
      0.0
      0.00
      0.01
      0 x 9471
      0
      20

115 rows × 8 columns

```python
puts = pd.read_excel("SPY_191020exp_290920.xlsx", sheet_name="put")
puts
```

      strike
      lastPrice
      change
      bid
      ask
      BxA
      volume
      openInterest

      0
      170
      0.01
      0.0
      0.00
      0.01
      0 x 14192
      0
      12433

      1
      175
      0.02
      0.0
      0.00
      0.01
      0 x 12853
      0
      1001

      2
      180
      0.01
      0.0
      0.00
      0.01
      0 x 13285
      0
      29

      3
      185
      0.01
      0.0
      0.00
      0.01
      0 x 12094
      0
      3

      4
      190
      0.01
      0.0
      0.00
      0.01
      0 x 11473
      0
      311

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      110
      405
      0.00
      0.0
      70.96
      71.29
      150 x 150
      0
      0

      111
      410
      0.00
      0.0
      75.97
      76.28
      150 x 150
      0
      0

      112
      415
      0.00
      0.0
      80.98
      81.27
      150 x 150
      0
      0

      113
      420
      0.00
      0.0
      85.97
      86.27
      150 x 150
      0
      0

      114
      425
      0.00
      0.0
      90.97
      91.28
      150 x 150
      0
      0

115 rows × 8 columns

```python
# Find midprices from bid/asks
calls["midprice"] = (calls.bid + calls.ask)/2
calls = calls[calls.midprice > 0]
puts["midprice"] = (puts.bid + puts.ask)/2
puts = puts[puts.midprice > 0]
calls.tail(30)
```

      strike
      lastPrice
      change
      bid
      ask
      BxA
      volume
      openInterest
      midprice

      85
      353
      0.07
      0.00
      0.07
      0.08
      8069 x 12235
      0
      447
      0.075

      86
      354
      0.06
      0.00
      0.06
      0.07
      3377 x 15224
      0
      621
      0.065

      87
      355
      0.06
      0.01
      0.05
      0.06
      693 x 16589
      10
      1685
      0.055

      88
      356
      0.05
      -0.01
      0.04
      0.05
      6205 x 13597
      1
      133
      0.045

      89
      357
      0.05
      0.00
      0.03
      0.04
      12642 x 10876
      0
      217
      0.035

      90
      358
      0.05
      0.00
      0.02
      0.03
      24527 x 3035
      0
      931
      0.025

      91
      359
      0.03
      0.00
      0.02
      0.03
      17058 x 11058
      1
      166
      0.025

      92
      360
      0.02
      -0.01
      0.02
      0.03
      3404 x 14911
      13
      857
      0.025

      93
      361
      0.01
      -0.01
      0.01
      0.02
      24305 x 4582
      6
      867
      0.015

      94
      362
      0.02
      0.00
      0.01
      0.02
      18921 x 11277
      0
      156
      0.015

      95
      363
      0.02
      0.00
      0.01
      0.02
      15318 x 13311
      0
      37
      0.015

      96
      364
      0.01
      0.00
      0.01
      0.02
      6999 x 15467
      0
      123
      0.015

      97
      365
      0.02
      0.01
      0.01
      0.02
      1916 x 16969
      1
      647
      0.015

      98
      366
      0.01
      0.00
      0.00
      0.01
      0 x 2241
      0
      40
      0.005

      99
      367
      0.01
      0.00
      0.00
      0.01
      0 x 3176
      0
      71
      0.005

      100
      368
      0.01
      0.00
      0.00
      0.01
      0 x 10160
      0
      6
      0.005

      101
      369
      0.01
      0.00
      0.00
      0.01
      0 x 12132
      0
      21
      0.005

      102
      370
      0.01
      0.00
      0.00
      0.01
      0 x 12649
      0
      571
      0.005

      103
      371
      0.01
      0.00
      0.00
      0.01
      0 x 13388
      0
      11
      0.005

      104
      375
      0.01
      0.00
      0.00
      0.01
      0 x 13630
      0
      424
      0.005

      105
      380
      0.01
      0.00
      0.00
      0.01
      0 x 8050
      0
      1072
      0.005

      106
      385
      0.01
      0.00
      0.00
      0.01
      0 x 9711
      0
      638
      0.005

      107
      390
      0.01
      0.00
      0.00
      0.01
      0 x 8524
      0
      67
      0.005

      108
      395
      0.02
      0.00
      0.00
      0.01
      0 x 9632
      0
      938
      0.005

      109
      400
      0.01
      0.00
      0.00
      0.01
      0 x 9658
      0
      306
      0.005

      110
      405
      0.01
      0.00
      0.00
      0.01
      0 x 11065
      0
      28
      0.005

      111
      410
      0.02
      0.00
      0.00
      0.01
      0 x 8620
      0
      200
      0.005

      112
      415
      0.01
      0.00
      0.00
      0.01
      0 x 8630
      0
      5
      0.005

      113
      420
      0.02
      0.00
      0.00
      0.01
      0 x 8641
      0
      55
      0.005

      114
      425
      0.05
      0.00
      0.00
      0.01
      0 x 9471
      0
      20
      0.005

```python
calls.iloc[70:83].reset_index(drop=True)
```

      strike
      lastPrice
      change
      bid
      ask
      BxA
      volume
      openInterest
      midprice

      0
      338
      1.99
      -0.15
      2.01
      2.03
      138 x 754
      77
      1718
      2.020

      1
      339
      1.69
      -0.11
      1.66
      1.68
      185 x 550
      121
      970
      1.670

      2
      340
      1.38
      -0.14
      1.36
      1.37
      121 x 1364
      426
      14048
      1.365

      3
      341
      1.08
      -0.21
      1.09
      1.11
      250 x 1089
      37
      2455
      1.100

      4
      342
      0.87
      -0.14
      0.87
      0.88
      250 x 131
      191
      11182
      0.875

      5
      343
      0.74
      -0.07
      0.69
      0.70
      119 x 3308
      103
      10889
      0.695

      6
      344
      0.55
      -0.08
      0.54
      0.55
      171 x 3441
      56
      1694
      0.545

      7
      345
      0.44
      -0.06
      0.42
      0.43
      277 x 2330
      52
      2292
      0.425

      8
      346
      0.34
      -0.07
      0.32
      0.33
      5831 x 310
      69
      721
      0.325

      9
      347
      0.28
      -0.03
      0.25
      0.26
      6338 x 1368
      11
      922
      0.255

      10
      348
      0.21
      -0.01
      0.20
      0.21
      3149 x 5999
      21
      545
      0.205

      11
      349
      0.16
      -0.03
      0.16
      0.17
      315 x 8259
      24
      691
      0.165

      12
      350
      0.14
      0.00
      0.12
      0.13
      13692 x 334
      193
      1840
      0.125

```python
puts.head(10)
```

      strike
      lastPrice
      change
      bid
      ask
      BxA
      volume
      openInterest
      midprice

      0
      170
      0.01
      0.0
      0.0
      0.01
      0 x 14192
      0
      12433
      0.005

      1
      175
      0.02
      0.0
      0.0
      0.01
      0 x 12853
      0
      1001
      0.005

      2
      180
      0.01
      0.0
      0.0
      0.01
      0 x 13285
      0
      29
      0.005

      3
      185
      0.01
      0.0
      0.0
      0.01
      0 x 12094
      0
      3
      0.005

      4
      190
      0.01
      0.0
      0.0
      0.01
      0 x 11473
      0
      311
      0.005

      5
      195
      0.01
      0.0
      0.0
      0.01
      0 x 11527
      0
      302
      0.005

      6
      200
      0.01
      0.0
      0.0
      0.01
      0 x 10963
      0
      446
      0.005

      7
      205
      0.02
      0.0
      0.0
      0.01
      0 x 7905
      0
      332
      0.005

      8
      210
      0.01
      0.0
      0.0
      0.01
      0 x 8400
      0
      27
      0.005

      9
      215
      0.02
      0.0
      0.0
      0.01
      0 x 10678
      0
      103
      0.005

```python
# Visualise put and call prices
fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(12,6))
ax0.scatter(calls.strike, calls.midprice);
ax1.scatter(puts.strike, puts.midprice);
plt.show()
```

```python
# Construct butterflies
data = []

for (_, left) ,(_,centre), (_, right) in zip(calls.iterrows(), calls.iloc[1:].iterrows(), calls.iloc[2:].iterrows()):
    # Filter out all zero volume
    if not any(vol > 0 for vol in {left.volume, centre.volume, right.volume}):
        continue
    # Filter out any zero open interest
    if not all(oi > 0 for oi in {left.openInterest, centre.openInterest, right.openInterest}):
        continue
    # Equidistant on either end
    if centre.strike - left.strike != right.strike - centre.strike:
        continue
    butterfly_price = left.midprice - 2* centre.midprice + right.midprice
    max_profit = centre.strike - left.strike
    data.append([centre.strike, butterfly_price, max_profit])

bflys = pd.DataFrame(data, columns=["strike", "price", "max_profit"])
bflys["prob"] = bflys.price / bflys.max_profit
bflys
```

      strike
      price
      max_profit
      prob

      0
      322
      1.000000e-02
      1
      1.000000e-02

      1
      323
      1.500000e-02
      1
      1.500000e-02

      2
      324
      1.500000e-02
      1
      1.500000e-02

      3
      325
      3.000000e-02
      1
      3.000000e-02

      4
      326
      1.500000e-02
      1
      1.500000e-02

      5
      327
      2.500000e-02
      1
      2.500000e-02

      6
      328
      2.500000e-02
      1
      2.500000e-02

      7
      329
      3.000000e-02
      1
      3.000000e-02

      8
      330
      3.500000e-02
      1
      3.500000e-02

      9
      331
      3.000000e-02
      1
      3.000000e-02

      10
      332
      4.000000e-02
      1
      4.000000e-02

      11
      333
      4.500000e-02
      1
      4.500000e-02

      12
      334
      3.500000e-02
      1
      3.500000e-02

      13
      335
      4.500000e-02
      1
      4.500000e-02

      14
      336
      4.000000e-02
      1
      4.000000e-02

      15
      337
      5.000000e-02
      1
      5.000000e-02

      16
      338
      4.500000e-02
      1
      4.500000e-02

      17
      339
      4.500000e-02
      1
      4.500000e-02

      18
      340
      4.000000e-02
      1
      4.000000e-02

      19
      341
      4.000000e-02
      1
      4.000000e-02

      20
      342
      4.500000e-02
      1
      4.500000e-02

      21
      343
      3.000000e-02
      1
      3.000000e-02

      22
      344
      3.000000e-02
      1
      3.000000e-02

      23
      345
      2.000000e-02
      1
      2.000000e-02

      24
      346
      3.000000e-02
      1
      3.000000e-02

      25
      347
      2.000000e-02
      1
      2.000000e-02

      26
      348
      1.000000e-02
      1
      1.000000e-02

      27
      349
      0.000000e+00
      1
      0.000000e+00

      28
      350
      2.000000e-02
      1
      2.000000e-02

      29
      351
      -2.775558e-17
      1
      -2.775558e-17

      30
      352
      1.000000e-02
      1
      1.000000e-02

      31
      353
      -2.775558e-17
      1
      -2.775558e-17

      32
      354
      6.938894e-18
      1
      6.938894e-18

      33
      355
      0.000000e+00
      1
      0.000000e+00

      34
      356
      6.938894e-18
      1
      6.938894e-18

      35
      357
      -6.938894e-18
      1
      -6.938894e-18

      36
      358
      1.000000e-02
      1
      1.000000e-02

      37
      359
      0.000000e+00
      1
      0.000000e+00

      38
      360
      -1.000000e-02
      1
      -1.000000e-02

      39
      361
      1.000000e-02
      1
      1.000000e-02

      40
      362
      0.000000e+00
      1
      0.000000e+00

      41
      364
      0.000000e+00
      1
      0.000000e+00

      42
      365
      -1.000000e-02
      1
      -1.000000e-02

      43
      366
      1.000000e-02
      1
      1.000000e-02

```python
# SPY was trading around 335 when this data was collected
plt.rcParams.update({'font.size': 16})
plt.figure(figsize=(9,6))
plt.scatter(bflys.strike, bflys.prob);
plt.xlabel("Strike")
plt.ylabel("Probability")
plt.show()
# plt.savefig("SPY_raw_bfly_prob.png", dpi=300)
```

```python
from scipy.ndimage import gaussian_filter1d

smoothed_prob = gaussian_filter1d(bflys.prob, 2)

plt.figure(figsize=(9,6))
plt.plot(bflys.strike, bflys.prob, "o", bflys.strike, smoothed_prob, "rx")
plt.legend(["raw prob", "smoothed prob"], loc="best")
plt.xlabel("Strike")
plt.ylabel("Probability")
plt.show()
# plt.savefig("SPY_smooth_bfly_prob.png", dpi=300)
```

```python
plt.figure(figsize=(9,6))
pdf = scipy.interpolate.interp1d(bflys.strike, smoothed_prob, kind="cubic",
                                 fill_value="extrapolate")
x_new = np.linspace(bflys.strike.min(), bflys.strike.max(), 100)
plt.plot(bflys.strike, smoothed_prob, "rx", x_new, pdf(x_new), "k-");
plt.legend(["smoothed prob", "fitted PDF"], loc="best")
plt.xlabel("K")
plt.ylabel("f(K)")
plt.tight_layout()
plt.show()
# plt.savefig("SPY_bfly_pdf.png", dpi=300)
```

```python
# Find area under curve
raw_total_prob = scipy.integrate.trapz(smoothed_prob, bflys.strike)
print(f"Raw total probability: {raw_total_prob}")

# Don't need to normalise because there is mass in the left tail that we are ignoring

# # Normalise
# normalised_prob = smoothed_prob / raw_total_prob
# total_prob = scipy.integrate.trapz(normalised_prob, bflys.strike)
# print(f"Normalised total probability: {total_prob}")
# # should be less than 1
```

    Raw total probability: 0.872620109383161

### Put butterflies

We can repeat the process with put butterflies instead of call butterflies.

```python
# Repeating the same with put butterflies
from scipy.ndimage import gaussian_filter1d

data = []

for (_, left) ,(_,centre), (_, right) in zip(puts.iterrows(), puts.iloc[1:].iterrows(), puts.iloc[2:].iterrows()):
    # Filter out all zero volume
    if not any(vol > 0 for vol in {left.volume, centre.volume, right.volume}):
        continue
    # Filter out any zero open interest
    if not all(oi > 0 for oi in {left.openInterest, centre.openInterest, right.openInterest}):
        continue
    # Equidistant on either end
    if centre.strike - left.strike != right.strike - centre.strike:
        continue
    butterfly_price = left.midprice - 2* centre.midprice + right.midprice
    max_profit = centre.strike - left.strike
    data.append([centre.strike, butterfly_price, max_profit])

put_bflys = pd.DataFrame(data, columns=["strike", "price", "max_profit"])
put_bflys["prob"] = put_bflys.price / put_bflys.max_profit
smoothed_prob_put = gaussian_filter1d(put_bflys.prob, 2)
plt.plot(put_bflys.strike, put_bflys.prob, "o", put_bflys.strike, smoothed_prob_put, "rx")
plt.legend(["raw prob", "smoothed prob"], loc="best")
plt.show()
```

```python
put_pdf = scipy.interpolate.interp1d(put_bflys.strike, smoothed_prob_put, kind="cubic",
                                     fill_value=0.0)
x_new = np.linspace(bflys.strike.min(), put_bflys.strike.max(), 100)
plt.plot(x_new, pdf(x_new), "m-", x_new, put_pdf(x_new), "k-");
plt.legend(["call PDF", "put PDF"], loc="best")
plt.show()
```

### Refactor butterfly PDF code

```python
def construct_pdf(calls_df, make_plot=True, fill_value="extrapolate"):
    if "midprice" not in calls_df.columns:
        calls_df["midprice"] = (calls_df.bid + calls_df.ask) /2

    # Construct butterflies
    data = []

    for (_, left) ,(_,centre), (_, right) in zip(calls_df.iterrows(), calls_df.iloc[1:].iterrows(), calls_df.iloc[2:].iterrows()):
        # Filter out all zero volume
        if not any(vol > 0 for vol in {left.volume, centre.volume, right.volume}):
            continue
        # Filter out any zero open interest
        if not all(oi > 0 for oi in {left.openInterest, centre.openInterest, right.openInterest}):
            continue
        # Equidistant on either end
        if centre.strike - left.strike != right.strike - centre.strike:
            continue
        butterfly_price = left.midprice - 2* centre.midprice + right.midprice
        max_profit = centre.strike - left.strike
        data.append([centre.strike, butterfly_price, max_profit])

    bflys = pd.DataFrame(data, columns=["strike", "price", "max_profit"])
    bflys["prob"] = bflys.price / bflys.max_profit

    smoothed_prob = gaussian_filter1d(bflys.prob, 2)
    pdf = scipy.interpolate.interp1d(bflys.strike, smoothed_prob, kind="cubic",
                                     fill_value=fill_value)
    if not make_plot:
        return pdf

    plt.figure(figsize=(9,6))
    x_new = np.linspace(bflys.strike.min(), bflys.strike.max(), 100)
    plt.plot(bflys.strike, smoothed_prob, "rx", x_new, pdf(x_new), "k-");
    plt.legend(["smoothed prob", "fitted PDF"], loc="best")
    plt.xlabel("K")
    plt.ylabel("f(K)")

```

## Implied PDF from Breeden-Litzenberger

$$ \frac{\partial^2 C}{\partial K^2} = e^{r\tau} \frac{\partial^2 C}{\partial K^2} $$

So we need to interpolate. The choice is between price space and volatility space.

```python
calls = pd.read_excel("SPY_191020exp_290920.xlsx", sheet_name="call")
calls["midprice"] = (calls.bid + calls.ask)/2
calls = calls[calls.midprice > 0]
calls
```

      strike
      lastPrice
      change
      bid
      ask
      BxA
      volume
      openInterest
      midprice

      0
      170
      0.00
      0.0
      163.70
      164.06
      146 x 146
      0
      0
      163.880

      1
      175
      0.00
      0.0
      158.70
      159.06
      146 x 146
      0
      0
      158.880

      2
      180
      0.00
      0.0
      153.69
      154.05
      146 x 102
      0
      0
      153.870

      3
      185
      0.00
      0.0
      148.70
      149.04
      146 x 102
      0
      0
      148.870

      4
      190
      0.00
      0.0
      143.70
      144.06
      146 x 146
      0
      0
      143.880

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      110
      405
      0.01
      0.0
      0.00
      0.01
      0 x 11065
      0
      28
      0.005

      111
      410
      0.02
      0.0
      0.00
      0.01
      0 x 8620
      0
      200
      0.005

      112
      415
      0.01
      0.0
      0.00
      0.01
      0 x 8630
      0
      5
      0.005

      113
      420
      0.02
      0.0
      0.00
      0.01
      0 x 8641
      0
      55
      0.005

      114
      425
      0.05
      0.0
      0.00
      0.01
      0 x 9471
      0
      20
      0.005

115 rows × 9 columns

```python
calls_sub = calls[(calls.strike > 250) & (calls.strike < 400)]
plt.figure(figsize=(12,6))
plt.plot(calls_sub.strike, calls_sub.midprice, ".");
plt.xlabel("strike")
plt.ylabel("price")
plt.savefig("call_prices.png", dpi=400)
plt.show()
```

### B-S IV

B-S doesn't have a closed-form inverse, which means to find the IV from the price we must resort to numerical methods. This in itself can be quite a deep topic, but we are going to be simplistic in our approach and just use Newton's method. To do, we need a function that computes the call price from inputs (already written above), and a function to evaluate the gradient of the call price function with respect to the volatility parameter - this is known as **vega**.

```python
def call_value(S, K, sigma, t=0, r=0):
    # use np.multiply and divide to handle divide-by-zero
    with np.errstate(divide='ignore'):
        d1 = np.divide(1, sigma * np.sqrt(t)) * (np.log(S/K) + (r+sigma**2 / 2) * t)
        d2 = d1 - sigma * np.sqrt(t)
    return np.multiply(norm.cdf(d1),S) - np.multiply(norm.cdf(d2), K * np.exp(-r * t))

def call_vega(S, K, sigma, t=0, r=0):
    with np.errstate(divide='ignore'):
        d1 = np.divide(1, sigma * np.sqrt(t)) * (np.log(S/K) + (r+sigma**2 / 2) * t)
    return np.multiply(S, norm.pdf(d1)) * np.sqrt(t)

def bs_iv(price, S, K, t=0, r=0, precision=1e-4, initial_guess=0.2, max_iter=1000, verbose=False):
    iv = initial_guess
    for _ in range(max_iter):
        P = call_value(S, K, iv, t, r)
        diff = price - P
        if abs(diff) < precision:
            return iv
        grad = call_vega(S, K, iv, t, r)
        iv += diff/grad
    if verbose:
        print(f"Did not converge after {max_iter} iterations")
    return iv
```

```python
c_test = call_value(100, 110, 0.2, t=1)
print(c_test)
```

    4.292010941409885

```python
# Check that it works
bs_iv(c_test, 100,110, t=1)
```

    0.2

```python
S = 332
t = 3/52
calls["iv"] = calls.apply(lambda row: bs_iv(row.midprice, S, row.strike, t, max_iter=500), axis=1)
```

    <ipython-input-21-08b1dc83cea8>:21: RuntimeWarning: divide by zero encountered in double_scalars
      iv += diff/grad
    <ipython-input-21-08b1dc83cea8>:4: RuntimeWarning: invalid value encountered in double_scalars
      d1 = np.divide(1, sigma * np.sqrt(t)) * (np.log(S/K) + (r+sigma**2 / 2) * t)
    <ipython-input-21-08b1dc83cea8>:10: RuntimeWarning: invalid value encountered in double_scalars
      d1 = np.divide(1, sigma * np.sqrt(t)) * (np.log(S/K) + (r+sigma**2 / 2) * t)

```python
def plot_vol_smile(calls, savefig=False):
    plt.figure(figsize=(9,6))
    plt.plot(calls.strike, calls.iv, ".")
    plt.xlabel("Strike")
    plt.ylabel("IV")
    if savefig:
        plt.savefig("vol_smile.png",dpi=300)
    plt.show()
```

```python
calls_no_na = calls.dropna()

fig, (ax1, ax2) = plt.subplots(2,1, figsize=(6,7), sharex=True)
ax1.plot(calls_no_na.strike, calls_no_na.midprice, "r.")
ax1.set_ylabel("Call price")
ax2.plot(calls_no_na.strike, calls_no_na.iv, ".")
ax2.set_ylabel("IV")
ax2.set_xlabel("Strike")
plt.tight_layout()
# plt.savefig("calls_to_iv.png", dpi=400)
plt.show()
```

```python
calls_clean = calls.dropna().copy()
calls_clean["iv"] = gaussian_filter1d(calls_clean.iv, 3)
```

```python
plot_vol_smile(calls_clean)
```

```python
calls_clean = calls_clean[(calls_clean.strike > 300) & (calls_clean.strike < 375)]
```

```python
plot_vol_smile(calls_clean, savefig=False)
```

```python
plt.figure(figsize=(9,6))
vol_surface = scipy.interpolate.interp1d(calls_clean.strike, calls_clean.iv, kind="cubic",
                                 fill_value="extrapolate")
x_new = np.arange(calls_clean.strike.min(), calls_clean.strike.max(), 0.1)
plt.plot(calls_clean.strike, calls_clean.iv, "bx", x_new, vol_surface(x_new), "k-");
plt.legend(["smoothed IV", "fitted smile"], loc="best")
plt.xlabel("Strike")
plt.ylabel("IV")
plt.tight_layout()
# plt.savefig("SPY_smile.png", dpi=300)
plt.show()
```

```python
C_interp = call_value(S, x_new, vol_surface(x_new), t)
```

```python
first_deriv = np.gradient(C_interp, x_new)
second_deriv = np.gradient(first_deriv, x_new)
```

```python
def pdf2(Krange, S, t=0, r=0):
    # x is a range of strikes
    Crange = call_value(S, Krange, vol_surface(Krange), t, r)
    first_deriv = np.gradient(Crange, x_new, edge_order=0)
    second_deriv = np.gradient(first_deriv, x_new, edge_order=0)
    return np.exp(r * t) * second_deriv
```

```python
fig, ax1 = plt.subplots(figsize=(9,6))

col="slategrey"
ax1.set_xlabel('Strike')
ax1.set_ylabel('Call price', color=col)
ax1.plot(x_new, C_interp, color=col)
ax1.tick_params(axis='y', labelcolor=col)
ax1.axvline(332, color="k", linestyle="--")

ax2 = ax1.twinx()
col = 'darkred'
ax2.set_ylabel('f(K)', color=col)
ax2.plot(x_new, pdf2(x_new, S, t), color=col)
ax2.tick_params(axis='y', labelcolor=col)

fig.tight_layout()
# plt.savefig("volspace_pdf.png", dpi=300)
plt.show()
```

```python
data = np.vstack([x_new, pdf(x_new), pdf2(x_new, S, t)]).T
data = data[(data[:, 0] < 360) & (data[:, 0] > 320)]
k, bfly_pdf, curve_pdf = np.hsplit(data,3)
```

```python
plt.figure(figsize=(9,6))
plt.plot(k, bfly_pdf, "steelblue", k, curve_pdf, "darkred")
plt.xlabel("Strike")
plt.ylabel("f(K)")
plt.legend(["Butterfly PDF", "Breeden-Litzenberger PDF"], loc="best")
# plt.savefig("butterfly_vs_breeden.png", dpi=300)
plt.show()
```

Note that the Breeden-Liztenberger PDF is much more well-behaved OTM (doesn't have the same kinks between 355-360).

Also, we have a lot more data (butterflies each require 3 prices), which is why we can extend the strikes further out as per the previous diagram – though note that even Breeden-Litzenberger starts to behave nonsensically.

BUt we have one other thing to compare it to: Breeden-Liztenberger applied directly to the interpolated calls (without interpolating in IV space).

```python
calls["price_clean"] = gaussian_filter1d(calls.midprice, 3)
price_interp = scipy.interpolate.interp1d(calls.strike, calls.price_clean, kind="cubic",
                                 fill_value="extrapolate")
C_interp_direct = price_interp(x_new)
```

```python
plt.plot(x_new, C_interp - C_interp_direct)
plt.xlabel("Strike")
plt.ylabel("Vol space - call space")
plt.show()
```

```python
def pdf3(calls, Krange, S, t=0, r=0):
    # x is a range of strikes
    calls["price_clean"] = gaussian_filter1d(calls.midprice, 3)
    price_interp = scipy.interpolate.interp1d(calls.strike, calls.price_clean, kind="cubic",
                                     fill_value="extrapolate")
    Crange = price_interp(Krange)
    first_deriv = np.gradient(Crange, x_new, edge_order=0)
    second_deriv = np.gradient(first_deriv, x_new, edge_order=0)
    return second_deriv
```

```python
plt.figure(figsize=(9,6))
plt.plot(x_new, pdf3(calls, x_new, S, t), "darkgreen", x_new, pdf2(x_new, S, t), "darkred")
plt.xlabel("Strike")
plt.ylabel("f(K)")
plt.legend(["Price-space PDF", "IV-space PDF"], loc="best")
# plt.savefig("price_vol_comparison.png", dpi=300)
plt.show()
```

```python
plt.plot(x_new, pdf2(x_new, S, t) - pdf3(calls, x_new, S, t), "r--");
plt.xlabel("Strike")
plt.ylabel("Vol-space PDF - price-space PDF")
plt.show()
```

```python

```


# 1 - Introduction to Acoustic Problem

# 1.1 - Introduction

This notebook brings the description of the acoustic wave equation, which we also refer as the *acoustic problem*,
including the discretization by finite differences and the setup for the tests we employ.

# 1.2 - The acoustic problem

Let $D=\Omega \times I\subset\mathbb{R}^{3}$ be the domain of the problem to be studied, where $\Omega=\left[x_{I},x_{F}\right] \times\left[z_{I},z_{F}\right]$ is the spatial domain $D$ and $I=\left[0,t_{F}\right]$ is the time interval of the integration. As usual, $\partial\Omega$ denotes the boundary of $\Omega$,
represented bellow.

The spatial variables are $x$ and $z$ (which represents depth) and  $t$ denotes time. The function $u(x,z,t):D\rightarrow \mathbb{R}$  represents the displacament of the acoustic wave, obeying the following equation:

\begin{equation}
u_{tt}(x,z,t)-c^2(x,z)\Delta(u(x,z,t))=c^2(x,z)f(x,z,t).
\end{equation}

where $f(x,z,t):D\rightarrow \mathbb{R}$ denotes external forcing and $c(x,z):\Omega\rightarrow \mathbb{R}$ is the speed of propagation of the wave. The initial conditions are given by:

\begin{equation}
u(x,z,0) = 0.0 \hspace{.5cm} \mbox{ and } \hspace{.5cm} u_t(x,z,0)= 0.0.
\end{equation}

The usual boundary conditions for the wave equation, leading to a well posed problem, are either of Dirichlet or Neumann type. At the surface ($z=0$) a Neumann boundary condition will be employed, while at the bottom and lateral boundaries we initially employ Dirichlet null boundary conditions:

\begin{equation}
\left. u(x,z,t) \right|_{\partial\Omega_{i}} = 0.0 \hspace{.3cm} \mbox{ for $i=1,2$ and $3$ } \hspace{.5cm} \mbox{ and } \hspace{.5cm}
\left. \displaystyle\frac{\partial u(x,z,t)}{\partial z} \right|_{\partial\Omega_{4}}= 0.0.
\end{equation}

These boundary conditions on  $\partial\Omega_{1}$, $\partial\Omega_{2}$ and $\partial\Omega_{3}$ are not natural for the seismic applications, being imposed by the necessity of employing a limited domain for computational purposes. They are the source of artificial wave reflections, which need to be mitigated by special treatments, with the use of absorbing boundary conditions or absorbing layers, which are the main subject of our notebooks.

As a source term we employ a Ricker wave, given by:

\begin{equation}
R(t)= \left(1-2\pi^{2}f_{0}^{2}\left(t-\displaystyle\frac{1}{f_{0}} \right)^{2}\right)e^{\pi^{2}f_{0}^{2}\left(t-\displaystyle\frac{1}{f_{0}} \right)^{2}},
\end{equation}

where $f_{0}\in\mathbb{R}_{+}$ is the peak frequency. This will be used as a point source at a fixed position $(\bar{x},\bar{z})$, such that $f(\bar{x},\bar{z},t)=R(t)$ and $f$ is zero elsewhere.

# 1.3 - Finite Difference Discretization

## 1.3.1 - The computational grid

The time interval $I=\left[0,t_{F}\right]$ is discretized as:

- $t_k=k\Delta t, k=0,1,\cdots,m, \mbox{ where } \Delta t=t_F/m$,
 while the spatial domain grid is given by
- $\Omega_\Delta = \{(x_i,z_j),i=0,\cdots,nx, \ j=0,\cdots,nz\}$,
 with $x_i=x_I+i\Delta x$ and $z_j=j\Delta z$, $\Delta_x=(x_F-x_I)/nx$ and $\Delta_z=(z_F-z_I)/nz$.

 The choices of the values of $\Delta t$, $\Delta x$ and $\Delta z$ will have to obey a CFL restriction,
 due to the fact that we will employ an explicit temporal discretization.

  Normally, the variables will be computed on the grid. For example, the velocity field $c(x,z)$ will be evaluated on $\Omega_\Delta$. The source term, which is time dependent, is evaluated at points $(x_i,z_j,t_k)$.

  In some cases, the discretization will require to have fields on grids which are staggered (in one or more directions). For instance, a spatial grid staggered only in the x-direction will be given by:
- $\Omega^x_\Delta=\{(x_{i+1/2},z_j), i=0,\cdots,nx-1,\ j=0,\cdots,nz\}, \mbox{ with } x_{i+1/2}=(i+\frac{1}{2})\Delta x$.

 Grid staggering in other directions, including the temporal one, are defined analogously.

## 1.3.2 - Discretization of the wave equation

 We employ a stantard second order explicit discretization of the wave equation, based on centered finite differences. Using the notation $u_{i,j,k}=u(x_i,z_j,t_k)$ we have the following discrete set of equations:

\begin{equation}
 \frac{u_{i,j,k+1}-2u_{i,j,k}+u_{i,j,k-1}}{\Delta^2 t}= c^2_{i,j,k}(\frac{u_{i-1,j,k}-2u_{i,j,k}+u_{i+1,j,k}}{\Delta^2 x} + \frac{u_{i,j-1,k}-2u_{i,j,k}+u_{i,j+1,k}}{\Delta^2 z}) + c^2_{i,j,k} f_{i,j,k}
\end{equation}
 for $i=1,\cdots,nx-1, \ j=1,\cdots,nz-1, k=1,\cdots,m-1$.

 The boundary conditions complete the set of equations. We have that $u_{0,j,k}=u_{nx,j,k}=u_{i,nz,k}=0$, according to the Dirichlet null conditions at the bottom and lateral boundaries. Moreover, $u_{i,0,k}=u_{i,1,k}$ due to the zero Neumman boundary condition at the surface and $u_{i,j,0}=u_{i,j,1}=0$ due to the initial conditions.

 This is the basic discretization of the wave equation, to be employed in all notebooks. It will be complemented
 by some other terms and / or equations, when considering how to discretize the absorbing boundary conditions
 and absorbing boundary layers.

# 1.4 - The standard problem

In order to simulate the acoustic problem with the several absorbing boundary treatments, we define a standard problem to be used not only in this notebook, but also in the following ones. The spatial domain $\Omega$ will be defined by the following parameters:

- $x_{I}$ =  0.0 Km;
- $x_{F}$ =  1.0 Km = 1000 m;
- $z_{I}$ =  0.0 Km;
- $z_{F}$ =  1.0 Km = 1000 m;

with

- $\Delta x$ = 0.01 km = 10m;
- $\Delta z$ = 0.01 km = 10m;

 The integration will be carried out from

- $t_{I}$ = 0 s = 0 ms;

to

- $t_{F}$ = 1 s = 1000 ms;

The choice of $\Delta t$ has to respect the CFL condition:

\begin{equation}
{\Delta t}\leq\displaystyle\frac{h\nu}{v_{max}},
\end{equation}

where $\nu\leq\frac{1}{\sqrt{2}}$ and

\begin{equation}
h = \min\{ \Delta x, \Delta z \}.
\end{equation}

The term $v_{max}$ is the maximum velocity propagation in $\Omega$ and is given by:

\begin{equation}
v_{max}=\max_{(x,z)\in \Omega}\left\vert c(x,z) \right\vert.
\end{equation}

The choice of $\Delta t$ respecting the CFL condition defines the number of timesteps $NT$:

\begin{equation}
\Delta t =\frac{T}{NT}.
\end{equation}

In all the simulations we employ $\nu=0.4$. In this way, the temporal parameters are given by:

- $\Delta t$ $\approx$ 0.0016 s = 1.6 ms;
- $NT$ = 626.

 The source $f(x, z, t)$  will be a Ricker source with the following properties:

- Postion at $x:$ $\bar{x} = 500 m = 0.5 Km$;
- Position at $z:$ $\bar{z} = 10 m = 0.01 Km$;
- Peak frequence: $f_{0} = 10 Hz = 0.01 Khz$;

The graph of $f(\bar{x}, \bar{z}, t)$ will be generated when building the code. We employ a synthetic velocity model $c(x,z)$ with the following properties:

- Minimum propagation speed: $v_{min} = 1500 m/s = 1,5 Km/s$;
- Maximum propagation speed: $v_{max} = 2500 m/s = 2,5 Km/s$;

The figure of the velocity profile will be generated when building the code. We will consider 'receivers' along the $x$ direction (at all discrete points between $0.0$ Km and $1.0$ Km) at the depth $z=0.01$ Km, where we measure the displacement $u(x,z,t)$ throughout the time integration. Based on these values at the receivers we bild a $seismogram$ at the end of the integration.

# 1.5 - Numerical Simulations

In the numerical simulations, we initially import the packages of interest, in particular, those that are part of the Devito library. First we import the basic Python packages.

```python
# NBVAL_IGNORE_OUTPUT

import numpy                   as np
import matplotlib.pyplot       as plot
import math                    as mt
import matplotlib.ticker       as mticker
from   mpl_toolkits.axes_grid1 import make_axes_locatable
from   matplotlib              import cm
```

From Devito's library of examples, we import some specific functions, such as:

- *TimeAxis* which generates a temporal structure;
- *RickerSource* for the setup of the source term;
- *Receiver* which generates a structure of receivers;

So, we import these sample libraries and other importants libraries with the following commands:

```python
# NBVAL_IGNORE_OUTPUT

%matplotlib inline
from   examples.seismic  import TimeAxis
from   examples.seismic  import RickerSource
from   examples.seismic  import Receiver
from   devito            import SubDomain, Grid, NODE, TimeFunction, Function, Eq, solve, Operator
```

Previously we used the expression *configuration ['log-level']='PERF'*, whose objective is that during execution we can verify the execution parameters of the operators used. We define the spatial parameters to be used in the simulations. In Devito, the mesh data must be defined taking into account *meter (m)* as the unit of measurement. Thus, we have the following variables:

- *nptx* and *nptz* correspond to the number of points (nx+1 and nz+1) in the $x$ and $z$ directions, respectively;
- *x0* and *x1* determine the starting and ending point in the direction $x$;
- *z0* and *z1* determine the starting and ending points in the $z$ direction;
- *compx* and *compz* the lengths of the domain in the directions $x$ and $z$, respectively. These values are obtained from *x0*, *x1*, *z0* and *z1*;
- *hx* and *hz* correspond to $\Delta x$ and $\Delta z$, respectively. These values are calculated in terms of the start/end points and the number of points in each direction, respectively;

These variables are defined in the code as follows:

```python
nptx   =  101
nptz   =  101
x0     =  0.
x1     =  1000.
compx  =  x1-x0
z0     =  0.
z1     =  1000.
compz  =  z1-z0;
hx     =  (x1-x0)/(nptx-1)
hz     =  (z1-z0)/(nptz-1)
```

Once the spatial parameters are defined, we start the construction of the *spatial grid* using the Devito DSL. The required parameters are:

- *origin* is the point of origin of the grid;
- *extent* is the vector with the lengths of the domain in each of the directions;
- *shape* is the vector with the number of points in each direction;
- *spacing* is the vector with the spacing in each of the directions;

These parameters are set as follows:

```python
origin  = (x0,z0)
extent  = (compx,compz)
shape   = (nptx,nptz)
spacing = (hx,hz)
```

Once the mesh parameters are built, we can define the structures that represent $\Omega$ subdomains, that is, particular regions of $\Omega$, named *subdomains*. In the present notebook, there is no need to split  the domain in particular subregions, so we define a single *subdomain* that correponds to the full domain $\Omega$. This *subdomain* is built with the following command:

```python
class d0domain(SubDomain):
    name = 'd0'
    def define(self, dimensions):
        x, z = dimensions
        return {x: z, z: z}
d0_domain = d0domain()
```

After defining the spatial parameters and *subdomains*, we generate the *spatial grid*:

```python
grid = Grid(origin=origin, extent=extent, shape=shape, subdomains=(d0_domain))
```

The velocity field is set bellow, representing two different layers:

```python
v0 = np.zeros((nptx,nptz))
p0 = 0
p1 = int((1/2)*nptz)
p2 = nptz
v0[0:nptx,p0:p1] = 1.5
v0[0:nptx,p1:p2] = 2.5
```

Below we include a routine to plot the velocity field.

```python
def graph2dvel(vel):
        plot.figure()
        plot.figure(figsize=(16,8))
        fscale =  1/10**(3)
        scale  = np.amax(vel)
        extent = [fscale*x0,fscale*x1, fscale*z1, fscale*z0]
        fig = plot.imshow(np.transpose(vel), vmin=0.,vmax=scale, cmap=cm.seismic, extent=extent)
        plot.gca().xaxis.set_major_formatter(mticker.FormatStrFormatter('%.1f km'))
        plot.gca().yaxis.set_major_formatter(mticker.FormatStrFormatter('%.1f km'))
        plot.title('Velocity Profile')
        plot.grid()
        ax = plot.gca()
        divider = make_axes_locatable(ax)
        cax = divider.append_axes("right", size="5%", pad=0.05)
        cbar = plot.colorbar(fig, cax=cax, format='%.2e')
        cbar.set_label('Velocity [km/s]')
        plot.show()
```

Below we include the plot of velocity field.

```python
# NBVAL_IGNORE_OUTPUT

graph2dvel(v0)
```

To build the *temporal grid* we use the command *TimeAxis*. Before using  *TimeAxis*, we need to set the following parameters:

- *t0* is the initial time simulation in milliseconds (ms);
- *tn* is the final time simulation in milliseconds (ms);
- *CFL* is the numerical value that imposes a restriction on the size of the time step, in order to obtain stability of the method;
- *vmax* denotes the maximum velocity propagation. This value is obtained by analyzing the maximum value of the *vel* array;
- *dtmax* is the upper limit for $\Delta t$. This value is constructed from the *hx*, *hy* and *vmax* values.
- *ntmax* is the number of time steps required to reach time $t_ {F}$;
- dt0 is the value of$\Delta t$ that can be used in the numerical method and that guarantees its stability;

The parameters mentioned above are given by:

```python
t0    = 0.
tn    = 1000.
CFL   = 0.4
vmax  = np.amax(v0)
dtmax = np.float64((min(hx,hz)*CFL)/(vmax))
ntmax = int((tn-t0)/dtmax)+1
dt0   = np.float64((tn-t0)/ntmax)
```

To generate the *temporal grid* we use the variables given above and allocate them to some local variables of the *TimeAxis* function:

- The start time *t0* is assigned to the local variable *start*;
- The final time *tn* is assigned to the local variable *stop*;
- The local variable *num* corresponds to the total number of steps in time,*ntmax*, plus 1, where this increase represents the initial time step.

We can then generate the *time_range* as follows:

```python
# NBVAL_IGNORE_OUTPUT

time_range = TimeAxis(start=t0,stop=tn,num=ntmax+1)
nt         = time_range.num - 1
```

Once we have defined the spatial and temporal structures, we need to know the symbolic values that are associated with these grids:

- *hxs* and *hzs* represent the symbolic values of the *hx* and *hz* parameters, respectively;
- *x* and *z* represent the symbolic values of the variables $x$ and $z$. We emphasize that in Devito we do not have the explicit values of $x$ and $z$, but their position in the spatial grid, that is, *x* and *z* are integer values and mark positions in the $x*hx$ grid and $z*hz$;
- *t* is the symbolic representation of the temporal dimension, similar to that represented by the variables *x* and *z* in the spatial dimension. *t* is also an integer position and represents $t*dt$;
- *dt* is the symbolic representation of $\Delta t$ time step;

These symbolic values are defined with the following commands:

```python
(hxs,hzs) = grid.spacing_map
(x, z)    = grid.dimensions
t         = grid.stepping_dim
dt        = grid.stepping_dim.spacing
```

To generate the Ricker source, we set the parameters:

- *f0*, the source frequency in $Khz$;
- *nsource* is the number of sources we want to include in the simulation;
- *xposf* is the position in meters of the source in the direction $x$. This position can be defined by a point value (when choosing 1 source) or a vector of positions (when choosing more than 1 source);
- *zposf* is the position in meters of the source in the $z$ direction. This position can be defined by a point value (when choosing 1 source) or a vector of positions (when choosing more than 1 source);

**Observation 1:** *xposf* and *zposf* are associated as follows: these generate the ordered pairs where the sources are positioned, that is, when we define these variables we are defining the positioning of the source(s).

**Observation 2:** The Ricker source will be used to compose the external force term $f(x, z, t)$ that appears in the acoustic equation.

We choose a single source, with frequency of $0.005Khz$, positioned at $\bar{x}$ = 500m and $\bar{z}$ = 10m.

```python
f0      = 0.01
nsource = 1
xposf   = 0.5*compx
zposf   = hz
```

Ricker's source is generated by the class *RickerSource* whose local variables are:

- *name* is the symbolic name of the source;
- *grid* is the grid where the font will be placed;
- *f0* is the frequency of the source;
- *npoint* is the number of fonts that will be placed;
- *time_range* is the structure that contains the time informations that we are using in our simulations;
- *staggered* is the type of positioning of the points;
- *src.coordinates.data[:, 0]* is the positioning of the source in the $x$ direction;
- *src.coordinates.data[:, 1]* is the positioning of the source in the $z$ direction;

We choose the following values for the local variables of the *RickerSource* command:

- *name=src;*
- *grid=grid;*
- *f0=f0;*
- *npoint=nsource*;
- *time_range=time_range;*
- *staggered=NODE;* We use the NODE option which is for grid points of type *non-staggered *;
- *src.coordinates.data [:, 0]=xposf;*
- *src.coordinates.data [:, 1]= zposf;*

With the above definitions we generate the Ricker font as follows:

```python
src = RickerSource(name='src',grid=grid,f0=f0,npoint=nsource,time_range=time_range,staggered=NODE,dtype=np.float64)
src.coordinates.data[:, 0] = xposf
src.coordinates.data[:, 1] = zposf
```

In order to set the receivers we need to define:

- *nrec*, the number of receivers that we want to insert in the grid;
- *nxpos*,the position in meters of receivers in the direction $x$. This position can be defined by a point value (when choosing 1 receiver) or a vector of positions (when choosing more than 1 receivers);
- *nzpos*, the position in meters of receivers in the $z$ direction. This position can be defined by a point value (when choosing 1 receiver) or a vector of positions (when choosing more than 1 receivers);

**Note:** *nxpos* and *nzpos* are associated as follows: these generate the ordered pairs where the receivers are positioned, that is, when we define these variables we are defining the positioning of the receiver(s).

Below we include the plot of Ricker source.

```python
# NBVAL_IGNORE_OUTPUT

src.show()
```

In our case, we choose the number of grid points in the $x$ direction for the number of receivers, which are  positioned along the grid line in the $x$ direction at the height $\bar{z}$ = 10m. In this way our variables are chosen as:

```python
nrec   = nptx
nxpos  = np.linspace(x0,x1,nrec)
nzpos  = hz
```

Receivers are generated by the command *Receiver* whose local variables are:

- *name* is the symbolic name of the Receiver;
- *grid* is the grid where the receivers will be placed;
- *npoint* is the number of receivers that will be placed;
- *time_range* is the structure that contains the time informations that we are using in our simulations;
- *staggered* is the type of positioning of the points;
- *rec.coordinates.data [:, 0]* is the positioning of receivers in the direction $x$;
- *rec.coordinates.data [:, 1]* is the positioning of receivers in the $z$ direction;

We employ the following values for the local variables of the *Receiver* command:

- *name=rec;*
- *grid=grid;*
- *npoint=nrec*;
- *time_range=time_range;*
- *staggered=NODE;* We use the NODE option which is for grid points of type *non-staggered*;
- *rec.coordinates.data[:, 0]=nxpos;*
- *rec.coordinates.data[:, 1]=nzpos;*

We then generate the Receivers:

```python
rec = Receiver(name='rec',grid=grid,npoint=nrec,time_range=time_range,staggered=NODE,dtype=np.float64)
rec.coordinates.data[:, 0] = nxpos
rec.coordinates.data[:, 1] = nzpos
```

The next step is to create a variable in which we want to allocate the displacement field, which varies in space and time. It will be a field of type *TimeFunction*. The parameters needed to create a *TimeFunction* are:

- *name* is the symbolic name that is given to this field;
- *grid* is the spatial and temporal grid in which this field will be evaluated;
- *time_order* is the order of approximation of the time derivatives;
- *space_order* is the order of approximation of spatial derivatives;
- *staggered* is the type of positioning of the points (NODE means that the variables are located at the grid points);

```python
u = TimeFunction(name="u",grid=grid,time_order=2,space_order=2,staggered=NODE,dtype=np.float64)
```

We also need to create a field for the wave propagation velocity, a field which will not change over time. Therefore it is defined as a *Function* type field. The parameters needed to create a *Function* are:

- *name* is the symbolic name that is given to this field;
- *grid* is the spatial grid on which this field will be evaluated;
- *space_order* is the order of approximation of spatial derivatives;
- *staggered* is the type of positioning of the points;

In addition, we need to associate the velocity values previously defined to the field's *.data* property, that is, we just need to do the following operation:

- *name.data[:,:]=m[:,:]*,

where *m* denotes the array with predefined values.

```python
vel0 = Function(name="vel0",grid=grid,space_order=2,staggered=NODE,dtype=np.float64)
vel0.data[:,:] = v0[:,:]
```

Once we have created the Ricker source, we can then create the external force term in the acoustics equation. This insertion of the external force term in the acoustic equation takes into account the discretization of the differential equation. Therefore, it is multiplied by $\Delta^2 t$ and $c^2(x,z)$.

To insert the Ricker source we use the *src.inject*, whose local variables are:

- *field* is the field in which the source term will be injected;
- *expr* defines the expression of the source to be injected;

We set

- *field=u.forward*, where * forward * indicates that we are working with field * u * at time * t + 1 *;
- *expr=$src*dt^{2}*vel0^{2}$ *, where src represents the Ricker source that was created in a previous structure and $vel0$ denotes the velocity field of the problem;

The source term is called *src_term* and it is created as follows:

```python
src_term = src.inject(field=u.forward,expr=src*dt**2*vel0**2)
```

To define the Receivers we use the structure called *rec.interpolate*. We need to define the expression *expr* to
be computed at the receivers location, which in this case is the value of the displacement.

In this way, the Receivers term is named *rec_term* and is created as follows:

```python
rec_term = rec.interpolate(expr=u)
```

We now create the acoustic equation without the external force term. As defined previusly, *u* represents the displacement field and *vel0* the field that carries the velocity information.

- The structure that creates Equations in Devito is *Eq(eq)*, where *eq* is the equation we want to assign;
- .dt2 calculates the second temporal derivative of the field it is applied to;
- .laplace builds the spatial Laplacian operator of a given field. In the present case, this operator is .dx2 + .dz2;

The acoustic equation without the source term has the following form:

- $ u_ {tt} - c ^ {2} (u_ {xx} + u_ {zz}) $

Using Devito's notation, we have

- eq = u.dt2 - vel0 * vel0 * u.laplace

Then, using the operator *Eq(eq)*  we create the *pde* that represents the acoustic equation without the source term:

```python
pde = Eq(u.dt2 - u.laplace*vel0**2)
```

For Devito to solve a predefined *pde* we use the expression *solve(pde,field)* where the parameters are the equation we want to solve (*pde*) and the field being  updated (*field1*). In our case, we want to solve the *pde* in the *u.forward* field, so we have

- solve(pde,u.forward)

Now we need Devito to assign this solution to a field, where we use the command *Eq()* again, with a few more options: more precisely we will have *Eq(field2,eq2,subdomain)*, where *field2* is the field who will receive the solution of *eq2* computed in the given *subdomain*. Since the only subdomain we have is *d0*,we define the expression of the subdomain as:

- subdomain=grid.subdomains ['d0']

Altogether we define the *stencil* for the acoustic problem:

```python
stencil  =  Eq(u.forward, solve(pde,u.forward),subdomain = grid.subdomains['d0'])
```

We also need to create the boundary conditions.

The *u* field is such that *u=u[t, x, y]* where *(t, x, y)* are integer positions. We know that $x\in \{0,nptx \}$ and $z\in \{0,nptz \}$. For the term $t$ the field *u* a priori stores only 3 values of *t*, since *time_order=2*. These values are:

- *u[t-1, x, z]* or *u.backward* is the solution at the instant *t-1*;
- *u[t, x, z]* or *u* is the solution at instant *t*;
- *u[t + 1, x, z]* or *u.forward* is the solution at the instant *t+1*;

We know that the first and last points of the mesh in the direction $x$ are associated with positions *0* and *nptx-1*. Similarly, the first and last point of the mesh in the $z$ direction are associated with positions *0* and *nptz-1*. Using the structure of the command *Eq(field,value)* we have that the condition of null Dirichilet can be described as:

- *Eq(u[t+1,0,y],0.);*
- *Eq(u[t+1,nptx-1,y],0.);*
- *Eq(u[t+1,x,npty-1],0.);*

The free-surface condition (using a backward discretization in the *z* direction) is given by:

- *Eq(u[t+1,x,0],u[t+1,x,1]);*

Putting all these boundary conditions together in a term named *bc* we have to:

```python
bc  = [Eq(u[t+1,0,z],0.),Eq(u[t+1,nptx-1,z],0.),Eq(u[t+1,x,nptz-1],0.),Eq(u[t+1,x,0],u[t+1,x,1])]
```

We then define the operator (*op*) that will combine the acoustic equation with the source term, boundary conditions and receivers.

- 1. The acoustic wave equation: *[stencil];*
- 2. Source term: *src_term;*
- 3. Boundary conditions: *bc;*
- 4. Receivers: *rec_term;*
- 5. To replace any symbolic terms associated with *grid*: *subs=grid.spacing_map;*

So we define the *op* generated by the following command:

```python
# NBVAL_IGNORE_OUTPUT

op  = Operator([stencil] + src_term + bc + rec_term,subs=grid.spacing_map)
```

Before starting we reset the field *u* in all its values.

```python
u.data[:] = 0.
```

We assign to *op* the number of time steps it must execute, using the local variable *time*, and the size of the time step, using the local variable *dt*, so that:

- *time=nt*;
- *dt=dt0*;

```python
# NBVAL_IGNORE_OUTPUT

op(time=nt,dt=dt0)
```

    Data type float64 of runtime value `dt` does not match the Constant data type <class 'numpy.float32'>
    Operator `Kernel` run in 0.01 s

    PerformanceSummary([(PerfKey(name='section0', rank=None),
                         PerfEntry(time=0.00603600000000006, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[])),
                        (PerfKey(name='section1', rank=None),
                         PerfEntry(time=4.199999999999998e-05, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[])),
                        (PerfKey(name='section2', rank=None),
                         PerfEntry(time=2.900000000000001e-05, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[])),
                        (PerfKey(name='section3', rank=None),
                         PerfEntry(time=0.00024199999999999943, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[])),
                        (PerfKey(name='section4', rank=None),
                         PerfEntry(time=0.00080700000000001, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[]))])

To view the result of the displacement field at the end time, let's create a plot routine as follows:

```python
def graph2d(U):
    plot.figure()
    plot.figure(figsize=(16,8))
    fscale =  1/10**(3)
    scale  = np.amax(U)/10.
    extent = [fscale*x0,fscale*x1,fscale*z1,fscale*z0]
    fig = plot.imshow(np.transpose(U),vmin=-scale, vmax=scale, cmap=cm.seismic, extent=extent)
    plot.gca().xaxis.set_major_formatter(mticker.FormatStrFormatter('%.1f km'))
    plot.gca().yaxis.set_major_formatter(mticker.FormatStrFormatter('%.1f km'))
    plot.axis('equal')
    plot.title('Map - Acoustic Problem with Devito')
    plot.grid()
    ax = plot.gca()
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("right", size="5%", pad=0.05)
    cbar = plot.colorbar(fig, cax=cax, format='%.2e')
    cbar.set_label('Displacement [km]')
    plot.draw()
    plot.show()
```

To access the solution in the *u* field, we will access the *0* position in *u.data*, that is,

-*u.data [0,:,:]*

We pass this value as an argument to the function *graph2d(U)*.

```python
# NBVAL_IGNORE_OUTPUT

graph2d(u.data[0,:,:])
```

Realize that the solution has a large amount of noise, which is generated by the reflections at the boudaries. The main objective of this series of notebooks is to present several numerical schemes designed to reduce the wave reflections on the computational boundaries of the domain during simulation.

We now create a routine to plot the shot records of the Receivers

```python
def graph2drec(rec):
        plot.figure()
        plot.figure(figsize=(16,8))
        fscaled = 1/10**(3)
        fscalet = 1/10**(3)
        scale   = np.amax(rec)/10.
        extent  = [fscaled*x0,fscaled*x1, fscalet*tn, fscalet*t0]
        fig = plot.imshow(rec, vmin=-scale, vmax=scale, cmap=cm.seismic, extent=extent)
        plot.gca().xaxis.set_major_formatter(mticker.FormatStrFormatter('%.1f km'))
        plot.gca().yaxis.set_major_formatter(mticker.FormatStrFormatter('%.1f s'))
        plot.axis('equal')
        plot.title('Receivers Signal Profile - Devito')
        ax = plot.gca()
        divider = make_axes_locatable(ax)
        cax = divider.append_axes("right", size="5%", pad=0.05)
        cbar = plot.colorbar(fig, cax=cax, format='%.2e')
        plot.show()
```

To access the result of the displacement in the receivers, we access the term *rec.data* and pass this term as an argument to the function *graph2drec*.

```python
# NBVAL_IGNORE_OUTPUT

graph2drec(rec.data)
```

From this plot we can clearly see the reflections of the waves at the lateral boundaries.

```python
assert np.isclose(np.linalg.norm(rec.data), 990, rtol=1)
```

# 1.6 - Conclusions

We conclude our introduction to the Acoustic Problem, including simulations in the Devito environment, highlighting the following items:

- The simulation time when using Devito is small, when compared to routines that use pure Python;
- Devito has a simplified language for building operators and equations, once the user acquires familiarity with the package;
- When no reflection reduction strategy is used, at the end of the simulation we observe a large amount of noise in our image;
- The constructions employed here can be used as a basis for implementing the methods with absorbing boundary conditions or absorbing boundary layers;

# 1.7 - References

- Berenger, J.-P. (1994). "A perfectly matched layer for the absorption of electromagnetic waves", Journal of Computational Physics, 114(2), 185-200. DOI: 10.1006/jcph.1994.1159. <a href="https://www.sciencedirect.com/science/article/pii/S0021999184711594">Reference Link.</a>

- Clayton, R., & Engquist, B. (1977). "Absorbing boundary conditions for acoustic and elastic wave equations", Bulletin of the seismological society of America, 67(6), 1529-1540. <a href="https://pubs.geoscienceworld.org/ssa/bssa/article/67/6/1529/117727?casa_token=4TvjJGJDLQwAAAAA:Wm-3fVLn91tdsdHv9H6Ek7tTQf0jwXVSF10zPQL61lXtYZhaifz7jsHxqTvrHPufARzZC2-lDw">Reference Link.</a>

- Engquist, B., & Majda, A. (1979). "Radiation boundary conditions for acoustic and elastic wave calculations," Communications on pure and applied mathematics, 32(3), 313-357. DOI: 10.1137/0727049. <a href="https://epubs.siam.org/doi/abs/10.1137/0727049">Reference Link.</a>

- Fichtner, A. (2010). ”Full seismic waveform modelling and inversion”, Springer Science and Business Media. <a href="https://www.springer.com/gp/book/9783642158063">Reference Link.</a>

- Grote, M. J. and Sim, I. (2010). "Efficient PML for the wave equation", arXiv preprint arXiv:1001.0319. <a href="https://arxiv.org/abs/1001.0319">Reference Link.</a>

- Higdon, R. L. (1987). "Absorbing boundary conditions for difference approximations to the multidimensional wave equation," Mathematics of computation, 47(176), 437-459. DOI: 10.1090/S0025-5718-1986-0856696-4. <a href="https://www.ams.org/journals/mcom/1986-47-176/S0025-5718-1986-0856696-4/">Reference Link.</a>

- Higdon, Robert L. "Numerical absorbing boundary conditions for the wave equation," Mathematics of computation, v. 49, n. 179, p. 65-90, 1987. DOI: 10.1090/S0025-5718-1987-0890254-1. <a href="https://www.ams.org/journals/mcom/1987-49-179/S0025-5718-1987-0890254-1/">Reference Link.</a>

- Liu, Y., & Sen, M. K. (2018). "An improved hybrid absorbing boundary condition for wave equation modeling," Journal of Geophysics and Engineering, 15(6), 2602-2613. DOI: 10.1088/1742-2140/aadd31. <a href="https://academic.oup.com/jge/article/15/6/2602/5209803">Reference Link.</a>

- Sochaki, J., Kubichek, R., George, J., Fletcher, W.R. and Smithson, S. (1987). "Absorbing boundary conditions and surface waves," Geophysics, 52(1), 60-71. DOI: 10.1190/1.1442241. <a href="https://library.seg.org/doi/abs/10.1190/1.1442241">Reference Link.</a>


# Linear Programming

Linear programming is the minimization (or maximization) of a linear objective subject to linear constraints. There are several widely adopted schemes for representing linear programming problems. Here we adopt a scheme corresponding where the linear objective is written in terms of decision variables $x_1, x_2, \ldots, x_N$ as

\begin{align}
\min_{x_1, x_1, \ldots, x_N} c_1x_1 + c_2x_2 + \cdots + c_Nx_N
\end{align}

subject to

\begin{align}
x_i  \geq 0 & \qquad i=1,\ldots,N\quad\mbox{lower bounds on decision variables}\\
\sum_{j=1}^N a^{ub}_{ij}x_j \leq b^{ub}_i & \qquad i=1,\ldots,M_{ub}\quad\mbox{upper bound constraints} \\
\sum_{j=1}^N a^{eq}_{ij}x_j = b^{eq}_i & \qquad i=1,\ldots,M_{eq}\quad\mbox{equality constraints}\\
\end{align}

## Matrix/Vector format

The notation can be simplified by adopting a matrix/vector formulation where

\begin{align}
\min_{x\geq 0} c^T x
\end{align}

subject to

\begin{align}
A_{ub} x \leq b_{ub} \\
A_{eq} x = b_{eq}
\end{align}

where $c$, $A_{ub}, b_{ub}$, and $A_{eq}, b_{eq}$, are vectors and matrices of coefficients constructed from the linear expressions given above.

## Example 19.3: Refinery Blending Problem

The decision variables are

| Variable | Description | Units |
| ---------|-------------| ------|
| $x_1$ | crude #1 | bbl/day |
| $x_2$ | crude #2 | bbl/day |
| $x_3$ | gasoline | bbl/day |
| $x_4$ | kerosine | bbl/day |
| $x_5$ | fuel oil | bbl/day |
| $x_6$ | residual | bbl/day |

The overall objective is to maximize profit

\begin{align}
\mbox{profit} & = \mbox{income} - \mbox{raw_material_cost} - \mbox{processing_cost}
\end{align}

where the financial components are given by

\begin{align}
\mbox{income} & = 72x_3 + 48x_4 + 42x_5 + 20x_6 \\
\mbox{raw_material_cost} & = 48x_1 + 30x_2 \\
\mbox{processing_cost} & = 1 x_1 + 2x_2
\end{align}

Combining these terms, the objective is to maximize

\begin{align}
f & = c^T x = - 49 x_1 - 32 x_2 + 72 x_3 + 48x_4 + 42x_5 + 20x_6
\end{align}

The material balance equations are

\begin{align}
\mbox{gasoline: } x_3 & = 0.80 x_1 + 0.44 x_2 \\
\mbox{kerosine: } x_4 & = 0.05 x_1 + 0.10 x_2 \\
\mbox{fuel oil: } x_5 & = 0.10 x_1 + 0.36 x_2 \\
\mbox{residual: } x_6 & = 0.05 x_1 + 0.10 x_2
\end{align}

Production limits

\begin{align}
\mbox{gasoline: } x_3 & \leq 24,000 \\
\mbox{kerosine: } x_4 & \leq 2,000 \\
\mbox{fuel oil: } x_5 & \leq 6,000
\end{align}

\begin{align}
\underbrace{\left[\begin{array}{cccccc}
0.80 & 0.44 & -1 & 0 & 0 & 0 \\
0.05 & 0.10 & 0 & -1 & 0 & 0 \\
0.10 & 0.36 & 0 & 0 & -1 & 0 \\
0.05 & 0.10 & 0 & 0 & 0 & -1
\end{array}\right]}_{A_{eq}}
\left[\begin{array}{c}
x_1 \\ x_2 \\ x_3 \\ x_4 \\ x_5 \\ x_6
\end{array}\right]
& =
\underbrace{\left[\begin{array}{c}
0 \\ 0 \\ 0 \\ 0
\end{array}\right]}_{b_{eq}}
\end{align}

```python
from scipy.optimize import linprog

c = [49, 32, -72, -48, -42, -20]

A_ub = [[0, 0, 1, 0, 0, 0],
        [0, 0, 0, 1, 0, 0],
        [0, 0, 0, 0, 1, 0]]

b_ub = [24000, 2001, 6000]

A_eq = [[0.80, 0.44, -1,  0,  0,  0],
        [0.05, 0.10,  0, -1,  0,  0],
        [0.10, 0.36,  0,  0, -1,  0],
        [0.05, 0.10,  0,  0,  0, -1]]

b_eq = [0, 0, 0, 0]

results = linprog(c, A_ub, b_ub, A_eq, b_eq)
results
p0 = 573517.24
print('additional profit', -results.fun - p0)
```

    additional profit 175.0356333295349

```python
print(results.message)
if results.success:
    for k in range(len(results.x)):
        print('x[{0:2d}] = {1:7.1f} bbl/day'.format(k+1, results.x[k]))
```

    Optimization terminated successfully.
    x[ 1] = 26199.3 bbl/day
    x[ 2] =  6910.3 bbl/day
    x[ 3] = 24000.0 bbl/day
    x[ 4] =  2001.0 bbl/day
    x[ 5] =  5107.7 bbl/day
    x[ 6] =  2001.0 bbl/day

```python

```

```python

```


In the paper, it is stated the premium of a call option implies a certain fair price for the corresponding put option (same asset, strike price and expiration date). The Put-Call Parity is used to validate option pricing models as any pricing model that produces option prices which violate the parity should be considered flawed.

Note, since American options can be exercised before the expiration date, the Put-Call Parity only applies to European options.

## The Formula

Let $C(t)$ and $P(t)$ be the call and put values at time $t$ for a European option with maturity $T$ and strike $K$ on a non-dividend paying asset with spot price $S(t)$. The Parity states:

$$P(t) + S(t) - C(t) = Ke^{-r(T - t)}$$

Rearranging the above to isolate $P(t)$ and $C(t)$ on the left-hand side for non-dividend paying assets:

$$C(t) = S(t) + P(t) - Ke^{-r(T-t)}$$ $$P(t) = C(t) - S(t) + Ke^{-r(T-t)}$$

Rearranging for continuous dividend paying assets:

$$C(t) = S(t)e^{-q(T-t)} + P(t) - Ke^{-r(T-t)}$$ $$P(t) = C(t) - S(t)e^{-q(T-t)} + Ke^{-r(T-t)}$$

With this, we can find a result for a European put option based on the corresponding call option. From the Black-Scholes formula, we can derive the call option:

$$C(S,t) = S \cdot N(d_1) - Ke^{-r(T - t)} N(d_2)$$

From the Put-Call parity statement, we can arrive at a formula for pricing European put options knowing that $1 - N(d) = N(-a)$ for any $a \in \mathbb{R}$

$$P(S,t) = Ke^{-r(T-t)} - S + C(S,t)$$ $$= Ke^{-r(T-t)} - S + (S \cdot N(d_1) - Ke^{-r(T-t)} N(d_2))$$ $$= Ke^{-r(T-t)}(1 - N(d_2)) - S(1 - N(d_1))$$

$$ P(S,t) = Ke^{-r(T-t)} N(-d_2) - S \cdot N(-d_1)$$

## Python Implementation

```python
import numpy as np
import scipy as si
import sympy as sy
import sympy.stats as systats
```

```python
def put_option_price(S, K, T, r, sigma):

    #S: spot price
    #K: strike price
    #T: time to maturity
    #r: interest rate
    #sigma: volatility of underlying asset

    S = float(S)

    d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
    d2 = (np.log(S / K) + (r - 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))

    put_price = K * np.exp(-r * T) * si.stats.norm.cdf(-d2, 0.0, 1.0) - S * si.stats.norm.cdf(-d1, 0.0, 1.0)

    return put_price
```

## Python Implementation with Sympy

```python
def put_option_price_sym(S, K, T, r, sigma):

    #S: spot price
    #K: strike price
    #T: time to maturity
    #r: interest rate
    #sigma: volatility of underlying asset

    N = Normal('x', 0.0, 1.0)

    S = float(S)

    d1 = (sy.ln(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * sy.sqrt(T))
    d2 = (sy.ln(S / K) + (r - 0.5 * sigma ** 2) * T) / (sigma * sy.sqrt(T))

    put_price = K * sy.exp(-r * T) * cdf(N)(-d2) - S * cdf(N)(-d1)

    return put_price
```

For dividend paying assets:

$$C(S,t) = Se^{-q(T - t)} N(d_1) - Ke^{-r(T - t)} N(d_2)$$

$$1 - N(d) = N(-a)$ for any $a \in \mathbb{R}$$

$$P(S,t) = Ke^{-r(T-t)} - Se^{-q(T-t)} + C(S,t)$$ $$= Ke^{-r(T-t)} - Se^{-q(T-t)} + (Se^{-q(T-t)} N(d_1) - Ke^{-r(T-t)} N(d_2))$$ $$= Ke^{-r(T-t)}(1 - N(d_2)) - Se^{-q(T-t)} (1 - N(d_1))$$

$$ P(S,t) = Ke^{-r(T-t)} N(-d_2) - Se^{-q(T-t)}N(-d_1)$$

## Python Implementation of Put Option Pricing for Dividend Paying Assets

```python
def put_option_price_div(S, K, T, r, sigma, q):

    #S: spot price
    #K: strike price
    #T: time to maturity
    #r: interest rate
    #sigma: volatility of underlying asset
    #q: continuous dividend rate

    S = float(S)

    d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
    d2 = (np.log(S / K) + (r - 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))

    put_price_dividend = K * np.exp(-r * T) * si.stats.norm.cdf(-d2, 0.0, 1.0) - S * np.exp(-q * T) * si.stats.norm.cdf(-d1, 0.0, 1.0)

    return put_price_dividend
```

## Sympy Implementation of Put Option Pricing for Dividend Paying Assets

```python
def put_option_price_div_sym(S, K, T, r, sigma, q):

    #S: spot price
    #K: strike price
    #T: time to maturity
    #r: interest rate
    #sigma: volatility of underlying asset
    #q: continuous dividend rate

    N = Normal('x', 0.0, 1.0)

    S = float(S)

    d1 = (sy.ln(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * sy.sqrt(T))
    d2 = (sy.ln(S / K) + (r - 0.5 * sigma ** 2) * T) / (sigma * sy.sqrt(T))

    put_price_dividend = K * sy.exp(-r * T) * cdf(N)(-d2) - S * sy.exp(-q * T) * cdf(N)(-d1)

    return put_price_dividend
```

## References

[Sherbin, A. (2015). How to price and trade options: identify, analyze, and execute the best trade probabilities.
    Hoboken, NJ: John Wiley & Sons, Inc.](https://amzn.to/37ajBnM)

[Ursone, P. (2015). How to calculate options prices and their Greeks: exploring the Black Scholes model from Delta
    to Vega. Chichester: Wiley.](https://amzn.to/2UzXDrD)

content/posts/Put-Call Parity of Vanilla European Options.ipynb