```python
!pip install box2d-py
```

# REINFORCE with a Learned Baseline

In this tutorial we will train an agent to play `LunarLander-v2` using REINFORCE with a learned baseline.

## Introduction

Previously we implemented the REINFORCE algorithm and discussed how introducing a baseline reduces variance and improves performance. We noted that the value function $v$ would be an ideal baseline (if we knew it) and used the average return as a rough approximation. In this tutorial we will try to learn the value function so that we can use it as a baseline in REINFORCE. Like the policy, we will parametrise the value function using a simple neural network with parameters $\omega$. Recall that the policy gradient theorem gives us
\begin{align}
\nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim p_\theta}\left[\sum_{t=0}^T \nabla_\theta \log \pi_\theta(a_t | s_t) \left(G_t - v_\omega(s_t) \right) \right], \, \text{ where } \,
G_t = \sum_{t'=t}^T \gamma^{t' - t} r(s_{t'}, a_{t'}).
\end{align}
Here we have used our parametrised value function $v_\omega$ as the baseline. **But how do we learn $v_\omega$?**. Well, we can use monte-carlo learning! Since we can calculate the discounted returns $G_t$ we can just minimize $\frac{1}{2}\sum_{t=0}^T|G_t - v_\omega(s_t)|^2$.

**REINFORCE with a Learned Baseline**:
1. sample a trajectory $\tau = (s_0, a_0, r_1, s_1, \ldots, s_{T}, r_{T})$ using the policy $\pi_\theta$.
2. compute the vector of returns $[G_0, G_1, \ldots, G_T]$.
3. compute the policy gradient $\nabla_\theta J(\theta) \approx \sum_{t=0}^T \nabla_\theta \log \pi_\theta(a_t | s_t) \left(G_t - v_\omega(s_t) \right)$.
4. compute the gradient of the value function loss $\nabla_\omega \mathcal{L}(\omega) = \nabla_\omega \frac{1}{2}\sum_{t=0}^T|G_t - v_\omega(s_t)|^2$.
5. update policy parameters $\theta \leftarrow \theta + \alpha \nabla_\theta J(\theta)$.
6. update the value function parameters $\omega \leftarrow \omega - \alpha \nabla_\omega \mathcal{L}(\omega)$.

```python
#import various packages

import gym
import numpy as np
from collections import deque
import matplotlib.pyplot as plt
%matplotlib inline
import random

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.distributions import Categorical
```

```python
# use gpu if available
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# configure matplotlib
%matplotlib inline
plt.rcParams['figure.figsize'] = (15.0, 10.0) # set default size of plots
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'
```

# The Environment

In `LunarLander-v2` our goal is to land a space shuttle on the moon. We have 4 actions: fire the left thruster, fire the right thruster, fire the main thruster, or do nothing. The state space consists of: the shuttle's position (x,y)-cooridinate, its velocity, the angle of tilt, the angular velocity, and 2 boolean flags indicating whether the left and right legs of the shuttle are in contact with the ground.

```python
env = gym.make('LunarLander-v2')
print('Action space: ', env.action_space)
print('Observation space: ', env.observation_space)
```

### Watching a Random Policy In Action

Let's see how a random policy performs in this enviroment:

```python
state = env.reset()
for t in range(500):
    # sample a random action
    action = env.action_space.sample()
    env.render()
    state, reward, done, _ = env.step(action)
    if done:
        state = env.reset()
env.close()
```

That's some awful flying! We'll try to do better with REINFORCE now.

## The Policy and Value Function Networks

First, let's define our policy and value function. For efficiency we can use a single network with two heads. For a given state, the first head will output a Categorical distribution over the actions while the second head will return the value of the state.

```python
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # IMPLEMENT-ME
        # The first layer should be a shared linear layer with
        # an input size of env.observation_space.shape and an output size of 128
        self.fc_shared = ...
        # The policy head should be a linear layer with input size of 128
        # and an output size of env.action_space.n
        self.fc_policy = ...
        # The value function head should be a linear layer with input size of 128
        # and an output size of 1
        self.fc_value_function = ...

    def forward(self, x):
        # IMPLEMENT-ME
        # Define the forward pass
        # apply a ReLU activation after the shared layer
        x = F.relu(self.fc_shared(x))
        # apply the policy head layer (without an activation).
        logits = ...
        # apply the value function head layer (without an activation)
        value = ...
        # define a Categorical distribution over the actions
        dist = ...
        return dist, value
```

### How do we use the net?

For a given state our policy returns a tuple consisting of a pytorch `Categorial` object and a pytorch `Tensor`. To recap, we can use `sample` to sample an action from the distribution and `log_prob` to find the log probability of a particular action.

```python
net = Net().to(device)
state = env.reset()
state = torch.from_numpy(state).float().to(device)
dist, value = net(state)
action = dist.sample()
print('Sampled action: ', action.item())
print('Log probability of action: ', dist.log_prob(action).item())
print('Estimated value of the state: ', value.item())
```

## Computing the Return

Given a sequence of returns compute the vector of discounted returns $[G_0, G_1, \ldots, G_T]$. Note that we alse use the trick of 'normalizing' the returns i.e. we subtract the mean and divide by the standard deviation.

```python
def compute_returns(rewards, gamma):
    R = 0
    returns = []
    for step in reversed(range(len(rewards))):
        R = rewards[step] + gamma * R
        returns.insert(0, R)
    returns = np.array(returns)
    returns -= returns.mean()
    returns /= returns.std()
    return returns
```

## REINFORCE with a Learned Baseline

1. sample a trajectory $\tau = (s_0, a_0, r_1, s_1, \ldots, s_{T}, r_{T})$ using the policy $\pi_\theta$.
2. compute the vector of returns $[G_0, G_1, \ldots, G_T]$.
3. compute the policy gradient $\nabla_\theta J(\theta) \approx \sum_{t=0}^T \nabla_\theta \log \pi_\theta(a_t | s_t) \left(G_t - v_\omega(s_t) \right)$.
4. compute the gradient of the value function loss $\nabla_\omega \mathcal{L}(\omega) = \nabla_\omega \frac{1}{2}\sum_{t=0}^T|G_t - v_\omega(s_t)|^2$.
5. update policy parameters $\theta \leftarrow \theta + \alpha \nabla_\theta J(\theta)$.
6. update the value function parameters $\omega \leftarrow \omega - \alpha \nabla_\omega \mathcal{L}(\omega)$.

In practice we combine steps 3. and 4. by defining the composite loss:
\begin{align}
\texttt{loss} = \sum_{t=0}^T \log \pi_\theta(a_t | s_t) \left(G_t - \text{detach}(v_\omega(s_t)) \right) + \frac{1}{2}\sum_{t=0}^T|G_t - v_\omega(s_t)|^2.
\end{align}
This also allows us to perform the policy and value function updates in a single step.

```python
# define some hyperparameters
gamma = 0.99
lr = 0.02
# if you use another seed you may have to train for longer
seed = 401
number_episodes = 1250
```

```python
def reinforce_learned_baseline(seed):
    env = gym.make('LunarLander-v2')

    # set random seeds (for reproducibility)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    np.random.seed(seed)
    env.seed(seed)
    random.seed(seed)

    # instantiate the policy and optimizer
    net = Net().to(device)
    optimizer = optim.Adam(net.parameters(), lr=lr)

    scores = []
    scores_deque = deque(maxlen=50)
    for episode in range(1, number_episodes+1):
        ##################################################################
        # 1. Collect trajectories using our policy and save the rewards, #
        # log probability, and the estimated value of each state.        #                                                #
        ##################################################################
        log_probs = []
        values = []
        rewards = []

        state = env.reset()
        for t in range(1000):
            # convert state to a torch Tensor
            state = torch.from_numpy(state).float().to(device)
            # get the distribution over actions and the estimated value of state
            dist, value = net(state)

            # sample an action from the distribution
            action = dist.sample()

            # compute the log probability
            log_prob = dist.log_prob(action)

            # take a step in the environment
            state, reward, done, _ = env.step(action.item())

            # save the reward, log probabily, and value
            rewards.append(reward)
            log_probs.append(log_prob.unsqueeze(0))
            values.append(value)

            if done:
                break

        # for reporting save the score
        scores.append(sum(rewards))
        scores_deque.append(sum(rewards))

        ##################################################################
        # 2. Compute the vector of discounted returns                    #
        ##################################################################
        returns = compute_returns(rewards, gamma)
        returns = torch.from_numpy(returns).float().to(device)

        ##################################################################
        # 3. and 4. Compute the loss for gradient descent                #
        ##################################################################
        values = torch.cat(values)
        log_probs = torch.cat(log_probs)

        # IMPLEMENT-ME
        # compute the difference between the returns and the values
        delta = ...

        # IMPLEMENT-ME
        # compute the policy loss term. multiply the log probabilities by delta and sum
        # (remeber to call .detach() on delta since we do not want the gradient to propogate
        # to the value function network here)
        policy_loss = ...

        # IMPLEMENT-ME
        # compute the value function loss term
        value_function_loss = ...

        # IMPLEMENT-ME
        # compute the composite loss
        loss = ...

        #################################################################
        # 4. and 5. update the policy and value function parameters     #
        #################################################################
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if episode % 50 == 0:
            print('Episode {}\tAverage Score: {:.2f}'.format(episode, np.mean(scores_deque)))

    return net, scores
```

```python
net, scores = reinforce_learned_baseline(seed)
```

## Watching Our Agent in Action

Finally, let's see how our agent performs in the `LunarLander` environment.

```python
state = env.reset()
for t in range(2000):
    state = torch.from_numpy(state).float().to(device)
    dist, value = net(state)
    action = dist.sample().item()
    env.render()
    state, reward, done, _ = env.step(action)
    if done:
        state = env.reset()
env.close()
```


# A7: Data analysis in Python

```python
# Run once then delete this cell
!pip install statsmodels
```

    Collecting statsmodels
      Downloading statsmodels-0.13.0-cp39-cp39-macosx_10_15_x86_64.whl (9.6 MB)
         |████████████████████████████████| 9.6 MB 2.3 MB/s eta 0:00:01
    Requirement already satisfied: numpy>=1.17 in /Users/dwyerry/miniconda3/lib/python3.9/site-packages (from statsmodels) (1.21.2)
    Collecting patsy>=0.5.2
      Downloading patsy-0.5.2-py2.py3-none-any.whl (233 kB)
         |████████████████████████████████| 233 kB 2.2 MB/s eta 0:00:01
    Requirement already satisfied: scipy>=1.3 in /Users/dwyerry/miniconda3/lib/python3.9/site-packages (from statsmodels) (1.7.1)
    Requirement already satisfied: pandas>=0.25 in /Users/dwyerry/miniconda3/lib/python3.9/site-packages (from statsmodels) (1.3.2)
    Requirement already satisfied: python-dateutil>=2.7.3 in /Users/dwyerry/miniconda3/lib/python3.9/site-packages (from pandas>=0.25->statsmodels) (2.8.2)
    Requirement already satisfied: pytz>=2017.3 in /Users/dwyerry/miniconda3/lib/python3.9/site-packages (from pandas>=0.25->statsmodels) (2021.1)
    Requirement already satisfied: six in /Users/dwyerry/miniconda3/lib/python3.9/site-packages (from patsy>=0.5.2->statsmodels) (1.16.0)
    Installing collected packages: patsy, statsmodels
    Successfully installed patsy-0.5.2 statsmodels-0.13.0

```python
import numpy as np
import pandas as pd
import plotly.express as px
from io import StringIO
```

## Data representations

### Pandas DataFrames

Consider an Excel or csv file with columns of data containing volumes and pressures:

We can bring this data into Python by making a pandas DataFrame; in general, the easiest way to do this is to import the `.csv` or `.xlsx` file. In this case, we'll just put the `.csv` data into a string in Python

```python
PV_csv=StringIO("""
V (L),P (atm)
16.56,1
1.561,10
0.7232,20
0.4402,30
0.2929,40
0.1978,50
""")
```

Then we can use `pd.read_csv(PV_csv)` to load the data. Do that below:

```python
df = pd.read_csv(PV_csv)
df
```

      V (L)
      P (atm)

      0
      16.5600
      1

      1
      1.5610
      10

      2
      0.7232
      20

      3
      0.4402
      30

      4
      0.2929
      40

      5
      0.1978
      50

Your result should look like this:

We can count the number of data points using Python's `len` function:

Make a scatter plot with plotly using `px.scatter(df, x='V (L)', y='P (atm)')`

```python
px.scatter(df, x='V (L)', y='P (atm)')
```

Let's make this easier to compare to the ideal gas law by plotting something that should be a straight line: We'll plot $P$ vs $1/V$ instead by defining a column for $1/V$ - we can do that using the bracket syntax - you can tab-complete to autocomplete the column name: `'V (L)'`

```python
df['1/V (1/L)'] = 1/df['V (L)']
df
```

      V (L)
      P (atm)
      1/V (1/L)

      0
      16.5600
      1
      0.060386

      1
      1.5610
      10
      0.640615

      2
      0.7232
      20
      1.382743

      3
      0.4402
      30
      2.271695

      4
      0.2929
      40
      3.414135

      5
      0.1978
      50
      5.055612

Now we can update our plot and add a trendline:

You can scan over your plot to get the trendline

```python
px.scatter(df, x='1/V (1/L)', y='P (atm)', trendline='ols')
```

### Importing from data files

Download a CSV file containing the temperature-dependent heat capacity of ammonia gas from http://hdl.handle.net/10066/17736 and save the Ammonia HeatCapacity.csv file to your Downloads folder. Load the file into Python using

```
df2 = pd.read_csv("~/Downloads/AmmoniaHeatCapacity.csv")
df2
```

```python
df2 = pd.read_csv("~/Downloads/AmmoniaHeatCapacity.csv")
df2
```

      T/K
      Cp/J/K/mol

      0
      300
      35.56

      1
      400
      38.82

      2
      500
      41.68

      3
      600
      44.38

      4
      700
      47.00

      5
      800
      49.59

      6
      900
      52.15

      7
      1000
      54.70

**Try it:** Use the same procedure from above to plot the data and add a trendline:

```python
fig = px.scatter(df2, x='T/K', y='Cp/J/K/mol', trendline='ols')
fig
```

```python
fig = px.scatter(df2, x='T/K', y='Cp/J/K/mol', trendline='ols')
fit_results = px.get_trendline_results(fig)
params = fit_results['px_fit_results'][0].params # Intercept, then slope
params
```

    array([2.79210714e+01, 2.70214286e-02])

For the next part, it's useful to have a version of the fit parameters than you can copy. Do that by saving your plotly figure to a variable then loading the parameters:

### Integrating the heat capacity to determine $\Delta H$

For this part (see last problem on assignment), you can use sympy again (you could also do this integral by hand (there's only two terms!).

```python
import sympy as sm
sm.init_printing()

T = sm.symbols('T', positive=True) # Define the symbol for temperature, our independent variable
```


# Helium-Helium Interaction Potential Comparison

## Purpose
In this notebook we compare various historical and modern approches to the He-He interaction potential demonstrating the use of the `heprops.potential` module

```python
import numpy as np
import matplotlib.pyplot as plt
from heprops import potential

%config InlineBackend.figure_format = 'svg'
%matplotlib inline

try:
    plt.style.use('notebook')
except:
    pass

colors = plt.rcParams['axes.prop_cycle'].by_key()['color']
```

```python
r = np.linspace(1,10,10000)
V = {}
```

## Lennard-Jones

\begin{equation}
V(r) = 4\varepsilon \left[\left(\frac{\sigma}{r}\right)^{12} - \left(\frac{\sigma}{r}\right)^6\right]
\end{equation}

Parameters taken from

R. A. Aziz, A. R. Janzen, and M. R. Moldover, Phys. Rev. Lett. 74, 1586 (1995).  https://doi.org/10.1103/PhysRevLett.74.1586

```python
V['Lennard-Jones'] = potential.lennard_jones
```

```python
fig,ax = plt.subplots()
ax.plot(r,V["Lennard-Jones"](r), label='Lennard-Jones')

ax.set_xlim(1.2,10)
ax.set_ylim(-15,50)

ax.set_xlabel('Separation (Å)')
ax.set_ylabel('Interaction Energy (K)')
ax.legend();
```

## Aziz 1979

R. A. Aziz, V. P. S. Nain, J. S. Carley, W. L. Taylor, and G. T. McConville, J. of Chem. Phys. 70, 4330 (1979).  https://doi.org/10.1063/1.438007

\begin{equation}
V(r)= \varepsilon\left[A \exp (-\alpha x + \beta x^2)-F(x) \sum_{j=0}^{2}\frac{C_{2 j+6}}{x^{2 j+6}}\right]
\label{eq:Vaziz}
\end{equation}

where

\begin{equation}
F(x)=
\begin{cases}
\exp \left[-\left(\frac{D}{x}-1\right)^{2}\right] &,&  x<D \\
1 &,& x \geq D
\end{cases}
\end{equation}

and

\begin{align}
\varepsilon=10.8\ \mathrm{K}, & \qquad  C_{6}=1.3732412\\
r_{m}=2.9673\ Å, &\qquad C_{8}=0.4253785\\
D = 1.241314, &\qquad C_{10}=0.1781\\
\alpha=13.353384, &\qquad  A=0.5448504 \times 10^{6} \\
\beta = 0, & \\
\end{align}

```python
V['Aziz 1979'] = potential.aziz_1979
```

## Aziz 1987

R. A. Aziz, F. McCourt, and C. Wong, Mol. Phys. 61, 1487 (1987).  https://doi.org/10.1080/00268978700101941

\begin{align}
\varepsilon=10.948\ \mathrm{K}, & \qquad  C_{6}=1.36745214\\
r_{m}=2.963\ Å, &\qquad C_{8}=0.42123807\\
D = 1.4826, &\qquad C_{10}=0.17473318\\
\alpha=10.43329537, &\qquad  A=1.8443101 \times 10^{5} \\
\beta=-2.27965105 & \\
\end{align}

```python
V['Aziz 1987'] = potential.aziz_1987
```

## Aziz 1995

R. A. Aziz, A. R. Janzen, and M. R. Moldover, Phys. Rev. Lett. 74, 1586 (1995).  https://doi.org/10.1103/PhysRevLett.74.1586

\begin{align}
\varepsilon=10.956\ \mathrm{K}, & \qquad  C_{6}=1.35186623\\
r_{m}=2.9683\ Å, &\qquad C_{8}=0.4149514\\
D = 1.438, &\qquad C_{10}=0.17151143\\
\alpha=10.5717543, &\qquad  A=1.86924404 \times 10^{5} \\
\beta=-2.07758779 & \\
\end{align}

```python
V['Aziz 1995'] = potential.aziz_1995
```

```python
fig,ax = plt.subplots()

for year in [1979,1987,1995]:
    ax.plot(r,V[f'Aziz {year}'](r), label=f'Aziz {year}')

ax.set_xlim(0.5,10)
ax.set_ylim(-15,50)

ax.set_xlabel('Separation (Å)')
ax.set_ylabel('Interaction Energy (K)')
ax.legend();
```

## Szalewicz 2012
* M. Przybytek, W. Cencek, J. Komasa, G. Łach, B. Jeziorski, and K. Szalewicz, Phys. Rev. Lett. 104, 183003 (2010).  https://doi.org/10.1103/PhysRevLett.104.183003
* W. Cencek, M. Przybytek, J. Komasa, J. B. Mehl, B. Jeziorski, and K. Szalewicz, J. Chem. Phys. 136, 224303 (2012).  https://doi.org/10.1063/1.4712218

\begin{equation}
 V(R)= e^{-a R}\sum_{j=0}^{2}P_{j}R^j+e^{-b R}\sum_{j=0}^{1}Q_{j}R^j -\sum_{n=3}^{16} f_{n}(\eta R) \frac{C_{n}}{R^{n}}
\end{equation}

where $f_n(x)$ is the Tang-Toennies damping function:

\begin{equation}
f_{n}(x)=1-e^{-x} \sum_{k=0}^{n} \frac{x^{k}}{k !}
\end{equation}

### Parameters
| Parameter | Value | Parameter| Value |
|-----------|-------|----------|-------|
|$C_0$|0.0|$a$ | 3.64890303652830|
|$C_1$|0.0|$b$|2.36824871743591|
|$C_2$|0.0| η | 4.09423805117871|
|$C_3$|0.000000577235|$P_0$ | -25.4701669416621|
|$C_4$|-0.000035322| $P_1$ | 269.244425630616 |
|$C_5$|0.000001377841|$P_2$ | -56.3879970402079|
|$C_6$|1.461830|$Q_0$ | 38.7957487310071 |
|$C_7$|0.0|$Q_1$ | -2.76577136772754|
|$C_8$|14.12350|||
|$C_9$|0.0|||
|$C_{10}$|183.7497|||
|$C_{11}$|-0.7674e2|||
|$C_{12}$|0.3372e4|||
|$C_{13}$|-0.3806e4|||
|$C_{14}$|0.8534e5|||
|$C_{15}$|-0.1707e6|||
|$C_{16}$|0.286e7|||

### Conversion Factors
Factors of 315774.65 from atomic units to kelvins and of 0.52917720859 from bohrs to angstroms were assumed.

```python

```

```python
V['Szalewicz 2012'] = potential.szalewicz_2012
```

## Plot a comparison of all the potentials

```python
fig,ax = plt.subplots()
ax.plot(r,V["Lennard-Jones"](r), label='Lennard-Jones')
ax.plot(r,V["Aziz 1979"](r), label='Aziz 1979')
ax.plot(r,V["Aziz 1987"](r), label='Aziz 1987')
ax.plot(r,V["Aziz 1995"](r), label='Aziz 1995')
ax.plot(r,V["Szalewicz 2012"](r), color=colors[-2],label='Szalewicz 2012')

ax.set_xlim(2.2,10)
ax.set_ylim(-12,20)

ax.set_xlabel('Separation (Å)')
ax.set_ylabel('Interaction Energy (K)')
ax.legend(ncol=2);
plt.savefig('potential_comparison.svg')
```

### Check the differences with Szalewicz

```python
fig,ax = plt.subplots()
ax.plot(r,np.abs(V["Lennard-Jones"](r)-V["Szalewicz 2012"](r)), label='Lennard-Jones')

ax.plot(r,(V["Aziz 1979"](r)-V["Szalewicz 2012"](r)), label='Aziz 1979')
ax.plot(r,np.abs(V["Aziz 1987"](r)-V["Szalewicz 2012"](r)), label='Aziz 1987')
ax.plot(r,np.abs(V["Aziz 1995"](r)-V["Szalewicz 2012"](r)), label='Aziz 1995')

ax.set_yscale('symlog')

ax.set_xlim(1.0,10)
ax.set_ylim(1E-8,1E5)

ax.set_xlabel('Separation (Å)')
ax.set_ylabel('Interaction Energy (K)')
ax.legend(ncol=2, loc='upper right')
ax.set_title('Difference with Szalewicz');
```

#### Now for r → 0 where there are some numerical instabilities

```python
tr = np.linspace(0.01,3,1000)
fig,ax = plt.subplots()
ax.plot(tr,V["Aziz 1995"](tr), label='Aziz 1995')
ax.plot(tr,V["Szalewicz 2012"](tr), color=colors[0], label='Szalewicz 2012', alpha=0.4)
ax.set_yscale('symlog')
ax.set_xlim(0,2)
ax.set_ylim(1E2,1E7)
ax.legend()
```

    <matplotlib.legend.Legend at 0x1112c7cd0>


 $X_{\pi/4}$ quantum gate

I start by writing a generic quantum control Hamiltonian, $\hat{H}(t) = \hat{H}_\text{drive}(t) + \hat{H}_\text{eff}(t)$, where
\begin{equation}\label{hamiltonian}
\hat{H}_\text{drive}(t)=\sum_i f_i(t)\hat{K}_i, \quad \hat{H}_\text{eff}(t)=\sum_i\epsilon_i\omega_i(t)\hat{K}_i,
\end{equation}
$f_i$ are the driving fields, $\omega_i$ are stochastic processes weighted individually by the strengths $\epsilon_i$, and $\hat{K}_i$ are generic operators satisfying a Lie algebra $\mathfrak{su}(N)$ with the property $\text{Tr}(K_iK_j)=N\delta_{ij}$.

The effective Hamiltonian, $\hat{H}_\text{eff}$, can be treated as the component of interaction, with the time evolution:
\begin{equation}
\hat{U}(t)=\hat{U}_\text{eff}^\dagger(t)\hat{U}_\text{drive},
\end{equation}
where the driving and the effective evolution operators fulfil the ordinary equations:
\begin{equation}\label{evolution}
\frac{\mathrm{d}\hat{U}_\text{drive}(t)}{\mathrm{d}t} =  -i\hat{H}_\text{drive}(t)\hat{U}_\text{drive}(t) \quad \text{and} \quad \frac{\mathrm{d}\hat{U}_\text{eff}(t)}{\mathrm{d}t} =  -i\hat{U}^\dagger_\text{drive}(t)\hat{H}_\text{eff}(t)\hat{U}_\text{drive}(t),
\end{equation}
respectively.

A robust QG needs of a minimum noise level during an operational time $T$,
such that the condition $\partial_{\epsilon_i}\hat{U}_\text{eff}(T)|_{\epsilon_i}=0$ is fulfilled. In turn, the effective evolution operator can be approximated as:
\begin{equation}\label{ueff}
U_\text{eff}(T)=\exp\left(-\frac{i}{\hbar}\sum_i \epsilon_i J_i(T)\right) + O(\epsilon_i^2),
\end{equation}
where
\begin{equation}\label{noise}
J_i(T)=\int_0^T\mathrm{d}t\,U_\text{drive}^\dagger(t)\omega_i(t)\hat{K}_i\hat{U}_\text{drive}(t).
\end{equation}

Problem. To optimise a cost function $C = C_1\left(\hat{U}_\text{drive}(t)\right) + C_2\left(\hat{U}_\text{drive}(t)\right)$, whose terms are
\begin{equation}\begin{split}
C_1\left(\hat{U}_\text{drive}(t)\right) &= \chi\left[\text{Tr}\left(\hat{\rho}^2\right) - \left|\text{Tr}\left(\hat{\Pi}\hat{U}_\text{drive}(T)\hat{\Pi}^\dagger \hat{U}_\text{drive}(0)\right)\right|^m \right]\\
C_2\left(\hat{U}_\text{drive}(t)\right) &= \varphi\left[\text{Tr}\left(\hat{\rho}^2\right) - \frac{1}{N}\sum_i^N\left(\epsilon_i^*||\hat{\Pi}J_i(T)||\right)^n\right],
\end{split}\end{equation}
where $\hat{\Pi}$ is a projection operator, $\hat{\rho}$ is a density matrix, and the monotonically increasing functions $\chi$ and $\varphi$ are Kolmogorov-Nagumo functions–for most problems they typically are $\chi(x)=x$ and $\varphi(x)=x$.

The Hamiltonian for a transmon qubit in a rotating frame can be put into the approximated form of a nonlinear oscillator of dimension $d$:
\begin{equation}
\hat{H}(t)=\sum_{j=1}^{d-1}(\delta(t)+\Delta_j)\hat{\Pi}_j + \sum_{j=1}^{d-1}\frac{\Omega_x(t)\sigma^x_{j-1,j} + \Omega_y(t)\sigma^y_{j-1,j}}{2},
\end{equation}
where $\sigma^x_{j,k}= \vert k \rangle \langle j \vert + \vert j \rangle \langle k \vert$ and $\sigma^y_{j,k}= i \vert k \rangle \langle j \vert -i\vert j \rangle \langle k \vert$ with $j<k$, $\hat{\Pi}_j=\vert j \rangle \langle j \vert$ is a projection operator onto the $j$th state, $\Delta_j$ is the anharmonicity, $\delta(t)$ is the qubit detuning and $\Omega(t)=\Omega_x(t)+i\Omega_y(t)$ is the complex modulated amplitude of the microwave driving field.

The goal is to find a microwave amplitude $\Omega(t)$ and a detuning function $\delta(t)$ such that the cost function vanishes or, at least, becomes negligible. Upon successful computation, a robust QG can be claimed.

 Packages

```julia
using DifferentialEquations
using OrdinaryDiffEq
using LinearAlgebra
using StaticArrays
using Flux
using Optim
# ----------------------------/
using ApproxFun
using SpecialFunctions
# ----------------------------/
using Plots
using PGFPlotsX
using PlotThemes
using PaddedViews
using LaTeXStrings
using BenchmarkTools
using Zygote
# ----------------------------/
using DiffEqFlux
using DiffEqSensitivity
```

```julia
pgfplotsx();
Plots.PGFPlotsXBackend();
```

```julia
const σ0 = Hermitian(Complex{Float64}[1 0; 0 1]);
const σx = Hermitian(Complex{Float64}[0 1; 1 0]);
const σy = Hermitian(Complex{Float64}[0 -im; im 0]);
const σz = Hermitian(Complex{Float64}[1 0; 0 -1]);
```

```julia
Ωp_nn = FastChain(FastDense(1,32), FastDense(32,32,tanh), FastDense(32,32,tanh), FastDense(32,6))
θ_transmon = initial_params(Ωp_nn);
```

 Main algorithm

```julia
const tol = 1e-7;

const Δ = -2π*0.6;
const HΔ = 1*Hermitian(Complex{Float64}[0 0 0 0; 0 0 0 0; 0 0 Δ 0; 0 0 0 3Δ]);
const Hϵ = Δ*Hermitian(Complex{Float64}[0 0 0 0; 0 1 0 0; 0 0 2 0; 0 0 0 3]);

const T = 15.0;
tspan = (0.0, T);

const θ = -π/2;
const Utarget = (cos(θ/2)*σ0 + sin(θ/2)im*σx);

const steepness = 20*T
smooth_square_envelope(t) = coth(steepness/4)*( tanh(steepness*t/(4*T))
    - tanh(steepness*(t-T)/(4*T)) ) - 1;

function schrodinger_transmon_nn(u, p, t)
    @views @inbounds U = u[1:4, 1:4];
    @views @inbounds ℰ = u[5:8, 1:4];
    envelope = smooth_square_envelope(t);

    @views @inbounds nn_output = Ωp_nn([t/T],p);
    @inbounds Ωx, Ωy, δ = envelope*( nn_output[1]*sin(nn_output[2]) ),
    envelope*(nn_output[3]*sin(nn_output[4]) ), envelope*nn_output[5]*sin(nn_output[6])
    Ω = Ωx - im*Ωy;
    Ωconj = Ωx + im*Ωy;
    @inbounds H = Hermitian([0 Ω 0 0; Ωconj δ √2*Ω 0; 0 √2*Ωconj (Δ+2δ) √3*Ω; 0 0 √3*Ωconj (3Δ+3δ)]);
    @inbounds local dℰ = U'*(Hϵ)*U;
    return [-(im)*(H)*U; dℰ]
end

const u0_transmon = Complex{Float64}[1 0 0 0; 0 1 0 0; 0 0 1 0; 0 0 0 1; 0 0 0 0; 0 0 0 0; 0 0 0 0; 0 0 0 0;];
ode_transmon_nn = ODEProblem(schrodinger_transmon_nn, u0_transmon, tspan, θ_transmon);

function callback(p, cost)
    return cost < 1e-6
end

function cost_adjoint_transmon_nn(p, w=1.0)
    local ode_sol = solve(ode_transmon_nn, BS5(), p=Complex{Float64}.(p), abstol=tol, reltol=tol);
    local usol = last(ode_sol);

    @views @inbounds local Ugate = usol[1:2, 1:2];
    @views @inbounds local ℰ = usol[5:6, 1:2];
    @views @inbounds local ℰleak1 = usol[5:6, 3:4];
    #@views @inbounds local ℰleak2 = usol[7:8, 1:2]; # ℰ is Hermitian

    @inbounds local loss = abs(1.0 - (abs(tr(Ugate*Utarget')/2) ))
    + w^2 * ((norm(ℰ)/2)^2 + 2*norm(ℰleak1)^2/8)

    return loss

end
```

    WARNING: redefinition of constant HΔ. This may fail, cause incorrect answers, or produce other errors.
    WARNING: redefinition of constant Hϵ. This may fail, cause incorrect answers, or produce other errors.
    WARNING: redefinition of constant Utarget. This may fail, cause incorrect answers, or produce other errors.
    WARNING: redefinition of constant u0_transmon. This may fail, cause incorrect answers, or produce other errors.

    cost_adjoint_transmon_nn (generic function with 2 methods)

 Minimizing cost function

```julia
optimized_sol_transmon_nn = DiffEqFlux.sciml_train(p -> cost_adjoint_transmon_nn(p, 0.01),
    θ_transmon, RADAM(0.003), maxiters = 500, allow_f_increases = true)
```

     * Status: success

     * Candidate solution
        Final objective value:     7.124902e-04

     * Found with
        Algorithm:     RADAM

     * Convergence measures
        |x - x'|               = NaN ≰ 0.0e+00
        |x - x'|/|x'|          = NaN ≰ 0.0e+00
        |f(x) - f(x')|         = NaN ≰ 0.0e+00
        |f(x) - f(x')|/|f(x')| = NaN ≰ 0.0e+00
        |g(x)|                 = NaN ≰ 0.0e+00

     * Work counters
        Seconds run:   414  (vs limit Inf)
        Iterations:    500
        f(x) calls:    500
        ∇f(x) calls:   500

```julia
optimized_sol_transmon_nn2 = DiffEqFlux.sciml_train(p -> cost_adjoint_transmon_nn(p, 0.01),
    optimized_sol_transmon_nn.minimizer, BFGS(initial_stepnorm=0.0001),
    maxiters = 10000, allow_f_increases = true)
```

     * Status: success

     * Candidate solution
        Final objective value:     2.470690e-12

     * Found with
        Algorithm:     BFGS

     * Convergence measures
        |x - x'|               = 0.00e+00 ≤ 0.0e+00
        |x - x'|/|x'|          = 0.00e+00 ≤ 0.0e+00
        |f(x) - f(x')|         = 0.00e+00 ≤ 0.0e+00
        |f(x) - f(x')|/|f(x')| = 0.00e+00 ≤ 0.0e+00
        |g(x)|                 = 1.54e-06 ≰ 1.0e-08

     * Work counters
        Seconds run:   1116  (vs limit Inf)
        Iterations:    76
        f(x) calls:    271
        ∇f(x) calls:   271

```julia
transmon_nn_solution = optimized_sol_transmon_nn2.minimizer;
```

Soft pulses

```julia
function Ωxy(t,p)
    envelope = smooth_square_envelope(t);
    nn_output = Ωp_nn([t/T],p);
    @inbounds Ωx, Ωy, δ = envelope*nn_output[1]*sin(nn_output[2]),
    envelope*nn_output[3]*sin(nn_output[4]),
    envelope*nn_output[5]*sin(nn_output[6])
    return [Ωx, Ωy, δ]
end

const fonstsize = 18;
theme(:bright)

Plots.plot( τ -> 2*Ωxy(τ / (abs(Δ)/(2π)),
        transmon_nn_solution)[1]/abs(Δ), 0, T*abs(Δ)/(2π), label = L"\Omega'_x(t)", lw=3,
    xlabel = L"t'",
    xtickfont=font(fonstsize-0),
    ytickfont=font(fonstsize-0),
    guidefont=font(fonstsize-0),
    legendfont=font(fonstsize-0),
    palette = :Accent_8, legend = :topright)
Plots.plot!(τ -> 2*Ωxy(τ/(abs(Δ)/(2π)),
        transmon_nn_solution)[2]/abs(Δ), 0, T*abs(Δ)/(2π),
    label = L"\Omega'_y(t)", lw=3, line=:dash)
Plots.plot!(τ -> Ωxy(τ/(abs(Δ)/(2π)),
        transmon_nn_solution)[3]/abs(Δ), 0, T*abs(Δ)/(2π),
    label = L"\delta'(t)",   lw=3, line=:dashdot)
```

```julia
savefig("transmon-pulse.pdf")
```


# **Unit 2. Review of Rock and Fluid Properties (Problems)**

```
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
```

```
!git clone https://github.com/yohanesnuwara/reservoir-engineering
```

    Cloning into 'reservoir-engineering'...
    remote: Enumerating objects: 30, done.
    remote: Counting objects: 100% (30/30), done.
    remote: Compressing objects: 100% (27/27), done.
    remote: Total 188 (delta 10), reused 0 (delta 0), pack-reused 158
    Receiving objects: 100% (188/188), 217.21 KiB | 602.00 KiB/s, done.
    Resolving deltas: 100% (77/77), done.

```
import sys, os
sys.path.append('/content/reservoir-engineering/Unit 2 Review of Rock and Fluid Properties/functions')
```

# Problem 2.2

```
flowrate = pd.read_csv('/content/reservoir-engineering/Unit 2 Review of Rock and Fluid Properties/data/Table 2.14-Data for Problem 2.2.csv')

"Question. calculate relative permeabilities of oil and water"

qo_sw0 = flowrate.loc[flowrate['Sw'] == 0]
qo_sw0 = np.array(qo_sw0.qo) # qo at Sw=0
qw_sw1 = flowrate.loc[flowrate['Sw'] == 1]
qw_sw1 = np.array(qw_sw1.qw) # qw at Sw=1
qw_sw1

kr_o = flowrate.qo / qo_sw0 #qo per q0 at Sw=0
kr_w = flowrate.qw / qw_sw1 #qw per qw at Sw=1

# plot result
p1 = plt.plot(flowrate.Sw, kr_o)
p2 = plt.plot(flowrate.Sw, kr_w)
plt.xlabel("Sw"); plt.ylabel("kr_o, kr_w (unitless)")
plt.legend((p1[0], p2[0]), ('kr_o', 'kr_w'))
plt.title("Relative Permeabilities")
```

# Problem 2.3

```
jfunction = pd.read_csv('/content/reservoir-engineering/Unit 2 Review of Rock and Fluid Properties/data/Table 2.15-J Function for Problem 2.3.csv')

"Question. calculate capillary pressures of four cores in the following conditions"

# k1 = 5; poro1 = 0.09; k2 = 35; poro2 = 0.123; k3 = 150; poro3 = 0.159; k4 = 458; poro4 = 0.195 # permeability in mD

k_conversion = 1 / 1000 * (0.98689233 * 1E-08) # convert k from mD to cm2
# core information: k, porosity
core1 = np.array([5 * k_conversion, 0.09]); core2 = np.array([35 * k_conversion, 0.123])
core3 = np.array([150 * k_conversion, 0.159]); core4 = np.array([458 * k_conversion, 0.195])

# known
sigma = 85 * (1/68947.57) # interfacial tension, from dyne/cm convert to psi/cm, 1 psi = 68947.57 dyne/cm2
theta = 65

# calculate Pc for all cores and all Sw
Pc_1 = (jfunction.J * sigma * np.cos(np.deg2rad(theta))) * np.sqrt(core1[1] / core1[0])
Pc_2 = (jfunction.J * sigma * np.cos(np.deg2rad(theta))) * np.sqrt(core2[1] / core2[0])
Pc_3 = (jfunction.J * sigma * np.cos(np.deg2rad(theta))) * np.sqrt(core3[1] / core3[0])
Pc_4 = (jfunction.J * sigma * np.cos(np.deg2rad(theta))) * np.sqrt(core4[1] / core4[0])

# plot all capillary pressure curves
fig, ax = plt.subplots(2, 2, figsize=(15,10))

ax[0, 0].plot(jfunction[['Sw']], Pc_1, 'r') #row=0, col=0
ax[0, 0].set_title('Capillary pressure, k = 5 mD, poro = 0.09', size = 15)
ax[0, 0].set(xlim=(0, 1), ylim=(0, 40))
ax[0, 0].set_xlabel("Sw")
ax[0, 0].set_ylabel("Pc (psi)")
ax[1, 0].plot(jfunction[['Sw']], Pc_2, 'b') #row=1, col=0
ax[1, 0].set_title('Capillary pressure, k = 35 mD, poro = 0.123', size = 15)
ax[1, 0].set(xlim=(0, 1), ylim=(0, 40))
ax[1, 0].set_xlabel("Sw")
ax[1, 0].set_ylabel("Pc (psi)")
ax[0, 1].plot(jfunction[['Sw']], Pc_3, 'g') #row=0, col=1
ax[0, 1].set_title('Capillary pressure, k = 150 mD, poro = 0.159', size = 15)
ax[0, 1].set(xlim=(0, 1), ylim=(0, 40))
ax[0, 1].set_xlabel("Sw")
ax[0, 1].set_ylabel("Pc (psi)")
ax[1, 1].plot(jfunction[['Sw']], Pc_4, 'k') #row=1, col=1
ax[1, 1].set_title('Capillary pressure, k = 458 mD, poro = 0.195', size = 15)
ax[1, 1].set(xlim=(0, 1), ylim=(0, 40))
ax[1, 1].set_xlabel("Sw")
ax[1, 1].set_ylabel("Pc (psi)")
```

# Problem 2.4

```
# Question. calculate z of sour gas using Dranchuk-Abou Kassem EOS

pressure = 2010
temp_fahrenheit = 75
sg = 0.7
x_h2s = 0.07
x_co2 = 0.1

temp = temp_fahrenheit + 459.67 # convert to Rankine

# calculate pseudocritical properties (Sutton, valid for 0.57<sg<1.68)
P_pc = 756.8 - (131.07 * sg) - (3.6 * sg**2)
T_pc = 169.2 + (349.50 * sg) - (74 * sg**2) # in Rankine

# calculate adjustment to pseudocritical properties for sour gas (Wiechert-Aziz, valid for x_co2<0.544 and x_h2s<0.738)
e = (120 * (((x_h2s + x_co2)**0.9) - ((x_h2s + x_co2)**1.6))) + (15 * (x_h2s**0.5 - x_h2s**4))
T_pc_corr = T_pc - e # corrected T_pc
P_pc_corr = (P_pc * T_pc_corr) / (T_pc - x_h2s * e * (1-x_h2s))

# calculate pseudoreduced properties
P_pr = pressure / P_pc_corr
T_pr = temp / T_pc_corr

"Dranchuk-Abou Kassem EOS Solver"

import dranchuk_aboukassem
from dranchuk_aboukassem import dranchuk

rho_pr, z = dranchuk(T_pr, P_pr)

print("Gas compressibility factor using Drancuk-Abou Kassem EOS: ",z)
```

    Gas compressibility factor using Drancuk-Abou Kassem EOS:  0.7730732979223308

# Problem 2.5

```
def mattar(T_pr, P_pr, rho_pr, z):
  import numpy as np

  a1 = 0.3265; a2 = -1.0700; a3 = -0.5339; a4 = 0.01569; a5 = -0.05165; a6 = 0.5475
  a7 = -0.7361; a8 = 0.1844; a9 = 0.1056; a10 = 0.6134; a11 = 0.7210

  do = ((a1 + (a2/T_pr) + (a3/T_pr**3) +(a4/T_pr**4) + (a5/T_pr**5)) * rho_pr) + \
      (2 * ((a6 + (a7/T_pr) + (a8/T_pr**2))) * rho_pr**2) - \
      (5 * a9 * (((a7/T_pr) + (a8/T_pr**2))) * rho_pr**4) + (1 + (a11 * rho_pr**2) - (a11 * rho_pr**2)**2) \
      * ((2 * a10 * rho_pr / T_pr**3)*np.exp(-a11 * rho_pr**2))

  c_pr_analytical = (1 / P_pr) - ((0.27 / (z**2 * T_pr)) * (do / (1 + ((rho_pr / z) * do))))
  cgas_analytical = c_pr_analytical / P_pc_corr
  return(cgas_analytical)
```

```
"Question. calculate gas compressibility analytically using Mattar method"

# known: properties similar to Problem 2.4

from mattar import mattar

cgas_analytical = mattar(T_pr, P_pr, rho_pr, z, P_pc_corr)

print("Gas isothermal compressibility at 1 atm using Mattar analytical: ",cgas_analytical,"psi^-1")
```

    Gas isothermal compressibility at 1 atm using Mattar analytical:  0.0004804635269145755 psi^-1

# Problem 2.6

Skipped, has been solved in Example 2.5 Question 3

# Problem 2.7

```
# Question. calculate z of sour gas using Dranchuk-Abou Kassem EOS

pressure = 2910
temp_fahrenheit = 165
sg = 0.76
x_n2 = 0.17 # x_h2s is now x_n2, nitrogen composition
x_co2 = 0.12

temp = temp_fahrenheit + 459.67 # convert to Rankine

# calculate pseudocritical properties (Sutton, valid for 0.57<sg<1.68)
P_pc = 756.8 - (131.07 * sg) - (3.6 * sg**2)
T_pc = 169.2 + (349.50 * sg) - (74 * sg**2) # in Rankine

# calculate adjustment to pseudocritical properties for sour gas (Wiechert-Aziz, valid for x_co2<0.544 and x_h2s<0.738)
e = (120 * (((x_n2 + x_co2)**0.9) - ((x_n2 + x_co2)**1.6))) + (15 * (x_n2**0.5 - x_n2**4))
T_pc_corr = T_pc - e # corrected T_pc
P_pc_corr = (P_pc * T_pc_corr) / (T_pc - x_n2 * e * (1-x_n2))

# calculate pseudoreduced properties
P_pr = pressure / P_pc_corr
T_pr = temp / T_pc_corr

"Dranchuk-Abou Kassem EOS Solver"

import dranchuk_aboukassem
from dranchuk_aboukassem import dranchuk

rho_pr, z = dranchuk(T_pr, P_pr)

print("Gas compressibility factor using Drancuk-Abou Kassem EOS: ",z)
```

    Gas compressibility factor using Drancuk-Abou Kassem EOS:  0.884387700002946

# Problem 2.8

```
"Question 1. calculate gas density"

R = 10.732 # gas constant in (ft3*psi)/(lb-mol*R), see gas constants above
rhogas = (28.97 * sg * pressure) / (z * R * temp)
print("Gas density: ",rhogas, "lbm/ft3")
```

    Gas density:  10.80640075648603 lbm/ft3

```
"Question 1. calculate gas FVF"

Bg = 0.0282793 * z * temp / pressure

print("Gas volume factor: ",Bg, "res ft3/scf")
```

    Gas volume factor:  0.005368698426960609 res ft3/scf

# Problem 2.9

Question 1 and 3 must be solved graphically.

```
"Question 2. calculate gas viscosity using Lee et al"

Mg = 28.97 * sg
K = ((0.00094 + 2E-06)*(temp**1.5)) / (209 + 19*Mg + temp)
rhogas_lee = rhogas * 0.0160185 # lbm/ft3 converted to gas density unit of Lee et al (g/cm3)
x = 3.5 + (986 / temp) + (0.01 * Mg)
y = 2.4 - 0.2*x

mu_lee = K * np.exp(x * (rhogas_lee**y))
print("Gas viscosity using Lee et al: ",mu_lee,"cP")
```

    Gas viscosity using Lee et al:  0.019464090612580302 cP

```
"Question 4. calculate gas compressibility analytically using Mattar method"

# known: properties similar to Problem 2.4

from mattar import mattar

cgas_analytical = mattar(T_pr, P_pr, rho_pr, z, P_pc_corr)

print("Gas isothermal compressibility at 1 atm using Mattar analytical: ",cgas_analytical,"psi^-1")
```

    Gas isothermal compressibility at 1 atm using Mattar analytical:  0.0002960305541823685 psi^-1

# Problem 2.10 (Not solved yet)

```
"Question 1. Calculate z factor using real gas law"

pressure = 1 # in MPa
volume = 20 * 1E+06 # from m3 to cm3
mol = 5373 * 1000 # from kg-mol to mol
temp = 37.8 + 273 # from celsius to Kelvin

R = 8.314 # gas constant in cm3*MPa/(K*mol)

z = (pressure * volume) / (mol * R * temp)
z = z * 1000 # why ???
print("Gas z factor: ",z)
```

    Gas z factor:  1.4405293950320823

```
# z = 1 - (c3*(rho_pr**5)) + (c2*(rho_pr**2)) + (c1*(rho_pr**1)) + c4

temp = temp * 1.8 # from Kelvin to Rankine

def f(sg, temp):
  # temp = temp * 1.8 # from Kelvin to Rankine

  P_pc = 756.8 - (131.07 * sg) - (3.6 * sg**2)
  T_pc = 169.2 + (349.50 * sg) - (74 * sg**2) # in Rankine

  T_pr = temp / T_pc
  P_pr = pressure / P_pc

  rho_pr = (0.27 * P_pr) / (z * T_pr)

  a1 = 0.3265; a2 = -1.0700; a3 = -0.5339; a4 = 0.01569; a5 = -0.05165; a6 = 0.5475
  a7 = -0.7361; a8 = 0.1844; a9 = 0.1056; a10 = 0.6134; a11 = 0.7210

  c1 = a1 + (a2/T_pr) + (a3/(T_pr**3))+ (a4/(T_pr**4))+ (a5/(T_pr**5))
  c2 = a6 + (a7/T_pr) + (a8/(T_pr**2))
  c3 = a9*((a7/T_pr) + (a8/(T_pr**2)))
  c4 = (a10)*(1+(a11*(rho_pr**2)))*((rho_pr**2)/(T_pr**3))*(np.exp(-a11*(rho_pr**2)))

  y = z + (c3*(rho_pr**5)) - (c2*(rho_pr**2)) - (c1*(rho_pr**1)) - c4 - 1
  # z = 1 - (c3*(rho_pr**5)) + (c2*(rho_pr**2)) + (c1*(rho_pr**1)) + c4

  return(y)

# graphical method

# sg = np.linspace(0, 2, 100)

# y_values = f(sg, temp)

# from scipy.optimize import fsolve

# sg = fsolve(f, 1, temp)
# sg

# use sympy

from sympy import Eq, Symbol, solve

# y = Symbol('y')
# eqn = Eq(y*(8.0 - y**3), 8.0)

# print solve(eqn)
```

# Problem 2.11

```
"Question. calculate gas FVF of similar gas in Examples 2.6 and 2.7"

# known
Rsb = 500 # gas-oil ratio AT and ABOVE BUBBLEPOINT pressure, in scf/STB
temp2 = 220 # reservoir temperature, in deg F
sg2 = 0.8
api = 30 # specific gravity of oil, deg API

pressure2 = np.array([100, 300, 500, 800, 1000, 1300, 1600, 2000, 2500, 2651.7])

# First, calculate bubblepoint pressure, using Vazquez-Beggs

from pbubble import pbubble_vazquez

P_bubble = pbubble_vazquez(Rsb, sg2, api, temp2)
print("Bubblepoint pressure of oil using Vazquez-Beggs: ",P_bubble,"psi")

# Second, calculate FVF using Levitan-Murtha

from oilfvf import oilfvf

Bo_array = oilfvf(P_bubble, api, Rsb, sg2, temp2, pressure2)
Bo_df = pd.DataFrame({"pressure(psia)": pressure2, "formation-volume-factor(RB/STB)": Bo_array})
Bo_df
```

    Bubblepoint pressure of oil using Vazquez-Beggs:  2650.3067919543523 psi

      pressure(psia)
      formation-volume-factor(RB/STB)

      0
      100.0
      1.110043

      1
      300.0
      1.121628

      2
      500.0
      1.134028

      3
      800.0
      1.153495

      4
      1000.0
      1.166884

      5
      1300.0
      1.187431

      6
      1600.0
      1.208431

      7
      2000.0
      1.237004

      8
      2500.0
      1.273475

      9
      2651.7
      1.335658

# Problem 2.12

Skipped, has been solved in Example 2.7

# Problem 2.13

```
# Question 1. calculate bubblepoint pressure

Rsb = 750
temp = 170
sg = 0.85
api = 40

from pbubble import pbubble_vazquez

P_bubble = pbubble_vazquez(Rsb, sg, api, temp)
print("Bubblepoint pressure of oil using Vazquez-Beggs: ",P_bubble,"psi")
```

    Bubblepoint pressure of oil using Vazquez-Beggs:  2507.977027993109 psi

```
# Question 2. Calculate coil, Rs, Bo, Bg, viscosity

pressure = np.arange(100, 5100, 100)

"calculate oil compressibility (coil)"

from oilcompressibility import oilcompressibility

coil = oilcompressibility(pressure, P_bubble, temp, api, Rsb, sg)

"calculate Rs"

from gasoilratio import gasoilratio

Rs = gasoilratio(pressure, P_bubble, sg, api, temp, Rsb)

"calculate Bo"

from oilfvf import oilfvf

Bo = oilfvf(P_bubble, api, Rsb, sg, temp, pressure)

"calculate Bg"

x_h2s = 0; x_co2 = 0

# First, calculate pseudoproperties

def pseudoprops(temp, pressure, sg, x_h2s, x_co2):
  temp = temp + 459.67 # convert to Rankine

  # calculate pseudocritical properties (Sutton, valid for 0.57<sg<1.68)
  P_pc = 756.8 - (131.07 * sg) - (3.6 * sg**2)
  T_pc = 169.2 + (349.50 * sg) - (74 * sg**2) # in Rankine

  # calculate adjustment to pseudocritical properties for sour gas (Wiechert-Aziz, valid for x_co2<0.544 and x_h2s<0.738)
  e = (120 * (((x_h2s + x_co2)**0.9) - ((x_h2s + x_co2)**1.6))) + (15 * (x_h2s**0.5 - x_h2s**4))
  T_pc_corr = T_pc - e # corrected T_pc
  P_pc_corr = (P_pc * T_pc_corr) / (T_pc - x_h2s * e * (1-x_h2s))

  # calculate pseudoreduced properties
  P_pr = pressure / P_pc_corr
  T_pr = temp / T_pc_corr

  return(P_pr, T_pr)

P_pr, T_pr = pseudoprops(temp, sg, x_h2s, x_co2)

# Second, compute z

from dranchuk_aboukassem import dranchuk

z_array = []

for j in range(len(P_pr)):
  rho_pr, z = dranchuk(T_pr, P_pr[j])
  z_array.append(float(z))

# ###
# for j in range(len(P_pr)):
#   if pressure[j] < P_bubble:
#     rho_pr, z = dranchuk(T_pr, P_pr[j])
#     z_array.append(float(z))

#   t = [z_array[i] / pressure[i] for i in range(len(pressure))]
#   t = np.array(t)
#   Bg = 0.0282793 * temp * t
#   if pressure[j] >= P_bubble:
#     Bg = 0
# ###

# Third, calculate Bg

# Formula: Bg = 0.0282793 * z * temp / pressure

t = [z_array[i] / pressure[i] for i in range(len(pressure))]
t = np.array(t)
Bg = 0.0282793 * temp * t

"calculate viscosity"

from oilmu import oilmu

mu_oil = oilmu(pressure, P_bubble, sg, api, temp, Rsb)

# result in dataframe
# print(mu_oil)
oily = pd.DataFrame({"Pressure (psia)": pressure, "Compressibility (psi^-1)": coil, "Gas-oil Ratio (scf/STB)": Rs, "Oil FVF (RB/STB)": Bo, "Gas FVF (res ft3/scf)": Bg, "Viscosity (cP)": mu_oil})
oily
```

      Pressure (psia)
      Compressibility (psi^-1)
      Gas-oil Ratio (scf/STB)
      Oil FVF (RB/STB)
      Gas FVF (res ft3/scf)
      Viscosity (cP)

      0
      100
      0.002174
      16.370585
      1.064700
      0.047376
      1.513951

      1
      200
      0.000796
      37.272353
      1.074605
      0.023341
      1.363871

      2
      300
      0.000442
      60.312473
      1.085525
      0.015331
      1.236408

      3
      400
      0.000291
      84.861248
      1.097159
      0.011328
      1.129932

      4
      500
      0.000211
      110.596555
      1.109355
      0.008928
      1.040692

      5
      600
      0.000162
      137.318718
      1.122019
      0.007330
      0.965238

      6
      700
      0.000129
      164.890478
      1.135086
      0.006191
      0.900792

      7
      800
      0.000107
      193.211076
      1.148508
      0.005339
      0.845195

      8
      900
      0.000090
      222.203074
      1.162248
      0.004678
      0.796778

      9
      1000
      0.000077
      251.804916
      1.176277
      0.004152
      0.754251

      10
      1100
      0.000067
      281.966376
      1.190571
      0.003725
      0.716603

      11
      1200
      0.000059
      312.645616
      1.205110
      0.003371
      0.683039

      12
      1300
      0.000053
      343.807197
      1.219879
      0.003075
      0.652923

      13
      1400
      0.000047
      375.420672
      1.234861
      0.002824
      0.625743

      14
      1500
      0.000043
      407.459576
      1.250045
      0.002609
      0.601084

      15
      1600
      0.000039
      439.900671
      1.265419
      0.002424
      0.578605

      16
      1700
      0.000036
      472.723371
      1.280975
      0.002264
      0.558023

      17
      1800
      0.000033
      505.909306
      1.296702
      0.002124
      0.539101

      18
      1900
      0.000030
      539.441965
      1.312594
      0.002002
      0.521642

      19
      2000
      0.000028
      573.306426
      1.328643
      0.001895
      0.505479

      20
      2100
      0.000026
      607.489124
      1.344843
      0.001800
      0.490467

      21
      2200
      0.000025
      641.977677
      1.361188
      0.001716
      0.476485

      22
      2300
      0.000023
      676.760723
      1.377672
      0.001641
      0.463426

      23
      2400
      0.000022
      711.827805
      1.394291
      0.001574
      0.451200

      24
      2500
      0.000020
      747.169258
      1.411040
      0.001515
      0.439726

      25
      2600
      0.000018
      750.000000
      1.438124
      0.001461
      0.442539

      26
      2700
      0.000018
      750.000000
      1.435690
      0.001413
      0.446725

      27
      2800
      0.000017
      750.000000
      1.433433
      0.001369
      0.451081

      28
      2900
      0.000016
      750.000000
      1.431335
      0.001329
      0.455600

      29
      3000
      0.000016
      750.000000
      1.429379
      0.001293
      0.460278

      30
      3100
      0.000015
      750.000000
      1.427553
      0.001260
      0.465111

      31
      3200
      0.000015
      750.000000
      1.425842
      0.001230
      0.470094

      32
      3300
      0.000014
      750.000000
      1.424237
      0.001202
      0.475223

      33
      3400
      0.000014
      750.000000
      1.422728
      0.001177
      0.480495

      34
      3500
      0.000014
      750.000000
      1.421307
      0.001153
      0.485905

      35
      3600
      0.000013
      750.000000
      1.419966
      0.001131
      0.491449

      36
      3700
      0.000013
      750.000000
      1.418698
      0.001111
      0.497126

      37
      3800
      0.000012
      750.000000
      1.417499
      0.001092
      0.502930

      38
      3900
      0.000012
      750.000000
      1.416362
      0.001075
      0.508860

      39
      4000
      0.000012
      750.000000
      1.415282
      0.001059
      0.514912

      40
      4100
      0.000012
      750.000000
      1.414256
      0.001043
      0.521082

      41
      4200
      0.000011
      750.000000
      1.413280
      0.001029
      0.527369

      42
      4300
      0.000011
      750.000000
      1.412349
      0.001015
      0.533769

      43
      4400
      0.000011
      750.000000
      1.411462
      0.001003
      0.540279

      44
      4500
      0.000011
      750.000000
      1.410614
      0.000991
      0.546897

      45
      4600
      0.000010
      750.000000
      1.409804
      0.000979
      0.553619

      46
      4700
      0.000010
      750.000000
      1.409028
      0.000969
      0.560444

      47
      4800
      0.000010
      750.000000
      1.408286
      0.000958
      0.567368

      48
      4900
      0.000010
      750.000000
      1.407574
      0.000949
      0.574388

      49
      5000
      0.000009
      750.000000
      1.406891
      0.000940
      0.581503

```
# Question 3. Calculate dead-oil viscosity using Egbogah correlation

loglog_mu_dead_oil_plus_one = 1.863 - (0.025086 * api) - (0.5644 * np.log10(temp))
log_mu_dead_oil_plus_one = 10**loglog_mu_dead_oil_plus_one
mu_dead_oil_plus_one = 10**log_mu_dead_oil_plus_one
mu_dead_oil = mu_dead_oil_plus_one - 1

print("Dead-oil viscosity using Egbogah correlation: ",mu_dead_oil,"cP")

# dead-oil viscosity can also be solved using Beggs-Robinson (available within oilmu.py)
```

    Dead-oil viscosity using Egbogah correlation:  1.5046349086558242 cP

```
# Question 4. Calculate oil FVF at bubblepoint pressure using Levitan-Murtha correlation

# the code is also present in oilfvf.py

so = 141.5 / (api + 131.5)
Bo_bubble = 1 + ((0.0005 * Rsb) * ((sg / so)**0.25)) + ((0.0004*(temp- 60)) / (so * sg)) # temp in def F
print("Oil formation volume factor at bubblepoint pressure using Levitan-Murtha: ",Bo_bubble,"RB/STB")
```

    Oil formation volume factor at bubblepoint pressure using Levitan-Murtha:  1.4405403961354764 RB/STB

# Problem 2.14

```
# Question 1. calculate bubblepoint pressure

Rsb = 150
temp = 155
sg = 0.82
api = 28

from pbubble import pbubble_vazquez

P_bubble = pbubble_vazquez(Rsb, sg, api, temp)
print("Bubblepoint pressure of oil using Vazquez-Beggs: ",P_bubble,"psi")
```

    Bubblepoint pressure of oil using Vazquez-Beggs:  833.6174138232668 psi

```
# Question 2. Calculate coil, Rs, Bo, Bg, viscosity

pressure = np.arange(100, 3600, 100)

"calculate oil compressibility (coil)"

from oilcompressibility import oilcompressibility

coil = oilcompressibility(pressure, P_bubble, temp, api, Rsb, sg)

"calculate Rs"

from gasoilratio import gasoilratio

Rs = gasoilratio(pressure, P_bubble, sg, api, temp, Rsb)

"calculate Bo"

from oilfvf import oilfvf

Bo = oilfvf(P_bubble, api, Rsb, sg, temp, pressure)

"calculate Bg"

x_h2s = 0; x_co2 = 0

# First, calculate pseudoproperties

def pseudoprops(temp, sg, x_h2s, x_co2):
  temp = temp + 459.67 # convert to Rankine

  # calculate pseudocritical properties (Sutton, valid for 0.57<sg<1.68)
  P_pc = 756.8 - (131.07 * sg) - (3.6 * sg**2)
  T_pc = 169.2 + (349.50 * sg) - (74 * sg**2) # in Rankine

  # calculate adjustment to pseudocritical properties for sour gas (Wiechert-Aziz, valid for x_co2<0.544 and x_h2s<0.738)
  e = (120 * (((x_h2s + x_co2)**0.9) - ((x_h2s + x_co2)**1.6))) + (15 * (x_h2s**0.5 - x_h2s**4))
  T_pc_corr = T_pc - e # corrected T_pc
  P_pc_corr = (P_pc * T_pc_corr) / (T_pc - x_h2s * e * (1-x_h2s))

  # calculate pseudoreduced properties
  P_pr = pressure / P_pc_corr
  T_pr = temp / T_pc_corr

  return(P_pr, T_pr)

P_pr, T_pr = pseudoprops(temp, sg, x_h2s, x_co2)

# Second, compute z

from dranchuk_aboukassem import dranchuk

z_array = []

for j in range(len(P_pr)):
  rho_pr, z = dranchuk(T_pr, P_pr[j])
  z_array.append(float(z))

# ###
# for j in range(len(P_pr)):
#   if pressure[j] < P_bubble:
#     rho_pr, z = dranchuk(T_pr, P_pr[j])
#     z_array.append(float(z))

#   t = [z_array[i] / pressure[i] for i in range(len(pressure))]
#   t = np.array(t)
#   Bg = 0.0282793 * temp * t
#   if pressure[j] >= P_bubble:
#     Bg = 0
# ###

# Third, calculate Bg

# Formula: Bg = 0.0282793 * z * temp / pressure

t = [z_array[i] / pressure[i] for i in range(len(pressure))]
t = np.array(t)
Bg = 0.0282793 * temp * t

"calculate viscosity"

from oilmu import oilmu

mu_oil = oilmu(pressure, P_bubble, sg, api, temp, Rsb)

# result in dataframe
# print(mu_oil)
oily = pd.DataFrame({"Pressure (psia)": pressure, "Compressibility (psi^-1)": coil, "Gas-oil Ratio (scf/STB)": Rs, "Oil FVF (RB/STB)": Bo, "Gas FVF (res ft3/scf)": Bg, "Viscosity (cP)": mu_oil})
oily
```

      Pressure (psia)
      Compressibility (psi^-1)
      Gas-oil Ratio (scf/STB)
      Oil FVF (RB/STB)
      Gas FVF (res ft3/scf)
      Viscosity (cP)

      0
      100
      0.001290
      14.751286
      1.062833
      0.043184
      5.071639

      1
      200
      0.000472
      31.482296
      1.069676
      0.021269
      4.477514

      2
      300
      0.000262
      49.052078
      1.076861
      0.013966
      3.990999

      3
      400
      0.000173
      67.189732
      1.084278
      0.010316
      3.592082

      4
      500
      0.000125
      85.761701
      1.091873
      0.008127
      3.261764

      5
      600
      0.000096
      104.687280
      1.099613
      0.006670
      2.985092

      6
      700
      0.000077
      123.912072
      1.107475
      0.005631
      2.750724

      7
      800
      0.000063
      143.396784
      1.115443
      0.004854
      2.550092

      8
      900
      0.000015
      150.000000
      1.124639
      0.004252
      2.503764

      9
      1000
      0.000014
      150.000000
      1.123215
      0.003772
      2.528518

      10
      1100
      0.000012
      150.000000
      1.122051
      0.003382
      2.556167

      11
      1200
      0.000011
      150.000000
      1.121081
      0.003059
      2.586532

      12
      1300
      0.000011
      150.000000
      1.120262
      0.002789
      2.619466

      13
      1400
      0.000010
      150.000000
      1.119560
      0.002560
      2.654846

      14
      1500
      0.000009
      150.000000
      1.118952
      0.002365
      2.692570

      15
      1600
      0.000009
      150.000000
      1.118420
      0.002196
      2.732550

      16
      1700
      0.000008
      150.000000
      1.117951
      0.002050
      2.774710

      17
      1800
      0.000008
      150.000000
      1.117534
      0.001923
      2.818987

      18
      1900
      0.000007
      150.000000
      1.117162
      0.001812
      2.865325

      19
      2000
      0.000007
      150.000000
      1.116826
      0.001714
      2.913674

      20
      2100
      0.000007
      150.000000
      1.116523
      0.001628
      2.963992

      21
      2200
      0.000006
      150.000000
      1.116247
      0.001552
      3.016242

      22
      2300
      0.000006
      150.000000
      1.115996
      0.001484
      3.070389

      23
      2400
      0.000006
      150.000000
      1.115765
      0.001424
      3.126402

      24
      2500
      0.000005
      150.000000
      1.115553
      0.001370
      3.184256

      25
      2600
      0.000005
      150.000000
      1.115357
      0.001322
      3.243924

      26
      2700
      0.000005
      150.000000
      1.115176
      0.001278
      3.305384

      27
      2800
      0.000005
      150.000000
      1.115008
      0.001238
      3.368613

      28
      2900
      0.000005
      150.000000
      1.114851
      0.001203
      3.433592

      29
      3000
      0.000005
      150.000000
      1.114705
      0.001170
      3.500301

      30
      3100
      0.000004
      150.000000
      1.114568
      0.001140
      3.568723

      31
      3200
      0.000004
      150.000000
      1.114440
      0.001113
      3.638837

      32
      3300
      0.000004
      150.000000
      1.114320
      0.001088
      3.710628

      33
      3400
      0.000004
      150.000000
      1.114206
      0.001065
      3.784077

      34
      3500
      0.000004
      150.000000
      1.114099
      0.001044
      3.859166

Unit 2 Review of Rock and Fluid Properties/notebook/2_problems.ipynb


# 成為初級資料分析師 | Python 程式設計

> 條件判斷：參考解答

## 郭耀仁

## 隨堂練習：定義一個函式 `odd_or_even(x)` 如果 `x` 是奇數回傳 `'odd'` 是偶數則回傳 `'even'`

- 預期輸入：一個整數 `x`
- 預期輸出：一個文字

```python
def odd_or_even(x):
    """
    >>> odd_or_even(0)
    'even'
    >>> odd_or_even(1)
    'odd'
    >>> odd_or_even(2)
    'even'
    >>> odd_or_even(3)
    'odd'
    """
    if x % 2 == 1:
        func_output = 'odd'
    else:
        func_output = 'even'
    return func_output
```

## 隨堂練習：定義一個函式 `absolute(x)` 如果 `x` 小於 0 回傳 `-x`，否則回傳 `x`

- 預期輸入：一個整數 `x`
- 預期輸出：一個整數

```python
def absolute(x):
    """
    >>> absolute(-5)
    5
    >>> absolute(-6)
    6
    >>> absolute(0)
    0
    >>> absolute(56)
    56
    """
    if x < 0:
        func_output = -x
    else:
        func_output = x
    return func_output
```

## 隨堂練習：定義一個函式 `step(z)` 如果 `z` 大於等於 0.5 回傳 `1`，否則回傳 `0`

- 預期輸入：一個浮點數 `z`
- 預期輸出：一個整數

```python
def step(z):
    """
    >>> step(0.1)
    0
    >>> step(0.4)
    0
    >>> step(0.5)
    1
    >>> step(0.9)
    1
    """
    if z >= 0.5:
        return 1
    else:
        return 0
```

## 隨堂練習：定義一個函式 `bmi_condition(height, weight)` 依據身高（公分）與體重（公斤）判斷 BMI 的分類敘述

\begin{equation}
BMI = \frac{weight_{kg}}{height_{m}^2}
\end{equation}

<https://en.wikipedia.org/wiki/Body_mass_index#/media/File:BMI_chart.svg>

- 預期輸入：兩個數值 `height` 與 `weight`
- 預期輸出：一個文字

```python
def bmi_condition(height, weight):
    """
    >>> bmi_condition(198, 129) # Zion Williamson
    '身高 198 公分體重 129 公斤是 Obese'
    >>> bmi_condition(206, 113) # LeBron James
    '身高 206 公分體重 113 公斤是 Overweight'
    >>> bmi_condition(198, 98)  # Michael Jordan
    '身高 198 公分體重 98 公斤是 Normal weight'
    """
    bmi = weight / (height*0.01)**2
    if bmi > 30:
        desc = 'Obese'
    elif bmi > 25:
        desc = 'Overweight'
    elif bmi >= 18.5:
        desc = 'Normal weight'
    else:
        desc = 'Underweight'
    return '身高 {:.0f} 公分體重 {:.0f} 公斤是 {}'.format(height, weight, desc)
```

## 隨堂練習：定義一個函式 `fizz_buzz(x)`，當 `x` 是 3 的倍數時回傳 `Fizz`、是 5 的倍數時回傳 `Buzz`、是 15 的倍數時回傳 `Fizz Buzz`，否則回傳 `x`，條件重疊時以 15 的倍數優先判斷

- 預期輸入：一個正整數 `x`
- 預期輸出：一個文字或一個整數

```python
def fizz_buzz(x):
    """
    >>> fizz_buzz(3)
    'Fizz'
    >>> fizz_buzz(5)
    'Buzz'
    >>> fizz_buzz(15)
    'Fizz Buzz'
    >>> fizz_buzz(16)
    16
    """
    if x % 15 == 0:
        func_output = 'Fizz Buzz'
    elif x % 3 == 0:
        func_output = 'Fizz'
    elif x % 5 == 0:
        func_output = 'Buzz'
    else:
        func_output = x
    return func_output
```

```python
# %load ../test_cases/test_cases_04.py
import unittest

class TestConditionals(unittest.TestCase):
    def test_odd_or_even(self):
        self.assertEqual(odd_or_even(0), 'even')
        self.assertEqual(odd_or_even(1), 'odd')
        self.assertEqual(odd_or_even(2), 'even')
        self.assertEqual(odd_or_even(3), 'odd')
    def test_absolute(self):
        self.assertEqual(absolute(-5), 5)
        self.assertEqual(absolute(-6), 6)
        self.assertEqual(absolute(0), 0)
        self.assertEqual(absolute(56), 56)
    def test_step(self):
        self.assertEqual(step(0.1), 0)
        self.assertEqual(step(0.4), 0)
        self.assertEqual(step(0.5), 1)
        self.assertEqual(step(0.9), 1)
    def test_bmi_condition(self):
        self.assertEqual(bmi_condition(198, 129), '身高 198 公分體重 129 公斤是 Obese')
        self.assertEqual(bmi_condition(206, 113), '身高 206 公分體重 113 公斤是 Overweight')
        self.assertEqual(bmi_condition(198, 98), '身高 198 公分體重 98 公斤是 Normal weight')
    def test_fizz_buzz(self):
        self.assertEqual(fizz_buzz(3), 'Fizz')
        self.assertEqual(fizz_buzz(5), 'Buzz')
        self.assertEqual(fizz_buzz(15), 'Fizz Buzz')
        self.assertEqual(fizz_buzz(16), 16)

suite = unittest.TestLoader().loadTestsFromTestCase(TestConditionals)
runner = unittest.TextTestRunner(verbosity=2)
test_results = runner.run(suite)
```

    test_absolute (__main__.TestConditionals) ... ok
    test_bmi_condition (__main__.TestConditionals) ... ok
    test_fizz_buzz (__main__.TestConditionals) ... ok
    test_odd_or_even (__main__.TestConditionals) ... ok
    test_step (__main__.TestConditionals) ... ok

    ----------------------------------------------------------------------
    Ran 5 tests in 0.026s

    OK


Text provided under a Creative Commons Attribution license, CC-BY. Code under MIT license.
(c)2014 Lorena A. Barba, Olivier Mesnard. Thanks: NSF for support via CAREER award #1149784.
##### Version 0.3 -- February 2015

# Vortex

Let's recap. In the first three lessons of _AeroPython_, you computed:

1. [a source-sink pair](01_Lesson01_sourceSink.ipynb)—you learned how to make a `streamplot()` and use `scatter()` to mark the location of the singular points. If you did your challenge task, you saw that equipotential lines are perpendicular to streamlines.

2. [a source-sink pair in a free stream](02_Lesson02_sourceSinkFreestream.ipynb), creating a _Rankine oval_. For the first time, we see that we can model the flow around an object using potential flow solutions. You also learned to define custom functions, to make Python work for you and look even more like plain English.

3. [a doublet](03_Lesson03_doublet.ipynb)—this one is wonderful, because the stream-line pattern gives the flow around a circular cylinder (in two dimensions). You encountered the _D'Alembert paradox_ and hopefully that got you thinking. (If not, go back to that lesson and _think_!)

Did you also do the [assignment](03_Lesson03_Assignment.ipynb)? It shows you how a *distribution of sources* can be used to model potential flow around an  airfoil. This starts looking like aerodynamics!

But what is the most important thing we want from applied aerodynamics? We want to make things fly, of course! And to fly, there must be a force of *aerodynamic lift* to counteract the weight of the object.

In this section of the course, we learn about lift. First, we will compute the flow of a potential vortex. It turns out, vortex circulation and lift are intimately related.

## What's a vortex?

This question is deeper than you might think! The simple answer is that a vortex is motion in circular streamlines. Imagine streamlines that are concentric circles about a given point—what's confusing is that this _does not mean_ that fluid elements are themselves rotating!

In an irrotational vortex, the tangential velocity is constant along a (circular) streamline and inversely proportional to the radius, while the radial velocity is zero. In polar coordinates:

\begin{equation}
u_\theta\left(r,\theta\right) = \frac{\text{constant}}{r}  \quad \text{,} \quad  u_r\left(r,\theta\right) = 0
\end{equation}

The vorticity is zero everywhere, except at the location of the point vortex, where the derivative of $u_\theta$ is infinite.

We introduced the concept of circulation in the first lesson ([Source & Sink](01_Lesson01_sourceSink.ipynb)). Let's use that. Around any circular streamline enclosing the vortex, and using the sign convention that a negative vortex circulates anti-clockwise, we have:

\begin{equation}\Gamma = -\oint \mathbf{v}\cdot d\vec{l} = -u_\theta 2 \pi r\end{equation}

Thus, the constant in the expression for $u_\theta$ in $(1)$ is equal to $\Gamma/2\pi$, and we now write:

\begin{equation}u_\theta\left(r,\theta\right) = \frac{\Gamma}{2\pi r}\end{equation}

We can get the stream function by integrating the velocity components:

\begin{equation}\psi\left(r,\theta\right) = \frac{\Gamma}{2\pi}\ln r\end{equation}

In Cartesian coordinates, the stream function is

\begin{equation}\psi\left(x,y\right) = \frac{\Gamma}{4\pi}\ln\left(x^2+y^2\right)\end{equation}

while the velocity components would be:

\begin{equation}u\left(x,y\right) = \frac{\Gamma}{2\pi}\frac{y}{x^2+y^2} \qquad v\left(x,y\right) = -\frac{\Gamma}{2\pi}\frac{x}{x^2+y^2}\end{equation}

This vortex flow is irrotational everywhere, except at the vortex center, where it is infinite. The strength of the point vortex is equal to the circulation $\Gamma$ around it.

## Let's compute a vortex

The set-up is the same as before: we load our favorite libraries, and we create a grid of points to evaluate the velocity field.

```python
import numpy
import math
from matplotlib import pyplot
# embed the figures into the notebook
%matplotlib inline
```

```python
N = 50                                # Number of points in each direction
x_start, x_end = -2.0, 2.0            # x-direction boundaries
y_start, y_end = -1.0, 1.0            # y-direction boundaries
x = numpy.linspace(x_start, x_end, N)    # computes a 1D-array for x
y = numpy.linspace(y_start, y_end, N)    # computes a 1D-array for y
X, Y = numpy.meshgrid(x, y)              # generates a mesh grid
```

Give your vortex a strength $\Gamma=5$ and place it at the center of your domain:

```python
gamma = 5.0                      # strength of the vortex
x_vortex, y_vortex = 0.0, 0.0    # location of the vortex
```

We will define two functions,

* `get_velocity_vortex()` and
* `get_stream_function_vortex()`,

to compute the velocity components and the stream function on our Cartesian grid, given the strength and the location of the vortex. Then, we will use our custom functions to evaluate everything on the grid points. Let's write those functions first.

```python
def get_velocity_vortex(strength, xv, yv, X, Y):
    """
    Returns the velocity field generated by a vortex.

    Parameters
    ----------
    strength: float
        Strength of the vortex.
    xv: float
        x-coordinate of the vortex.
    yv: float
        y-coordinate of the vortex.
    X: 2D Numpy array of floats
        x-coordinate of the mesh points.
    Y: 2D Numpy array of floats
        y-coordinate of the mesh points.

    Returns
    -------
    u: 2D Numpy array of floats
        x-component of the velocity vector field.
    v: 2D Numpy array of floats
        y-component of the velocity vector field.
    """
    u = + strength/(2*math.pi)*(Y-yv)/((X-xv)**2+(Y-yv)**2)
    v = - strength/(2*math.pi)*(X-xv)/((X-xv)**2+(Y-yv)**2)

    return u, v
```

```python
def get_stream_function_vortex(strength, xv, yv, X, Y):
    """
    Returns the stream-function generated by a vortex.

    Parameters
    ----------
    strength: float
        Strength of the vortex.
    xv: float
        x-coordinate of the vortex.
    yv: float
        y-coordinate of the vortex.
    X: 2D Numpy array of floats
        x-coordinate of the mesh points.
    Y: 2D Numpy array of floats
        y-coordinate of the mesh points.

    Returns
    -------
    psi: 2D Numpy array of floats
        The stream-function.
    """
    psi = strength/(4*math.pi)*numpy.log((X-xv)**2+(Y-yv)**2)

    return psi
```

An now, call the functions with the vortex strength and position, plus the coordinates of the evaluation grid, to get the velocity and streamfunction of the vortex.

```python
# compute the velocity field on the mesh grid
u_vortex, v_vortex = get_velocity_vortex(gamma, x_vortex, y_vortex, X, Y)

# compute the stream-function on the mesh grid
psi_vortex = get_stream_function_vortex(gamma, x_vortex, y_vortex, X, Y)
```

We are now able to visualize the streamlines of a vortex, and they look like concentric circles around the vortex center, as expected.

```python
# plot the streamlines
size = 10
pyplot.figure(figsize=(size, (y_end-y_start)/(x_end-x_start)*size))
pyplot.xlabel('x', fontsize=16)
pyplot.ylabel('y', fontsize=16)
pyplot.xlim(x_start, x_end)
pyplot.ylim(y_start, y_end)
pyplot.streamplot(X, Y, u_vortex, v_vortex, density=2, linewidth=1, arrowsize=1, arrowstyle='->')
pyplot.scatter(x_vortex, y_vortex, color='#CD2305', s=80, marker='o');
```

## Vortex & Sink

For fun, let's use our superposition powers. Add a vortex to a sink, using two new functions to compute the velocity components and the stream function of the sink, and adding to those of a vortex (remember that the sink can be easily replaced by a source by just changing the sign of the strength).

```python
strength_sink = -1.0            # strength of the sink
x_sink, y_sink = 0.0, 0.0       # location of the sink
```

```python
def get_velocity_sink(strength, xs, ys, X, Y):
    """
    Returns the velocity field generated by a sink.

    Parameters
    ----------
    strength: float
        Strength of the sink.
    xs: float
        x-coordinate of the sink.
    ys: float
        y-coordinate of the sink.
    X: 2D Numpy array of floats
        x-coordinate of the mesh points.
    Y: 2D Numpy array of floats
        y-coordinate of the mesh points.

    Returns
    -------
    u: 2D Numpy array of floats
        x-component of the velocity vector field.
    v: 2D Numpy array of floats
        y-component of the velocity vector field.
    """
    u = strength/(2*math.pi)*(X-xs)/((X-xs)**2+(Y-ys)**2)
    v = strength/(2*math.pi)*(Y-ys)/((X-xs)**2+(Y-ys)**2)

    return u, v
```

```python
def get_stream_function_sink(strength, xs, ys, X, Y):
    """
    Returns the stream-function generated by a sink.

    Parameters
    ----------
    strength: float
        Strength of the sink.
    xs: float
        x-coordinate of the sink.
    ys: float
        y-coordinate of the sink.
    X: 2D Numpy array of floats
        x-coordinate of the mesh points.
    Y: 2D Numpy array of floats
        y-coordinate of the mesh points.

    Returns
    -------
    psi: 2D Numpy array of floats
        The stream-function.
    """
    psi = strength/(2*math.pi)*numpy.arctan2((Y-ys), (X-xs))

    return psi
```

```python
# compute the velocity field on the mesh grid
u_sink, v_sink = get_velocity_sink(strength_sink, x_sink, y_sink, X, Y)

# compute the stream-function on the mesh grid
psi_sink = get_stream_function_sink(strength_sink, x_sink, y_sink, X, Y)
```

Now, let's visualize the streamlines of the vortex-sink, and admire our artistic creation:

```python
# superposition of the sink and the vortex
u = u_vortex + u_sink
v = v_vortex + v_sink
psi = psi_vortex + psi_sink

# plot the streamlines
size = 10
pyplot.figure(figsize=(size, (y_end-y_start)/(x_end-x_start)*size))
pyplot.xlabel('x', fontsize=16)
pyplot.ylabel('y', fontsize=16)
pyplot.xlim(x_start, x_end)
pyplot.ylim(y_start, y_end)
pyplot.streamplot(X, Y, u, v, density=2, linewidth=1, arrowsize=1, arrowstyle='->')
pyplot.scatter(x_vortex, y_vortex, color='#CD2305', s=80, marker='o');
```

Very cool op-art. (And a good model for your typical bath-tub vortex.) But is all this useful? Yes!

First, you will take your training wheels off and—on your own—compute the flow of an [infinite row of vortices](05_Lesson05_InfiniteRowOfVortices.ipynb). Your mission, should you choose to accept it, is in the next notebook.

After that, we will learn about the connection between a vortex, and the force of lift. This turns out to be very important in aerodynamics!

## What's this "irrotational" vortex thing?

I know what you are thinking.

What does it mean that the vortex is *irrotational*? Surely if there's a vortex, there's rotation!

You are not crazy. It's natural to think this way, but the potential vortex is a flow where streamlines are circular, yet fluid elements do not rotate around themselves—they just go around the circular path.

This classic video will help you understand ... just watch the 25 seconds of video after time 4m 25s, and see a "vorticity meter" go around a free vortex without rotating itself.

```python
from IPython.display import YouTubeVideo
from datetime import timedelta

start=int(timedelta(hours=0, minutes=4, seconds=25).total_seconds())

YouTubeVideo("loCLkcYEWD4", start=start)
```

Remember: vorticity measures the local angular velocity of each fluid element. If the fluid elements go around a circular path, but do not spin themselves, there is no vorticity!

This animation from [Wikipedia](http://en.wikipedia.org/wiki/Vortex#Irrotational_vortices) helps further illustrate what happens in an irrotational vortex: the orange markers with a line across them are going around in circles, but they are not themselves rotating (notice the white lines keep their orientation).

## Next ...

The next IPython Notebook of the *AeroPython* series presents the exercise ["Infinite row of vortices,"](05_Lesson05_InfiniteRowOfVortices.ipynb) as independent student work.

---
Please ignore the cell below. It just loads our style for the notebook.

```python
from IPython.core.display import HTML
def css_styling():
    styles = open('../styles/custom.css', 'r').read()
    return HTML(styles)
css_styling()
```

<link href='http://fonts.googleapis.com/css?family=Fenix' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400' rel='stylesheet' type='text/css'>
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 750px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1000px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
margin-top:0.8em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Alegreya Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:600px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Alegreya Sans', sans-serif;
    font-style:regular;
    font-weight: 200;
    font-size: 50pt;
    line-height: 100%;
    color:#CD2305;
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}
.text_cell_render h2 {
    font-family: 'Fenix', serif;
    font-size: 22pt;
    line-height: 100%;
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Fenix', serif;
    margin-top:12px;
	font-size: 16pt;
    margin-bottom: 3px;
    font-style: regular;
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Fenix', serif;
    font-size: 2pt;
    text-align: center;
    margin-top: 0em;
    margin-bottom: 2em;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Alegreya Sans', sans-serif;
    font-weight: 300;
    font-size: 16pt;
    color: #CD2305;
    font-style: italic;
    margin-bottom: .5em;
    margin-top: 0.5em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'Source Code Pro', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

    .CodeMirror{
            font-family: "Source Code Pro";
			font-size: 90%;
    }
/*    .prompt{
        display: None;
    }*/

    .warning{
        color: rgb( 240, 20, 20 )
        }
</style>


```python
%matplotlib inline
import pandas as pd

import numpy as np
from __future__ import division
import itertools

import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams['axes.grid'] = False

import logging
logger = logging.getLogger()
```

5 Link Analysis
========

### 5.1 PageRank
#### 5.1.1 Eearly Search Engines and Term Spam
**inverted index**:
a data structure that makes it easy to find all the palces where that a term given occurs.

**term spam**:
techniques for fooling search engines.

To combat term spam, Google introduced two innovations:

1. PageRank was used to simulate where Web surfers.  $\gets$ random surfer.

2. Use those links that links to the page.  $\gets$ "vote with their feet."

#### 5.1.2 Definiton of PageRank
intent: the higher the PageRank of a page, the more "important" it is.

**transition matrix**:
to describe what happens to random surfers after one stop.
$m_{ij} = \frac{1}{k}$ if page $j$ has $k$ arcs out, and one of them is to page $i$.
Otherwise, $m_{ij} = 0$.

```python
plt.imshow(plt.imread('./res/fig_5_1.png'))
```

```python
plt.imshow(plt.imread('./res/eg_5_1.png'))
```

PageRank $v$ simulate random surfers:

1. start at a random page of all $n$.
   $v_i^0 = \frac{1}{n} \quad i = 1, 2, \dotsc, n$.

2. randomly choose next page linked.
   $v^{k+1} = M v^{k}$
   give us the distribution of the surfer after $k+1$ stpes.

```python
# eg. 5.1
matrix_5_1 = np.array([
                [0, 1/3, 1/3, 1/3],
                [1/2, 0, 0, 1/2],
                [1, 0, 0, 0],
                [0, 1/2, 1/2, 0]
             ]).T

matrix_5_1
```

    array([[ 0.        ,  0.5       ,  1.        ,  0.        ],
           [ 0.33333333,  0.        ,  0.        ,  0.5       ],
           [ 0.33333333,  0.        ,  0.        ,  0.5       ],
           [ 0.33333333,  0.5       ,  0.        ,  0.        ]])

```python
n = matrix_5_1.shape[1]

v = np.ones((n,1)) / n
v
```

    array([[ 0.25],
           [ 0.25],
           [ 0.25],
           [ 0.25]])

```python
def dist_after_surfing(M, v=None, steps=1):
    if v is None:
        n = M.shape[1]
        v = np.ones((n,1)) / n

    for __ in xrange(steps):
        v = M.dot(v)

    return v

steps = 1
v_ = dist_after_surfing(matrix_5_1, v, steps)
v_
```

    array([[ 0.375     ],
           [ 0.20833333],
           [ 0.20833333],
           [ 0.20833333]])

**Markov processes**:
It is known that the distribution of the surfer approaches a limiting distribution $v$ that satisfies $v = Mv$, provided two conditions are met:

1. The graph is trongly connnected.
   namely, it is possible to get from any node to any other node.

2. There are no dead ends.

eigenvalue and eigenvector: $Mx = \lambda x$
$v = x : \lambda = 1 \text{ and } \sum_j x_{ij} = 1 \text{ for every } i$.

$v$ is a [stochastic vector](http://www.math.harvard.edu/~knill/teaching/math19b_2011/handouts/lecture33.pdf), and $M$ is a [stochastic/Markov matrix](https://en.wikipedia.org/wiki/Stochastic_matrix).

**Iteration** $O(M^2)$ is better than **Gaussian elimination** $O(M^3)$.

in practice, for the Web itself, 50-75 itreations are sufficient to converge to within the error limits of double-precision arithmetic.

```python
# eg 5.2
v_ = dist_after_surfing(matrix_5_1, v, 10)
v_
```

    array([[ 0.33325195],
           [ 0.22224935],
           [ 0.22224935],
           [ 0.22224935]])

```python
v_ = dist_after_surfing(matrix_5_1, v, 50)
v_
```

    array([[ 0.33333333],
           [ 0.22222222],
           [ 0.22222222],
           [ 0.22222222]])

```python
v_ = dist_after_surfing(matrix_5_1, v, 75)
v_
```

    array([[ 0.33333333],
           [ 0.22222222],
           [ 0.22222222],
           [ 0.22222222]])

#### 5.1.3 Structure of the Web
Some structures in reality violate the assumptions needed for the Markov-process iteration to converge to a limit.

```python
plt.figure(figsize = (10,10))
plt.imshow(plt.imread('./res/fig_5_2.png'))
```

**Two problems** we need to avoid by modifing PageRank:

1. the dead end.

2. spider traps.
   the groups of pages that all have outlinks but they never link to any other pages.

#### 5.1.4 Avoiding Dead Ends
Dead Ends $\to$ $M$ is no longer stochastic, since some of the columns will sum to 0 rather than 1.

If we compute $M^iv$ and $M$ is substochastic, then some or all of the components of the vector go to 0.

```python
# eg 5.3
plt.imshow(plt.imread('./res/fig_5_3.png'))
```

```python
M = np.array([
        [0, 1/3, 1/3, 1/3],
        [1/2, 0, 0, 1/2],
        [0, 0, 0, 0],
        [0, 1/2, 1/2, 0]
    ]).T

M
```

    array([[ 0.        ,  0.5       ,  0.        ,  0.        ],
           [ 0.33333333,  0.        ,  0.        ,  0.5       ],
           [ 0.33333333,  0.        ,  0.        ,  0.5       ],
           [ 0.33333333,  0.5       ,  0.        ,  0.        ]])

```python
dist_after_surfing(M, v, 50)
```

    array([[  2.55407417e-08],
           [  3.72237693e-08],
           [  3.72237693e-08],
           [  3.72237693e-08]])

##### Two Solutions
###### 1. Drop the dead end
   recursive deletion of dead ends, and solve the remaining graph $G'$.
   then we restore $G$ from $G'$, recursivly.
   $$ e = \sum \frac{v_p}{k_p} $$
   where $e \in (G - G')$, $p \in G'$ and is the predecessor of $e$, $k$ is the number of successors of $p$ in $G$.

```python
# eg 5.4
plt.imshow(plt.imread('./res/fig_5_4.png'))
```

```python
M_G = np.array([
        [0, 1/3, 1/3, 1/3, 0],
        [1/2, 0, 0, 1/2, 0],
        [0, 0, 0, 0, 1],
        [0, 1/2, 1/2, 0, 0],
        [0, 0, 0, 0, 0]
    ]).T
M_G
```

    array([[ 0.        ,  0.5       ,  0.        ,  0.        ,  0.        ],
           [ 0.33333333,  0.        ,  0.        ,  0.5       ,  0.        ],
           [ 0.33333333,  0.        ,  0.        ,  0.5       ,  0.        ],
           [ 0.33333333,  0.5       ,  0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  1.        ,  0.        ,  0.        ]])

```python
from sklearn.preprocessing import normalize

index = [0, 1, 3]
M = M_G.take(index, axis=0).take(index, axis=1)
M = normalize(M, norm='l1', axis=0)
M
```

    array([[ 0. ,  0.5,  0. ],
           [ 0.5,  0. ,  1. ],
           [ 0.5,  0.5,  0. ]])

```python
n = M.shape[1]
v = np.ones((n,1)) / n

v_ = dist_after_surfing(M, v, 50)
v_
```

    array([[ 0.22222222],
           [ 0.44444444],
           [ 0.33333333]])

```python
# the PageRank of C, whose predecessors are A and D.
1/3 * v_[0] + 1/2 * v_[2]
```

    array([ 0.24074074])

```python
# the PageRank of D, which is the only one successor of C.
# hence, it is the same with C.
_
```

    array([ 0.24074074])

###### 2. modify the process of moving
"taxation"

#### 5.1.5 Spider Traps and Taxation
**spider traps**:
a set of nodes with no dead ends but no arcs out.
they cause the PageRank calculation to place all the weights within the spider traps.

```python
plt.imshow(plt.imread('./res/fig_5_6.png'))
```

```python
M = np.array([
        [0, 1/3, 1/3, 1/3],
        [1/2, 0, 0, 1/2],
        [0, 0, 1, 0],
        [0, 1/2, 1/2, 0]
    ]).T
M
```

    array([[ 0.        ,  0.5       ,  0.        ,  0.        ],
           [ 0.33333333,  0.        ,  0.        ,  0.5       ],
           [ 0.33333333,  0.        ,  1.        ,  0.5       ],
           [ 0.33333333,  0.5       ,  0.        ,  0.        ]])

```python
np.round(dist_after_surfing(M, steps=50), 3)
```

    array([[ 0.],
           [ 0.],
           [ 1.],
           [ 0.]])

**Solution**:
allow each random surfer a small probability of teleporting to a random page.
$$v = \beta M v + (1 - \beta) \frac{e}{n}$$
where $n$ is the number of nodes in $G$, and $e$ is a vector of all 1's.

```python
def dist_using_taxation(M, v=None, beta=1, steps=1):
    n = M.shape[1]

    if v is None:
        v = np.ones((n,1)) / n

    e = np.ones(v.shape)
    for __ in xrange(steps):
        v = beta * M.dot(v) + (1-beta) * e / n

    return v

dist_using_taxation(M, beta=0.8, steps=30)
```

    array([[ 0.10135136],
           [ 0.12837839],
           [ 0.64189186],
           [ 0.12837839]])

Although C gets more than half of the PageRank for itself, the effect has been limited.

**Note** that for a random surfer, there are **three path** to move:

1. follow a link.

2. teleport to a random page. $\gets$ taxation

3. goes nowhere. $\gets$ dead ends

Since there will always be some fraction of a surfer operating on the Web, so even if there are dead ends, the sum of the ocmponents of $v$ may be less than 1, but it will never reacher 0.

#### 5.1.6 Using PageRank in a Search Engine
1. find the qualified pages, which have at least one of the search terms in the query.

2. calculate a score for those pages, including PageRank.

#### 5.1.7 Exercises

###### 5.1.1
略

###### 5.1.2
略

###### 5.1.3
$n$ nodes: 1/n
the additional node: n * 1/n * 1/n = 1/n

###### 5.1.4
todo

###### 5.1.5
略

###### 5.1.6
the first node: 1
the left nodes: 1/2

###### 5.1.7
root: 1
height = 1:  1/3
height = 2:  1/3 * 1/2
height = k:  $\frac{1}{3} \times (\frac{1}{2})^{k-1}, k > 1$

### 5.2 Efficient Computation of PageRank
PageRank: matrix-vector multiplicaton $\to$ MapReduce.
However, we must deal with two issues:

1. $M$ is very sparse.
   list its nonzero elements.

2. we may wish to use a combiner to reduce the amount of data (Map $\to$ Reduce).
   striping approach.

#### 5.2.1 Representing Transition Matrices
The proper way to represent any sparse matrix is to list the **locations** and **values** of the nonzero entries.
The space needed is linear in the number of nonzero entries.

Represent a column by:
    1. one integer for the out-degree,
    2. one integer for rowname per nonzero entry.

```python
matrix_5_1
```

    array([[ 0.        ,  0.5       ,  1.        ,  0.        ],
           [ 0.33333333,  0.        ,  0.        ,  0.5       ],
           [ 0.33333333,  0.        ,  0.        ,  0.5       ],
           [ 0.33333333,  0.5       ,  0.        ,  0.        ]])

```python
import string

df_M = pd.DataFrame(matrix_5_1, index=list(string.uppercase[0:4]), columns=list(string.uppercase[0:4]))
df_M
```

      A
      B
      C
      D

      A
      0.000000
      0.5
      1
      0.0

      B
      0.333333
      0.0
      0
      0.5

      C
      0.333333
      0.0
      0
      0.5

      D
      0.333333
      0.5
      0
      0.0

```python
def compact_representation_of_sparse_matrix(df):
    """It is introduced in Example 5.7"""

    degree = df.apply(np.count_nonzero, axis=0)

    dest = df.apply(np.nonzero, axis=0)
    dest = dest.apply(lambda x: x[0])

    return pd.concat([degree, dest], axis=1, keys=['Degree', 'Destinations'])

compact_representation_of_sparse_matrix(df_M)
```

      Degree
      Destinations

      A
      3
      [1, 2, 3]

      B
      2
      [0, 3]

      C
      1
      [0]

      D
      2
      [1, 2]

#### 5.2.2 PageRank Iteration Using MapReduce
estimate: $$v' = \beta M v + (1 - \beta) \frac{e}{n}$$

if $v$ is much too large to fit in main memory, we could use the method of triping:
break $M $into stripes and break $v$ into corresponding horizontal stripes.

#### 5.2.3 Use of Combiners to Consolidate the Result Vector
There are two reasons the method of Section 5.2.2 might not be adequate:

1. We might wish to add term for $v'_i$.

2. We might execute the iteration step at a single machine or a collection of machines instead.

Hence, we need to implement a combiner in conjunction with a Map task.

**thrashing**:
repeatly read some components of $v'$.
An alternative strategy is based on partitioning $M$ into $k^2$ blocks.

```python
plt.imshow(plt.imread('./res/fig_5_12.png'))
```

Each task gets $M_{ij}$ and $v_j$.
Thus, $v$ is transmitted over the network $k$ times, but $M$ is sent only once.

The advantage of this approach is:
We can keep both the $v_j$ and $v'_i$ in main memory as we process $M_{ij}$.

####  5.2.4 Representing Blocks of the Transition Matrix
for each columns of $M_{ij}$, we need list its out-degree and rows of nonzero entry.
And it might lead to that the out-degree will be repeated as many times as the out-degree itself.

```python
plt.figure(figsize=(10,10))
plt.imshow(plt.imread('./res/fig_5_14.png'))

#todo
```

#### 5.2.5 Other Efficient Approaches to PageRank Iteration
We can assign all the blocks of one row of blocks to a single Map task,
namely, use blocks $M_{i1}$ through $M_{ik}$ and all of $v$ to compute $v'i$.

#### 5.2.6 Exercise

###### ex 5.2.1
the fraction of 1's should be less than $\frac{log_2 n}{n}$.

###### ex 5.2.2
略

###### ex 5.2.3
略

###### ex 5.2.4
k = n

### 5.3 Topic-Sensitive PageRank
###### improvement
we can weight certain pages more heavily because of their topic.

###### motivation
1. Different people have different interests, and sometimes distinct interests are expressed using the same term in a query.

2. It is not feasible to build a private PageRank vector for each user.

**solution**:
create one vector for each of some small number of topics, biasing the PageRank to favor pages of that topic,
and classif users accroding to the degree of their interests in each of the selected topics.

pons: only store a short vector for each user.
cons: lose some accuracy.

_note_: One useful topic set is the 16 top-level categories of the Open Directory ([DMOZ](www.dmoz.org)).

#### 5.3.2 Biased Random Walks
**Intuition**:
Pages linked to by sports pages are themselves likely to be about sports.

The only difference is how we add the new surfers:
$$v' = \beta M v + (1 - \beta) \frac{e_s}{|S|}$$
where $S$ is the teleport set (the pages identified as belonging to certain topic), and $e_s$ is a vector that has 1 in the components in $S$ and 0 in other components.

```python
plt.imshow(plt.imread('./res/fig_5_15.png'))
```

```python
beta = 0.8
M = matrix_5_1
S = ['B', 'D']

e_s = pd.Series(np.zeros(4), index=list(string.uppercase[0:4]))
for s in S:
    e_s[s] = 1

e_s
```

    A    0
    B    1
    C    0
    D    1
    dtype: float64

```python
M
```

    array([[ 0.        ,  0.5       ,  1.        ,  0.        ],
           [ 0.33333333,  0.        ,  0.        ,  0.5       ],
           [ 0.33333333,  0.        ,  0.        ,  0.5       ],
           [ 0.33333333,  0.5       ,  0.        ,  0.        ]])

```python
print('v = \n{} v \n+ \n{}'.format(beta*M, (1-beta)*e_s/np.sum(e_s)))
```

    v =
    [[ 0.          0.4         0.8         0.        ]
     [ 0.26666667  0.          0.          0.4       ]
     [ 0.26666667  0.          0.          0.4       ]
     [ 0.26666667  0.4         0.          0.        ]] v
    +
    A    0.0
    B    0.1
    C    0.0
    D    0.1
    dtype: float64

#### 5.3.3 Using Topic-Sensitive PageRank
###### Integrate topic-sensitive PageRank into a search engine
1. Select topics

2. Pick a teleport set for each topic, and compute the topic-sensitive PageRank vector.

3. Find a way of determining the topic of a particular search query.

4. Use the corresponding topic-sensitive PageRank to respond.

###### How to  select topics
+ use the top-level topics of the Open Directory.

+ Allow the user to select a topic from a menu.

+ Infer the topics by the words in search query recently.

+ Infer the topics by information about the user.

#### 5.3.4 Inferring Topics from Words
**Idea**:
Topics are characterized by words that appear surprisingly often in documents on that topic.

Be **careful**:
avoid some extremely rare word (eg: misspell) $gets$ put a floor on the number of times a word appears.

It is important to pick reasonably large set $S$ to make sure that we cover all aspects of the topic represented by the set.

We could also blend the topic-sensitive PageRank vectors to reflect user's hybird interests.

### 5.4 Link Spam
**link spam**: the techniques for artificially increasing the PageRank of a page.

**spam farm**: a collection of pages whose purpose is to increase the PageRank of a certain page or pages.

#### 5.4.1 Architecture of a Spam Farm

```python
plt.figure(figsize=(10,10))
plt.imshow(plt.imread('./res/fig_5_16.png'))
```

Links from Accessible Pages: comments contained spam link in blog or news sites.

#### 5.4.2 Analysis of a Spam Farm
Given: $n$ pages in total, $m$ support pages, $y$ is the PageRank of target page $t$.

then:
the PageRank of each support page:
$$\frac{\beta y}{m} + \frac{1 - \beta}{n}$$

support $x$ is the contribute of all accessible pages for target page.
we can get the PageRank $p$ of targe page:

\begin{align}
    p &= x + \beta (\frac{\beta y}{m} + \frac{1 - \beta}{n}) m  + \frac{1 - \beta}{n} \\
      &\approx x + \beta (\frac{\beta y}{m} + \frac{1 - \beta}{n}) m  \\
      &= x + \beta^2 y + \beta (1-\beta) \frac{m}{n}
\end{align}

so we can solve $y$:
$$y = \frac{x}{1-\beta^2} + c \frac{m}{n}$$
where $c = \frac{\beta}{1+\beta}$.

```python
beta = 0.85
x_coe = 1 / (1 - beta**2)
c = beta / (1+beta)

print('y = {} x + {} m/n'.format(x_coe, c))
```

    y = 3.6036036036 x + 0.459459459459 m/n

$\beta = 0.85$ 时，放大 3.6 倍 $x$，同时吸收了 46% 的 $m/n$。

#### 5.4.3 Combating Link Spam
Two Solutions:

1. Identify the structure of spam farm.
   cons: spammer can modify and develop new spam farm. The war is no end.

2. Use infomation (trustyworthy pages) to help judge.
   + TrustRank
   + Spam mass

##### TrustRank
topic-sensitive PageRank, and the topic is a set of pages believed to be trustworthy (not spam).

**theory**:
while a spam page might easily be mad to link to a trustworthy page, it is unlikely that a trustworthy page would link to a spam page.

**develop teleport set of trustworthy pages**:
exclude blog, news sites

+ Let human examine and decide.
  eg: pick the pages of highest PageRank to examine.

+ Pick a domain whose membership is controlled.
  eg: .edu, .gov, .edu.cn

##### Spam Mass
**idea**: we measure for each page that fraction of its PageRank that comes from spam.

Suppose page $p$ has PageRank $r$ and TrustRank $t$, then the spam mass of $p$ is $$sm(p) = \frac{r-t}{r}$$.

if $sm(p)$ is close to 1, then $p$ is probably spam.
if $sm(p)$ is negative or small positive, then $p$ might not spam.

#### 5.4.6 Exercise for Section 5.4
###### 5.4.1
(a)
targe page $y$: $y = x + \frac{1-\beta}{n}$
support page $z$:

\begin{align}
    z &= \beta(\frac{y}{m} + z) + \frac{1-\beta}{n} \\
      &= \frac{\beta}{(1-\beta) m} y + \frac{1}{n}
\end{align}

(b)
targe page $y$: $y = x + \frac{1-\beta}{n}$
support page $z$:  $$z = \beta(\frac{y}{m}) + \frac{1-\beta}{n}$$

(c)
targe page $y$: $$y = x + \beta (\frac{z}{2}) m + \frac{1-\beta}{n}$$
support page $z$:  $$z = \beta(\frac{y}{m} + \frac{z}{2}) + \frac{1-\beta}{n}$$

then solve $y$ and $z$. 略

###### 5.4.2
略

###### 5.4.3
`#todo`

### 5.5 Hubs and Authorities (HITS, hyperlink-induced topic search)
used by the Ask search eigine.

#### 5.5.1 The Intuition behind HITS
Two flavors of importances:

1. authorities: provide information about a topic.

2. hubs: tell you where to go to find out about that topic.

PageRank: a page is important if important pages link to it.
HITS: a page is a good hub if it links to good authorities, and a page is a good authority if it is linked to by good hubs.

#### Formalizing Hubbiness and Authority
$h$ - hubbiness vector, $a$ - authority vector.

In case of growing beyond bounds, we normally scale the values of $h$ and $a$: force either its largest component is 1 or the sum of components is 1.

**link matrix**: $L_{ij} = 1$ if page $i$ links page $j$.

We have: $$h = \lambda L a$$
then: $$a = u L^T h$$

By substituting:
\begin{align}
    h &= \lambda u L L^T h \\
    a &= \lambda u L^T L a
\end{align}
Since $L L^T$ and $L^T L$ are not as sparse as $L$ and $L^T$, for web-sized graphs, the only way of computing the solution is **iteratively**:
1. start with $h$ a vector of all 1's.
2. Compute $a = L^T h$ and then scale so the largest component is 1.
3. Next, compute $h = L a$ and scale again.
4. Repeat step 2-3 until statisfication.

#### 5.5.3 Exercises for Section 5.5
略