Consider 2-d PDE
$$\frac{1}{2} \Delta v(x) - v(x) + x_{1}^2 + x_{2}^2 - x_{1} - x_{2} - \frac{3}{2} = 0, x \in O=(0,1)^2$$
with its boundary data
$$v(x) = (x_{1} - \frac{1}{2})^2 + (x_{2} - \frac{1}{2})^2, x \notin O.$$
The exact solution is
$$v(x) = (x_{1} - \frac{1}{2})^2 + (x_{2} - \frac{1}{2})^2.$$

- Idendify $\mathrm{MRP}$ with $\mathrm{CFD}$ in the form of

\begin{align}
v(x) = \gamma \{ \ell ^h (x) + \sum_{i=1}^{d} p^h (x + he_{i}|x)v(x + he_{i}) + p^h(x-he_{i}|x)v(x - he_{i}) \} \tag{1} \\
\end{align}

__Soln:__

First, we change the form of the equation (1):

\begin{align}
v(x) = \gamma \cdot \ell ^h (x) + \gamma \cdot \sum_{i=1}^{2} p^h (x + he_{i}|x)v(x + he_{i}) + p^h(x-he_{i}|x)v(x - he_{i}) \tag{2} \\
\end{align}

Also, we already calculate that
$$\gamma = \frac{2}{2+h^{2}}, \quad \ell^{h}(x) = \frac{h^{2}}{2} [(x_{1} - \frac{1}{2})^{2} + (x_{2} - \frac{1}{2})^{2} -2], \quad p^{h}(x\pm he_{i}|x) = \frac{1}{4}.$$
Then compare (2) to the Bellman Equation:
$$v(s) = R(s) + \gamma \sum_{s' \in \mathcal S} P(s,s')v(s'),$$
we can deduce that

$$P(s,s') =
\begin{cases}
\frac{1}{4},  & \mbox{if } ||s'-s||_1 = h \\
0, & \mbox{otherwise} \\
\end{cases}$$

and
$$ R(s) = \gamma \cdot \ell^h(s) = \frac{h^2}{2+h^2} [(x_{1} - \frac{1}{2})^{2} + (x_{2} - \frac{1}{2})^{2} -2] $$

- For $h$ = 1/8, compute $\mathrm{CFD}$ solution by value iteration.
- For $h$ = 1/8, compute $\mathrm{CFD}$ solution by Monte-Carlo method.
- For $h$ = 1/8, compute $\mathrm{CFD}$ solution by $\mathrm{TD}$ method.
- Compare above three methodsand conclude your observations.

```
# Value Iteration

import numpy as np

def gamma(N):
  h = 1/N
  gamma_value = 2/(2 + h**2)
  return gamma_value
def l(N):
  h = 1/N
  l_h = np.zeros([N+1, N+1])
  for i in range(N+1):
    for j in range(N+1):
      if i == 0 or i == N or j == 0 or j == N:
        l_h[i, j] = 0
      else:
        l_h[i, j] = h**2 / 2 *  ((i/N - 0.5)**2 + (j/N - 0.5)**2 - 2)
  return l_h
def F(N, u):
  h = 1/N
  l_h = l(N)
  v = np.zeros([N+1, N+1])
  for i in range(0, N+1):
    for j in range(0, N+1):
      if i == 0 or i == N or j == 0 or j == N:
        v[i, j] = u[i, j]
      else:
        v[i, j] = gamma(N) * l_h[i, j] + gamma(N) * (u[i+1, j] + u[i, j+1] + u[i, j-1] + u[i-1, j]) / 4
  return v
def initial_value(N):
  u = np.zeros([N+1, N+1])
  for i in range(0, N+1):
    for j in range(0, N+1):
      if i == 0 or i == N or j == 0 or j == N:
        u[i, j] = ((i/N - 0.5)**2 + (j/N - 0.5)**2)
      else:
        u[i, j] = 0
  return u
def value_iteration(N, error_hat = 0.0001):
  v = initial_value(N)
  error = 1
  step = 0
  while error > error_hat:
    step +=1
    u = v
    v = F(N, u)
    error = np.max(np.abs(u - v))
  return [error, step, v]
VI_soln = value_iteration(8)[2]
# print(VI_soln)
```

```
# Monte-Carlo Method
import numpy as np
import random as rd

N = 8
h = 1/N
alpha = 0.04

s = [x for x in range(1,N)]
#rd.seed(1)
s0 = [rd.choice(s), rd.choice(s)]

S_list = [s0]
print(S_list)         # Still change S_list[0] even though set it firstly
ei = [-1,1]
T = 0                 # T is the stopping time

while s0[0]>0 and s0[0]<N and s0[1]>0 and s0[1]<N:
  s0[0] += rd.choice(ei)
  s0[1] += rd.choice(ei)
  S_list.append([s0[0],s0[1]])
  T += 1              # S_list is the state space excepet S_list[0]
print(S_list)

R = np.zeros(T+1)
v = 0
for i in range(T+1):
  x1 = S_list[i][0]
  x2 = S_list[i][1]
  R[i] = h**2/(2+h**2)*((x1/N-0.5)**2+(x2/N-0.5)**2-2)        # The reward list, (terminal is equal to 0?)
G = R[T]
V_list = [0]
for t in range(T-1,0,-1):
  G = R[t] + gamma(N) * G
  v += alpha * (G - v)
  V_list.append(v)               # Owing to the bug in S_list[0], there is no value when T=1
print(V_list)

#print(l(8)*gamma(8))   # This is the complete R(s)

```

    [[4, 5]]
    [[3, 8], [3, 6], [4, 5], [3, 6], [4, 5], [3, 6], [4, 7], [3, 6], [2, 7], [3, 8]]
    [0, -0.0010907923201730666, -0.002725427425677404, -0.004858217694416519, -0.007484248233291602, -0.010598684378157017, -0.014158009696807684, -0.018159393997334208, -0.02256124459492185]

```
gamma(8)
```

    0.9922480620155039

```
# TD Method

import numpy as np
from tqdm import tqdm
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style("darkgrid")
%pylab inline
import random

# parameters
gamma = 0.9922480620155039 # discounting rate: gamma(8)
rewardSize = -1
gridSize = 8
alpha = 0.05 # (0,1] // stepSize
terminationStates = []
for i in range(gridSize):
  for j in [0,gridSize-1]:
    terminationStates.append([i,j])
for j in range(gridSize):
  for i in [0,gridSize-1]:
    terminationStates.append([i,j])
actions = [[-1, 0], [1, 0], [0, 1], [0, -1]]
numIterations = 10000

# initialization
V = np.zeros((gridSize, gridSize))
returns = {(i, j):list() for i in range(gridSize) for j in range(gridSize)}
deltas = {(i, j):list() for i in range(gridSize) for j in range(gridSize)}
states = [[i, j] for i in range(gridSize) for j in range(gridSize)]

# utils
def InitialState():
    initState = random.choice(states[1:-1])
    return initState

def NextAction():
    return random.choice(actions)

def takeAction(state, action):
    if list(state) in terminationStates:
        return 0, None
    finalState = np.array(state)+np.array(action)
    # if robot crosses wall
    if -1 in list(finalState) or gridSize in list(finalState):
        finalState = state
    return rewardSize, list(finalState)

for it in tqdm(range(numIterations)):
    state = InitialState()
    while True:
        action = NextAction()
        reward, finalState = takeAction(state, action)

        # we reached the end
        if finalState is None:
            break

        # modify Value function
        before =  V[state[0], state[1]]
        V[state[0], state[1]] += alpha*(reward + gamma*V[finalState[0], finalState[1]] - V[state[0], state[1]])
        deltas[state[0], state[1]].append(float(np.abs(before-V[state[0], state[1]])))

        state = finalState

V

```

    /usr/local/lib/python3.6/dist-packages/IPython/core/magics/pylab.py:161: UserWarning: pylab import has clobbered these variables: ['random', 'gamma']
    `%matplotlib` prevents importing * from pylab and numpy
      "\n`%matplotlib` prevents importing * from pylab and numpy"
      8%|▊         | 803/10000 [00:00<00:01, 7999.00it/s]

    Populating the interactive namespace from numpy and matplotlib

    100%|██████████| 10000/10000 [00:01<00:00, 8347.32it/s]

    array([[  0.        ,   0.        ,   0.        ,   0.        ,
              0.        ,   0.        ,   0.        ,   0.        ],
           [  0.        ,  -4.09969866,  -5.56377584,  -6.09681717,
             -6.45255468,  -5.48030703,  -4.13661165,   0.        ],
           [  0.        ,  -5.76426512,  -8.75052814, -10.3352667 ,
            -10.76445422,  -8.98010012,  -5.51206118,   0.        ],
           [  0.        ,  -7.35928395, -10.74648593, -12.55108284,
            -12.90099036, -10.48939126,  -6.73781723,   0.        ],
           [  0.        ,  -6.80514483, -10.23425256, -12.43228416,
            -12.79417756, -11.12199325,  -7.56685202,   0.        ],
           [  0.        ,  -6.87413127,  -9.85151012, -10.78410785,
            -11.18296224,  -9.93520138,  -5.82961633,   0.        ],
           [  0.        ,  -4.46171763,  -6.26363985,  -7.51211311,
             -7.77354972,  -5.96834008,  -4.22258815,   0.        ],
           [  0.        ,   0.        ,   0.        ,   0.        ,
              0.        ,   0.        ,   0.        ,   0.        ]])


## Linear-Chain CRF in GTN

We show how to implement a simple linear-chain conditional random field (CRF) entirely in GTN.

[](https://colab.research.google.com/github/facebookresearch/gtn/blob/master/examples/notebooks/linear_crf.ipynb)

### Setup

First install GTN if you haven't yet and then import the packages we'll use.

```python
!pip install gtn
import gtn
import numpy as np
from IPython.display import display, Image
```

### Linear-chain CRF

We'll start by defining some problem dimensions. Each example consists of an input sequence $X = [x_1, \ldots, x_T]$ and a label sequence $Y = [y_1, \ldots, y_T]$ of the same length. The number of inputs (`num_features`) represents the number of symbols each $x_t$ can take. Similarly the number of classes $C$ (in code `num_classes`) is the number of classes for each $y_t$.

We model the conditional log probability of $Y$ given $X$ as:
\begin{equation}
\log p(Y \mid X) = \sum^T_{t=1} f(x_t, y_t) + h(y_t, y_{t-1}) - Z
\end{equation}
where the partition function $Z$ is given by
\begin{equation}
Z = \log \sum^T_{t=1} \sum_{i=1}^C \sum_{j=1}^{C} e^{f(x_t, i)  + h(i, j)}.
\end{equation}
The function $f(\cdot)$ gives the so-called unary potentials and the function $h(\cdot)$ gives the transition scores. Note also that in the transition function we let $y_0$ be the start of sequence class `<s>`.

```python
num_features = 3  # number of input features
num_classes = 2   # number of output classes
num_train = 1000  # size of the training set
num_test = 200    # size of the testing set

# Define the input and output symbols used for drawing the graphs:
feature_symbols = {0: 'a', 1: 'b', 2: 'c'}
class_symbols = {0: '0', 1: '1'}
```

Ok, now we're ready to build, train, and test this model with GTN. Here's a high-level outline of what we need to do:

1. **Generate toy model:** Generate a synthetic, ground-truth model which represents the conditional distribution we aim to recover.
2. **Sample datasets:** Sample a training and test set from this model.
3. **Learn the model:** Learn a randomly initialized model with the training set using stochastic gradient descent.
5. **Evaluate:** Evaluate the learned model on the test set.

### Step 1: Generate a toy model

To generate the model, we only need to construct two graphs: the graph to represent the unary potential function $f(\cdot)$ and the graph to represent the transition function $h(\cdot)$. The code to generate these graphs is below. Let's take a look at what these graphs look like.

```python
def gen_transitions(num_classes, calc_grad=False):
    """Make a bigram transition graph."""
    g = gtn.Graph(calc_grad)
    for i in range(num_classes):
        g.add_node(False, True)
    g.add_node(True, True)
    for i in range(num_classes):
        g.add_arc(num_classes, i, i)  # h(<s>, i)
        for j in range(num_classes):
            g.add_arc(i, j, j)  # h(i, j)
    return g

def gen_potentials(num_features, num_classes, calc_grad=False):
    """Make the unary potential graph"""
    g = gtn.Graph(calc_grad)
    g.add_node(True, True)
    for i in range(num_features):
        for c in range(num_classes):
            g.add_arc(0, 0, i, c)  # f(i, c)
    return g

def gen_model(num_features, num_classes, calc_grad=False, init=True):
    transitions = gen_transitions(num_classes, calc_grad)
    potentials = gen_potentials(num_features, num_classes, calc_grad)

    # Randomly set the arc weights of the graphs:
    if init:
        transitions.set_weights(
            10 * np.random.randn(transitions.num_arcs()))
        potentials.set_weights(
            10 * np.random.randn(potentials.num_arcs()))
    return potentials, transitions
```

```python
potentials, transitions = gen_model(num_features, num_classes)
gtn.draw(potentials, "potentials.png", feature_symbols, class_symbols)
display(Image("potentials.png"))
```

The `potentials` graph transduces all possibile input features to all possible class labels and assigns a score for each pair. The graph accomodates arbitrary sequence lengths. The edge label
\begin{equation}
    \xrightarrow{x:y/w}
\end{equation}
means transduce $x$ to $y$ with weight $w$.

```python
gtn.draw(transitions, "transitions.png", class_symbols)
display(Image("transitions.png"))
```

The `transitions` graph models the bigram score for each $(i, j)$ pair of class labels. So the transition from state `0` to state `1` models the score of class `1` following class `0`. Similarly the transition from state `2` to state `0` gives the score of class `0` starting at the beginning of $Y$ (i.e. following the start of sequence `<s>`).

### Step 2: Sample the datasets

Next we sample a training and test dataset from the synthetic toy model. The code to sample examples from the model is  below.

```python
def make_chain_graph(seq, calc_grad=False):
    """Make a simple chain graph from an iterable of integers."""
    g = gtn.Graph(calc_grad)
    g.add_node(True)
    for e, s in enumerate(seq):
        g.add_node(False, e + 1 == len(seq))
        g.add_arc(e, e + 1, s)
    return g

def sample_model(
        num_features, num_classes,
        potentials, transitions,
        num_samples, max_len=20):
    """
    Sample `num_samples` from a linear-chain CRF specified
    by a `potentials` graph and a `transitions` graph. The
    samples will have a random length in `[1, max_len]`.
    """
    model = gtn.compose(potentials, transitions)

    # Draw a random X with length randomly from [1, max_len] and find the
    # most likely Y under the model:
    samples = []
    while len(samples) < num_samples:
        # Sample X:
        T = np.random.randint(1, max_len + 1)
        X = np.random.randint(0, num_features, size=(T,))
        X = make_chain_graph(X)
        # Find the most likely Y given X:
        Y = gtn.viterbi_path(gtn.compose(X, model))
        # Clean up Y:
        Y = gtn.project_output(Y)
        Y.set_weights(np.zeros(Y.num_arcs()))
        samples.append((X, Y))
    return samples
```

Let's break this down into a few steps.

- We construct the full model by composing the `potentials` graph and the `transitions` graph. We can get the score for any $(X, Y)$ pair by following it's path in this graph. We view the composition below. The model looks a bit hairy but really just gives a score for any $(X, Y)$ according to $f(\cdot)$ and $h(\cdot)$.

```python
model = gtn.compose(potentials, transitions)
gtn.draw(model, "model.png", feature_symbols, class_symbols)
display(Image("model.png"))
```

- The next step is to sample a random $X$ with a random length and convert it into a GTN graph. We draw a random input of length $5$ below.

```python
X = make_chain_graph(np.random.randint(0, num_features, size=(5,)))
gtn.draw(X, "X.png", feature_symbols)
display(Image("X.png"))
```

- Next, we find the most likely $Y$ for the given $X$ according to the model. We first constrain the model to only allow paths with inputs given by $X$ (with `gtn.compose(X, model)`).

```python
constrained = gtn.compose(X, gtn.compose(potentials, transitions))
gtn.draw(constrained, "constrained.png", feature_symbols, class_symbols)
display(Image("constrained.png"))
```

- We compute $Y$ by finding the most likely path in this constrained model with `gtn.viterbi_path`. We clean $Y$ up by removing the input features and clearing the weights from the graph. Then we save the sample to our dataset.

We can now use `sample_model` to sample our training and test datasets:

```python
# Sample training and test datasets:
samples = sample_model(
    num_features, num_classes, potentials, transitions, num_train + num_test)
train, test = samples[:num_train], samples[num_train:]
```

### Step 3: Learn the model

We proceed to learn the parameters of a randomly initialized model using the training dataset. We use a simple SGD optimization procedure. The first step is to define the loss function following the equation we gave earlier for $\log P(Y\mid X)$.

```python
def crf_loss(X, Y, potentials, transitions):
    feature_graph = gtn.compose(X, potentials)

    # Compute the unnormalized score of `(X, Y)`
    target_graph = gtn.compose(feature_graph, gtn.intersect(Y, transitions))
    target_score = gtn.forward_score(target_graph)

    # Compute the partition function
    norm_graph = gtn.compose(feature_graph, transitions)
    norm_score = gtn.forward_score(norm_graph)

    return gtn.subtract(norm_score, target_score)
```

The next bit is a standard SGD loop, which computes the loss for an example, then computes the gradients of the loss with respect to the model parameters and takes a gradient step.

```python
def update_params(learning_rate, *graphs):
    """Take a gradient step on each graph in `graphs`."""
    for graph in graphs:
        params = graph.weights_to_numpy()
        grad = graph.grad().weights_to_numpy()
        params += learning_rate * grad
        graph.set_weights(params)

def sampler(dataset):
    """Iterator which randomly samples from a dataset."""
    while True:
        indices = np.random.permutation(len(dataset))
        for idx in indices:
            yield dataset[idx]

# Make the graphs to be learned:
potentials, transitions = gen_model(
    num_features, num_classes, calc_grad=True, init=False)

# Run the SGD loop:
learning_rate = 1e-2
max_iter = 10000
losses = []
for it, (X, Y) in enumerate(sampler(train)):
    # Compute the loss and take a gradient step:
    loss = crf_loss(X, Y, potentials, transitions)
    gtn.backward(loss)
    update_params(-learning_rate, potentials, transitions)

    # Clear the gradients:
    transitions.zero_grad()
    potentials.zero_grad()

    losses.append(loss.item())
    if (it + 1) % 1000 == 0:
        print("=" * 50)
        print(f"Iteration {it + 1}, Avg. Loss {np.mean(losses):.3f}")
        losses = []
    if it == max_iter:
        break
```

    ==================================================
    Iteration 1000, Avg. Loss 1.743
    ==================================================
    Iteration 2000, Avg. Loss 0.994
    ==================================================
    Iteration 3000, Avg. Loss 0.831
    ==================================================
    Iteration 4000, Avg. Loss 0.744
    ==================================================
    Iteration 5000, Avg. Loss 0.687
    ==================================================
    Iteration 6000, Avg. Loss 0.644
    ==================================================
    Iteration 7000, Avg. Loss 0.610
    ==================================================
    Iteration 8000, Avg. Loss 0.586
    ==================================================
    Iteration 9000, Avg. Loss 0.565
    ==================================================
    Iteration 10000, Avg. Loss 0.548

### Step 4: Evaluate

The last step is to evaluate the learned model's accuracy on the test set.

```python
correct = 0.0
total = 0
for X, Y in test:
    full_graph = gtn.compose(gtn.compose(X, potentials), transitions)
    prediction = gtn.viterbi_path(full_graph).labels_to_list(False)
    correct += np.sum(np.array(Y.labels_to_list()) == prediction)
    total += len(prediction)
print("Test: Accuracy {:.3f}".format(correct / total))
```

    Test: Accuracy 0.968


# Notation

$a_i$ - number of shares or units of asset i
$E[\cdot]$ - expectation
$\eta$ - vector of excess returns $\left(\mu-r_{f}\bf1\right)$
$\gamma$ - relative risk aversion coefficient
$\mu_{i}$ - drift of stock i
$N(\mu,\sigma)$ - a normally distributed random variable with mean $\mu$ and variance $\sigma^2$
$\mathcal{N}(\cdot)$ - cumulative distribution function of a $N(0,1)$ random variable
$r_f$ - risk free rate of interest
$S_{i}(t)$ - price per share of stock i
$\sigma_i$ - 1 x $m$ vector of coefficient of corresponding to $Z(t)$ in the model for the return of a stock
$\Sigma$ - covariance matrix of returns of stocks
$t$ - time
$u_i$ - dollar amount held of asset $i$
$V$ - Value function in dynamic programming
$w_i$ - porfolio weights
$W(t)$ - wealth
$Z(t)$ - standard Brownian Motion

# Modeling of Asset Price Dynamics

## Modeling the Dynamics of a Stock

주식의 수익률이 정규분포를 따른다는 가정부터 시작
(실제론 평상시엔 leptokurtic 형태에 가깝고, rare event가 터지는 상황은 levy 분포로 보통 모델링)

$$r(t) = \frac{S(t+\Delta t)-S(t)}{S(t)} \sim N(\mu\Delta t, \sigma^2\Delta t) \label{eq1}\tag{1}$$
$$r(t) = \frac{S(t+\Delta t)-S(t)}{S(t)}= \mu\Delta t+ \sigma\epsilon(\Delta t) \label{eq2}\tag{2}$$
, where $\epsilon(\Delta t) \sim N(0,\Delta t)$, $\mu$ is a drift, $\sigma$ is a volatility
(2)식에서 $\Delta t \to 0$,으로 하면, 식 (3)의 SDE가 도출됨

## A continuous time limit

$$dS(t) = \mu S(t)dt + \sigma SdZ(t)  \label{eq3}\tag{3}$$
,where $Z(t)$ 는 standard brownian motion이고, $dZ \to N(0,dt).$
식 (3)은 standard brownian motion으로 finance의 연속시간모델링에서 가장 많이 사용됨

Ito's lemma를 통해서 식(3)은 다음과 같은 log꼴로 표현 가능하고,
$$d(ln S) = \left(\mu-\frac{1}{2}\sigma^2\right)dt + \sigma dZ \label{eq4}\tag{4}$$

위의 solution을 구하면 다음과 같은 시간관계 식이 됨
$$S(t) = S(0)e^{\left(\mu-\frac{1}{2}\sigma^2\right)t+\sigma Z(t)} \label{eq5} \tag{5}$$

## Modeling Multiple Stocks

여러 주식들의 가격은 대체로 $correlated$ geometric Brownian motions로 모델링된다.
즉 n번째 주식 가격을 $S_i(t)$ for $i = 1, \cdots, n$으로 표현하면,

$$\frac{dS_i}{S_i}=\mu_i dt + \sigma_i dZ(t) \label{eq6} \tag{6}$$
where $Z(t) \in \mathbf{R}^m $은 standard $vector$ Brownian motion들이고 $\sigma_i \in \mathbf{R}^{1\times m}$이다.

이 경우엔 $dZ \sim N(0, Idt)$, where $I$는 $n \times n$의 항등행렬이 되고, 그러므로 vector Brownian motion의 $Z_j, j=1...m$ element들은 $uncorrelated$되어 있다고 가정한다

주식들의 수익률은 다음의 vector로 관리할 수 있고 $\mathbf r = \left[\frac{dS_1}{S_1},\cdots,\frac{dS_n}{S_n}\right]^T$, 그러면 (6)식을 다음과 같이 간결하게 쓸 수 있다.

$$ \mathbf{r} = \boldsymbol{\mu}dt + \boldsymbol{\sigma}dZ \label{eq7} \tag{7}$$
,where
$$\boldsymbol{\mu} =
\begin{bmatrix}
\mu_1 \\
\vdots \\
\mu_n
\end{bmatrix}
,
\boldsymbol{\sigma} =
\begin{bmatrix}
\sigma_{1}\\
\vdots\\
\sigma_{n}\\
\end{bmatrix}
\label{eq8} \tag{8}
$$

부가적으로 다음을 정의하면,
$$\Sigma = \boldsymbol{\sigma\sigma^T} \label{eq9} \tag{9}$$
$\Sigma dt$는 즉각적인 주식 수익률 vector의 covariance matrix가 된다.

## A Risk Free Asset

finance에서 채권이나 현금 같은 risk free 자산에 다음의 수익률을 가정하는 건 매우 일반적이다.

$$dB = B\space r_f dt \label{eq10} \tag{10}$$

여기에 양변을 dt로 나누어 solution을 구하면 다음과 같이 된다.

$$B(t) = B(0)e^{r_{f}t} \label{eq11} \tag{11}$$
그러나 위의 SDE notation과의 일관성을 위해 (10)처럼 쓰도록 한다.

# Modeling Wealth Dynamics

## Denominating a portfolio via weights

$W(t)$를 우리가 가진 위험자산과 무위험 자산으로 구성된 포트폴리오의 가치(부)로 정의하면, 다음과 같이 그 자산에 투자된 가치(부)의 조각에 의해 각 자산에 대한 지분을 기술할 수 있다.

$$\frac{dW}{W} = \left(1- \sum_{i=1}^{n}{w_i}\right)\frac{dB}{B}+\sum_{i=1}^{n}{\left(w_i \frac{dS_i}{S_i}\right)} \label{eq12} \tag{12}$$

$dB$에 (11)을, $dS$에 (6)을 대입하면 다음과 같은 포트폴리오 가치의 dynamics가 나오게 된다.

$$
\begin{align}
\frac{dW}{W} & = r_f dt+ \sum_{i=1}^{n}{w_i\left[\left(\mu_i - r_f\right)dt + \sigma_i dZ\right]} \label{eq13} \tag{13} \\
& = \left[r_f + \mathbf{w}^T(\boldsymbol{\mu}-r_f \mathbf{1}) \right]dt + \mathbf{w}^T \boldsymbol{\sigma} dZ \\
\end{align}$$

,where $\mathbf{w}$ : (n,1), $\mathbf{1} : (n, 1)$

여기서 앞으로 자주 쓰게될 $(\boldsymbol{\mu}-r_f \mathbf{1})$ 부분을 $\boldsymbol{\eta}$로 치환하면 다음과 같다.

$$\frac{dW}{W} = \left[r_f + \mathbf{w}^T\boldsymbol{\eta} \right]dt + \mathbf{w}^T \boldsymbol{\sigma} dZ \label{eq14} \tag{14}$$

포트폴리오의 지분을 기술할 때 포트폴리오 (12)식 처럼 weight $w_i$를 사용하는 것은 매우 편리하다. 그러나 이 지분을 표기할 때 아래와 같이, 해당 위험 자산을 몇 주 단위로 갖고 있는지(3.2)나 실제 현금 단위로 표기할 수도 있을 것이다.

$ Ww_i = \alpha_i S_i = u_i $

## Denominating a porfolio in shares

$$ \begin{align}
dW & = r_f W dt+ \sum_{i=1}^{n}{\alpha_i S_i\left[\left(\mu_i - r_f\right)dt + \sigma_i dZ\right]} \label{eq15} \tag{15}  \\
& =  r_f W dt+ \sum_{i=1}^{n}{\alpha_i S_i\left[\eta_i dt + \sigma_i dZ\right]} \label{eq16} \tag{16}
\end{align}
$$

, where $\alpha$는 위험자산의 지분(단위 : 주)를 의미하고 S는 위험자산의 가격을 나타낸다.

## Denominating a portfolio in dollars

$$ \begin{align}
dW & = r_f W dt+ \sum_{i=1}^{n}{u_i\left[\left(\mu_i - r_f\right)dt + \sigma_i dZ\right]} \label{eq17} \tag{17}  \\
& =  r_f W dt+ \sum_{i=1}^{n}{u_i\left[\eta_i dt + \sigma_i dZ\right]} \label{eq18} \tag{18}
\end{align}
$$

, where $u_i$는 포트폴리오에서 해당 자산에 대한 현금 지분을 나타낸다.

# Portfolio Optimization

## The Classical Model of Merton

## Dynamic Programming Solution

## Portfolio Optimization of Advanced Models

### The Portfolio Optimization Problem

### Solution via the HJB Equation

### The optimal control action

## An Application to Pairs Trading

### Wealth Dynamics in a Portfolio of Pairs

### Numerical Example

# Options and hedging

 이 section에서는 무배당 주식에 대한 European call option의 고전 Black-Scholes option pricing equation을 도출할 것이다. 기본적인 아이디어는 기초자산과 무위험자산으로 구성된 포트폴리오를 만들어, 만기 때의 옵션 가치를 복제하는 것이다. 투자자의 관점에선 실제 옵션을 들고 있거나, 옵션을 복제하는 포트폴리오를 들고 있는 것에는 관심이 없을 수 있다.
 이렇게 복제 포트폴리오(replicating portfolio)를 만드는 것을 동적헷징(dynamic hedging)이라고도 한다. 이를 하는 이유는, 예컨대 옵션 매도자의 경우 복제 포트폴리오를 만듦으로써 매도한 옵션에 대한 위험을 헷지할 수 있기 때문이다.

## The Payoff Structure of a European Call Option

Long position payoff of European call option
$$c(S,T) = (S-K)^+$$

## The Classical Option Pricing Model of Black and Scholes

Option pricing equation을 도출하기 위해, Geometric Brownian Motion을 따르는 기초자산과 무위험자산으로 구성된 포트폴리오를 European Call의 만기시점 payoff와 "최대한 가깝게" 운용하는 Objective의 Control problem을 formulating해보자.

$$\min_{\alpha}{E\left[\left(W(T)-f(S(T),T) \right)^2\right]} \label{eq19} \tag{19}$$
,where $f(S(T),T) = (S(T)-K)^+ $

$\text{s.t.}$

$$dW = r_f W dt+ \alpha S\left(\left(\mu - r_f\right)dt + \sigma dZ\right) \label{eq20} \tag{20} $$
$$dS = \mu S dt + \sigma S dZ \label{21} \tag{21}$$

포트폴리오 가치의 dynamics ($dW$)를 3.2의 (15)형태로 표현함

Dynamic Programming을 통한 solution이 Initial condition(즉, 포트폴리오의 Initial Wealth)을 가장 잘 복제하기 위해 어떤 것이어야 하는지를 암시할 것이기 때문에 Initial condition을 지정하는 것에 대해 의도적으로 모호하게 표현했다.
이러한 최적의 initial Wealth은 옵션의 공정가치(fair value)로 작용하며 블랙-숄즈 가격에 해당한다.

### Solution via the HJB equation

Dynamic programming을 이용해 이 problem을 위한 HJB eqn은 다음과 같이 쓸 수 있다.

$$0 = \min_{\alpha}{\left( V_t + V_S \mu S +V_W(r_f W + \alpha S(\mu-r_f)) + V_{SS}\frac{1}{2}\sigma^2 S^2 + V_{SW}\sigma^2 S^2 \alpha + V_{WW}\frac{1}{2}\alpha^2 S^2 \sigma^2 \right)} \label{22} \tag{22}$$

(19)가 $W(t)$로 구성된 linear quadratic structure의 problem이므로, HJB eqn을 풀기 위해 다음 형태의 solution을 갖는다고 추측할 수 있다.

$$V(t, W, S) = a(t,S)(W(t) - b(t,S))^2 + c(t,S) \label{23} \tag{23}$$

이 문제는 min에 대한 문제이기 때문에 Value function은 "손실"로 해석해야 한다.
$W(t)$가 포트폴리오 Wealth에 대한 함수이므로, $b(t,S)$는 해당 value function을 최소화하는 $W(t)$의 값이다.
따라서 최선의 복제를 위해 복제 포트폴리오의 초기 자산을 선택할 수 있다면(즉, 이 선택으로 인해 가장 작은 value function이 나오므로 가장 좋은 복제) $W(0) = b(0, S)$를 선택해야 한다. 그래서 $b(t, S)$는 자연스레 옵션의 '공정 가격(fair price)'으로 해석된다.

$c(t, S)$는 value function의 최소값이며, 따라서 expected quadratic cost에 따라 옵션을 실제로 복제하는 데 얼마나 근접할 수 있는지를 알려준다.

마지막으로 $a(t, S)$는 $b(t, S)$의 '공정 가격'과 같은 포트폴리오 Wealth를 갖지 못할 경우 value function(따라서 복제 오류)이 얼마나 빨리 커지는지를 나타낸다.

(23)의 value function을 HJB eqn(22)에 대입하면,

$0  = \min_{\alpha}{\Big[a_t(W-b)^2 - ab_t(W-b) + c_t + \mu S(a_S (W-b)^2 - 2ab_S (W-b) + c_S)}$
$\quad\quad\quad\space + 2a(W-b)(r_f (W-b) + r_f b + \alpha S(\mu - r_f)$
$\quad\quad\quad\space + \frac{1}{2} \sigma^2 S^2 (a_{SS}(W-b)^2 - 2(2a_S b_S + ab_{SS})(W-b) + 2ab_{S}^{2} + c_{SS})$
$\quad\quad\quad\space + \alpha\sigma^2 S^2 (2a_S (W-b) - 2ab_S) + \alpha^2 \frac{1}{2}\sigma^2 S^2 (2a)\Big]$

$$ =  \min_{\alpha}{
\begin{Bmatrix}
\begin{bmatrix}
1 &
W-b &
\alpha
\end{bmatrix}
&
\begin{bmatrix}
P_{11} & P_{12} & P_{13} \\
P_{21} & P_{22} & P_{23} \\
P_{31} & P_{32} & P_{33}
\end{bmatrix}
&
\begin{bmatrix}
1 \\
W-b \\
\alpha
\end{bmatrix}
&
\end{Bmatrix}
\label{eq24} \tag{24}
}$$

,where
$P_{11} = \left(c_t + \mu S c_S + \frac{1}{2} \sigma^2 S^2 (2ab_{S}^{2} + c_{SS})\right)$
$P_{12} = \left( abr_f - ab_t - \mu S ab_S - \sigma^2 S^2 (a_S b_S + \frac{1}{2}ab_{SS})\right)$
$P_{13} = -\sigma^2 S^2 ab_S$
$P_{22} = \left( a_t + \mu Sa_S + 2ar_f + \frac{1}{2}\sigma^2 S^2 a_{SS} \right)$
$P_{23} = \left[ a(S(\mu-r_f)) + \sigma^2 S^2 a_S \right]$
$P_{33} = \sigma^2 S^2 a$

(24)를 $\alpha$에 대해 미분하고 풀면,
$$
\begin{align}
\alpha^* & = -P_{33}^{-1}
\begin{bmatrix}
P_{13}^{T} & P_{23}^{T}
\end{bmatrix}
\begin{bmatrix}
1 \\ W-b
\end{bmatrix}
\\
& = b_S - \left( \frac{(\mu-r_f)}{\sigma^2 S} + \frac{a_S}{a} \right) (W-b)
\\
\end{align}
\label{eq25} \tag{25}
$$

이 optimal strategy $a^*$를 다시 HJB (24)에 대입하면 아래와 같다.

$$
\begin{align}
0 & = \begin{bmatrix} 1 & W-b \end{bmatrix}
\left\{
\begin{bmatrix} P_{11} & P_{12}\\ * & P_{22} \end{bmatrix}
-
\begin{bmatrix} P_{13} \\ P_{23}\end{bmatrix} P_{33}^{-1} \begin{bmatrix} P_{13}^T & P_{23}^T\end{bmatrix}
\right\}
\begin{bmatrix} 1 \\ W-b\end{bmatrix}
\\
& = \begin{bmatrix} 1 & W-b \end{bmatrix}
\begin{bmatrix}
P_{11}-P_{13}P_{33}^{-1} P_{13}^T & P_{12}-P_{13}P_{33}^{-1} P_{23}^T \\
* & P_{22}-P_{23}P_{33}^{-1} P_{23}^T
\end{bmatrix}
\begin{bmatrix} 1 \\ W-b\end{bmatrix}
\\
\end{align}
\label{eq26}
\tag{26}
$$

위 행렬의 모든 element는 0 이어야 하므로 (26) 가운데 행렬의 (1,1), (1,2), (2,2) elements의 equation들은 다음과 같다.

$$ 0 = c_t + \mu S c_S + \frac{1}{2}\sigma^2 S^2 c_{SS} \label{eq28} \tag{28} $$
$$ 0 = br_f - b_t - r_f Sb_S - \frac{1}{2}\sigma^2 S^2 b_{SS} \label{eq29} \tag{29}$$
$$ 0 = (a_t - \mu S a_S + 2r_f Sa_S + 2ar_f + \frac{1}{2}\sigma^2 S^2 a_{SS}) - \frac{(a(\mu - r_f)^2)}{\sigma^2}-\frac{\sigma^2 S^2 a_{S}^{2}}{a} \label{eq30} \tag{30}$$

$s.t.$ 경계조건들 : $c(T,S)=0,\quad b(T,S) = f(T,S),\quad a(T,S) = 1$

## The Black-Scholes option pricing equation

$b(t,S)$의 해석을 옵션의 "공정가격"이라고 했던 것을 상기시켜보면, $b(0,S)$는 optimal strategy 하에서 가장 payoff를 잘 복제한 결과이다. (29)로부터 우리는 다음을 따르는 공정가격을 이끌어낼 수 있다.

$$b_t + r_f S b_S + \frac{1}{2}\sigma^2 S^2 b_{SS} = r_f b \label{31} \tag{31}$$

s.t. 경계조건 $b(T,S) = f(T,S)$

(31)이 바로 그 Black-Scholes eqn이 된다. $\left(\frac{\partial V}{\partial t} + \frac{1}{2}\sigma^2 S^2 \frac{\partial^2 V}{\partial S^2} + r_f S \frac{\partial V}{\partial S}-r_f V = 0\right)$

만약 payoff function $f(T,S)$이 European call option이면 (즉 $f(T,S) = (S-K)^+$), 다음과 같은 closed form solution이 존재하게 된다.

$$b(t,S) = S \mathcal N(d_1) - K e^{r_f(T-t)} \mathcal N(d_2) \label{eq32} \tag{32}$$

$$d_1 = \frac{ln(\frac{S}{K}) + (r+\frac{1}{2} \sigma^2)(T-t)}{\sigma \sqrt{T-t}}, \quad d_2 = d_1 - \sigma \sqrt{T-t} $$

, where $\mathcal{N}(\cdot)$은 표준정규분포의 CDF이고 위의 (32)를 Black-Scholes formula라고 한다.

Black-Scholes equation을 이끌어 내는 다양한 방법들이 있지만, 위의 방법은 고전적인 방법이 아니라서 아마 이러한 도출은 처음 접하리라 생각된다. 이 도출의 접근방식은 Option Pricing과 Control의 연결을 강조해서 보여주는 의의가 있다.

### Perfect Replication and the Optimal Control Strategy

이제 (28)에 대해 살펴보자.
$$c_t + \mu S c_S + \frac{1}{2}\sigma^2 S^2 (c_{SS}) = 0$$

c의 말기조건은 0이므로, solution은 모든 t와 S에 대해 $c(t,S) = 0$이 된다.

이 결과는 만약 $b(t,S)$와 똑같은 wealth로 시작해 optimal strategy(25)에 따라 거래한다면 우리가 완벽히 option의 payoff를 에러없이 완벽히 복제할 수 있다는 것을 의미한다. 즉 value function은 $V(t, S, b(t,S)=0$이 된다.

optimal strategy $\left( \alpha^*= b_S - \left( \frac{(\mu-r_f)}{\sigma^2 S} + \frac{a_S}{a} \right) (W-b)\right)$(25)에 대해 생각해보자. 만약 우리가 초기 자본(initial wealth)을 $W(0) = b(0,S)$와 같이 시작한다면, 우리는 완벽한 복제를 할 수 있게 된다. 이는 우리가 모든 t에 대해 $W(t) = b(t, S(t)$를 갖는 것을 의미한다. 그래서 optimal strategy는 다음과 같이 축소된다.

$$\alpha^* = b_S \label{eq33} \tag{33}$$

이 거래 전략을 $delta \space hedge$라고 하고, 우변을 option의 $delta$라고 한다. 다음 섹션을 통해 이 델타헷징 전략의 수치적 예제를 살펴보자.

## Numerical Simulation of the Delta Hedging Strategy

```python
from scipy import stats
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
```

```python
total_date = 84
S = np.zeros((total_date+1, 1)) # 주가벡터 생성(781,1)
delta= np.zeros((total_date+1, 1))
C= np.zeros((total_date+1, 1))
b= np.zeros((total_date+1, 1))
W= np.zeros((total_date+1, 1))

S[0] = 1
r = 0.05 # risk free rate
mu = 0.15
sigma = 0.2
K = 1

dt = 1/252
```

```python
def call_price(S, K, ttm, r, sigma):
    d1 = ( np.log(S / K) + (r + sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
    d2 = ( np.log(S / K) + (r - sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
    val = ( S * stats.norm.cdf(d1, 0.0, 1.0) ) - K * np.exp( -r * ttm ) * stats.norm.cdf(d2, 0.0, 1.0)
    return val

def call_delta(S, K, ttm, r, sigma):
    d1 = ( np.log(S / K) + (r + sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
    val = stats.norm.cdf(d1, 0.0, 1.0)
    return val
```

```python
W[0] = call_price(S[0], K, 84/252, r, sigma)
x=np.linspace(0.7, 1.4, 100)

plt.rcParams['figure.figsize'] = [30, 15]
plt.rcParams.update({'font.size': 20})

plt.figure()
plt.xlim(0.7, 1.4)
plt.ylim(-0.1, 0.4)
plt.plot(x,np.maximum(x-K,0), color='k', label='Call payoff') # payoff graph
plt.title("Hedging Option", fontdict={'fontsize':40})
plt.xlabel('Strike')
plt.ylabel('P&L')

S_list=[]
W_list=[]
for i in range(150):
    for t in range(0, total_date): # 84 영업일 이후를 의미
        Z = np.random.randn(1)
        S[t+1] = S[t]*np.exp((mu-0.5*sigma**2)*dt + sigma*np.sqrt(dt)*Z)
        ttm = (84-t)/252 # time to maturity 84/252
        delta[t+1] = call_delta(S[t+1], K, ttm, r, sigma)
        C[t+1] = call_price(S[t+1], K, ttm, r, sigma)

        dZ = np.sqrt(1*dt)*Z # Brownian Motion
        dW = r*W[t]*dt + delta[t]*S[t]*((mu-r)*dt + sigma * dZ) # dynamics of Replicated Portfolio

        W[t+1] = W[t] + dW # rough dynamic equation

    S_list.append(S[-1][0])
    W_list.append(W[-1][0])

plt.plot(S_list,W_list,'ro', alpha=0.5, markersize=40, label= 'Replicated portfolio') # Portfolio payoff at Maturity
plt.legend()
```

# Appendix

## Dynamic Programming and the Hamilton-Jacobi-Bellman Equation

다음 형태의 optimal control problem을 상정해보자

$$\max_{u}{E[L(X(T))]}$$
s.t.
$$dX = a(t, X, u)dt + b(t, X, u)dZ$$

Dynamic programming approach를 써서, 현재 상태에서 최적의 "보상"을 확보한 value function $V(t,x)$를 정의함으로써 이 문제를 해결할 수 있다.

$$V(t,x) = \max_{u}{E[L(X(T))|X(t)=x]}$$

그러면 iterated expectations의 성질과 Ito's lemma로 인해, $V(t,x)$는 다음을 만족한다.

$$
\begin{align}
V(t,x) & = \max_{u}{E[V(t+dt, X(t+dt)) \space\space| X(t) = x]}\\
& = \max_{u}{E[V(t, X(t)) + dV(t, X(t)) \space\space| X(t) = x]}\\
& = \max_{u}{E[V(t, X(t)) + V_tdt + V_X adt + \frac{1}{2}V_{XX}b^2 dt + V_X bdZ \space\space | X(t) = x]}\\
& = \max_{u}{[V(t, X(t)) + V_tdt + V_X adt + \frac{1}{2}V_{XX}b^2 dt]}\\
\end{align}
$$

그러므로,
$$0 = \max_{u}{[V(t, X(t)) + V_X a(t, X, u) + \frac{1}{2}V_{XX}b^2(t, X, u)]}$$

Advanced/1. Hedging Option with Replicating Portfolio.ipynb


```python
from Integrais import Integrais as it
from sympy import var, Lambda, cos, sin, tan, exp, log, ln, sqrt, solve
x = var('x')
```

```python
# Questão 1
f = Lambda(x, cos(x)/(x+2))
a = 1
b = 2
display(f)

Q1 = it(f, a, b)
Q1.trapezio(n=6, dp=6)
Q1_int13 = Q1.simpson1_3(n = 6, dp=6)
Q1_int38 = Q1.simpson3_8(n=6, dp=6)
```

$\displaystyle \left( x \mapsto \frac{\cos{\left(x \right)}}{x + 2} \right)$

    *************** Regra do Trapézio ***************
    Portanto a integral é:
    0.02653 com |E| < 0.000474

    *************** Regra de 1/3 de Simpson ***************
    Portanto, o valor da integral é:
    0.026207, com |E| < 1e-06

    *************** Regra de 3/8 de Simpson ***************
    Portanto, o valor da integral é:
    0.026206, com |E| < 2e-06

```python
# Questão 2
x = var('x')
f = Lambda(x, sqrt(1 + x**2))
a = 0
b = 2
display(f)

Q2 = it(f, a, b)
Q2_int13 = Q2.trapezio(n = 4, dp=6)

```

$\displaystyle \left( x \mapsto \sqrt{x^{2} + 1} \right)$

    *************** Regra do Trapézio ***************
    Portanto a integral é:
    2.976529 com |E| < 0.041667

    ********************  AVISO  ********************
    Para garantia de um |E| < 0.01, utilize n = 9.0

```python
# Questão 3
x = var('x')
f = Lambda(x, 4/(1+x**2))
a = 0
b = 1
display(f)

Q3 = it(f, a, b)

Q3_int13 = Q3.simpson1_3(n = 10, dp=6)
```

$\displaystyle \left( x \mapsto \frac{4}{x^{2} + 1} \right)$

    *************** Regra de 1/3 de Simpson ***************
    Portanto, o valor da integral é:
    3.274926, com |E| < 5.3e-05

```python
# Questão 4
x = var('x')
f = Lambda(x, ln(x**3 + sqrt(exp(x)+1)))
a = 1
b = 4
display(f)

Q4 = it(f, a, b)

Q4_int13 = Q4.simpson3_8(n = 6, dp=6)
```

$\displaystyle \left( x \mapsto \log{\left(x^{3} + \sqrt{e^{x} + 1} \right)} \right)$

    *************** Regra de 3/8 de Simpson ***************
    Portanto, o valor da integral é:
    8.563331, com |E| < 0.007543

```python
# QUESTÃO 5
x = [0, .2, .4, .6, .8, 1]
y = [1, 1.197, 1.374, 1.503, 1.552, 1.468]

y1 = y[:4] # 3 primeiros 3/8 de simpson
y2 = y[3:] # 2 ultimos 1/3 de simpson

v1 = it(0, 0, 0, pts=(x, y1))
v2 = it(0, 0, 0, pts=(x, y2))

print(f'{(v1.num_38(6) + v2.num_13(6))}')

y3 = y[:3] # dois primeiros 1/3 de simpson
y4 = y[2:] # 3 ultimos 3/8 de simpson

v3 = it(0, 0, 0, pts=(x, y3))
v4 = it(0, 0, 0, pts=(x, y4))

print(f'{(v3.num_13(6) + v4.num_38(6)):.6f}')
```

    1.378133
    1.377992

```python
# Questão 6
from numpy import array
x = [0, 10, 20, 30, 50]
y1 = [50.8, 86.2, 136, 72.8, 51]
y2 = [113.6, 144.5, 185, 171.2, 95.3]

y = array(y2) - array(y1)

v1 = it(0, 0, 0, pts=(x[:4], y[:4])) # tres primeiros sub intervalos
v2 = it(0, 0, 0, pts=(x[3:], y[3:])) # ultimo sub intervalo
v1.num_38() + v2.num_trapezio()
```

    3238.625

```python
# Questão 7
x = var('x')
a = .1
b = .7
f = Lambda(x, exp(-3*x)+ 7 * x)
display(f)

Q7 = it(f, a, b)
Q7.simpson1_3(6, dp=7) # para h = .1 então n = 6
```

$\displaystyle \left( x \mapsto 7 x + e^{- 3 x} \right)$

    *************** Regra de 1/3 de Simpson ***************
    Portanto, o valor da integral é:
    1.8861298, com |E| < 2e-05

    1.8861298

```python
# Questão 8
x = var('x')
a = 1.6
b = 5.6
f = Lambda(x, ln(x+8)-2*x)
display(f)

Q8 = it(f, a, b)
Q8.trapezio(n = 2, erro=10**-6)
Q8.simpson1_3(n = 2, erro=10**-6)
Q8.simpson3_8(n = 3, erro=10**-6)

```

$\displaystyle \left( x \mapsto - 2 x + \log{\left(x + 8 \right)} \right)$

    *************** Regra do Trapézio ***************
    Portanto a integral é:
    -19.0262 com |E| < 0.0145

    ********************  AVISO  ********************
    Para garantia de um |E| < 1e-06, utilize n = 241.0

    *************** Regra de 1/3 de Simpson ***************
    Portanto, o valor da integral é:
    -19.0161, com |E| < 0.0003

    ********************  AVISO  ********************
    Para garantia de um |E| < 1e-06, utilize n = 8.0

    *************** Regra de 3/8 de Simpson ***************
    Portanto, o valor da integral é:
    -19.016, com |E| < 0.0001

    ********************  AVISO  ********************
    Para garantia de um |E| < 1e-06, utilize n = 12.0

    -19.016

```python
# Questão 9
f = Lambda(x, 1500 * (2 - x / (2*x +5)))
a = 2
b = 3
display(f)

Q9 = it(f, a, b)
Q9.simpson3_8(n=12)
```

$\displaystyle \left( x \mapsto - \frac{1500 x}{2 x + 5} + 3000 \right)$

    *************** Regra de 3/8 de Simpson ***************
    Portanto, o valor da integral é:
    2626.2576, com |E| < 0.0

    2626.2576

```python
# Questão 10
x = var('x')
y = var('y')
a1 = 0
b1 = 5
a2 = 0
b2 = 1

n = 5

f = Lambda((x,y), exp(x + y))

display(f)

ex1 = it(f, 0, 5)

f1 = ex1.trapezio(n = n, dupla = True)

ex2 = it(f1, 0, 1)

ex2.trapezio(n = n)
```

$\displaystyle \left( \left( x, \  y\right) \mapsto e^{x + y} \right)$

    *************** Regra do Trapézio -- Integral Dupla ***************

$\displaystyle 85.2910248837216 e^{x} + 0.5 e^{x + 5}$

    *************** Regra do Trapézio ***************
    Portanto a integral é:
    274.9748 com |E| < 1.4452

    ********************  AVISO  ********************
    Para garantia de um |E| < 0.01, utilize n = 61.0

    274.9748


```python
%matplotlib inline
```

`Learn the Basics <intro.html>`_ ||
`Quickstart <quickstart_tutorial.html>`_ ||
`Tensors <tensorqs_tutorial.html>`_ ||
`Datasets & DataLoaders <data_tutorial.html>`_ ||
`Transforms <transforms_tutorial.html>`_ ||
`Build Model <buildmodel_tutorial.html>`_ ||
**Autograd** ||
`Optimization <optimization_tutorial.html>`_ ||
`Save & Load Model <saveloadrun_tutorial.html>`_

Automatic Differentiation with ``torch.autograd``
=======================================

When training neural networks, the most frequently used algorithm is
**back propagation**. In this algorithm, parameters (model weights) are
adjusted according to the **gradient** of the loss function with respect
to the given parameter.

To compute those gradients, PyTorch has a built-in differentiation engine
called ``torch.autograd``. It supports automatic computation of gradient for any
computational graph.

Consider the simplest one-layer neural network, with input ``x``,
parameters ``w`` and ``b``, and some loss function. It can be defined in
PyTorch in the following manner:

```python
import torch

x = torch.ones(5)  # input tensor
y = torch.zeros(3)  # expected output
w = torch.randn(5, 3, requires_grad=True)
b = torch.randn(3, requires_grad=True)
z = torch.matmul(x, w)+b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)
```

Tensors, Functions and Computational graph
------------------------------------------

This code defines the following **computational graph**:

.. figure:: /_static/img/basics/comp-graph.png
   :alt:

In this network, ``w`` and ``b`` are **parameters**, which we need to
optimize. Thus, we need to be able to compute the gradients of loss
function with respect to those variables. In order to do that, we set
the ``requires_grad`` property of those tensors.

Note

You can set the value of ``requires_grad`` when creating a
          tensor, or later by using ``x.requires_grad_(True)`` method.

A function that we apply to tensors to construct computational graph is
in fact an object of class ``Function``. This object knows how to
compute the function in the *forward* direction, and also how to compute
it's derivative during the *backward propagation* step. A reference to
the backward propagation function is stored in ``grad_fn`` property of a
tensor. You can find more information of ``Function`` `in the
documentation <https://pytorch.org/docs/stable/autograd.html#function>`__.

```python
print('Gradient function for z =',z.grad_fn)
print('Gradient function for loss =', loss.grad_fn)
```

Computing Gradients
-------------------

To optimize weights of parameters in the neural network, we need to
compute the derivatives of our loss function with respect to parameters,
namely, we need $\frac{\partial loss}{\partial w}$ and
$\frac{\partial loss}{\partial b}$ under some fixed values of
``x`` and ``y``. To compute those derivatives, we call
``loss.backward()``, and then retrieve the values from ``w.grad`` and
``b.grad``:

```python
loss.backward()
print(w.grad)
print(b.grad)
```

Note

- We can only obtain the ``grad`` properties for the leaf
    nodes of the computational graph, which have ``requires_grad`` property
    set to ``True``. For all other nodes in our graph, gradients will not be
    available.
  - We can only perform gradient calculations using
    ``backward`` once on a given graph, for performance reasons. If we need
    to do several ``backward`` calls on the same graph, we need to pass
    ``retain_graph=True`` to the ``backward`` call.

Disabling Gradient Tracking
---------------------------

By default, all tensors with ``requires_grad=True`` are tracking their
computational history and support gradient computation. However, there
are some cases when we do not need to do that, for example, when we have
trained the model and just want to apply it to some input data, i.e. we
only want to do *forward* computations through the network. We can stop
tracking computations by surrounding our computation code with
``torch.no_grad()`` block:

```python
z = torch.matmul(x, w)+b
print(z.requires_grad)

with torch.no_grad():
    z = torch.matmul(x, w)+b
print(z.requires_grad)
```

Another way to achieve the same result is to use the ``detach()`` method
on the tensor:

```python
z = torch.matmul(x, w)+b
z_det = z.detach()
print(z_det.requires_grad)
```

There are reasons you might want to disable gradient tracking:
  - To mark some parameters in your neural network at **frozen parameters**. This is
    a very common scenario for
    `finetuning a pretrained network <https://pytorch.org/tutorials/beginner/finetuning_torchvision_models_tutorial.html>`__
  - To **speed up computations** when you are only doing forward pass, because computations on tensors that do
    not track gradients would be more efficient.

More on Computational Graphs
----------------------------
Conceptually, autograd keeps a record of data (tensors) and all executed
operations (along with the resulting new tensors) in a directed acyclic
graph (DAG) consisting of
`Function <https://pytorch.org/docs/stable/autograd.html#torch.autograd.Function>`__
objects. In this DAG, leaves are the input tensors, roots are the output
tensors. By tracing this graph from roots to leaves, you can
automatically compute the gradients using the chain rule.

In a forward pass, autograd does two things simultaneously:

- run the requested operation to compute a resulting tensor
- maintain the operation’s *gradient function* in the DAG.

The backward pass kicks off when ``.backward()`` is called on the DAG
root. ``autograd`` then:

- computes the gradients from each ``.grad_fn``,
- accumulates them in the respective tensor’s ``.grad`` attribute
- using the chain rule, propagates all the way to the leaf tensors.

Note

**DAGs are dynamic in PyTorch**
  An important thing to note is that the graph is recreated from scratch; after each
  ``.backward()`` call, autograd starts populating a new graph. This is
  exactly what allows you to use control flow statements in your model;
  you can change the shape, size and operations at every iteration if
  needed.

Optional Reading: Tensor Gradients and Jacobian Products
--------------------------------------

In many cases, we have a scalar loss function, and we need to compute
the gradient with respect to some parameters. However, there are cases
when the output function is an arbitrary tensor. In this case, PyTorch
allows you to compute so-called **Jacobian product**, and not the actual
gradient.

For a vector function $\vec{y}=f(\vec{x})$, where
$\vec{x}=\langle x_1,\dots,x_n\rangle$ and
$\vec{y}=\langle y_1,\dots,y_m\rangle$, a gradient of
$\vec{y}$ with respect to $\vec{x}$ is given by **Jacobian
matrix**:

\begin{align}\begin{align}J=\left(\begin{array}{ccc}
      \frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}}\\
      \vdots & \ddots & \vdots\\
      \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}}
      \end{array}\right)\end{align}\end{align}

Instead of computing the Jacobian matrix itself, PyTorch allows you to
compute **Jacobian Product** $v^T\cdot J$ for a given input vector
$v=(v_1 \dots v_m)$. This is achieved by calling ``backward`` with
$v$ as an argument. The size of $v$ should be the same as
the size of the original tensor, with respect to which we want to
compute the product:

```python
inp = torch.eye(5, requires_grad=True)
out = (inp+1).pow(2)
out.backward(torch.ones_like(inp), retain_graph=True)
print("First call\n", inp.grad)
out.backward(torch.ones_like(inp), retain_graph=True)
print("\nSecond call\n", inp.grad)
inp.grad.zero_()
out.backward(torch.ones_like(inp), retain_graph=True)
print("\nCall after zeroing gradients\n", inp.grad)
```

Notice that when we call ``backward`` for the second time with the same
argument, the value of the gradient is different. This happens because
when doing ``backward`` propagation, PyTorch **accumulates the
gradients**, i.e. the value of computed gradients is added to the
``grad`` property of all leaf nodes of computational graph. If you want
to compute the proper gradients, you need to zero out the ``grad``
property before. In real-life training an *optimizer* helps us to do
this.

Note

Previously we were calling ``backward()`` function without
          parameters. This is essentially equivalent to calling
          ``backward(torch.tensor(1.0))``, which is a useful way to compute the
          gradients in case of a scalar-valued function, such as loss during
          neural network training.

--------------

Further Reading
~~~~~~~~~~~~~~~~~
- `Autograd Mechanics <https://pytorch.org/docs/stable/notes/autograd.html>`_


```python
import math
import matplotlib.pyplot as plt
from sympy import symbols, diff
import re
import numpy as np

import pandas as pd
from statsmodels.tsa.arima_model import ARIMA
import statsmodels.api as sm
```

```python
# Data :
# Продаж солодкої води :

x = [i for i in range(1,37)]

y = [9,10,10,11,12,18,26,40,39,28,20,13,
     8,9,11,9,13,15,33,45,45,25,18,10,
     7,9,10,11,13,15,31,40,35,26,19,13]

```

```python
plt.plot(x, y)
plt.axis([0, 50, 0, 50])
plt.show()
```

```python
## Stage find regressions :

def Cov(a,b):
    a = list(a)
    b = list(b)

    avr_a = 0
    for i in range(len(a)):
        avr_a +=a[i]
    avr_a /=len(a)

    avr_b = 0
    for i in range(len(b)):
        avr_b += b[i]
    avr_b /= len(b)

    znamenik = 0
    for i in range(len(a)):
        znamenik += (a[i] - avr_a) * (b[i] - avr_b)

    chiselnik = len(a) - 1

    cov = 0

    cov = znamenik / chiselnik

    return cov

```

```python
print('кореляція = ', Cov(x,y))
```

    кореляція =  18.62857142857143

```python
def standart_error(a,b):
    # Дисперсії :
    avr_a = 0
    for i in range(len(a)):
        avr_a += a[i]
    avr_a /=len(a)

    avr_b = 0
    for i in range(len(b)):
        avr_b += b[i]
    avr_b /= len(b)

    d_a = 0 # dispersia
    for i in range(len(a)):
        d_a += math.pow((a[i] - avr_a),2)

    d_b = 0 # dispersia
    for i in range(len(b)):
        d_b += math.pow((b[i] - avr_b),2)

    # Стандартне відхилення :
    std_a = 0
    std_b = 0

    std_a = math.pow(d_a,(0.5))
    std_b = math.pow(d_b ,(0.5))

    return std_a,std_b

def rxy(a,b):
    std_a,std_b = standart_error(a,b)
    # cof_korell :
    cov = Cov(a,b)
    cof_korell = cov / (std_a * std_b)

    return cof_korell
print('Кофіцієнт кореляції :',rxy(x,y))
```

    Кофіцієнт кореляції : 0.00429572464444998

```python
print('Кофіцієнт детермінації : ',math.pow(rxy(x,y),(0.5)))
```

    Кофіцієнт детермінації :  0.06554177785542577

```python
def firstRegressor(a,b):
    std_a,std_b = standart_error(a,b)
    cov = Cov(a,b)

    # Регресор :
    first = (std_a / std_b) * cov
    return first
firstRegressor(x,y)
```

    16.688890243688178

```python
def secondRegressor(a,b):
    avr_a = 0
    for i in range(len(a)):
        avr_a += a[i]
    avr_a /= len(a)

    avr_b = 0
    for i in range(len(b)):
        avr_b += b[i]
    avr_b /= len(b)
    first = firstRegressor(a,b)

    second = avr_b - (first * avr_a)
    return second
secondRegressor(x,y)
```

    -289.1333583971202

```python

```

```python

```

```python
def minMNK(a,b):
    function = 0
    w0, w1 = symbols('x y', real=True)

    for i in range(len(a)):
        function +=(w1*a[i] + w0 - b[i]) ** 2

    #print(function)

    function1W0 = diff(function,w0)
    function2W1 = diff(function,w1)

    #print(function1W0)
   # print(function2W1)

    function1W0 = str(function1W0)
    function2W1 = str(function2W1)

    result1 = re.findall(r'\d+',function1W0)
    result2 = re.findall(r'\d+',function2W1)

    for i in range(len(result1)):
        result1[i] = int(result1[i])

    for i in range(len(result2)):
        result2[i] = int(result2[i])

    matrix = [result1[0:2],result2[0:2]]
    matrix_result = [result1[2],result2[2]]

    #print('\n')

    #for i in range(2):
       # print(matrix[i],'  \t=   ',matrix_result[i])

    #print(matrix)
    #print(matrix_result)
    resultAll = np.linalg.solve(matrix,matrix_result)

    new_w0, new_w1 = resultAll

    return new_w0, new_w1
minMNK(x,y)
```

    (16.506349206349206, 0.16782496782496775)

```python
def linerRegression(a,b,x):
    w0,w1 = minMNK(a,b)
    y = w0 + (w1*x)
    return y
```

```python
print('Результат лінійної регресі на 37 місяць : ', linerRegression(x,y,37))
```

    Результат лінійної регресі на 37 місяць :  22.715873015873015

```python

```

## DegreeData :

```python
data = pd.read_csv('full_external_temperatures.csv',header=0, parse_dates=[0], index_col=0, squeeze=True)
```

```python
x = []
temp_x = []
temp_x.extend(data['dateTime'])
for i in range(2000):
    x.append(temp_x[i])

x_data = []
for i in range(200):
    x_data.append(i)
```

```python
y = []
temp_y = []
temp_y.extend(data['data'])
for i in range(200):
    y.append(temp_y[i])
```

```python
test_y = []
temp_y = []
temp_y.extend(data['data'])
for i in range(2003):
    test_y.append(temp_y[i])
```

```python
train = pd.DataFrame({'time':x_data,'value':y})
```

```python
# Автокорегресія :
def auto_reg(a,b,p=1):
    w0,w1 = minMNK(a,b)
    function = w0
    #print(w0)
    result_array = []
    for i in range(p):
        len_arr = len(a) - i -1

        w0,w1 = minMNK(a[0:len_arr],b[0:len_arr])
        #print('liner - ',res_liner_regr, 'b - ',b[len_arr])
        function += b[len_arr] * w1

        result_array.append(function)
    return function
auto_reg(x_data,y,p=5)
```

    8.931993883482486

```python
# Скільзьке середнє :
def SMA(b,p=3):
    SMA_res = []
    sum_b = 0
    num = 0
    for i in range(len(b) - p):
        for j in range(p):
            sum_b += b[i+j]
        SMA_res.append(sum_b / p)
        sum_b = 0
    return SMA_res
def MA_foresee(b,p):
    SMA_res = 0

    for i in range(p):
        sum_b = len(b) - i -1
        SMA_res += b[sum_b]
        SMA_res /= p
    return SMA_res
def MAGrath(b,p=3):
    SMA_res = []
    sum_b = 0
    num = 0
    for i in range(len(b) - p):
        for j in range(p):
            sum_b += b[i+j]
        SMA_res.append(sum_b / p)
        sum_b = 0
    return SMA_res

MA_foresee(y,2)
```

    8.254999999999999

```python
MA_arr = [0,0,0]
temp_arr = SMA(y)
for i in range(len(temp_arr)):
    MA_arr.append(temp_arr[i])
```

```python

```

```python
plt.plot(x_data,MA_arr,'r')
plt.plot(x_data, y,'g')
plt.axis([0, 50, 0, 50])
plt.show()
```

```python
test_y = []
temp_y = []
temp_y.extend(data['data'])
for i in range(2003):
    test_y.append(temp_y[i])
```

```python
print(test_y[201])
```

    10.29

```python
def ARIMA(x,y,my_x,p,q,d):
    new_y = [0]
    new_y1 = []

    MA_plust_ar = 0

    if d == 0:
        MA_res = MA_foresee(y,q)
        AR_res = auto_reg(x_data,y,p=p)
        print("result MA = ", type(MA_res))
        print("result AR = ", type(AR_res))
        MA_plust_ar = MA_res + AR_res

        arr_MA = [0,0,0]
        temp_MA = MAGrath(y)
        for i in range(len(temp_MA)):
            tr = temp_MA[i]
            arr_MA.append(tr)
        plt.plot(x_data,arr_MA,'r')
        plt.plot(x_data,y,'b')
        plt.axis([-10, 210, -10, 20])
        plt.show()
    elif d == 1:
        for i in range(len(y)):
            num = y[i] - y[i-1]
            new_y.append(int(num))
            num = 0
        MA_res = MA_foresee(new_y,q)
        AR_res = auto_reg(x_data,new_y,p=p)
        print("result MA = ", MA_res)
        print("result AR = ", AR_res)
        MA_plust_ar = MA_res + AR_res

    elif d == 2:
        for i in range(len(y)):
            num = y[i] - y[i-1]
            new_y.append(int(num))
            num = 0
        for i in range(len(new_y)):
            num = new_y[i] - new_y[i-1]
            new_y1.append(num)
            num = 0
        arr_MA = SMA(new_y1,p)
        res_MA = 0
        for i in range(len(arr_MA)):
            res_MA += arrMA[i]
        arr_AR = auto_reg()

    return "predict " + str(MA_plust_ar)

print(ARIMA(x_data,y,my_x=1,p=1,q=7,d=0))
print("True value",test_y[201])
```

```python
print(ARIMA(x_data,y,my_x=201,p=2,q=6,d=0))
```

```python
def ARIMA_with_Prediction(step, x,y,p,q,d):
  new_y = [0]
  new_y1 = []

  resut_arr = []
  MA_plust_ar = 0

  if d == 0:
    new_x = []
    new_y = []
    new_x = x
    new_y = y
    my_x_x = 199
    for i in range(step):
      MA_res = MA_foresee(new_y,q)
      AR_res = auto_reg(new_x,new_y,p=p)
      MA_plust_ar = MA_res + AR_res
      resut_arr.append(MA_plust_ar)
      new_y.append(MA_plust_ar)
      my_x_x +=1
      new_x.append(my_x_x)

  return resut_arr
print("Predicted = ",ARIMA_with_Prediction(3,x_data,y,p=1,q=7,d=0))
print("True value = ", test_y[201],",   ",test_y[202],",   ",test_y[203])
```

    Predicted =  [10.243201426497652, 10.239743137128386, 10.273958496080168]
    True value =  10.29 ,    10.13 ,    10.11

```python

from pyramid.arima import auto_arima
model = auto_arima(y, trace=True, error_action='ignore', suppress_warnings=True)
model.fit(y)
forecast = model.predict(3)
```

    Fit ARIMA: order=(2, 1, 2) seasonal_order=(0, 0, 0, 1); AIC=14.654, BIC=34.503, Fit time=0.811 seconds
    Fit ARIMA: order=(0, 1, 0) seasonal_order=(0, 0, 0, 1); AIC=91.853, BIC=98.470, Fit time=0.067 seconds
    Fit ARIMA: order=(1, 1, 0) seasonal_order=(0, 0, 0, 1); AIC=34.098, BIC=44.023, Fit time=0.107 seconds
    Fit ARIMA: order=(0, 1, 1) seasonal_order=(0, 0, 0, 1); AIC=57.924, BIC=67.849, Fit time=0.127 seconds
    Fit ARIMA: order=(1, 1, 2) seasonal_order=(0, 0, 0, 1); AIC=12.702, BIC=29.243, Fit time=0.569 seconds
    Fit ARIMA: order=(1, 1, 1) seasonal_order=(0, 0, 0, 1); AIC=11.723, BIC=24.956, Fit time=0.285 seconds
    Fit ARIMA: order=(2, 1, 1) seasonal_order=(0, 0, 0, 1); AIC=12.655, BIC=29.196, Fit time=0.392 seconds
    Total fit time: 2.369 seconds
    [10.27357231 10.27332909 10.27322035]

```python
print("AutoArima : ",forecast)
print("Predicted = ",ARIMA_with_Prediction(3,x_data,y,p=1,q=7,d=0))
print("True value = ", test_y[201],",   ",test_y[202],",   ",test_y[203])
```

    AutoArima :  [10.27357231 10.27332909 10.27322035]
    Predicted =  [10.39171210757265, 10.392524629546758, 10.381670780971682]
    True value =  10.29 ,    10.13 ,    10.11


# Convexity of monomial expressions

In this document, we study the convexity of monomial expressions like $x,y \mapsto x^a.y^b$
on the positive orthant ($x,y>0$).
The goal is to find conditions (e.g. on coefficients $a,b$) under which such expression is convex.

PH, December 2020

```python
import sympy
from sympy import symbols, Function, Matrix, simplify, invert
```

```python
a,b,c = symbols('a b c')
x,y,z = symbols('x y z', positive=True)
```

```python
def Hessian(f, *args):
    """returns Hessian of expression `f`, with variables specified by `*args`

    Example:
    >>> Hessian(x**3 * y**2, x, y)
    Matrix([[6*x*y**2, 6*x**2*y], [6*x**2*y, 2*x**3]])
    """
    H = [[f.diff(xi).diff(xj) for xi in args] for xj in args]
    H = Matrix(H)
    H = simplify(H)
    return H
```

## 2D separable expression, linear in $x$

This is section deals with a more general case than monomial expression.
We consider a 2D separable expression which is linear in $x$:

$$f x,y \mapsto x.\rho(y)$$

i.e. this corresponds to the case $a=1$ in the next section,
but the dependency in $y$ is any function $\rho$, rather than the power law $y\mapsto y^b$.

The Hessian of $f$ is

$$H(x,y) =
\begin{bmatrix}
    0        &  \rho'(y) \\
    \rho'(y) & x\rho''(y)
\end{bmatrix}
$$

The conditions for $H$ to be definite semi-positive (for all $x,y>0$) are:
* non-negative diagonal terms, i.e. $\rho''>0$, that is $\rho$ should be convex
* non-negative determinant

The 2nd condition is:

$$0 - \rho'^2 \geq 0$$

which becomes $\rho' = 0$.

This means that only **constant** $rho$ functions are allowed for the $x.\rho(y)$ to be convex.

Translated in the context of storage loss models, this means that among PWL-in-$P$ models,
only *constant efficiency charge/discharge coefficients* yield convex expressions.
In particular, energy dependent efficiency coefficients ($\eta(E)$), even if convex in $E$,
do not yield convex loss models.

```python
ρ = Function('ρ')
Hessian(ρ(y)*x, x,y)
```

$\displaystyle \left[\begin{matrix}0 & \frac{d}{d y} ρ{\left(y \right)}\\\frac{d}{d y} ρ{\left(y \right)} & x \frac{d^{2}}{d y^{2}} ρ{\left(y \right)}\end{matrix}\right]$

```python
Hessian(ρ(y)*x, x,y).det()
```

$\displaystyle - \left(\frac{d}{d y} ρ{\left(y \right)}\right)^{2}$

## 2D monomial

E.g useful for loss model dependent in power ($x$) and energy ($y$) or any other 2nd variable (temperature, aging).
In this context, coefficient $a$ can be supposed positive (losses increase with $x$).

$f_2: x,y \mapsto x^a.y^b$

```python
f2 = x**a * y**b
f2
```

$\displaystyle x^{a} y^{b}$

```python
H2 = Hessian(f2, x, y)
H2
```

$\displaystyle \left[\begin{matrix}a x^{a - 2} y^{b} \left(a - 1\right) & a b x^{a - 1} y^{b - 1}\\a b x^{a - 1} y^{b - 1} & b x^{a} y^{b - 2} \left(b - 1\right)\end{matrix}\right]$

The Hessian has a simpler expression if divided by $f_2$:

```python
simplify(H2/f2)
```

$\displaystyle \left[\begin{matrix}\frac{a \left(a - 1\right)}{x^{2}} & \frac{a b}{x y}\\\frac{a b}{x y} & \frac{b \left(b - 1\right)}{y^{2}}\end{matrix}\right]$

Perhaps Hessian is even simpler if multiplied by $\frac{xy}{f}$

```python
simplify(H2*x*y/f2)
```

$\displaystyle \left[\begin{matrix}\frac{a y \left(a - 1\right)}{x} & a b\\a b & \frac{b x \left(b - 1\right)}{y}\end{matrix}\right]$

Determinant:

$$\det  \frac{xy}{f} H_2 = ab(1-a-b)$$

so we have

$$\det  H_2 = \frac{f_2^2}{x^2y^2}[ab(1-a-b)]$$

(reminder: $\det kA = k^n \det A$, where $n$ is the dimention of matrix $A$)

```python
d2 = simplify((H2*x*y/f2).det())
d2
```

$\displaystyle a b \left(- a - b + 1\right)$

**TODO**:

- Write down convexity proof here
- Plot 2D heatmap of the determinant, to show where it is positive
- Insert png version of the illustration:

### Inverse of Hessian

Inverse of $H_2$ (needed for next section):

again the same trick of multiplying by $f^{-1}$ or $\frac{xy}{f}$ makes the expression simpler:

```python
simplify((H2/f2).inverse())
```

$\displaystyle \left[\begin{matrix}\frac{x^{2} \left(1 - b\right)}{a \left(a + b - 1\right)} & \frac{x y}{a + b - 1}\\\frac{x y}{a + b - 1} & \frac{y^{2} \left(1 - a\right)}{b \left(a + b - 1\right)}\end{matrix}\right]$

multiply by $ab(1-a-b)$ (det of $H_2 x y/f_2$):

```python
simplify((H2/f2).inverse()*d2)
```

$\displaystyle \left[\begin{matrix}b x^{2} \left(b - 1\right) & - a b x y\\- a b x y & a y^{2} \left(a - 1\right)\end{matrix}\right]$

```python
simplify((H2*x*y/f2).inverse())
```

$\displaystyle \left[\begin{matrix}\frac{x \left(1 - b\right)}{a y \left(a + b - 1\right)} & \frac{1}{a + b - 1}\\\frac{1}{a + b - 1} & \frac{y \left(1 - a\right)}{b x \left(a + b - 1\right)}\end{matrix}\right]$

## 3D monomial

$f_3: x,y,z \mapsto x^a.y^b.z^c$

For convexity analysis, we will build on the convexity of the 2D case, by using [Conditions for semi-definiteness using Schur complement](https://en.wikipedia.org/wiki/Schur_complement#Conditions_for_positive_definiteness_and_semi-definiteness).

```python
f3 = x**a * y**b * z**c
f3
```

$\displaystyle x^{a} y^{b} z^{c}$

```python
H3 = Hessian(f3, x, y, z)
H3
```

$\displaystyle \left[\begin{matrix}a x^{a - 2} y^{b} z^{c} \left(a - 1\right) & a b x^{a - 1} y^{b - 1} z^{c} & a c x^{a - 1} y^{b} z^{c - 1}\\a b x^{a - 1} y^{b - 1} z^{c} & b x^{a} y^{b - 2} z^{c} \left(b - 1\right) & b c x^{a} y^{b - 1} z^{c - 1}\\a c x^{a - 1} y^{b} z^{c - 1} & b c x^{a} y^{b - 1} z^{c - 1} & c x^{a} y^{b} z^{c - 2} \left(c - 1\right)\end{matrix}\right]$

Again same trick is to consider $H/f$ to get simpler expressions:

```python
J3 = simplify(H3/f3)
J3
```

$\displaystyle \left[\begin{matrix}\frac{a \left(a - 1\right)}{x^{2}} & \frac{a b}{x y} & \frac{a c}{x z}\\\frac{a b}{x y} & \frac{b \left(b - 1\right)}{y^{2}} & \frac{b c}{y z}\\\frac{a c}{x z} & \frac{b c}{y z} & \frac{c \left(c - 1\right)}{z^{2}}\end{matrix}\right]$

We split the matrix into blocks to apply the Schur complement based method

$$J = H_3/f = \begin{bmatrix}
    A   &B\\
    B^T &d
\end{bmatrix}
$$

with:
* $A = H_2/f_2$, studied above
* $B^T = [ab/xy, ac/xz]$
* $d = c(c-1)/z^2$

We use the following result:

If $A\succ 0$ (equiv. to $\succeq 0$ and invertible?),
then:

> $J \succeq 0$ is equivalent to $J/A\succeq 0$

where $$J/A = d - B^T A^{-1} B$$ is the _Schur complement_ of block $A$ of matrix $J$.
Because of the way we split $J$, it is a _scalar_ ($1\times1$).

So we have to compute this Schur complement.

First, extract the three blocks of $J$.

```python
A = J3[0:2,0:2]
A #  == H2/f2
```

$\displaystyle \left[\begin{matrix}\frac{a \left(a - 1\right)}{x^{2}} & \frac{a b}{x y}\\\frac{a b}{x y} & \frac{b \left(b - 1\right)}{y^{2}}\end{matrix}\right]$

```python
simplify(H2/f2)
```

$\displaystyle \left[\begin{matrix}\frac{a \left(a - 1\right)}{x^{2}} & \frac{a b}{x y}\\\frac{a b}{x y} & \frac{b \left(b - 1\right)}{y^{2}}\end{matrix}\right]$

```python
d = J3[2,2]
d
```

$\displaystyle \frac{c \left(c - 1\right)}{z^{2}}$

```python
B = J3[0:2,2]
B
```

$\displaystyle \left[\begin{matrix}\frac{a c}{x z}\\\frac{b c}{y z}\end{matrix}\right]$

### Computation of Schur complement $J/A$

step by step:
1. Invert $A$
2. Multiply with $B^T$ and $B$

```python
Ainv = simplify(A.inv())
Ainv
```

$\displaystyle \left[\begin{matrix}\frac{x^{2} \left(1 - b\right)}{a \left(a + b - 1\right)} & \frac{x y}{a + b - 1}\\\frac{x y}{a + b - 1} & \frac{y^{2} \left(1 - a\right)}{b \left(a + b - 1\right)}\end{matrix}\right]$

```python
simplify(Ainv*B)
```

$\displaystyle \left[\begin{matrix}\frac{c x}{z \left(a + b - 1\right)}\\\frac{c y}{z \left(a + b - 1\right)}\end{matrix}\right]$

Clarify the expression:

$A^{-1}.B = -\frac{c}{z(1-a-b)} (x,y)^T$

```python
simplify(Ainv*B /(-c/z/(1-a-b)))
```

$\displaystyle \left[\begin{matrix}x\\y\end{matrix}\right]$

$B^TA^{-1}.B$ is quite simple:

```python
BtAiB = simplify((B.T*Ainv*B)[0,0])
BtAiB
```

$\displaystyle \frac{c^{2} \left(a + b\right)}{z^{2} \left(a + b - 1\right)}$

Finally, the Schur complement: (which need to be positive for convexity)

```python
S = simplify(d - BtAiB)
S
```

$\displaystyle - \frac{c \left(a + b + c - 1\right)}{z^{2} \left(a + b - 1\right)}$

Remove positive factors $z^2$ and $(a+b-1)$: (2nd one TO BE CHECKED)

```python
simplify(S * z**2 * (a+b-1))
```

$\displaystyle c \left(- a - b - c + 1\right)$

Conclusion: (TO BE CHECKED and WRITTEN MORE CLEARLY)

for $c<0$:

$$a+b+c \geq 1$$


```julia
using LinearAlgebraStudy
using LinearAlgebra
using Plots

```

    ┌ Warning: Package LinearAlgebraStudy does not have LinearAlgebra in its dependencies:
    │ - If you have LinearAlgebraStudy checked out for development and have
    │   added LinearAlgebra as a dependency but haven't updated your primary
    │   environment's manifest file, try `Pkg.resolve()`.
    │ - Otherwise you may need to report an issue with LinearAlgebraStudy
    └ Loading LinearAlgebra into LinearAlgebraStudy from project dependency, future warnings for LinearAlgebraStudy are suppressed.

```julia
RX=[1,0]
RY=[0,1]
gr()

drawVector(plot, RX,[0,0], (-3,3), (-3,3), "Vectors", "RX")
drawVector(plot!, RY,[0,0], (-3,3), (-3,3), "", "RY")

P=[2,1]
drawVector(plot!, P,[0,0], (-3,3), (-3,3), "", "P")

```

# Vector can be written as linear combination of scalar coefficients and basis

```julia
drawVector(plot!, TX, [0,0], (-3,3), (-3,3), "", "TX")
drawVector(plot!, TY, [0,0], (-3,3), (-3,3), "", "TY")
drawVector(plot!, t, [0,0], (-3,3), (-3,3), "", "t")

```

# BASIS T

```julia
TX = [1,1]
TY=[0,2]

```

    2-element Vector{Int64}:
     0
     2

```julia
t = transformVectorBasis2D(P, TX, TY)
```

    2-element Vector{Float64}:
      2.0
     -0.5

```julia
M2 = hcat(TX, TY)
```

    2×2 Matrix{Int64}:
     1  0
     1  2

```julia
f = plot
```

    plot (generic function with 4 methods)

```julia
plot
```

    plot (generic function with 4 methods)

```julia
#solve  system of linear equations
A=[1 0;1 2]
```

    2×2 Matrix{Int64}:
     1  0
     1  2

```julia
b=[2,1]
```

    2-element Vector{Int64}:
     2
     1

```julia
A\b
```

    2-element Vector{Float64}:
      2.0
     -0.5

```julia
A =[TX TY]
```

    2×2 Matrix{Int64}:
     1  0
     1  2

# Find dot product

```julia
v₀ = [1/2, 1/2, 1/2, 1/2]
```

    4-element Vector{Float64}:
     0.5
     0.5
     0.5
     0.5

```julia
v₁ = [1/2, 1/2, -1/2, -1/2]
```

    4-element Vector{Float64}:
      0.5
      0.5
     -0.5
     -0.5

```julia
dot(v₀, v₁)
```

    0.0

# How many different vectors $\mathbf{v}^{(3)}v $
could we find such that $\{\mathbf{v}^{(0)}$ ,$\mathbf{v}^{(1)}$,$ \mathbf{v}^{(2)}$,$\mathbf{v}^{(3)} $

${v (0), v (1), v (2), v (3)}$ is a full orthogonal basis in $\mathbb R^4R
4$
 ?

```julia
v₀ = [1/2, 1/2, 1/2, 1/2];
v₁ = [ 1/2, 1/2, -1/2, -1/2];
v₂ = [1/2, -1/2, 1/2, -1/2];
```

```julia
size(v₀) == size(v₁) == size(v₂)
```

    true

Inner product. The inner product of n-vectors x and y is denoted as x
T y. In
Julia, the inner product of x and y is denoted as x'*y.

```julia
v₀'* (v₁'*v₂)
```

    1×4 adjoint(::Vector{Float64}) with eltype Float64:
     0.0  0.0  0.0  0.0

checking for unit norm:

```julia
norm(v₀) == norm(v₁) == norm(v₂)
```

    true

```julia
using Symbolics
using SymbolicUtils
```

```julia
@variables a₁ a₂ a₃ a₄
@variables v₃
@variables y

@variables  v₃ = [a₁ , a₂, a₃, a₄]
```

\begin{equation}
\left[
\begin{array}{c}
v_3 \\
\end{array}
\right]
\end{equation}

```julia
v₀'*v₃
```

\begin{equation}
\left[
\begin{array}{cccc}
0.5 v_3 & 0.5 v_3 & 0.5 v_3 & 0.5 v_3 \\
\end{array}
\right]
\end{equation}

```julia
v₁'*v₃
```

\begin{equation}
\left[
\begin{array}{cccc}
0.5 v_3 & 0.5 v_3 &  - 0.5 v_3 &  - 0.5 v_3 \\
\end{array}
\right]
\end{equation}

```julia
v₂'*v₃
```

\begin{equation}
\left[
\begin{array}{cccc}
0.5 v_3 &  - 0.5 v_3 & 0.5 v_3 &  - 0.5 v_3 \\
\end{array}
\right]
\end{equation}

```julia
v₀' * v₁
```

    0.0

```julia
#find out how different vectors v₃ could we find such that {v₀, v₁, v₂, v₃} are independant

A = [v₀'*v₃;
    v₁'*v₃;
    v₂'*v₃]
```

\begin{equation}
\left[
\begin{array}{cccc}
0.5 v_3 & 0.5 v_3 & 0.5 v_3 & 0.5 v_3 \\
0.5 v_3 & 0.5 v_3 &  - 0.5 v_3 &  - 0.5 v_3 \\
0.5 v_3 &  - 0.5 v_3 & 0.5 v_3 &  - 0.5 v_3 \\
\end{array}
\right]
\end{equation}

```julia
b = [0,0,0]
```

    3-element Vector{Int64}:
     0
     0
     0

```julia
eq = y ~ 0.5*v₃ + 0.5*v₀ + 0.5*v₁ + 0.5*v₂
```

```julia
Symbolics.solve_for(eq, y)
```

# expansion coefficients on new Basis

```julia
v₀ = [1/2, 1/2, 1/2, 1/2];
v₁ = [ 1/2, 1/2, -1/2, -1/2];
v₂ = [1/2, -1/2, 1/2, -1/2];
v₃ = [1/2, -1/2, -1/2, 1/2];
y = [3.0, 1.0, 1.0, -1.0] #original basis

transformVectorBasis4D(y, v₀, v₁, v₂, v₃) # vector on orignial basis is first parameter
```

    4-element Vector{Float64}:
     2.0
     2.0
     2.0
     0.0

Which of the following sets form a basis of \mathbb R^4R
4
 ?

```julia
testVectorsIndependance(y, v₁ +  v₂, v₂, v₃)
```

    true

```julia
testVectorsIndependance(y, v₀, v₁, v₃)
```

    true

```julia
testVectorsIndependance(y, v₁, v₂, v₃)
```

    true

```julia
testVectorsIndependance(y, v₀, v₁, v₂)

```

    false

```julia
A = [0 0 0 1
1 0 0 0
0 1 0 0
0 0 1 0]
```

    4×4 Matrix{Int64}:
     0  0  0  1
     1  0  0  0
     0  1  0  0
     0  0  1  0

```julia
A ^ 4
```

    4×4 Matrix{Int64}:
     1  0  0  0
     0  1  0  0
     0  0  1  0
     0  0  0  1

```julia

```


```python
import sympy as sy
import control as pc
```

### Linear Steady-State Modelling

Beamsplitter
\begin{equation}
\begin{bmatrix} E_{out0} \\ E_{out1} \end{bmatrix} =
\begin{bmatrix} r_{ac}& t_{bc} \\  t_{ad}& r_{bd} \end{bmatrix}
\begin{bmatrix} E_{in0} \\ E_{in1} \end{bmatrix}
\end{equation}

So our inputs into the control system are the electromagnetic signals, that are time-domain constrained

#### Beamsplitter
\begin{equation}
\begin{bmatrix} E_{out0}(t) \\ E_{out1}(t) \end{bmatrix} =
\begin{bmatrix} r_{ac}& t_{bc} \\  t_{ad}& r_{bd} \end{bmatrix}
\begin{bmatrix} E_{in0}(t) \\ E_{in1}(t) \end{bmatrix} =
\begin{bmatrix} \sqrt{r} &  i\sqrt{1-r} \\  i \sqrt{1-r} & \sqrt{r} \end{bmatrix}
\begin{bmatrix} E_{in0}(t) \\ E_{in1}(t) \end{bmatrix}  =
\textbf{B}\textbf{E(t)}
\end{equation}

#### Phase Shifter on one arm

Assuming a phase shifter on top arm
\begin{equation}
\textbf{ $\Phi$ } = \begin{bmatrix} e^{i\phi} & 0 \\ 0 & 1\end{bmatrix}
\end{equation}

Note we're not yet taking into account Linear and LinearGroup delay, or the quantum probability variable within the design of the device.

Hence the MZI configuration is:

\begin{equation}
M\textbf{E(t)}  = \textbf{B} \textbf{ $\Phi$ } \textbf{B}\textbf{E(t)}
\end{equation}

\begin{equation}
M\textbf{E(t)}  = \begin{bmatrix} \sqrt{r} &  i\sqrt{1-r} \\  i \sqrt{1-r} & \sqrt{r} \end{bmatrix} \begin{bmatrix} e^{i\phi} & 0 \\ 0 & 1\end{bmatrix} \begin{bmatrix} \sqrt{r} &  i\sqrt{1-r} \\  i \sqrt{1-r} & \sqrt{r} \end{bmatrix}\begin{bmatrix} E_{in0}(t) \\ E_{in1}(t) \end{bmatrix} = \begin{bmatrix} E_{out0}(t) \\ E_{out1}(t) \end{bmatrix}
\end{equation}

Now we could just crunch some matrices together and input our time signals, but what if we wanted to leverage classical control system theory with regards to MIMO inputs and outputs, so that it is easier to design this type of systems togther and include feedback and nonlinearity back into the classical control models?

The standard way to start is to create the differential equations for this type of system and we can use the fast linear properties of SymPy to create a single MZI component, and the quantum probability variable could be added into this later. Right now let´s consider that we've only got differential equations, with equal reflectivity on both sides of the matrix:

```python
t = sy.Symbol("t")
E_0 = sy.Function("E_{in0}")(t)
E_1 = sy.Function("E_{in1}")(t)
phi = sy.Function("\phi")(t)
r_0, r_1 = sy.symbols("r_0 r_1")
B_0  = sy.Matrix([[sy.sqrt(r_0), sy.I*sy.sqrt(1 - r_0)],
				[sy.I*sy.sqrt(1 - r_0), sy.sqrt(r_0)]])
B_1  = sy.Matrix([[sy.sqrt(r_1), sy.I*sy.sqrt(1 - r_1)],
				[sy.I*sy.sqrt(1 - r_1), sy.sqrt(r_1)]])
Phi = sy.Matrix([[sy.exp(sy.I * phi), 0],
				[0, 1]])
E = sy.Matrix([[E_0], [E_1]])

out = B_0 * Phi * B_1 * E
out
```

$\displaystyle \left[\begin{matrix}\left(\sqrt{r_{0}} \sqrt{r_{1}} e^{i \phi{\left(t \right)}} - \sqrt{1 - r_{0}} \sqrt{1 - r_{1}}\right) \operatorname{E_{in0}}{\left(t \right)} + \left(i \sqrt{r_{0}} \sqrt{1 - r_{1}} e^{i \phi{\left(t \right)}} + i \sqrt{r_{1}} \sqrt{1 - r_{0}}\right) \operatorname{E_{in1}}{\left(t \right)}\\\left(\sqrt{r_{0}} \sqrt{r_{1}} - \sqrt{1 - r_{0}} \sqrt{1 - r_{1}} e^{i \phi{\left(t \right)}}\right) \operatorname{E_{in1}}{\left(t \right)} + \left(i \sqrt{r_{0}} \sqrt{1 - r_{1}} + i \sqrt{r_{1}} \sqrt{1 - r_{0}} e^{i \phi{\left(t \right)}}\right) \operatorname{E_{in0}}{\left(t \right)}\end{matrix}\right]$

So quite interesting, optics simply behaves different to other dynamic systems, although you could argue the delay in the heater or electro-optic modulated response is what really drives the dynamic response of the phase change. Yes, of course, because currently it's just a signal going through a block of glass with a given phase change when it is constant. We can assume the differential response of the system in time really is within $\phi (t)$. Also we are not considering the optical delay of the response yet.

Note that we're currently assuming two equal signal outputs to what came in, not necessarily the effect of a MZI.

Now this is fully just in the optical domain, and we can begin doing more with further domains inputs, but we'll get to that. Note that the case below is for linear state space, but we'll go into nonlinear in a bit.

\begin{equation}
\begin{bmatrix} E_{out0}(t) \\ E_{out1}(t) \end{bmatrix} =
\begin{bmatrix}
\left(\sqrt{r_{0}} \sqrt{r_{1}} e^{i \phi{\left(t \right)}} - \sqrt{1 - r_{0}} \sqrt{1 - r_{1}}\right) \operatorname{E_{in0}}{\left(t \right)} + \left(i \sqrt{r_{0}} \sqrt{1 - r_{1}} e^{i \phi{\left(t \right)}} + i \sqrt{r_{1}} \sqrt{1 - r_{0}}\right) \operatorname{E_{in1}}{\left(t \right)}
\\
\left(\sqrt{r_{0}} \sqrt{r_{1}} - \sqrt{1 - r_{0}} \sqrt{1 - r_{1}} e^{i \phi{\left(t \right)}}\right) \operatorname{E_{in1}}{\left(t \right)} + \left(i \sqrt{r_{0}} \sqrt{1 - r_{1}} + i \sqrt{r_{1}} \sqrt{1 - r_{0}} e^{i \phi{\left(t \right)}}\right) \operatorname{E_{in0}}{\left(t \right)}
\end{bmatrix}
\end{equation}

The electromagnetic waves should also be the output of the system even if they're also states that have been changed throughout their dynamic response of the system. See this example https://lpsa.swarthmore.edu/Representations/SysRepSS.html on generating state space systems. Hence the output will be just:

\begin{equation}
\textbf{y} = \begin{bmatrix} E_{out0}(t) \\ E_{out1}(t) \end{bmatrix}
\end{equation}

We know our state vector is:

\begin{equation}
\textbf{x} = \begin{bmatrix} E_{in0}(t) \\ E_{in1}(t) \end{bmatrix}
\end{equation}

However, our input control variable $\phi$ operates nonlinearly in our system. So we're going to put the equations in a canonical nonlinear state space form, without any inputs in the system.
$R$ really gives response properties rather than control properties. But this can be modelled in different ways anyways if we wanted. In any case, now we're going to assume it's not an input vector.

\begin{equation}
\textbf{u} = 0
\end{equation}

Now we need to generate our state matrix $\textbf{A}$. Now the reason why state space is so popular is because of how it deals with linear systems, but it can also deal with very complex nonlinear systems at controlled operating conditions.

In the case above, the MZI is a nonlinear device given interrelated states and inputs $\textbf{x}$ and $\textbf{u}$ with the $E \phi$ relationship, so they can't be separated into independent $\textbf{A}$ and $\textbf{B}$ matrices canonically, but they can if we consider the outputs of the system after it has reached steady-state equilibrium after a given change. This should be useful for most modelling situations, as you can discretize differentially the desired changes and only model the changed output in equilibrium without transient changes. This should also be less computationally expensive.

A mathemtical representation of delays in state-space systems are here: https://www.mathworks.com/help/control/ref/delayss.html and https://www.mathworks.com/help/control/ref/delayss.html .

\begin{equation}
\textbf{y} = \textbf{C} \textbf{x} + \textbf{D} \textbf{u}
\end{equation}

\begin{equation}
\begin{bmatrix} E_{out0}(t) \\ E_{out1}(t) \end{bmatrix} =
\begin{bmatrix}\sqrt{r_{0}} \sqrt{r_{1}} e^{i \phi{\left(t \right)}} - \sqrt{1 - r_{0}} \sqrt{1 - r_{1}} & i \sqrt{r_{0}} \sqrt{1 - r_{1}} e^{i \phi{\left(t \right)}} + i \sqrt{r_{1}} \sqrt{1 - r_{0}} \\
i\sqrt{r_{0}} \sqrt{1 - r_{1}} + i \sqrt{r_{1}} \sqrt{1 - r_{0}} e^{i \phi{\left(t \right)}} & \sqrt{r_{0}} \sqrt{r_{1}} - \sqrt{1 - r_{0}} \sqrt{1 - r_{1}} e^{i \phi{\left(t \right)}}\end{bmatrix}
 \begin{bmatrix} E_{in0}(t) \\ E_{in1}(t) \end{bmatrix}
\end{equation}

\begin{equation}
\textbf{D} = \begin{bmatrix} 0 & 0 \\ 0 & 0 \end{bmatrix}
\end{equation}

\begin{equation}
\textbf{C} = \begin{bmatrix}\sqrt{r_{0}} \sqrt{r_{1}} e^{i \phi{\left(t \right)}} - \sqrt{1 - r_{0}} \sqrt{1 - r_{1}} & i \sqrt{r_{0}} \sqrt{1 - r_{1}} e^{i \phi{\left(t \right)}} + i \sqrt{r_{1}} \sqrt{1 - r_{0}} \\
i\sqrt{r_{0}} \sqrt{1 - r_{1}} + i \sqrt{r_{1}} \sqrt{1 - r_{0}} e^{i \phi{\left(t \right)}} & \sqrt{r_{0}} \sqrt{r_{1}} - \sqrt{1 - r_{0}} \sqrt{1 - r_{1}} e^{i \phi{\left(t \right)}}\end{bmatrix}
\end{equation}

So in reality, for a given phi at any equilibrium moment in time, we can just input this into a state space model as a standard direct response, or include any feedback input in the input vector.

We know, from the differential equations above that, because these terms are not present.
\begin{equation}
\dot{\textbf{x}} = \begin{bmatrix} 0 \\ 0 \end{bmatrix}
\end{equation}

Hence, we can write in the nonlinear equations:

\begin{equation}
\dot{\textbf{x}} = \textbf{0} = \textbf{A} \textbf{x} +  \textbf{B} \textbf{u}
\end{equation}

or
\begin{equation}
\textbf{A} \textbf{x} = -  \textbf{B} \textbf{u}
\end{equation}

You could say that $\textbf{A} = \textbf{B}$ in this case, since $\textbf{B}\textbf{u} = 0$

But this does not solve the actual problem of inputting two input signals. Note we want to control those inputs, and currently we'd have to discretize to solve them or connect them separately. So state space is not applicable in this case alone, let's consider

```python
pc.ss()
```


```python
import sys
sys.path.append('..')
import os
import torch
import numpy as np
import matplotlib.pyplot as plt
from sympy import simplify_logic
import time
from sklearn.metrics import accuracy_score
import pandas as pd
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.tree import _tree, export_text

import lens
from lens.utils.base import validate_network, set_seed, tree_to_formula
from lens.utils.layer import prune_logic_layers
from lens import logic

results_dir = 'results_ll/xor'
if not os.path.isdir(results_dir):
    os.makedirs(results_dir)

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
concepts = ['c1', 'c2']
n_rep = 10
tot_epochs = 2001
```

```python
# XOR problem
x_train = torch.tensor([
    [0, 0],
    [0, 1],
    [1, 0],
    [1, 1],
], dtype=torch.float)
y_train = torch.tensor([0, 1, 1, 0], dtype=torch.float)

x_test = x_train
y_test = y_train
```

```python
def train_nn(x_train, y_train, seed, device, verbose=False):
    set_seed(seed)
    x_train = x_train.to(device)
    y_train = y_train.to(device)
    layers = [
        lens.nn.XLogic(2, 5, activation='identity', first=True),
        torch.nn.LeakyReLU(),
        torch.nn.Linear(5, 5),
        torch.nn.LeakyReLU(),
        torch.nn.Linear(5, 1),
        lens.nn.XLogic(1, 1, activation='sigmoid', top=True),
    ]
    model = torch.nn.Sequential(*layers).to(device)

    optimizer = torch.optim.AdamW(model.parameters(), lr=0.01)
    loss_form = torch.nn.BCELoss()
    model.train()
    need_pruning = True
    for epoch in range(tot_epochs):
        optimizer.zero_grad()
        y_pred = model(x_train).squeeze()
        loss = loss_form(y_pred, y_train)

        loss.backward()
        optimizer.step()

        # compute accuracy
        if epoch % 100 == 0 and verbose:
            y_pred_d = y_pred > 0.5
            accuracy = y_pred_d.eq(y_train).sum().item() / y_train.size(0)
            print(f'Epoch {epoch}: train accuracy: {accuracy:.4f}')

    return model
```

```python
def c_to_y(method, verbose=False):
    methods = []
    splits = []
    explanations = []
    model_accuracies = []
    explanation_accuracies = []
    explanation_fidelities = []
    explanation_complexities = []
    elapsed_times = []
    for seed in range(n_rep):
        explanation, explanation_inv = '', ''
        explanation_accuracy, explanation_accuracy_inv = 0, 0

        print(f'Seed [{seed+1}/{n_rep}]')

        if method == 'tree':
            classifier = DecisionTreeClassifier(random_state=seed)
            classifier.fit(x_train.detach().numpy(), y_train.detach().numpy())
            y_preds = classifier.predict(x_test.detach().numpy())
            model_accuracy = accuracy_score(y_test.detach().numpy(), y_preds)

            target_class = 1
            start = time.time()
            explanation = tree_to_formula(classifier, concepts, target_class)
            elapsed_time = time.time() - start

            target_class_inv = 0
            start = time.time()
            explanation_inv = tree_to_formula(classifier, concepts, target_class_inv)
            elapsed_time = time.time() - start

        else:
            model = train_nn(x_train, y_train, seed, device, verbose=False)
            y_preds = model(x_test.to(device)).cpu().detach().numpy() > 0.5
            model_accuracy = accuracy_score(y_test.cpu().detach().numpy(), y_preds)

            # positive class
            start = time.time()
            class_explanation, class_explanations = lens.logic.explain_class(model.cpu(), x_train.cpu(), y_train.cpu(),
                                                                           binary=True, target_class=1,
                                                                           topk_explanations=10)
            elapsed_time = time.time() - start

            if class_explanation:
                explanation = logic.base.replace_names(class_explanation, concepts)
                explanation_accuracy, y_formula = logic.base.test_explanation(class_explanation,
                                                                              target_class=1,
                                                                              x=x_train, y=y_train,
                                                                              metric=accuracy_score)
                explanation_fidelity = lens.logic.fidelity(y_formula, y_preds)
                explanation_complexity = lens.logic.complexity(class_explanation)

        if verbose:
            print(f'\t Model\'s accuracy: {model_accuracy:.4f}')
            print(f'\t Class 1 - Global explanation: "{explanation}" - Accuracy: {explanation_accuracy:.4f}')
            print(f'\t Elapsed time {elapsed_time}')

        methods.append(method)
        splits.append(seed)
        explanations.append(explanation)
        model_accuracies.append(model_accuracy)
        explanation_accuracies.append(explanation_accuracy)
        explanation_fidelities.append(explanation_fidelity)
        explanation_complexities.append(explanation_complexity)
        elapsed_times.append(elapsed_time)

    results = pd.DataFrame({
        'method': methods,
        'split': splits,
        'explanation': explanations,
        'model_accuracy': model_accuracies,
        'explanation_accuracy': explanation_accuracies,
        'explanation_fidelity': explanation_fidelities,
        'explanation_complexity': explanation_complexities,
        'elapsed_time': elapsed_times,
    })
    results.to_csv(os.path.join(results_dir, f'results_{method}.csv'))

    return results
```

# General pruning

```python
results_pruning = c_to_y(method='logic_layer', verbose=False)
results_pruning
```

    Seed [1/10]
    Seed [2/10]
    Seed [3/10]
    Seed [4/10]
    Seed [5/10]
    Seed [6/10]
    Seed [7/10]
    Seed [8/10]
    Seed [9/10]
    Seed [10/10]

      method
      split
      explanation
      model_accuracy
      explanation_accuracy
      explanation_fidelity
      explanation_complexity
      elapsed_time

      0
      logic_layer
      0
      (c1 & ~c2) | (c2 & ~c1)
      1.0
      1.0
      1.0
      4
      0.023936

      1
      logic_layer
      1
      (c1 & ~c2) | (c2 & ~c1)
      1.0
      1.0
      1.0
      4
      0.021942

      2
      logic_layer
      2
      (c1 & ~c2) | (c2 & ~c1)
      1.0
      1.0
      1.0
      4
      0.015993

      3
      logic_layer
      3
      (c1 & ~c2) | (c2 & ~c1)
      1.0
      1.0
      1.0
      4
      0.016955

      4
      logic_layer
      4
      (c1 & ~c2) | (c2 & ~c1)
      1.0
      1.0
      1.0
      4
      0.013962

      5
      logic_layer
      5
      (c1 & ~c2) | (c2 & ~c1)
      1.0
      1.0
      1.0
      4
      0.016948

      6
      logic_layer
      6
      (c1 & ~c2) | (c2 & ~c1)
      1.0
      1.0
      1.0
      4
      0.012965

      7
      logic_layer
      7
      (c1 & ~c2) | (c2 & ~c1)
      1.0
      1.0
      1.0
      4
      0.019945

      8
      logic_layer
      8
      (c1 & ~c2) | (c2 & ~c1)
      1.0
      1.0
      1.0
      4
      0.015952

      9
      logic_layer
      9
      (c1 & ~c2) | (c2 & ~c1)
      1.0
      1.0
      1.0
      4
      0.018949


# Baseball problem: Solution
Copyright © 2019 Oliver Beckstein.

This notebook is based on `12-ODE-applications.ipynb`. The whole problem was live-coded from scratch during class and this notebook is a cleaned-up and commented solution. The lesson showed how
1. to define the physical problem (obtain trajectory of a baseball with spin and air resistance)
2. to derive the underlying equations (Newton's equations of motions and the forces acting on the ball)
3. to adapt an algorithm to solve the equations (use RK4 for integrating the ODEs and express the problem in ODE standard form so that one can use `ode.rk4()`)
4. to visualize and discuss the results (plot trajectories and assess the influence of spin)

## Packages
We use numpy and our ODE solvers (`ode.py`):

```python
import numpy as np
import ode
```

For 3D graphs you can use
```
%matplotlib ipympl
```
but you have to do this as the very first thing (before loading `matplotlib`). You will also have to "close" active figures before you can plot into a new one.

```python
%matplotlib inline
#%matplotlib ipympl
import matplotlib.pyplot as plt
plt.style.use('ggplot')
```

## Simple Baseball physics

- quadratic air resistance (with velocity-independent drag coefficient)
- Magnus force due to spin

### Quadratic air resistance
Occurs at high Reynolds numbers, i.e., turbulent flow. Only approximate:

$$
\mathbf{F}_2 = -b_2 v \mathbf{v}
$$

with
$$
b_2 = \frac{1}{2} C_D \rho A
$$
where $C_D$ is the drag coefficient, $\rho$ the density of air, and $A = r^2 \pi$ the cross sectional area of the baseball.

### Magnus effect

**Magnus effect**: airflow is changed around a spinning object. The Magnus force is

$$
\mathbf{F}_M = \alpha \boldsymbol{\omega} \times \mathbf{v}
$$

where $\boldsymbol{\omega}$ is the ball's angular velocity in rad/s (e.g., 200/s for a baseball).

For a sphere the proportionality constant $\alpha$ can be written

$$
\mathbf{F}_M = \frac{1}{2} C_L \rho A \frac{v}{\omega} \boldsymbol{\omega} \times \mathbf{v}
$$

where $C_L$ is the lift coefficient, $\rho$ the air density, $A$ the ball's cross section. (Advantage of defining $C_L$ this way: when spin and velocity are perpendicular, the Magnus force is simply $F_M = \frac{1}{2} C_L \rho A v^2$.)

$C_L$ is mainly a function of the *spin parameter*

$$
S = \frac{r\omega}{v}
$$

with the radius $r$ of the ball. In general we write

$$
\mathbf{F}_M = \frac{1}{2} C_L(S)  \frac{\rho A r}{S} \boldsymbol{\omega} \times \mathbf{v}
$$

For a baseball, experimental data show approximately a power law dependence of $C_L$ on $S$

$$
C_L(S) = 0.62 \times S^{0.7}
$$

All together:

\begin{align}
\mathbf{F}_M &= \alpha\ \boldsymbol{\omega} \times \mathbf{v}\\
v &= \sqrt{\mathbf{v}\cdot\mathbf{v}}\\
S &= \frac{r\omega}{v}\\
C_L &= 0.62 \times S^{0.7}\\
\alpha &= \frac{1}{2} C_L  \frac{\rho A r}{S}
\end{align}

### Equations of motion

\begin{align}
\frac{d\mathbf{r}}{dt} &= \mathbf{v}\\
\frac{d\mathbf{v}}{dt} &= -g \hat{\mathbf{e}}_y -\frac{b_2}{m} v \mathbf{v} + \alpha\ \boldsymbol{\omega} \times \mathbf{v}
\end{align}

(quadratic drag $-\frac{b_2}{m} v \mathbf{v}$ included.)

### Baseball simulation

Implement the full baseball equations of motions:
- gravity $a_\text{gravity}$
- quadratic drag $a_\text{drag}$
- Magnus effect $a_\text{Magnus}$

For the cross product you can look at [numpy.cross()](https://docs.scipy.org/doc/numpy/reference/generated/numpy.cross.html).

```python
def C_L(S):
    return 0.62 * S**0.7

def simulate_baseball(v0, omega, r0=None,
                      h=0.01, C_D=0.40, g=9.81, rho=1.225,
                      r=0.07468/2, m=0.14883,
                      R_homeplate=18.4):
    """simulate baseball pitch

    Parameters
    ----------
    v0 : array
         initial velocity (vx, vy, vz) in m/s
    omega : array
         angular velocity vector of the ball ("spin"), in rad/s
    r0 : array, optional
         initial position of the ball (in m) when it leaves the pitcher's hand
         as (x, y, z); the default is (0, 2, 0)
    h : float, optional
         integration time step in s, default is 0.01 s
    C_D : float, optional
         drag coefficient, default is 0.40
    g : float, optional
         acceleration due to gravity, default 9.81 kg/(m*s^2)
    rho : float, optional
         density of air, default 1.225 kg/m^3
    r : float, optional
         radius of the baseball
    m : float, optional
         mass of the baseball
    R_homeplate : float, optional
         distance of the catcher from the pitcher

    Returns
    -------

    positions : array
         The array contains an entry (time, x, y, z) for each time step.
    """
    # all SI units (kg, m)
    if r0 is None:
        r0 = np.array([0, 2, 0])  # pitching at 2m height

    omega = np.asarray(omega)

    domega = np.linalg.norm(omega)
    A = np.pi*r**2
    rhoArm = rho * A * r / m
    b2 = 0.5 * C_D * rho * A

    a_gravity = np.array([0, -g, 0])

    def f(t, y):
        # y = [x, y, z, vx, vy, vz]
        v = y[3:]
        dv = np.linalg.norm(v)
        S = r*domega/dv
        a_magnus = 0.5 * C_L(S) * rhoArm / S * np.cross(omega, v)
        a_drag = -b2/m * dv * v
        a = a_gravity + a_drag + a_magnus
        return np.array([y[3], y[4], y[5],
                         a[0], a[1], a[2]])

    t = 0
    # initialize 3D!
    y = np.array([r0[0], r0[1], r0[2], v0[0], v0[1], v0[2]], dtype=np.float64)
    positions = [[t, y[0], y[1], y[2]]] # record t, x and y, z

    while y[0] < R_homeplate and y[1] >= 0.2:
        y[:] = ode.rk4(y, f, t, h)
        positions.append([t, y[0], y[1], y[2]])  # record t, x and y, z
        t += h

    return np.array(positions)

```

#### Simulate throws

Simulate baseball throw for initial velocity $\mathbf{v} = (30\,\text{m/s}, 0)$.

Plot x vs y and x vs z (to see curving).

Try out different spins; a good value is $\boldsymbol{\omega} = 200\,\text{rad/s} \times (0, 1, 1)$.

200 rad/s is 32 revolutions per second or almost 2000 rpm.

```python
print(200/(2*np.pi), "revolutions per second")
print(200/(2*np.pi) * 60, "rpm")
```

    31.830988618379067 revolutions per second
    1909.859317102744 rpm

Simulate the baseball throw with
- almost no spin: $\omega = 0.001 \times (0, 0, 1)$  (our code does not handle $\omega = 0$ gracefully...)
- full upward spin: $\omega = 200 \times (0, 0, 1)$
- sideways spin: $\omega = 200 \times (0, 1, 1)$

```python
r = simulate_baseball([30, 0, 0], omega=0.001 * np.array([0,0,1]))
rz = simulate_baseball([30, 0, 0], omega=200. * np.array([0,0,1]))
rzy = simulate_baseball([30, 0, 0], omega=200. * 2**(-0.5) * np.array([0,1,1]))
```

#### Plotting

Plot the three scenarios in 2D planes: x-y (side view) and x-z (top view).

```python
idx = 2  # y
plt.plot(r[:,1], r[:,idx], label="no spin")
plt.plot(rz[:,1], rz[:,idx], label="upward spin")
plt.plot(rzy[:,1], rzy[:,idx], label="sideways spin")
plt.xlabel("$x$ (m)")
plt.ylabel("$y$ (m)")
plt.legend(loc="best")
plt.title("Side view");
```

Note how the balls with spin fly higher: the Magnus force lifts them up (due to the component of the spin along $z$).

```python
idx = 3  # z
plt.plot(r[:,1], r[:,idx], label="no spin")
plt.plot(rz[:,1], rz[:,idx], label="upward spin")
plt.plot(rzy[:,1], rzy[:,idx], label="sideways spin")
plt.xlabel("$x$ (m)")
plt.ylabel("$z$ (m)")
plt.legend(loc="best")
plt.title("Top-down view");
```

The ball with a component of the spin along the $y$ direction ($\boldsymbol{\omega} = \omega_0 \, \left(\begin{array}{c} 0\\1\\1\end{array}\right)$) has a 0.6 m deflection in the $-z$ direction.

#### 3D plot
Use simple `matplotlib` 3D plot. (BONUS: Make it work with vpython)

If we use the [`notebook` backend for matplotlib](http://ipython.readthedocs.io/en/stable/interactive/plotting.html) then we will be able to interactively rotate our [matplotlib 3D graphics](http://matplotlib.org/mpl_toolkits/mplot3d/tutorial.html). (Note: If this does not seem to work, disable adblockers and allow javascript on the page.)

```python
%matplotlib ipympl
```

```python
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot(r[:,1], r[:,3], r[:,2], '.', label="no spin")
ax.plot(rz[:,1], rz[:,3], rz[:,2], '.', label="up spin")
ax.plot(rzy[:,1], rzy[:,3], rzy[:,2], '.', label="sideways spin")

# hand of the catcher, 0.2m above homeplate
ax.plot([18.4, 18.4], [0, 0], [0, 0.2], color="black", lw=6)

# drop line for last point
x,y,z = rzy[-1, 1:]
ax.plot([x,x], [z,z], [y,0], color="red")

ax.set_xlabel("$x$ (m)")
ax.set_ylabel("$z$ (m)")
ax.set_zlabel("$y$ (m)")
ax.legend(loc="upper left", numpoints=1)
ax.figure.tight_layout()
fig.savefig("baseball.png")
```

In the plot above we also added the hand of the catcher as a heavy black bar at 0.2 m above the home plate, and a drop-down bar in red to indicate the height of the ball thrown with *sideways spin* to make cleared that it also arrives at almost the same height as the ball with *up spin*.

```python

```