```python
import numpy as np
import matplotlib.pyplot as plt
import math
import scipy.io as sio
import cv2
import matplotlib.image as mpimg
from matplotlib import path
```

#### Homography Estimation
To project one image patch onto another, for each point inside the goal in the video frame, to find the corresponding point from the logo image to copy over. In the other words, the homography between the two image patches is required to compute. The homography is a $3\times 3$matrix that satisfies the following:
$$\mathrm{x}_{logo}\sim H \mathrm{x}_{video}$$
Or, equivalently:
$$\lambda\mathrm{x}_{logo}\sim H \mathrm{x}_{video}$$
where $\mathrm{x}_{logo}$ and $\mathrm{x}_{video}$ are homogeneous image coordinates from each patch and $\lambda$ is some scaling constant

A homography $H$ maps a set of points $\mathrm{x}=(x,y,1)^\top$ to another set of points $\mathrm{x}^\prime = (x',y',1)^\top$ up to a scalar.

\begin{align}
\mathrm{x}^\prime &\sim H\mathrm{x}\\
\lambda
\begin{pmatrix}
x'\\ y'\\ 1
\end{pmatrix} &= \begin{pmatrix}
h_{11} & h_{12} & h_{13}\\
h_{21} & h_{22} & h_{23}\\
h_{31} & h_{32} & h_{33}\\
\end{pmatrix}
\begin{pmatrix}
x \\ y \\ 1
\end{pmatrix}\\
\lambda x' &= h_{11}x+h_{12}y+h_{13}\\
\lambda y' &= h_{21}x+h_{22}y+h_{23}\\
\lambda &= h_{31}x+h_{32}y+h_{33}
\end{align}

Therefore, $x'$ and $y'$ are given by,

\begin{align}
x' &= \frac{h_{11}x+h_{12}y+h_{13}}{h_{31}x+h_{32}y+h_{33}} = \frac{h_{11}x_1+h_{12}x_2+h_{13}}{h_{31}x_1+h_{32}x_2+h_{33}} = x_1'\\
y' &= \frac{h_{21}x+h_{22}y+h_{23}}{h_{31}x+h_{32}y+h_{33}} = \frac{h_{21}x_1+h_{22}x_2+h_{23}}{h_{31}x_1+h_{32}x_2+h_{33}} = x_2'
\end{align}

Rearranging the terms above,

\begin{align}
-h_{11}x_1-h_{12}x_2-h_{13}+h_{31}x_1x_1'+h_{32}x_2x_1'+h_{33}x_3x_1'&= 0 \\
-h_{21}x_1-h_{22}x_2-h_{23}+h_{31}x_1x_2'+h_{32}x_2x_2'+h_{33}x_3x_2'&= 0 \\
\begin{pmatrix} a_x\\a_y \end{pmatrix} h &= 0
\end{align}

where

\begin{align}
a_x &= (-x_1,-x_2,-1,0,0,0,x_1x_1',x_2x_1',x_1') \\
a_y &= (0,0,0,-x_1,-x_2,-x_3,x_1x_2',x_2x_2',x_2') \\
h &= (h_{11},h_{12},h_{13},h_{21},h_{22},h_{23},h_{31},h_{32},h_{33})^\top
\end{align}

Matrix $H$ has a degree of freedom, as each point gives 2 sets of equations, so 4 points are required to solve for $h$ uniquely.By stacking,
$$\mathrm{A} = \begin{pmatrix}
a_{x1}\\a_{y1}\\ \vdots \\ a_{xn}\\a_{yn}\\
\end{pmatrix}$$
Solving the following equations, $$\mathrm{A}h=0$$
The vector $h$ is the last column of $V$ where $\mathrm{A}=U\Sigma V^\top$.

```python
image = cv2.imread('D:/44754/Documents/Data/image.png')
plt.imshow(image)
print(image.shape)
logoy,logox,_ =  image.shape
logo_pts = np.array([[0,0],[logox,0],
                     [logox,logoy],[0,logoy]])
```

```python
dirdata = 'D:/44754/Documents/robotics/Perception/RoboticsPerceptionWeek2AssignmentCode/data/BarcaReal_pts.mat'
video_pts = sio.loadmat(dirdata)['video_pts']
print(video_pts.shape)
num_ima = video_pts.shape[2]
test_images = np.linspace(1,num_ima,num_ima).astype(int)
num_test = len(test_images)

readdir = 'D:/44754/Documents/robotics/Perception/RoboticsPerceptionWeek2AssignmentCode/images/barcaReal/BarcaReal{:03d}.jpg'
```

    (4, 2, 129)

```python
def calculateInteriorPts(img_size,corners):
    x,y = np.meshgrid(np.linspace(1,img_size[1],img_size[1]),
                     np.linspace(1,img_size[0],img_size[0]))
    X=x.reshape(-1,1); Y=y.reshape(-1,1)

    p = path.Path(corners)
    allpts = np.array([X,Y]).squeeze().T
    interior_bol=p.contains_points(allpts)
    return allpts[interior_bol,:]
```

```python
tmp = cv2.imread(readdir.format(test_images[0]))
tmpvidpts = video_pts[:,:,0]
interior_pts = calculateInteriorPts(tmp.shape,tmpvidpts)
plt.imshow(tmp)
plt.plot(interior_pts[:,0],interior_pts[:,1],'r.')
```

```python
def vectorize(r,rp):
    x1=r[0]; x2=r[1]; x3=1
    x1p=rp[0]; x2p=rp[1]
    ax=[-x1,-x2,-1,0,0,0,x1*x1p,x2*x1p,x1p]
    ay=[0,0,0,-x1,-x2,-x3,x1*x2p,x2*x2p,x2p]
    return ax,ay
```

```python
def est_homography(video_pts,logo_pts):
    A = []
    for i in range(len(logo_pts)):
        r = video_pts[i,:]; rp = logo_pts[i,:]
        ax,ay = vectorize(r,rp)
        A.append(ax)
        A.append(ay)
    A = np.array(A)
    u, s, vh = np.linalg.svd(A, full_matrices=True)
    H = vh[-1,:].reshape((3,3))
    return H
```

```python
H = est_homography(tmpvidpts,logo_pts)
H
```

    array([[-3.79571153e-03,  1.48711665e-04,  5.11156805e-01],
           [-8.99478947e-04, -4.17599005e-03,  8.59468283e-01],
           [ 3.50401989e-07,  1.98161589e-08, -5.59264660e-04]])

```python
def warp_pts(video_pts,logo_pts,sample_pts):
    H = est_homography(video_pts,logo_pts)
    warped_pts = []
    for i in range(len(sample_pts)):
        tmp = np.concatenate((sample_pts[i,:],np.array([1])),axis=0)
        result = np.matmul(H,tmp)
        warped_pts.append(result[0:2]/result[2])
    return np.array(warped_pts)
```

```python
warped_logo_pts=warp_pts(tmpvidpts,logo_pts,interior_pts)
```

```python
def inverse_warping(imgf,imgi,ptsf,ptsi):
    ptsf = np.floor(ptsf).astype(int)
    ptsi = np.floor(ptsi).astype(int)
    projected_img = imgf
    for i in range(3):
        sif = imgf[:,:,i]
        sii = imgi[:,:,i]
        sif[ptsf[:,1],ptsf[:,0]]=sii[ptsi[:,1],ptsi[:,0]]*.5+sif[ptsf[:,1],ptsf[:,0]]*.5
        projected_img[:,:,i]=sif
    return projected_img
```

```python
result = inverse_warping(tmp,image,interior_pts,warped_logo_pts)
```

```python
plt.imshow(result)
```

```python
iod = 100
tmp = cv2.imread(readdir.format(test_images[iod]))
tmpvidpts = video_pts[:,:,iod]
interior_pts = calculateInteriorPts(tmp.shape,tmpvidpts)
warped_logo_pts=warp_pts(tmpvidpts,logo_pts,interior_pts)
result = inverse_warping(tmp,image,interior_pts,warped_logo_pts)

plt.figure(figsize=(20,10))
plt.subplot(1,2,1)
plt.imshow(tmp)
plt.plot(interior_pts[:,0],interior_pts[:,1],'r.')
plt.axis('off')

plt.subplot(1,2,2)
plt.imshow(result)
plt.axis('off')
```

```python
resultlist = []
for iod in range(129):
    tmp = cv2.imread(readdir.format(test_images[iod]))
    tmpvidpts = video_pts[:,:,iod]
    interior_pts = calculateInteriorPts(tmp.shape,tmpvidpts)
    warped_logo_pts=warp_pts(tmpvidpts,logo_pts,interior_pts)
    result = inverse_warping(tmp,image,interior_pts,warped_logo_pts)
    resultlist.append(result)
```

```python
resultlist = np.array(resultlist)
```

```python
%matplotlib inline
%load_ext autoreload
%autoreload 2
%matplotlib notebook
fig = plt.figure(figsize=(20,10))
ax = plt.gca()
plt.ion()
fig.show()
fig.canvas.draw()
for i in range(129):
    ax.clear()
    ax.imshow(resultlist[i,:,:,:])
    fig.canvas.draw()
```

    <IPython.core.display.Javascript object>

    <IPython.core.display.Javascript object>

```python

```


# Taylor problem 2.20 Template

last revised: 08-Jan-2019 by Dick Furnstahl [furnstahl.1@osu.edu]

**This is a template for solving problem 2.20.  Go through and fill in the blanks where ### appears.**

The goal of this problem is to plot and comment on the trajectory of a projectile subject to linear air resistance, considering four different values of the drag coefficient.

The problem statement fixes the initial angle above the horizontal and suggests using convenient values for the initial speed (magnitude of the velocity) and gravitational strength $g$.  We'll set up the problem more generally and look at special cases.

The equations are derived in the book:

$$\begin{align}
  x(t) &= v_{x0}\tau (1 - e^{-t/\tau}) \\
  y(t) &= (v_{y0} + v_{\textrm{ter}}) \tau (1 - e^{-t/\tau}) - v_{\textrm{ter}} t
\end{align}$$

where $v_{\textrm{ter}} = g\tau$.

Plan:
1. Define functions for $x$ and $y$, which will depend on $t$, $\tau$, $g$, and the initial velocity.  Make the functions look like the equations from Taylor to reduce the possibility of error.
2. Set up an array of the time $t$.
3. Determine $x$ and $y$ arrays for different values of $\tau$.
4. Make a plot of $y$ versus $x$ for each value of $\tau$, all on the same plot.
5. Save the plot for printing.

```python
### What modules do we need to import? (Can always add more later!)
```

### 1. Define functions for $x$ and $y$

```python
def x_traj(t, tau, v_x0=1., g=1.):
    """Horizontal position x(t) from equation (2.36) in Taylor.
       The initial position at t=0 is x=y=0.
    """
    return v_x0 * tau * (1. - np.exp(-t/tau))

def y_traj(t, tau, v_y0=1., g=1.):
    """Vertical position y(t) from equation (2.36) in Taylor.
       The initial position at t=0 is x=y=0.
    """
    v_ter = g * tau
    return ### fill in the correct expression here
```

### 2. Set up an array of the time $t$

```python
t_min = 0.
t_max = 3.
delta_t =       ### pick a reasonable delta_t
t_pts = np.arange()  ### fill in the blanks
t_pts  # check that we did what we thought!
```

### 3., 4. Make $x$ and $y$ arrays for different $\tau$ and plot

```python
%matplotlib inline
### What module(s) should you import?
```

```python
plt.rcParams.update({'font.size': 16})  # This is to boost the font size

fig = plt.figure(figsize=(10,10))
ax =    ### How do you create a single subplot?

tau_1 = 0.3
ax.plot(x_traj(t_pts, tau_1), y_traj(t_pts, tau_1), 'b-',
        label=r'$\tau = 0.3$')

tau_2 = 1.0
ax.plot(x_traj(t_pts, tau_2), y_traj(t_pts, tau_2), 'r:',
        label=r'$\tau = 1.0$')

tau_3 = 3.0
### plot a line with tau_3 and line type 'g--' with a label

tau_4 = 10000.
### plot a line with tau_4 and line type 'k- with a label

ax.set_ylim(-0.1, 0.6)
ax.set_xlim(0., 2.5)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_aspect(1)  # so that the x and y spacing is the same

ax.legend();

```

### 5. Save the plot for printing

```python
# save the figure
fig.savefig('Taylor_prob_2.20.png', bbox_inches='tight')
### Find the figure file and display it in your browser, then save or print.
### Check you graph against the one from the next section.
```

## More advanced python: plot again with a loop

Now do it as a loop, cycling through properties, and add a vertical line at the asymptotic distance.

```python
import matplotlib.pyplot as plt

plt.rcParams.update({'font.size': 18})

from cycler import cycler
my_cycler = (cycler(color=['k', 'g', 'b', 'r']) +
             cycler(linestyle=['-', '--', ':', '-.']))

fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1,1,1)
ax.set_prop_cycle(my_cycler)

v_x0 = 1.
tau_list = [10000, 3.0, 1.0, 0.3, 0.1]
for tau in tau_list:
    ax.plot(x_traj(t_pts, tau), y_traj(t_pts, tau),
            label=rf'$\tau = {tau:.1f}$')
    ax.axvline(v_x0 * tau, color='black', linestyle='dotted')

ax.set_ylim(-0.1, 0.6)
ax.set_xlim(0., 2.5)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_aspect(1)  # so that the x and y spacing is the same

ax.legend();

```

**If it is new to you, look up how a for loop in Python works and try to figure out what is happening here.  Ask if you are confused!**

```python

```


# Solution {-}

Stationary Gaussian random process

```python
from sympy import symbols, factor

s = symbols('s')

Sy = factor((-s**2 + 1)/((-s**2)**2 + 20*(-s**2) + 64))
Sy
```

$\displaystyle - \frac{\left(s - 1\right) \left(s + 1\right)}{\left(s - 4\right) \left(s - 2\right) \left(s + 2\right) \left(s + 4\right)}$

```python
Gs = (s + 1)/(s**2 + 6*s + 8)
Gs
```

$\displaystyle \frac{s + 1}{s^{2} + 6 s + 8}$

Let the KF state vector consist of the phase variables, i.e. $r$ and $\dot{r}$.

The the KF measurement equation will be:
\begin{equation}
  z=
  \begin{bmatrix}
    1 &1 \\
  \end{bmatrix}
  \begin{bmatrix}
    r\\
    \dot{r}\\
  \end{bmatrix}
  + v
\end{equation}

wiht the corresponding scalar differential equation is given as:
\begin{equation}
  \ddot{r} + 6\dot{r} + 8 = u(t)
\end{equation}

\begin{equation}
  \begin{bmatrix}
    \dot{r}\\
    \ddot{r}\\
  \end{bmatrix}
  =
  \begin{bmatrix}
     0 & 1 \\
    -8 &-6 \\
  \end{bmatrix}
  \begin{bmatrix}
    r\\
    \dot{r}\\
  \end{bmatrix}
  +
  \begin{bmatrix}
    0\\
    1\\
  \end{bmatrix}
  u(t)
\end{equation}


# SIRモデル

## 記号の定義

* $S(t)$ : 時点$t$における未感染者数
* $I(t)$ : 時点$t$における感染者数
* $R(t)$ : 時点$t$における感染済かつ回復者数(免疫保持者数)
* $S(t)+I(t)+R(t) \equiv N(t)=N$ :総人口(死亡者数を含め保存されるものとする)
* $\beta$ : 未感染者が感染者と1回の接触で感染する確率
* $\gamma$ : 感染者が1日の内に回復し感染力を失う確率

(※)便宜的に各パラメータを定義する際，時間軸の単位を日としている

## 感染ダイナミクス

時点$t$において，未感染者1人がのべ$j$人と接触したときに，感染から**免れる確率**は以下で与えられる：

$$
\left( 1 - \frac{\beta I(t)}{N} \right)^{j}
$$

ここで，ある1人が1日にのべ$j$人と接触する確率を$P(j)$と定義すると，時点$t$から時点$t+1$の間に増加する感染者数の期待値は次のように書ける：
$$
S(t+1)-S(t) =
S(t) \times
\sum^{\infty}_{j=0}
\left[1-
\left( 1 - \frac{\beta I(t)}{N} \right)^{m}
\right]P(m)
$$

この感染者数の増分期待値および回復率$\gamma$を用いて，SIRダイナミクスは
次の差分方程式で表される：

$$
\begin{align}
  S(t+1) &= S(t) -
  S(t)
  \sum^{\infty}_{j=0}
  \left[1-
  \left( 1 - \frac{\beta I(t)}{N} \right)^{j}
  \right]P(j)
  \\
  I(t+1) &=   I(t) + S(t)
  \sum^{\infty}_{j=0}
  \left[1-
  \left( 1 - \frac{\beta I(t)}{N} \right)^{j}
  \right]P(j)　- \gamma I(t)
  \\
  R(t+1) &= R(t) + \gamma I(t)
\end{align}
$$

ここで，人口に対する各群の比率$\widehat{S}(t) \equiv \dfrac{S(t)}{N}
,\widehat{I}(t) \equiv \dfrac{I(t)}{N}
,\widehat{R}(t) \equiv \dfrac{R(t)}{N}$を導入すれば，先の差分方程式は以下に帰着する：

$$
\begin{align}
  \widehat{S}(t+1) &= \widehat{S}(t) -
  \widehat{S}(t)
  \sum^{\infty}_{j=0}
  \left[1-
  \left( 1 - \beta \widehat{I}(t) \right)^{j}
  \right]P(j)
  \\
  \widehat{I}(t+1) &=   \widehat{I}(t) +
  \widehat{S}(t)
  \sum^{\infty}_{j=0}
  \left[1-
  \left( 1 - \beta \widehat{I}(t) \right)^{j}
  \right]P(j) -  \gamma \widehat{I}(t)
  \\
  \widehat{R}(t+1) &= \widehat{R}(t) + \gamma \widehat{I}(t)
\end{align}
$$

>(※)以下補足：$P(j)$が期待値$\lambda$のポアソン分布$P(j)=\lambda^{j}\mathrm{e}^{-\lambda}/j!$であるとき，時間ステップ長の極限$\Delta t \rightarrow 0$において，次に示す連続時間型SIRモデルと対応する．
>
>$$
\begin{align}
  \dfrac{\mathrm{d} \widehat{S}(t)}{\mathrm{d}t} &= -
  \lambda \beta \widehat{S}(t) \widehat{I}(t)
  \\
  \dfrac{\mathrm{d} \widehat{I}(t)}{\mathrm{d}t} &=
  \lambda \beta \widehat{S}(t) \widehat{I}(t) - \gamma \widehat{I}(t)
  \\
  \dfrac{\mathrm{d} \widehat{R}(t)}{\mathrm{d}t} &=
  \gamma \widehat{I}(t)
\end{align}
$$
>
>
>多くの連続時間モデルでは，$\lambda \beta$を感染率と定義しているケースが多い．このときの感染率は，ウイルス固有の値でなく人々の接触の仕方の情報(ポアソン分布)も含まれていると捉えることもできる．連続モデルと離散モデルの対応の詳細は[瀬野,2011](https://repository.kulib.kyoto-u.ac.jp/dspace/bitstream/2433/171302/1/1757-07.pdf)を参照した．

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

```python
T_ini = 100
S_ini = 1
I_ini = 0
R_ini = 0

class Prameter:
    def __init__(self,N):
        self.N = N
        self.gamma = 0.2
        self.beta = 0.3
        self.m = 5

class Model:
    def __init__(self, prm, S=S_ini, I=I_ini, R=R_ini, T=T_ini):
        self.prm  = prm

        self.S = float(S)
        self.I = float(I)
        self.R = float(R)

        self.T = T

        self.S_list = []
        self.S_list.append(S)

        self.I_list = []
        self.I_list.append(I)

        self.R_list = []
        self.R_list.append(R)

        self.m_list = []

    def solve_NC(self):

        for t in range(self.T):
            m = self.prm.m
            self.newI = (1-(1-self.prm.beta*self.I)**m)*self.S

            self.m_list.append(m)

            New_S = self.S_dynamics()
            New_I = self.I_dynamics()
            New_R = self.R_dynamics()

            self.Update(New_S, New_I, New_R)

    def S_dynamics(self):
        m = self.m_list[-1]
        New_S \
        = self.S \
        - (1-(1-self.prm.beta*self.I)**m)*self.S

        self.S_list.append(New_S)
        return New_S

    def I_dynamics(self):
        m = self.m_list[-1]
        New_I \
        = self.I\
        +(1-(1-self.prm.beta*self.I)**m)*self.S\
        - self.I*self.prm.gamma

        self.I_list.append(New_I)
        return New_I

    def R_dynamics(self):
        New_R \
        = self.R\
        + self.I * self.prm.gamma

        self.R_list.append(New_R)
        return New_R

    def Update(self,New_S, New_I, New_R, ):
        self.S = New_S
        self.I = New_I
        self.R = New_R

```

```python
prm = Prameter(N=1)

prm.gamma = 0.07
prm.beta = 0.1
prm.m =5

model = Model(prm=prm, T=150, I=10**(-3))
model.solve_NC()
```

```python
plt.plot(model.S_list, color='green', label='S(t) :Susceptible')
plt.plot(model.I_list, color='pink', label='I(t) : Infectious')
plt.plot(model.R_list, color='blue', label='R(t) : Recovered')

plt.legend()
```


# CHEM 1000 - Spring 2022
Prof. Geoffrey Hutchison, University of Pittsburgh

## 8 Integrals and Normalizing Functions

Chapter 8 in [*Mathematical Methods for Chemists*](http://sites.bu.edu/straub/mathematical-methods-for-molecular-science/)

By the end of this session, you should be able to:
- Understand how to normalize functions using integrals

### Integrals and Probability

An integral in one dimension computes the area under a curve. If we take an integral over "everything," it's equivalent to adding up all the area.

For example, the [Maxwell-Boltzmann distribution](https://en.wikipedia.org/wiki/Maxwell%E2%80%93Boltzmann_distribution) gives the probability of an ideal gas molecule moving at various velocities as a function of the mass of the molecule and the temperature.

Image from Wikipedia (at 298.15K):

For $^4\mathrm{He}$ the most likely speed is clearly ~1,100 m/s, but a large fraction of gas molecules have velocities slower or faster than that.

If we take:

$$
\int_0^{\infty} f(x) dx = 1
$$

Naturally, the total of *all* probabilities should be 100%.

As a result, sometimes we need to ***normalize*** functions - scale them by a constant to ensure that the total is 100%.

#### Example - Particle in a Box

In quantum mechanics, wavefunctions $\psi$ have no known meaning - they could be complex numbers. On the other hand, the product $\psi^* \psi$ should represent the probability, e.g. of finding an electron at a particular point or region of space.

The "particle in a box" model assumes an electron is confined to a one-dimensional system, for example, a conjugated polymer:

If the length of the box is $L$, the lowest energy wavefunction is:

$$
\psi = N \sin \left(\frac{\pi x}{L}\right)
$$

The notation $\psi^*$ reflects the complex conjugate of the wavefunction. (Remember the complex conjugate?)

So what we need is a constant $N$ to normalize the wavefunction:

$$
\int_{0}^{L} N^2 \sin \left(\frac{\pi x}{L}\right)\sin \left(\frac{ \pi x}{L}\right) d x = 1
$$

That is, we're assuming the electron is *somewhere* inside the box from $0 < x < L$, so the integral should take the area of everything inside the box and add up to 100%.

Let's do this with an [integral table](http://integral-table.com/) but pick your favorite method.

$$
\int \sin ^{2} a x \quad d x=\frac{x}{2}-\frac{\sin 2 a x}{4 a}
$$

Here $a = \pi/L$ and we'll need to remember the **normalization constant** - which we can pull out of the integral:

$$
N^2 \int \sin ^{2} a x \quad d x = N^2 \left[\frac{x}{2} - \frac{\sin 2 \pi x/L}{4 \pi / L}  \right]^L_0 = 1
$$

$$
N^2 \left(\frac{L}{2} - 0 - 0 - 0\right) = 1
$$

So $N = \sqrt{2/L}$

$$
\psi = \sqrt{2/L} \sin \left(\frac{\pi x}{L}\right)
$$

Phew! 😅

Normalizing functions requires some work, but it ensures we can consider probabilities accurately.

The electron must be *somewhere* - and if there's only one, it should only add up to 100%.

### Integration over Multiple Variables

Another good example is the $1s$ orbital for a hydrogen atom. We won't go into how we get the answer, but:
- we know it should be spherically symmetric
- we know it should contain one electron (and only one)

$$
\psi(r, \theta, \phi) = N e^{-r/a_0}
$$

Where $a_0$ is the Bohr radius ${\approx}0.5292 Å$. Notice that for the $1s$ orbital, there's no $\theta$ or $\phi$ component, so the wavefunction is spherically symmetric.

So we need to normalize $\psi$ to find the normalization constant $N$
(in other words, the electron has to be somewhere...)

$$
\int \psi^* \psi dr = 1
$$

(Remember $\psi^*$ is the complex conjugate...)

$$
\int_{0}^{2 \pi} \int_{0}^{\pi} \int_{0}^\infty N^2 e^{-\alpha r} r^{2} dr \; \sin \theta \; d \theta \; d \varphi = 1
$$

Here, $\alpha = 2/a_0$

As a reminder that the $r^{2} dr \; \sin \theta d \theta \; d \varphi$ is the volume element - it's the replacement for $dx \: dy \: dz$. We need this each and *every* time we integrate in spherical coordinates.

```python
from sympy import init_session
init_session()
```

```python
a0, r, theta, phi = symbols('a0 r theta phi')
f = exp(-2*r/a0)

# let's integrate "away" the angular parts
integrate(sin(theta), (theta, 0, pi), (phi, 0, 2*pi))
```

```python
# 4*pi makes sense - it's the surface area of a sphere
integrate(4*pi * f * r**2, (r, 0, oo))
```

```python
# so then:
N = sqrt(1 / (pi * a0**3))
simplify(N)
```

We usually write the normalization constant as:

$$
\frac{1}{\sqrt{\pi}}\frac{1}{a_0^{3/2}}
$$

So the full, normalized hydrogen 1s wavefunction is:

$$
\psi(r, \theta, \phi) = \frac{1}{\sqrt{\pi}}\frac{1}{a_0^{3/2}} e^{-r/a_0}
$$

$$
\int_{0}^{2 \pi} \int_{0}^{\pi} \int_{0}^{V} v^{2} \sin \theta d v d\theta d\phi
$$

-------
This notebook is from Prof. Geoffrey Hutchison, University of Pittsburgh
https://github.com/ghutchis/chem1000


Elasticity solution for wedge-shaped problems
======================================

In this notebook an analytic solution for a wedge is constructed. Later on, we couple two wedge solutions to compute the displacement for a dipping fault with free surface.

Wedge solution
----------------------

Polar coordinates are given by
\begin{align}
    x &= r\cos(\theta) \\
    y &= r\sin(\theta) \\
    r &= \sqrt{x^2+y^2} \\
    \theta &= \arctan(y/x) \\
\end{align}

For a plane-strain problem, we assume there exists a biharmonic function $\phi$ such that [1]

\begin{align}
    2\mu u_r &= -\frac{\partial \phi}{\partial r} + (1-\nu)r\frac{\partial\psi}{\partial \theta} \\
    2\mu u_\theta &= -\frac{1}{r}\frac{\partial \phi}{\partial \theta} + (1-\nu)r^2\frac{\partial\psi}{\partial r} \\
    \sigma_{rr} &= \frac{1}{r}\frac{\partial \phi}{\partial r} +
    \frac{1}{r^2}\frac{\partial^2 \phi}{\partial \theta^2} \\
    \sigma_{\theta\theta} &= \frac{\partial^2 \phi}{\partial r^2} \\
    \sigma_{r\theta} &= -\frac{\partial }{\partial r} \left(
      \frac{1}{r}\frac{\partial \phi}{\partial\theta}
    \right) \\
\end{align}
where $\mu$ is the shear modulus, $\nu$ is Poisson's ratio, and $\phi$ and $\psi$ are related by
$$
\nabla^2\phi = \frac{\partial}{\partial r}\left(r\frac{\partial\psi}{\partial\theta}\right)
$$

Remark: Plane-stress is modelled in [1], therefore the formula are adjusted here for plane-strain.

[1] "Stress Singularites Resulting From Various Boundary Conditions in Angular Corners of Plates in Extension", M. L. Williams, Journal of Applied Mechanics, 19 (4), pp. 526-528.

```python
from sympy import *
from sympy.plotting.plot import plot3d
init_printing()
%matplotlib notebook
```

Assume

$$
\phi = r^{\lambda+1} \left(A_1\sin((\lambda+1)\theta) + A_2\cos((\lambda+1)\theta) + A_3\sin((\lambda-1)\theta) + A_4\sin((\lambda-1)\theta)\right)
$$
and
$$
\psi = r^{\lambda-1}\left(-\frac{4A_3}{\lambda-1}\cos((\lambda-1)t) + \frac{4A_4}{\lambda-1}\sin((\lambda-1)t)\right)
$$

```python
r, t, x, y, lam = symbols('r theta x y lamda')
mu, nu, m, A0, A1, A2, A3, A4, S = symbols('mu nu m A_0 A_1 A_2 A_3 A_4 S')

phi = r**(lam+1) * (A1 * sin((lam+1)*t) + A2*cos((lam+1)*t) + A3*sin((lam-1)*t) + A4*cos((lam-1)*t))
psi = r**(lam-1) * (-4*A3/(lam-1)*cos((lam-1)*t) + 4*A4/(lam-1)*sin((lam-1)*t))

phi2 = phi.subs(lam, 2)
psi2 = psi.subs(lam, 2)
(phi2, psi2)
```

$\psi$ must be harmonic, i.e. $\nabla^2\psi=0$, and $\phi$ must be biharmonic, i.e. $\nabla^2(\nabla^2\phi=0)$:

```python
def laplace(f):
    return f.diff(r, 2) + f.diff(r)/r + f.diff(t, 2)/r**2
(laplace(psi).simplify(), laplace(laplace(phi)).simplify())
```

The relation of $\phi$ and $\psi$ must be satisfied:

```python
((r*psi.diff(t)).diff(r) - laplace(phi)).simplify()
```

Define displacement and stress:

```python
def quants(phi, psi):
    ur = (-phi.diff(r) + (1-nu)*r*psi.diff(t)) / (2 * mu)
    ut = (-phi.diff(t)/r + (1-nu)*r**2*psi.diff(r)) / (2 * mu)
    srr = (phi.diff(r) / r + phi.diff(t, 2) / r**2)
    stt = phi.diff(r, 2)
    srt = -(phi.diff(t)/r).diff(r)
    return (ur.simplify(), ut.simplify(), srr.simplify(), stt.simplify(), srt.simplify())
ur, ut, srr, stt, srt = quants(phi, psi)
```

Check that stress-displacement relation is satisfied:

```python
err = ur.diff(r)
ett = ut.diff(t)/r + ur/r
ert = (ur.diff(t)/r + ut.diff(r) - ut/r) / 2
((err - ((1-nu)*srr - nu*stt)/(2*mu)).simplify(),
 (ett - ((1-nu)*stt - nu*srr)/(2*mu)).simplify(),
 (ert - srt/(2*mu)).simplify())
```

The planes $\theta=0$ and $\theta=-\pi/3$ form an infinite wedge.
We set $\lambda=2$ and recover the coefficients $A_1,A_2,A_3,A_4$ which satisfy
\begin{align}
    u_\theta(r,-\pi/3) &= 0 \\
    u_r(r,-\pi/3) &= Sr^2 \\
    \sigma_{r\theta}(r,0) &= 0 \\
    \sigma_{\theta\theta}(r,0) &= 0
\end{align}

```python
w1 = 0
w2 = -pi/3
mylam = 2
solution = linsolve([ut.subs(lam, mylam).subs(t, w2)/r**2,
                     ur.subs(lam, mylam).subs(t, w2)/r**2 - S,
                     srt.subs(lam, mylam).subs(t, w1)/r,
                     stt.subs(lam, mylam).subs(t, w1)/r],
         A1, A2, A3, A4)
a1, a2, a3, a4 = solution.args[0]
a1, a2, a3, a4 = a1.simplify(), a2.simplify(), a3.simplify(), a4.simplify()
(a1, a2, a3, a4)
```

We check that the boundary conditions are satisfied:

```python
srr_s = srr.subs(lam, mylam).subs(A1, a1).subs(A2, a2).subs(A3, a3).subs(A4, a4).simplify()
stt_s = stt.subs(lam, mylam).subs(A1, a1).subs(A2, a2).subs(A3, a3).subs(A4, a4).simplify()
srt_s = srt.subs(lam, mylam).subs(A1, a1).subs(A2, a2).subs(A3, a3).subs(A4, a4).simplify()
(stt_s.subs(t, w1), srt_s.subs(t, w1))
```

```python
ur_s = ur.subs(lam, mylam).subs(A1, a1).subs(A2, a2).subs(A3, a3).subs(A4, a4).simplify()
ut_s = ut.subs(lam, mylam).subs(A1, a1).subs(A2, a2).subs(A3, a3).subs(A4, a4).simplify()
(ur_s.subs(t, w2).simplify(), ut_s.subs(t, w2).simplify())
```

We convert the solution into Lua code:

```python
def lua_code(expr):
    return rcode(expr).replace('cos', 'math.cos').replace('sin', 'math.sin').replace('sqrt', 'math.sqrt').replace('pi', 'math.pi')
print("local A_1 =", lua_code(a1))
print("local A_2 =", lua_code(a2))
print("local A_3 =", lua_code(a3))
print("local A_4 =", lua_code(a4))
print("local ur =", lua_code(ur.subs(lam, mylam).simplify()))
print("local ut =", lua_code(ut.subs(lam, mylam).simplify()))
```

    local A_1 = (1.0/3.0)*math.sqrt(3)*S*mu*(4*nu - 5)/(-16*nu^2 + 20*nu + 5)
    local A_2 = S*mu*(7 - 4*nu)/(-16*nu^2 + 20*nu + 5)
    local A_3 = math.sqrt(3)*S*mu*(5 - 4*nu)/(-16*nu^2 + 20*nu + 5)
    local A_4 = S*mu*(4*nu - 7)/(-16*nu^2 + 20*nu + 5)
    local ur = (1.0/2.0)*r^2*(-3*A_1*math.sin(3*theta) - 3*A_2*math.cos(3*theta) - 4*A_3*nu*math.sin(theta) + A_3*math.sin(theta) - 4*A_4*nu*math.cos(theta) + A_4*math.cos(theta))/mu
    local ut = (1.0/2.0)*r^2*(-3*A_1*math.cos(3*theta) + 3*A_2*math.sin(3*theta) - A_3*math.cos(theta) + A_4*math.sin(theta) + (4*nu - 4)*(A_3*math.cos(theta) - A_4*math.sin(theta)))/mu

Dipping fault
------------------

We now couple two wedge solutions to impose a displacement in a dipping fault with a free-surface condition, as illustrated in the following figure:

where $[\![ u ]\!] = u_A - u_B$. The A-side and B-side of the fault are separated by the fault plane $\theta=\omega$. Also note that the origin, marked with a circle, is at the intesection of the fault and the free surface.

We introduce potentials for the A-side and the B-side of the fault:

```python
B1, B2, B3, B4 = symbols('B_1 B_2 B_3 B_4')

phiA = phi.simplify()
psiA = psi.simplify()
phiB = phiA.subs(A1, B1).subs(A2, B2).subs(A3, B3).subs(A4, B4).simplify()
psiB = psiA.subs(A1, B1).subs(A2, B2).subs(A3, B3).subs(A4, B4).simplify()
(phiA, psiA, phiB, psiB)
```

```python
urA, utA, srrA, sttA, srtA = quants(phiA, psiA)
urB, utB, srrB, sttB, srtB = quants(phiB, psiB)
```

We remove the dependency on $r$ here to solve for the A and B coefficients:

```python
sttA_e = (sttA*r**(1-lam)/lam).simplify()
srtA_e = (srtA*r**(1-lam)/lam).simplify()
sttB_e = (sttB*r**(1-lam)/lam).simplify()
srtB_e = (srtB*r**(1-lam)/lam).simplify()
(sttA_e, srtA_e, sttB_e, srtB_e)
```

```python
def simp_A(expr):
    return factor(expr, *parsA).simplify()
def simp_B(expr):
    return factor(expr, *parsB).simplify()

parsA = (A1, A2, A3, A4)
urA_e = simp_A(urA*r**(-lam)*2*mu)
utA_e = simp_A(utA*r**(-lam)*2*mu)
parsB = (B1, B2, B3, B4)
urB_e = simp_B(urB*r**(-lam)*2*mu)
utB_e = simp_B(utB*r**(-lam)*2*mu)
(urA_e, utA_e, urB_e, utB_e)
```

We first compute coefficients which ensure the free-surface boundary condition. These calculations are independent for the A-side and B-side. Note that the A-wedge is given by planes $\theta=\pi$ and $\theta=\omega$, and the B-wedge is given by planes $\theta=0$ and $\theta=\omega$.

```python
part_solution_A = solve([sttA_e.subs(t, pi),
                         srtA_e.subs(t, pi)],
                         *parsA)
part_solution_A = {k: v.simplify() for k, v in part_solution_A.items()}
part_solution_B = solve([sttB_e.subs(t, 0),
                         srtB_e.subs(t, 0)],
                         *parsB)
part_solution_B = {k: v.simplify() for k, v in part_solution_B.items()}
(part_solution_A, part_solution_B)
```

We can now express $A_1,A_2,B_1,B_2$ in terms of $A_3,A_4,B_3,B_3$, which we substitute in the following:

```python
sttA_e2 = simp_A(sttA_e.subs(part_solution_A.items()))
srtA_e2 = simp_A(srtA_e.subs(part_solution_A.items()))
sttB_e2 = simp_B(sttB_e.subs(part_solution_B.items()))
srtB_e2 = simp_B(srtB_e.subs(part_solution_B.items()))
(sttA_e2, srtA_e2, sttB_e2, srtB_e2)
```

```python
utA_e2 = simp_A(utA_e.subs(part_solution_A.items()))
urA_e2 = simp_A(urA_e.subs(part_solution_A.items()))
urB_e2 = simp_B(urB_e.subs(part_solution_B.items()))
utB_e2 = simp_B(utB_e.subs(part_solution_B.items()))
(urA_e2, utA_e2, urB_e2, utB_e2)
```

We now find the remaining coefficients for the imposed displacement on the fault plane $\omega = -\pi/3$. Note that we require the stress tensor to be continuous.

```python
w = -pi/3
equations = [(sttA_e2 - sttB_e2).subs(t, w).simplify(),
             (srtA_e2 - srtB_e2).subs(t, w).simplify(),
             (utA_e2 - utB_e2).subs(t, w).simplify(),
             (urA_e2 - urB_e2 - S).subs(t, w).simplify()]
equations
```

```python
part_equations_0 = [factor(equations[0] + equations[3], *parsA, *parsB).simplify(),
                    factor(equations[1] - equations[2], *parsA, *parsB).simplify()]
part_equations_0
```

```python
part_solution_0 = solve(part_equations_0, A3, A4, B3, B4)
part_solution_0 = {k: v.simplify() for k, v in part_solution_0.items()}
part_solution_0
```

```python
part_equations_1 = [factor(equations[0].subs(part_solution_0.items()).simplify(), *parsB).simplify(),
                    factor(equations[1].subs(part_solution_0.items()).simplify(), *parsB).simplify()]
```

```python
solution = solve(part_equations_1, B3, B4)
solution = {k: v.simplify() for k, v in solution.items()}
solution
```

Note that $\sin(\pi\lambda)$ appears in the denominator above.
Thus, $\lambda$ must not be an integer.

We substitute the parameters into the quantities for $\lambda = 0.5$:

```python
mylam = 0.5
def subs_solution_A(expr):
    return expr.subs(part_solution_A.items()).subs(part_solution_0.items()).subs(solution.items()).subs(lam, mylam)
def subs_solution_B(expr):
    return expr.subs(part_solution_B.items()).subs(part_solution_0.items()).subs(solution.items()).subs(lam, mylam)
```

```python
srrA_s = subs_solution_A(srrA).simplify()
sttA_s = subs_solution_A(sttA).simplify()
srtA_s = subs_solution_A(srtA).simplify()
urA_s = subs_solution_A(urA).simplify()
utA_s = subs_solution_A(utA).simplify()

srrB_s = subs_solution_B(srrB).simplify()
sttB_s = subs_solution_B(sttB).simplify()
srtB_s = subs_solution_B(srtB).simplify()
urB_s = subs_solution_B(urB).simplify()
utB_s = subs_solution_B(utB).simplify()
```

In the following we test whether the boundary conditions are satisifed.

```python
(sttA_s.subs(t, pi).simplify(),
 srtA_s.subs(t, pi).simplify(),
 sttB_s.subs(t, 0).simplify(),
 srtB_s.subs(t, 0).simplify())
```

```python
((sttA_s.subs(t, w)-sttB_s.subs(t, w)).simplify().evalf(),
 (srtA_s.subs(t, w)-srtB_s.subs(t, w)).simplify().evalf(),
 (utA_s.subs(t, w)-utB_s.subs(t, w)).simplify().evalf())
```

```python
((urA_s.subs(t, w) - urB_s.subs(t, w) - S).simplify() + S).evalf().simplify()
```

Lastly, we generate Lua code for the A-side and the B-side:

```python
def lua_code_cse(var_expr_pairs):
    vs, es = zip(*var_expr_pairs)
    e_cse = cse(es)
    for k, v in e_cse[0]:
        print("local {} = {}".format(k, lua_code(v)))
    for k, v in zip(vs, e_cse[1]):
        print("local {} = {}".format(k, lua_code(v)))
```

```python
lua_code_cse(solution.items())
lua_code_cse(part_solution_0.items())
lua_code_cse(part_solution_A.items())
lua_code_cse([('urA', urA), ('utA', utA)])
```

    local x0 = math.pi*((1.0/3.0)*lamda + 1.0/3.0)
    local x1 = math.sin(x0)
    local x2 = 2*x1
    local x3 = math.pi*((7.0/3.0)*lamda + 1.0/3.0)
    local x4 = math.sin(x3)
    local x5 = 2*x4
    local x6 = 3*lamda
    local x7 = math.cos(x0)
    local x8 = math.sqrt(3)*lamda
    local x9 = math.cos(x3)
    local x10 = math.pi*lamda
    local x11 = (8.0/3.0)*x10
    local x12 = math.sin(x11)
    local x13 = 2*x7
    local x14 = 2*x9
    local x15 = math.cos(x11)
    local x16 = (1.0/32.0)*S/((nu - 1)*math.sin(x10)^2)
    local B_3 = x16*(x1*x6 + x12*x13 - x12*x14 - x15*x2 + x15*x5 + x2 - x4*x6 - x5 + x7*x8 - x8*x9)
    local B_4 = x16*(-x1*x8 - x12*x2 + x12*x5 - x13*x15 + x13 + x14*x15 - x14 + x4*x8 + x6*x7 - x6*x9)
    local x0 = nu - 1
    local x1 = 1.0/x0
    local x2 = math.pi*((1.0/3.0)*lamda + 1.0/6.0)
    local x3 = (1.0/4.0)*S
    local A_3 = x1*(B_3*x0 - x3*math.cos(x2))
    local A_4 = x1*(B_4*x0 - x3*math.sin(x2))
    local x0 = 1.0/(lamda + 1)
    local x1 = 2*math.pi*lamda
    local x2 = math.cos(x1)
    local x3 = math.sin(x1)
    local A_1 = x0*(-A_3*lamda + A_3*x2 - A_4*x3)
    local A_2 = -x0*(A_3*x3 + A_4*lamda + A_4*x2)
    local x0 = lamda - 1
    local x1 = theta*x0
    local x2 = math.sin(x1)
    local x3 = math.cos(x1)
    local x4 = A_3*x2 + A_4*x3
    local x5 = lamda + 1
    local x6 = theta*x5
    local x7 = math.sin(x6)
    local x8 = math.cos(x6)
    local x9 = (1.0/2.0)*r^lamda/mu
    local x10 = A_4*x2
    local x11 = A_3*x3
    local urA = x9*(4*x4*(1 - nu) - x5*(A_1*x7 + A_2*x8 + x4))
    local utA = x9*(-A_1*x5*x8 + A_2*x5*x7 + x0*x10 - x0*x11 + (4*nu - 4)*(-x10 + x11))

```python
lua_code_cse(solution.items())
lua_code_cse(part_solution_0.items())
lua_code_cse(part_solution_B.items())
lua_code_cse([('urB', urB), ('utB', utB)])
```

    local x0 = math.pi*((1.0/3.0)*lamda + 1.0/3.0)
    local x1 = math.sin(x0)
    local x2 = 2*x1
    local x3 = math.pi*((7.0/3.0)*lamda + 1.0/3.0)
    local x4 = math.sin(x3)
    local x5 = 2*x4
    local x6 = 3*lamda
    local x7 = math.cos(x0)
    local x8 = math.sqrt(3)*lamda
    local x9 = math.cos(x3)
    local x10 = math.pi*lamda
    local x11 = (8.0/3.0)*x10
    local x12 = math.sin(x11)
    local x13 = 2*x7
    local x14 = 2*x9
    local x15 = math.cos(x11)
    local x16 = (1.0/32.0)*S/((nu - 1)*math.sin(x10)^2)
    local B_3 = x16*(x1*x6 + x12*x13 - x12*x14 - x15*x2 + x15*x5 + x2 - x4*x6 - x5 + x7*x8 - x8*x9)
    local B_4 = x16*(-x1*x8 - x12*x2 + x12*x5 - x13*x15 + x13 + x14*x15 - x14 + x4*x8 + x6*x7 - x6*x9)
    local x0 = nu - 1
    local x1 = 1.0/x0
    local x2 = math.pi*((1.0/3.0)*lamda + 1.0/6.0)
    local x3 = (1.0/4.0)*S
    local A_3 = x1*(B_3*x0 - x3*math.cos(x2))
    local A_4 = x1*(B_4*x0 - x3*math.sin(x2))
    local B_2 = -B_4
    local B_1 = B_3*(1 - lamda)/(lamda + 1)
    local x0 = lamda - 1
    local x1 = theta*x0
    local x2 = math.sin(x1)
    local x3 = math.cos(x1)
    local x4 = B_3*x2 + B_4*x3
    local x5 = lamda + 1
    local x6 = theta*x5
    local x7 = math.sin(x6)
    local x8 = math.cos(x6)
    local x9 = (1.0/2.0)*r^lamda/mu
    local x10 = B_4*x2
    local x11 = B_3*x3
    local urB = x9*(4*x4*(1 - nu) - x5*(B_1*x7 + B_2*x8 + x4))
    local utB = x9*(-B_1*x5*x8 + B_2*x5*x7 + x0*x10 - x0*x11 + (4*nu - 4)*(-x10 + x11))

```python

```


# Introduction to Models and Reachability in Kaa

We first import the necessary modules and classes from Kaa. To start defining models, we at least need the sympy and numpy packages alongside the Model and Bundle classes.

```python
import sympy as sp
import numpy as np

from kaa.bundle import Bundle
from kaa.model import Model
```

# Basic Parallelotope

A model file in Kaa consists of first defining the variables and the dynamics. Kaa is designed for the analysis of **polynomial discrete dynamics**. The following model definition illustates how to define the unitbox shifting diagonally up to the right.

```python
class Basic(Model):

    def __init__(self):

        x,y = sp.Symbol('x'), sp.Symbol('y')

        dx = x + 1
        dy = y + 1

        dyns  = [dx, dy]
        vars = [x, y]

        L = np.empty([2,2])
        T = np.empty(2)

        L[0] = [1, 0]
        L[1] = [0, 1]

        T[0][0] = 0
        T[0][1] = 1

        offu = np.empty(2)
        offl = np.empty(2)

        offu[0] = 1
        offu[1] = 1

        offl[0] = 1
        offl[1] = 1

        b = Bundle(T, L, offu, offl, vars)

        super().__init__(b, dyns, vars)

```

The numpy matrix $L$ represents the directions matrix of the model. Each row of the matrix reprsents a normal vector to the plane used to define the positive halfspace i.e the $i^{th}$ row of $L$ defines the halfspaces $L_i{\bf x} \leq \text{offu}_i$ and $-L_i{\bf x} \leq \text{offl}_i$ where $\text{offu}$ is the vector of offsets for the upper facets (corresponding to the $L_i$) and $\text{offl}$ is the vector of offsets for the lower facets (corresponding to the $-L_i$).

The matrix $T$ represents the template matrix defining the parallelotope bundle of the model. Each row $T_i$ represents a vector of row-indices in $L$ defining parallelotope $i$ in the bundle. This defines $n$ hyperplanes and their parallel counterparts from the directions matrix. In other words, element $T_{ij}$ indicates that the postive and negative halfspaces defined by normal vector $L_i, -L_i$ with offsets in $\text{offu}_i$ and $\text{offl}_i$ respectively is contained in parallelotope $j$. The intersection of the positive and negative halfspaces will be the parallelotope in question. Note that the number of columns of $T$ must exactly be the dimension of the system.

Thus, observe that definitions of $L$, $T$, $\text{offu}$. and $\text{offl}$ designate the initial set as the **unit box** in $\mathbb{R}^2$. To initialize the reachability computation, we must first create an initial bundle denoted by the variable $b$ in this case. The bundle is a data structure which contains the directions matrix $L$ along with the upper and lower offsets $\text{offu}$, $\text{offl}$ and the template matrix $T$. It additionally encapsulates methods describing the parallelotope bundle in terms of linear constraints (half-spaces).

In this case, there is only one row in the template matrix. Examples below will leverage multiple parallelotopes to generate tighter over-approximations.

We now construct a Reach object and a FlowPipePlotter object to calculate the reachable set and plot the projection to the $x$-axis for 150 steps.

First, we import the necessary modules and classes as usual:

```python
from kaa.reach import ReachSet
from kaa.flowpipe import FlowPipePlotter
```

kaa.reach.ReachSet is the object responsible for computing the reachale set for a supplied number of time steps. The constructor takes in a Model object defining the dynamics and the initial bundle. Calling **ReachSet.computeReachSet(num)** will calculate the reachable set for $num$ time steps. To plot the projection on the $x$-axis, we first create a **FlowPipePlotter** object supplied with the **Flowpipe** returned by **ReachSet.computeReachSet(num)** and feed it into **FlowPipePlotter.plot2DProj(var)** where $var$ is the index of the desired variable. The relevant code is posted below:

```python
basic_model = Basic()
basic_reach = ReachSet(basic_model)
basic_flowpipe = basic_reach.computeReachSet(150)
FlowPipePlotter(basic_flowpipe).plot2DProj(0)
```

We can also plot the phase diagram for this model as follows. This should just be the unit box shifting along the positive diagonal for 150 steps.

## Phase Plot for Basic (x,y)

```python
basic_model = basic.Basic()
basic_reach = ReachSet(basic_model)
FlowPipePlotter(basic_reach.computeReachSet(150)).plot2DPhase(0,1)
```

# SIR Epidemic Model

We now investigate a slightly more complicated model, the SIR epidemic model. Following the format from the example above, we define the following model:

```python
class SIR(Model):

  def __init__(self):

      s, i, r = sp.Symbol('s'), sp.Symbol('i'), sp.Symbol('r')

      ds = s - (0.34*s*i)*0.05;
      di = i + (0.34*s*i - 0.05*i)*0.05;
      dr = r + 0.05*i*0.05;

      dyns = [ds, di, dr]
      vars = [s, i, r]
      sys_dim = len(vars)

      num_direct = 3
      num_temps = 1

      L = np.zeros([num_direct, sys_dim])
      T = np.zeros([num_temps, sys_dim])

      # Directions matrix
      L[0][0] = 1  #[1 0 0 ]^T
      L[1][1] = 1  #[0 1 0 ]^T
      L[2][2] = 1  #[0 0 1 ]^T

      # Template matrix
      T[0][0] = 0
      T[0][1] = 1
      T[0][2] = 2

      offu = np.zeros(num_direct)
      offl = np.zeros(num_direct)

      offu[0] = 0.8
      offl[0] = -0.79

      offu[1] = 0.2
      offl[1] = -0.19

      offu[2] = 0.001
      offl[2] = -0.00099

      b = Bundle(T, L, offu, offl, vars)
      super().__init__(b, dyns, vars)

```

The initial set is taken from Dreossi et. al. Note that we only use one parallelotope here as $T$ only has one row. Furthermore, this parallelotope uses the axis-aligned unit vectors as its directions. Based on the definitions of $\text{offu}$ and $\text{offl}$, observe that initial set will be:
$$ s \in [0.79, 0.8] \quad i \in [0.19, 0.2]  \quad r = 0 $$

We can now calculate the flowpipe for 150 steps, and plot the resulting projections and phase plots.

## SIR Model projection to r

```python
sir_model = SIR()
sir_reach = ReachSet(sir_model)
sir_flow = sir_reach.computeReachSet(150)
FlowPipePlotter(sir_flow).plot2DProj(2)
```

## SIR Model projection to i

```python
FlowPipePlotter(sir_flow).plot2DProj(1)
```

## Phase Plot for SIR (S,I)

```python
FlowPipePlotter(flowpipe).plot2DPhase(0,1)
```

## Phase Plot for SIR (I,R)

```python
FlowPipePlotter(flowpipe).plot2DPhase(1,2)
```

# Rossler Model

We finally introduce the Rossler model with parameters set by Dreossi et. al as follows:

```python
class Rossler(Model):

    def __init__(self):

        x, y, z = sp.Symbol('x'), sp.Symbol('y'), sp.Symbol('z')
        vars = [x, y, z]

        dim_sys = len(vars)

        dx = x + (-y-z)*0.025
        dy = y + (x + 0.1*y)*0.025
        dz = z + (0.1 + z*(x-14))*0.02

        dyns = [dx, dy ,dz]

        num_direct = 5
        num_temps = 3

        L = np.zeros([num_direct, dim_sys])
        T = np.zeros([num_temps, dim_sys])

        L[0][0] = 1
        L[1][1] = 1
        L[2][2] = 1

        L[3][0] = 1
        L[3][1] = 0.5

        L[4][0] = 0.5
        L[4][2] = 0.5

        T[0][0] = 0; T[0][1] = 1; T[0][2] = 2;
        T[1][0] = 1; T[1][1] = 2; T[1][2] = 3;
        T[2][0] = 2; T[2][1] = 3; T[2][2] = 4;

        offu = np.zeros(num_direct)
        offl = np.zeros(num_direct)

        offu[0] = 0.1; offl[0] = -0.09;
        offu[1] = 5; offl[1] = -4.99;
        offu[2] = 0.1; offl[2] = -0.09;
        offu[3] = 10; offl[3] = 0;
        offu[4] = 10; offl[4] = 0;

        b = Bundle(T, L, offu, offl, vars)

        super().__init__(b, dyns, vars)
```

## Rossler Model projection to x

```python
rossler_model = rossler.Rossler()
rossler_reach = ReachSet(rossler_model)
rossler_flow = rossler_reach.computeReachSet(150)
FlowPipePlotter(rossler_flow).plot2DProj(0)
```

## Rossler Model projection to z

```python
FlowPipePlotter(rossler_flow).plot2DProj(1)
```

Kaa also supports phase plotting. Run the examples below to see the phase plots for the Rossler model.

## Phase Plot for Rossler (x,y)

```python
FlowPipePlotter(rossler_flow).plot2DPhase(0,1)
```

## Phase Plot for Rossler (y,z)

```python
FlowPipePlotter(rossler_flow).plot2DPhase(0,2)
```

# Quadcopter model

The Quadcopter model is a system of 17 dimensions modeling the dynamics of a quadcopter. We define its dynamics below:

```python
class Quadcopter(Model):

    def __init__(self):

           dim_sys = 17
           pn, pe, h, u, v, w, q0v, q1v = sp.Symbol("pn"), sp.Symbol("pe"), sp.Symbol("h"), sp.Symbol("u"), sp.Symbol("v"), sp.Symbol("w"), sp.Symbol("q0v"), sp.Symbol("q1v")
           q2v, q3v, p, q, r, hI, uI, vI, psiI = sp.Symbol("q2v"), sp.Symbol("q3v"), sp.Symbol("p"),sp.Symbol("q"),sp.Symbol("r"), sp.Symbol("hI"), sp.Symbol("uI"), sp.Symbol("vI"), sp.Symbol("psiI")

           vars = [pn, pe, h, u, v, w, q0v, q1v, q2v, q3v, p, q, r, hI, uI, vI, psiI]

           M = 0.0015;
           mr = 0.001;
           R = 0.020;
           l = 0.045;
           g = 9.81;
           m = M + 4*mr;
           Jx = (2*M*R**2)/5 + (2*l**2)*mr;
           Jy = (2*M*R**2)/5 + (2*l**2)*mr;
           Jz = (2*M*R**2)/5 + (4*l**2)*mr;

           ur = 0;
           vr = 0;
           psir = 0;
           hr = 1;

           phi = 2*q1v;
           theta = 2*q2v;
           psi = 2*q3v;

           delta = 0.01;

           F = 0.0361*hI + 0.0694*h + 0.0603*w;
           tauphi = -0.0003*vI - 0.0005*v - 0.0018*phi - 0.0004*p;
           tautheta = 0.0003*uI + 0.0005*u - 0.0018*theta - 0.0004*q;
           taupsi = -0.0003*psiI - 0.0006*psi - 0.0003*r;

           dpn = pn + (u*(2*q0v**2 + 2*q1v**2 - 1) - v*(2*q0v*q3v - 2*q1v*q2v ) + w*(2*q0v*q2v + 2*q1v*q3v ))*delta;
           dpe = pe + (v*(2*q0v**2 + 2*(q2v**2) - 1) + u*(2*q0v*q3v + 2*q1v*q2v ) - w*(2*q0v*q1v - 2*q2v*q3v ))*delta;
           dh = h + (w*(2*(q0v**2) + 2*(q3v**2) - 1) - u*(2*q0v*q2v - 2*q1v*q3v ) + v*(2*q0v*q1v + 2*q2v*q3v ))*delta;

           du = u + (r*v - q*w - g*(2*q0v*q2v - 2*q1v*q3v ))*delta;
           dv = v + (p*w - r*u + g*(2*q0v*q1v + 2*q2v*q3v ))*delta;
           dw = w + (q*u - p*v - F/m + g*(2*(q0v**2) + 2*(q3v**2) - 1 ))*delta;

           dq0v = q0v +(-(q1v/2)*p - (q2v/2)*q - (q3v/2)*r)*delta;
           dq1v = q1v + ((q0v/2)*p - (q3v/2)*q + (q2v/2)*r)*delta;
           dq2v = q2v + ((q3v/2)*p + (q0v/2)*q - (q1v/2)*r)*delta;
           dq3v = q3v + ((q1v/2)*q - (q2v/2)*p + (q0v/2)*r)*delta;

           dp = p + ((1/Jx)*tauphi + ((Jy - Jz)/Jx)*q*r)*delta;
           dq = q + ((1/Jy)*tautheta - ((Jx - Jz)/Jy)*p*r)*delta;
           dr = r + ((1/Jz)*taupsi + ((Jx - Jy)/Jz)*p*q)*delta;

           dhI = hI + (h - hr)*delta;
           duI = uI +(u - ur)*delta;
           dvI = vI + (v - vr)*delta;
           dpsiI = psiI + (psi - psir)*delta;

           dyns = [dpn,dpe,dh,du,dv,dw,dq0v,dq1v,dq2v,dq3v,dp,dq,dr,dhI,duI,dvI,dpsiI]

           num_dirs = 18
           num_temp = 2

           L = np.zeros([num_dirs,dim_sys])

           for i in range(dim_sys):
               L[i][i] = 1

           L[17][2] = 0.5; L[17][5] = 0.5; L[17][6] = 0.5; L[17][15] = 0.25;

           T = np.zeros([num_temp, dim_sys]);
           for i in range(dim_sys):
               T[0][i] = i
               T[1][i] = i

           T[1][5] = 17

           offu = np.zeros(num_dirs);
           offl = np.zeros(num_dirs);

           offu[2] = 0.21; offl[2] = -0.20;
           offu[6] = 1; offl[6] = -1;
           offu[17] = 100; offl[17] = 100;

           b = Bundle(T, L, offu, offl, vars)
           super().__init__(b, dyns, vars)
```

We now run the model. Note that this is an example where multiple parallelotopes increase the quality of the over-approximation.

As of writing this, the model takes a while to compute.

```python
model = Quadcopter()
mod_reach = ReachSet(model)
mod_flow = mod_reach.computeReachSet(300)

FlowPipePlotter(mod_flow).plot2DProj(2,5,13)
```

There are other models defined in the **/models** directory. Basic tests are contained in the **/tests** directory. The reader is encouraged to experiment with the values found in these files to learn about reachability with multiple parallelotopes.

To have a more in-depth look at the theoretical foundations of these techniques, check out the following papers:

[Dang, T., Dreossi, T., Piazza, C.: Parameter synthesis using parallelotopic enclosure and appli- cations to epidemic models](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.707.7012&rep=rep1&type=pdf)

[Dreossi, T., Dang, T., Piazza, C.: Parallelotope bundles for polynomial reachability.](https://dl.acm.org/doi/abs/10.1145/2883817.2883838)

[Dang, T., Testylier, R.: Reachability analysis for polynomial dynamical systems using the bern- stein expansion.](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.300.4012&rep=rep1&type=pdf)