## Configurations for Colab

```python
import sys
IN_COLAB = "google.colab" in sys.modules

if IN_COLAB:
    !apt install python-opengl
    !apt install ffmpeg
    !apt install xvfb
    !pip install pyvirtualdisplay
    !pip install gym
    from pyvirtualdisplay import Display

    # Start virtual display
    dis = Display(visible=0, size=(400, 400))
    dis.start()
```

# 05. Noisy Networks for Exploration

[M. Fortunato et al., "Noisy Networks for Exploration." arXiv preprint arXiv:1706.10295, 2017.](https://arxiv.org/pdf/1706.10295.pdf)

NoisyNet is an exploration method that learns perturbations of the network weights to drive exploration. The key insight is that a single change to the weight vector can induce a consistent, and potentially very complex, state-dependent change in policy over multiple time steps.

Firstly, let's take a look into a linear layer of a neural network with $p$ inputs and $q$ outputs, represented by

$$
y = wx + b,
$$

where $x \in \mathbb{R}^p$ is the layer input, $w \in \mathbb{R}^{q \times p}$, and $b \in \mathbb{R}$ the bias.

The corresponding noisy linear layer is defined as:

$$
y = (\mu^w + \sigma^w \odot \epsilon^w) x + \mu^b + \sigma^b \odot \epsilon^b,
$$

where $\mu^w + \sigma^w \odot \epsilon^w$ and $\mu^b + \sigma^b \odot \epsilon^b$ replace $w$ and $b$ in the first linear layer equation. The parameters $\mu^w \in \mathbb{R}^{q \times p}, \mu^b \in \mathbb{R}^q, \sigma^w \in \mathbb{R}^{q \times p}$ and $\sigma^b \in \mathbb{R}^q$ are learnable, whereas $\epsilon^w \in \mathbb{R}^{q \times p}$ and $\epsilon^b \in \mathbb{R}^q$ are noise random variables which can be generated by one of the following two ways:

1. **Independent Gaussian noise**: the noise applied to each weight and bias is independent, where each random noise entry is drawn from a unit Gaussian distribution. This means that for each noisy linear layer, there are $pq + q$ noise variables (for $p$ inputs to the layer and $q$ outputs).
2. **Factorised Gaussian noise:** This is a more computationally efficient way. It produces 2 random Gaussian noise vectors ($p, q$) and makes $pq + q$ noise entries by outer product as follows:

$$
\begin{align}
\epsilon_{i,j}^w &= f(\epsilon_i) f(\epsilon_j),\\
\epsilon_{j}^b &= f(\epsilon_i),\\
\text{where } f(x) &= sgn(x) \sqrt{|x|}.
\end{align}
$$

In all experiements of the paper, the authors used Factorised Gaussian noise, so we will go for it as well.

```python
import math
import os
from typing import Dict, List, Tuple

import gym
import matplotlib.pyplot as plt
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from IPython.display import clear_output
```

## Replay buffer

Please see *01.dqn.ipynb* for detailed description.

```python
class ReplayBuffer:
    """A simple numpy replay buffer."""

    def __init__(self, obs_dim: int, size: int, batch_size: int = 32):
        self.obs_buf = np.zeros([size, obs_dim], dtype=np.float32)
        self.next_obs_buf = np.zeros([size, obs_dim], dtype=np.float32)
        self.acts_buf = np.zeros([size], dtype=np.float32)
        self.rews_buf = np.zeros([size], dtype=np.float32)
        self.done_buf = np.zeros(size, dtype=np.float32)
        self.max_size, self.batch_size = size, batch_size
        self.ptr, self.size, = 0, 0

    def store(
        self,
        obs: np.ndarray,
        act: np.ndarray,
        rew: float,
        next_obs: np.ndarray,
        done: bool,
    ):
        self.obs_buf[self.ptr] = obs
        self.next_obs_buf[self.ptr] = next_obs
        self.acts_buf[self.ptr] = act
        self.rews_buf[self.ptr] = rew
        self.done_buf[self.ptr] = done
        self.ptr = (self.ptr + 1) % self.max_size
        self.size = min(self.size + 1, self.max_size)

    def sample_batch(self) -> Dict[str, np.ndarray]:
        idxs = np.random.choice(self.size, size=self.batch_size, replace=False)
        return dict(obs=self.obs_buf[idxs],
                    next_obs=self.next_obs_buf[idxs],
                    acts=self.acts_buf[idxs],
                    rews=self.rews_buf[idxs],
                    done=self.done_buf[idxs])

    def __len__(self) -> int:
        return self.size
```

## Noisy Layer

**References:**
- https://github.com/higgsfield/RL-Adventure/blob/master/5.noisy%20dqn.ipynb
- https://github.com/Kaixhin/Rainbow/blob/master/model.py

```python
class NoisyLinear(nn.Module):
    """Noisy linear module for NoisyNet.

    Attributes:
        in_features (int): input size of linear module
        out_features (int): output size of linear module
        std_init (float): initial std value
        weight_mu (nn.Parameter): mean value weight parameter
        weight_sigma (nn.Parameter): std value weight parameter
        bias_mu (nn.Parameter): mean value bias parameter
        bias_sigma (nn.Parameter): std value bias parameter

    """

    def __init__(self, in_features: int, out_features: int, std_init: float = 0.5):
        """Initialization."""
        super(NoisyLinear, self).__init__()

        self.in_features = in_features
        self.out_features = out_features
        self.std_init = std_init

        self.weight_mu = nn.Parameter(torch.Tensor(out_features, in_features))
        self.weight_sigma = nn.Parameter(
            torch.Tensor(out_features, in_features)
        )
        self.register_buffer(
            "weight_epsilon", torch.Tensor(out_features, in_features)
        )

        self.bias_mu = nn.Parameter(torch.Tensor(out_features))
        self.bias_sigma = nn.Parameter(torch.Tensor(out_features))
        self.register_buffer("bias_epsilon", torch.Tensor(out_features))

        self.reset_parameters()
        self.reset_noise()

    def reset_parameters(self):
        """Reset trainable network parameters (factorized gaussian noise)."""
        mu_range = 1 / math.sqrt(self.in_features)
        self.weight_mu.data.uniform_(-mu_range, mu_range)
        self.weight_sigma.data.fill_(
            self.std_init / math.sqrt(self.in_features)
        )
        self.bias_mu.data.uniform_(-mu_range, mu_range)
        self.bias_sigma.data.fill_(
            self.std_init / math.sqrt(self.out_features)
        )

    def reset_noise(self):
        """Make new noise."""
        epsilon_in = self.scale_noise(self.in_features)
        epsilon_out = self.scale_noise(self.out_features)

        # outer product
        self.weight_epsilon.copy_(epsilon_out.ger(epsilon_in))
        self.bias_epsilon.copy_(epsilon_out)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """Forward method implementation.

        We don't use separate statements on train / eval mode.
        It doesn't show remarkable difference of performance.
        """
        return F.linear(
            x,
            self.weight_mu + self.weight_sigma * self.weight_epsilon,
            self.bias_mu + self.bias_sigma * self.bias_epsilon,
        )

    @staticmethod
    def scale_noise(size: int) -> torch.Tensor:
        """Set scale to make noise (factorized gaussian noise)."""
        x = torch.randn(size)

        return x.sign().mul(x.abs().sqrt())
```

## Noisy Network

We use NoisyLinear for the last two FC layers, and there is a method to reset noise at every step.
These are the only differences from the example of *01.dqn.ipynb*.

```python
class Network(nn.Module):
    def __init__(self, in_dim: int, out_dim: int):
        """Initialization."""
        super(Network, self).__init__()

        self.feature = nn.Linear(in_dim, 128)
        self.noisy_layer1 = NoisyLinear(128, 128)
        self.noisy_layer2 = NoisyLinear(128, out_dim)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """Forward method implementation."""
        feature = F.relu(self.feature(x))
        hidden = F.relu(self.noisy_layer1(feature))
        out = self.noisy_layer2(hidden)

        return out

    def reset_noise(self):
        """Reset all noisy layers."""
        self.noisy_layer1.reset_noise()
        self.noisy_layer2.reset_noise()
```

## DQN + NoisyNet Agent (w/o DuelingNet)

Here is a summary of DQNAgent class.

| Method           | Note                                                 |
| ---              | ---                                                  |
|select_action     | select an action from the input state.               |
|step              | take an action and return the response of the env.   |
|compute_dqn_loss  | return dqn loss.                                     |
|update_model      | update the model by gradient descent.                |
|target_hard_update| hard update from the local model to the target model.|
|train             | train the agent during num_frames.                   |
|test              | test the agent (1 episode).                          |
|plot              | plot the training progresses.                        |

In the paper, NoisyNet is used as a component of the Dueling Network Architecture, which includes Double-DQN and Prioritized Experience Replay. However, we don't implement them to simplify the tutorial. One thing to note is that NoisyNet is an alternertive to $\epsilon$-greedy method, so all $\epsilon$ related lines are removed. Please check all comments with *NoisyNet*.

```python
class DQNAgent:
    """DQN Agent interacting with environment.

    Attribute:
        env (gym.Env): openAI Gym environment
        memory (ReplayBuffer): replay memory to store transitions
        batch_size (int): batch size for sampling
        target_update (int): period for target model's hard update
        gamma (float): discount factor
        dqn (Network): model to train and select actions
        dqn_target (Network): target model to update
        optimizer (torch.optim): optimizer for training dqn
        transition (list): transition information including
                           state, action, reward, next_state, done
    """

    def __init__(
        self,
        env: gym.Env,
        memory_size: int,
        batch_size: int,
        target_update: int,
        gamma: float = 0.99,
    ):
        """Initialization.

        Args:
            env (gym.Env): openAI Gym environment
            memory_size (int): length of memory
            batch_size (int): batch size for sampling
            target_update (int): period for target model's hard update
            gamma (float): discount factor
        """
        # NoisyNet: All attributes related to epsilon are removed
        obs_dim = env.observation_space.shape[0]
        action_dim = env.action_space.n

        self.env = env
        self.memory = ReplayBuffer(obs_dim, memory_size, batch_size)
        self.batch_size = batch_size
        self.target_update = target_update
        self.gamma = gamma

        # device: cpu / gpu
        self.device = torch.device(
            "cuda" if torch.cuda.is_available() else "cpu"
        )
        print(self.device)

        # networks: dqn, dqn_target
        self.dqn = Network(obs_dim, action_dim).to(self.device)
        self.dqn_target = Network(obs_dim, action_dim).to(self.device)
        self.dqn_target.load_state_dict(self.dqn.state_dict())
        self.dqn_target.eval()

        # optimizer
        self.optimizer = optim.Adam(self.dqn.parameters())

        # transition to store in memory
        self.transition = list()

        # mode: train / test
        self.is_test = False

    def select_action(self, state: np.ndarray) -> np.ndarray:
        """Select an action from the input state."""
        # NoisyNet: no epsilon greedy action selection
        selected_action = self.dqn(
            torch.FloatTensor(state).to(self.device)
        ).argmax()
        selected_action = selected_action.detach().cpu().numpy()

        if not self.is_test:
            self.transition = [state, selected_action]

        return selected_action

    def step(self, action: np.ndarray) -> Tuple[np.ndarray, np.float64, bool]:
        """Take an action and return the response of the env."""
        next_state, reward, done, _ = self.env.step(action)

        if not self.is_test:
            self.transition += [reward, next_state, done]
            self.memory.store(*self.transition)

        return next_state, reward, done

    def update_model(self) -> torch.Tensor:
        """Update the model by gradient descent."""
        samples = self.memory.sample_batch()

        loss = self._compute_dqn_loss(samples)

        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()

        # NoisyNet: reset noise
        self.dqn.reset_noise()
        self.dqn_target.reset_noise()

        return loss.item()

    def train(self, num_frames: int, plotting_interval: int = 200):
        """Train the agent."""
        self.is_test = False

        state = self.env.reset()
        update_cnt = 0
        losses = []
        scores = []
        score = 0

        for frame_idx in range(1, num_frames + 1):
            action = self.select_action(state)
            next_state, reward, done = self.step(action)

            state = next_state
            score += reward

            # NoisyNet: removed decrease of epsilon

            # if episode ends
            if done:
                state = self.env.reset()
                scores.append(score)
                score = 0

            # if training is ready
            if len(self.memory) >= self.batch_size:
                loss = self.update_model()
                losses.append(loss)
                update_cnt += 1

                # if hard update is needed
                if update_cnt % self.target_update == 0:
                    self._target_hard_update()

            # plotting
            if frame_idx % plotting_interval == 0:
                self._plot(frame_idx, scores, losses)

        self.env.close()

    def test(self) -> List[np.ndarray]:
        """Test the agent."""
        self.is_test = True

        state = self.env.reset()
        done = False
        score = 0

        frames = []
        while not done:
            frames.append(self.env.render(mode="rgb_array"))
            action = self.select_action(state)
            next_state, reward, done = self.step(action)

            state = next_state
            score += reward

        print("score: ", score)
        self.env.close()

        return frames

    def _compute_dqn_loss(self, samples: Dict[str, np.ndarray]) -> torch.Tensor:
        """Return dqn loss."""
        device = self.device  # for shortening the following lines
        state = torch.FloatTensor(samples["obs"]).to(device)
        next_state = torch.FloatTensor(samples["next_obs"]).to(device)
        action = torch.LongTensor(samples["acts"].reshape(-1, 1)).to(device)
        reward = torch.FloatTensor(samples["rews"].reshape(-1, 1)).to(device)
        done = torch.FloatTensor(samples["done"].reshape(-1, 1)).to(device)

        # G_t   = r + gamma * v(s_{t+1})  if state != Terminal
        #       = r                       otherwise
        curr_q_value = self.dqn(state).gather(1, action)
        next_q_value = self.dqn_target(next_state).max(
            dim=1, keepdim=True
        )[0].detach()
        mask = 1 - done
        target = (reward + self.gamma * next_q_value * mask).to(self.device)

        # calculate dqn loss
        loss = F.smooth_l1_loss(curr_q_value, target)

        return loss

    def _target_hard_update(self):
        """Hard update: target <- local."""
        self.dqn_target.load_state_dict(self.dqn.state_dict())

    def _plot(
        self,
        frame_idx: int,
        scores: List[float],
        losses: List[float],
    ):
        """Plot the training progresses."""
        clear_output(True)
        plt.figure(figsize=(20, 5))
        plt.subplot(131)
        plt.title('frame %s. score: %s' % (frame_idx, np.mean(scores[-10:])))
        plt.plot(scores)
        plt.subplot(132)
        plt.title('loss')
        plt.plot(losses)
        plt.show()
```

## Environment

You can see the [code](https://github.com/openai/gym/blob/master/gym/envs/classic_control/cartpole.py) and [configurations](https://github.com/openai/gym/blob/master/gym/envs/__init__.py#L53) of CartPole-v0 from OpenAI's repository.

```python
# environment
env_id = "CartPole-v0"
env = gym.make(env_id)
if IN_COLAB:
    env = gym.wrappers.Monitor(env, "videos", force=True)
```

## Set random seed

```python
seed = 777

def seed_torch(seed):
    torch.manual_seed(seed)
    if torch.backends.cudnn.enabled:
        torch.backends.cudnn.benchmark = False
        torch.backends.cudnn.deterministic = True

np.random.seed(seed)
seed_torch(seed)
env.seed(seed)
```

    [777]

## Initialize

```python
# parameters
num_frames = 20000
memory_size = 10000
batch_size = 128
target_update = 150

# train
agent = DQNAgent(env, memory_size, batch_size, target_update)
```

    cpu

## Train

```python
agent.train(num_frames)
```

## Test

Run the trained agent (1 episode).

```python
frames = agent.test()
```

    score:  200.0

## Render

```python
if IN_COLAB:  # for colab
    import base64
    import glob
    import io
    import os

    from IPython.display import HTML, display

    def ipython_show_video(path: str) -> None:
        """Show a video at `path` within IPython Notebook."""
        if not os.path.isfile(path):
            raise NameError("Cannot access: {}".format(path))

        video = io.open(path, "r+b").read()
        encoded = base64.b64encode(video)

        display(HTML(
            data="""

            """.format(encoded.decode("ascii"))
        ))

    list_of_files = glob.glob("videos/*.mp4")
    latest_file = max(list_of_files, key=os.path.getctime)
    print(latest_file)
    ipython_show_video(latest_file)

else:  # for jupyter
    from matplotlib import animation
    from JSAnimation.IPython_display import display_animation
    from IPython.display import display

    def display_frames_as_gif(frames: List[np.ndarray]) -> None:
        """Displays a list of frames as a gif, with controls."""
        patch = plt.imshow(frames[0])
        plt.axis('off')

        def animate(i):
            patch.set_data(frames[i])

        anim = animation.FuncAnimation(
            plt.gcf(), animate, frames = len(frames), interval=50
        )
        display(display_animation(anim, default_mode='loop'))

    # display
    display_frames_as_gif(frames)
```

    <input id="_anim_sliderJWZOFYPKDTOSUZQQ" type="range" style="width:350px" name="points" min="0" max="1" step="1" value="0" onchange="animJWZOFYPKDTOSUZQQ.set_frame(parseInt(this.value));"></input>

    <button onclick="animJWZOFYPKDTOSUZQQ.slower()">–</button>
    <button onclick="animJWZOFYPKDTOSUZQQ.first_frame()"></button>
    <button onclick="animJWZOFYPKDTOSUZQQ.previous_frame()"></button>
    <button onclick="animJWZOFYPKDTOSUZQQ.reverse_animation()"></button>
    <button onclick="animJWZOFYPKDTOSUZQQ.pause_animation()"></button>
    <button onclick="animJWZOFYPKDTOSUZQQ.play_animation()"></button>
    <button onclick="animJWZOFYPKDTOSUZQQ.next_frame()"></button>
    <button onclick="animJWZOFYPKDTOSUZQQ.last_frame()"></button>
    <button onclick="animJWZOFYPKDTOSUZQQ.faster()">+</button>
  <form action="#n" name="_anim_loop_selectJWZOFYPKDTOSUZQQ" class="anim_control">
    <input type="radio" name="state" value="once" > Once </input>
    <input type="radio" name="state" value="loop" checked> Loop </input>
    <input type="radio" name="state" value="reflect" > Reflect </input>
  </form>


Водопьян А.О. Хабибуллин Р.А. 2019 - 2022 г.

## Содержание:
* [1.1. Давление насыщения нефти газом](#Pb)
    * [1.1.1. Давление насыщения, корреляция Стендинга](#Pb_Standing)
    * [1.1.2. Давление насыщения, корреляция Валко Маккейна](#Pb_Valco)
* [1.2. Газосодержание](№Rs)
    * [1.2.1. Газосодержание, корреляция Стендинга](#Rs_Standing)
    * [1.2.2. Газосодержание, корреляция Веларде-Маккейна](#Rs_McCain)
    * [1.2.3. Оценка объема растворенного газа в сепараторе, для уточнения газосодержания по замерам на сепараторе. Корреляция Маккейна.](#Rsb_McCain)
* [1.3. Объемный коэффициент нефти](#FVF)
    * [1.3.1. Объемный коэффициент нефти при давлении выше давления насыщения](#FVF_above_Pb)

# 1. PVT свойства пластовых флюидов

## 1.1. Давление насыщения нефти газом

### 1.1.1. Давление насыщения, корреляция Стендинга

Корреляция Стендинга (Standing, 1947) для оценки давления насыщения нефти газом.

$$ P_b =  0.5197  \left( \frac{R_{sb}}{\gamma_g}\right)^{0.83} 10 ^{y_g} \tag{1.1.1.1} $$

где

$P_b$ - давление насыщения, $МПа$

$R_{sb}$ - газосодержание при давлении насыщения, $м^3/м^3 $

$\gamma_g$ - относительная плотность газа (по воздуху), безразмерная величина

$y_g$ - мольная доля газа, $ y_g = 1.225 +0.00164 T - \frac{ 1.769}{\gamma_o}$

$\gamma_o$ - относительная плотность нефти (по воде), безразмерная величина

$ T $ - температура, $ ^{\circ}\mathrm{K}$

Корреляции Standing базируются на 105 экспериментально определенных давлениях насыщения нефтяных систем Калифорнии. Диапазоны значений основных свойств, использованных для разработки данной корреляции, приведены в таблице ниже.

|
 Параметр  | Диапазон |
| :--- | :--- |
|
 давление насыщения,$P_b$ , $ МПа $  | 0.896…48.263  |
|
 температура, $^{\circ}\mathrm{K} $ | 310…400 |
|
 газосодержание при давлении насыщения, $R_{sb}$ , $м^3/м^3 $ | 3.6…254 |
|
 относительная плотность нефти по воде, $\gamma_o$ | 0.725…0.956  |
|
 относительная плотность газа, $\gamma_g$  | 0.59…0.95  |

ref "A Pressure-Volume-Temperature Correlation for Mixtures of California Oil and Gases", M.B. Standing, Drill. & Prod. Prac., API, 1947.

```python
import sys
sys.path.append('..')
import neftpy.upvt as pvt
import neftpy.uconvert as uc
```

```python
import matplotlib.pyplot as plt
import numpy as np
import scipy.constants as const
from sympy import *
init_printing()
```

```python
# Корреляция Стендинга для давления насыщения реализована
# в виде функции unf_pb_Standing_MPaa в модуле neftpy.upvt

pvt.unf_pb_Standing_MPaa(rsb_m3m3=100,  gamma_oil=0.86, gamma_gas=0.6, t_K=350)
```

    array(20.1702107)

в приведеном коде использована коррекция значений давления насыщения при низких значениях газосодержания при давлении насыщения для обеспечения выхода на значение $P_b = 1$ при $R{sb} = 0$

```python
# параметры определяющие диапазоны значений для построения графиков
rsb_set=np.arange(1,300,10)

t_set=np.arange(273,380,30)
t_set_def=np.array([313])

gg_set=np.arange(0.6,1,0.1)
gg_set_def=np.array([0.8])

go_set=np.arange(0.8,1,0.05)
go_set_def=np.array([0.86])

# функция настройки графиков по давлению насыщения от газосодержания
def plot_pb_rsb(plt, func,
                tset, goset, ggset,
                plot_title, plot_xlab, plot_ylab):
    for t in tset:
        for gg in ggset:
            for go in goset:
                plt.plot(rsb_set,
                         func(rsb_set,t_K = t,gamma_gas = gg,gamma_oil = go),
                         label='t = %1.0f $ ^{\circ}\mathrm{K}$'%t +
                                    ' $\gamma_g$ = %1.2f'%gg +
                                    ' $\gamma_o$ = %1.2f'%go
                        )
    plt.title(plot_title)
    plt.ylabel(plot_ylab, color = 'black')
    plt.xlabel(plot_xlab, color = 'black')
    plt.legend()
```

```python
# код для построения графиков
plt.figure(figsize=(15, 8))
# рисуем первый график
plt.subplot(221)
plot_pb_rsb(plt, pvt.unf_pb_Standing_MPaa,
            t_set, go_set_def, gg_set_def,
           'Давление насыщения от газосодержания',
           '$R_{sb}, м^3/м^3$',
           '$P_b, MPa$')
plt.grid()
# рисуем второй график
plt.subplot(222)
plot_pb_rsb(plt, pvt.unf_pb_Standing_MPaa,
            t_set_def, go_set, gg_set_def,
            'Давление насыщения от газосодержания',
            '$R_{sb}, м^3/м^3$',
            '$P_b, MPa$')
plt.grid()
# рисуем третий график
plt.subplot(223)
plot_pb_rsb(plt, pvt.unf_pb_Standing_MPaa,
            t_set_def, go_set_def, gg_set,
            'Давление насыщения от газосодержания',
            '$R_{sb}, м^3/м^3$',
            '$P_b, MPa$')

plt.subplots_adjust(top=0.92, bottom=0.08, left=0.10, right=0.95, hspace=0.25, wspace=0.35)
# рисуем все
plt.grid()
plt.show()
```

---

### 1.1.2. Давление насыщения, корреляция Valko McCain

Корреляция Valco McCain (2003) для оценки давления насыщения нефти газом разработана на основе банка данных нефтей со всего мира. На рисунке показаны источники данных, которые были использованы авторами для настройки корреляции.

Диапазоны значений основных свойств (1745 значений), использованных для разработки данной корреляции, приведены в таблице ниже.

|
 Параметр  | Мин|Среднее|Макс|
| :--- | :---: |:---:|:---:|
|
 давление насыщения,$P_b$ , $ МПа $  | 0.55  |15.0|45.5|
|
 температура, $^{\circ}\mathrm{С} $ | 15 |85|172|
|
 газосодержание при давлении насыщения, $R_{sb}$ , $м^3/м^3 $ | 2 |104|395|
|
 относительная плотность нефти по воде, $\gamma_o$ | 0.724  |0.846|1.02|
|
 относительная плотность газа на сепараторе, $\gamma_g$  | 0.555  |0.838|1.685|

По результатам сравнений с замеренными значениями абсолютная значение средней относительной ошибки (AARE) для корреляции составляет около 11%. Авторы отмечают, что полученная точность соответствует точности замеров использованных для построения корреляции и для построения более точных зависимостей потребуется сбор новых данных с повышенной точностью.

$$
ln P_b = 7.475 + 0.713 z + 0.0075 z^2 \tag{1.1.2.1}
$$
где

$$
z = z_1 + z_2 + z_3 + z_4
$$
$$
z_1 = -5.48 - 0.0375\cdot ln R_{sb}+0.281\cdot (ln R_{sb})^2 - 0.0206\cdot (ln R_{sb})^3
$$
$$
z_2 = 1.27 - 0.0449\cdot API +4.36 \cdot 10^{-4} API^2 -4.76 \cdot 10^{-6} API^3
$$
$$
z_3 = 4.51 - 10.84 \cdot \gamma_{gSP} +8.39\cdot \gamma_{gSP}^2 -2.34\cdot \gamma_{gSP}^3
$$
$$
z_4 = -0.7835 + 6.23 \cdot 10^{-3} \cdot T_R - 1.22 \cdot 10^{-5} \cdot T_R^2+ 1.03 \cdot 10^{-8} \cdot T_R^3
$$

где

* $P_b$ - давление насыщения, $psia$
* $R_{sb}$ - газосодержание при давлении насыщения, ${scf}/{STB}$
* $\gamma_{gSP}$ - удельная плотность газа, отобранного на сепараторе, безразмерная величина
* $T_R$ - пластовая температура, $F$

ref Reservoir oil bubblepoint pressures revisited; solution gas-oil ratios and surface gas specific gravities. P.P.Valko, W.D.McCain Jr. Journal of petroleum science and engineering 37(2003) 153-169

---
#### Пребразование единиц измерения для корреляции Валко Маккейна

```python
# объявления переменных необходимых для преобразования единиц в вырожении
rsb_scfSTB, rsb_m3m3 = symbols('R_sb[scfSTB] R_sb[m3m3]')
API, gamma_o = symbols('API gamma_o')
gamma_gSP = symbols('gamma_gSP')
T_RF,T_RK = symbols('T_R[F] T_R[K]')
z,z1,z2,z3,z4 = symbols('z,z1,z2,z3,z4')
p_bpsia, p_bMPaa = symbols('p_b[psia],p_b[MPaa]')
```

```python
# определение алгоритма расчета в американских промысловых единицах
eq1 = Eq(z,z1+z2+z3+z4)
eq2 = Eq(z1,  -5.48 - 0.03758 * ln(rsb_scfSTB)+ 0.281*  ln(rsb_scfSTB)**2 - 0.0206* ln(rsb_scfSTB)**3)
eq3 = Eq(z2,  1.27 - 0.0449* API +4.36 * 10**-4 *API**2 -4.76 * 10**-6 *API**3)
eq4 = Eq(z3,  4.51- 10.84 *gamma_gSP +8.39*gamma_gSP**2 -2.34*gamma_gSP**3 )
eq5 = Eq(z4,  -0.7835 + 6.23 * 10**-3 * T_RF - 1.22 * 10**-5 * T_RF**2+ 1.03 * 10**-8 * T_RF**3)
eq6 =Eq(ln(p_bpsia),(7.475 + 0.713 * z + 0.0075 * z**2))
# покажем выражения в печатном виде
display(eq6)
display(eq1)
display(eq2)
display(eq3)
display(eq4)
display(eq5)
```

```python
# Некоторые переводные константы
print(uc.m3m3_2_scfstb(1))
print(uc.MPa_2_psi(1))
```

    5.614583333333334
    145.03773773020922

```python
# выражения для преобразования единиц измерения из американских промысловых в практические метрические
scfSTB_to_m3m3 = rsb_m3m3 * uc.m3m3_2_scfstb(1)
#API_to_gamma_o = 141.5 / gamma_o - 131.5
API_to_gamma_o = uc.gamma_oil_2_api(gamma_o)
#F_to_K = T_RK * 9 / 5 - 459.67
F_to_K = uc.K_2_F(T_RK)
psi_to_MPa = p_bMPaa * uc.MPa_2_psi(1)
# покажем выражения в печатном виде
display(Eq(rsb_scfSTB , scfSTB_to_m3m3))
display(Eq(API,API_to_gamma_o))
display(Eq(T_RF,F_to_K))
display(Eq(p_bpsia,psi_to_MPa))
```

```python
# преобразование алгоритма в метрические единицы с использованием символьных вычислений
eq2_m=simplify(eq2.subs(rsb_scfSTB,scfSTB_to_m3m3))
eq3_m=simplify(eq3.subs(API,API_to_gamma_o))
eq5_m=simplify(eq5.subs(T_RF,F_to_K))
eq6_m=eq6.subs(p_bpsia, psi_to_MPa)
eq8=solve(eq6_m,p_bMPaa)
eq9=Eq(p_bMPaa, eq8[0])
# вывод результатов преобразований
display(eq9)
display(eq1)
display(eq2_m)
display(eq3_m)
display(eq4)
display(eq5_m)
```

```python
pvt.unf_pb_Valko_MPaa(rsb_m3m3 = 100, gamma_oil=0.86, gamma_gas=0.6, t_K=350)
```

    array(23.29991091)

```python
plt.figure(figsize=(15,8))
f = pvt.unf_pb_Valko_MPaa
# рисуем первый график
plt.subplot(221)
plot_pb_rsb(plt, pvt.unf_pb_Valko_MPaa,
            t_set,go_set_def,gg_set_def,
            'Давление насыщения от газосодержания',
            '$R_{sb}, м^3/м^3$',
            '$P_b, MPa$')
plt.grid()
# рисуем второй график
plt.subplot(222)
plot_pb_rsb(plt, pvt.unf_pb_Valko_MPaa,
            t_set_def,go_set,gg_set_def,
            'Давление насыщения от газосодержания',
            '$R_{sb}, м^3/м^3$',
            '$P_b, MPa$')
plt.grid()
# рисуем третий график
plt.subplot(223)
plot_pb_rsb(plt, pvt.unf_pb_Valko_MPaa,
            t_set_def,go_set_def,gg_set,
            'Давление насыщения от газосодержания',
            '$R_{sb}, м^3/м^3$',
            '$P_b, MPa$')

plt.subplots_adjust(top=0.92, bottom=0.08, left=0.10, right=0.95, hspace=0.25, wspace=0.35)
# рисуем все
plt.grid()
plt.show()
```

в приведеном коде использована коррекция значений давления насыщения при низких значениях газосодержания при давлении насыщения для обеспечения выхода на значение $P_b = 1$ при $R{sb} = 0$ и при больших значениях газосодержания

следует отметить, что в отличии от корреляций типа Стендинга корреляция Валко Макейна хорошо описывает исходный набор данных в пределах области применимости, но дает нефизичные результаты за пределами диапазона применимости. Приведенная в коде корректировке может частично сгладить экспраполированные значения, но лучше при проведении расчетов контролировать, чтобы корреляция применялась в пределах диапазона примемости.

##  1.2. Газосодержание

Газосодержание $R_s$ - содержание растворенного газа в нефти при заданных термобарических условиях (solution gas ratio). Определяется как отношение объема газа выделившегося из нефти при приведении ее к стандартным условиям к объему оставшейся нефти в стандартных условиях

###  1.2.1 Газосодержание, корреляция Стендинга

Для расчета газосодержания используется корреляция, обратная корреляции Стендинга (1.1.1.1) для давления насыщения нефти газом.

$$ R_s =  \gamma_g  \left( \frac{1.92 p}{\ 10^{y_g}}\right)^{1.204} \tag{1.2.1.1} $$

где:

$R_s$ - газосодержание, $м^3/м^3 $

$P$ - давление, $МПа$

$\gamma_g$ - относительная плотность газа, безразмерная величина

$y_g$ - мольная доля газа, $ y_g = 1.225 +0.00164 T - \dfrac{ 1.769}{\gamma_o}$

$\gamma_o$ - относительная плотность нефти, безразмерная величина

$ T $ - температура, $ ^{\circ}\mathrm{K}$

Газосодержание является одним из ключевых свойств нефти при расчётах производительности скважин и работы скважинного оборудования. Динамика изменения газосодержания во многом определяет количество свободного газа в потоке и должна учитываться при проведении расчётов.

Если известно газосодержание при давлении насыщения, то газосодержание при давлениях ниже давления насыщения может быть получено из пропорции:

$$ R_s =  \ R_{sb}\left( \frac{P}{\ P_b}\right)^{1.204} \tag{1.2.1.2} $$

где:

$R_s$ - газосодержание, $м^3/м^3 $

$P$ - давление, $МПа$

$P_b$ - давление насыщения, $МПа$

$R_{sb}$ - газосодержание при давлении насыщения, $м^3/м^3 $

Корреляции Standing базируются на 105 экспериментально определенных давлениях насыщения нефтяных систем Калифорнии. Диапазоны значений основных свойств, использованных для разработки данной корреляции, приведены в таблице ниже.

|
 Параметр  | Диапазон |
| :--- | :--- |
|
 давление насыщения,$P_b$ , $ МПа $  | 0.896…48.263  |
|
 температура, $^{\circ}\mathrm{K} $ | 310…400 |
|
 газосодержание при давлении насыщения, $R_{sb}$ , $м^3/м^3 $ | 3.6…254 |
|
 относительная плотность нефти по воде, $\gamma_o$ | 0.725…0.956  |
|
 относительная плотность газа, $\gamma_g$  | 0.59…0.95  |

ref "A Pressure-Volume-Temperature Correlation for Mixtures of California Oil and Gases", M.B. Standing, Drill. & Prod. Prac., API, 1947.

```python
pvt.unf_rs_Standing_m3m3(p_MPaa=3, pb_MPaa=10, rsb_m3m3=130, gamma_oil=0.86, gamma_gas=0.6, t_K=350)
```

    array(30.50684834)

```python
np.zeros_like(1.2)
```

    array(0.)

```python
# параметры определяющие диапазоны значений для построения графиков
p_set=np.linspace(0.11,11,100)
t_set=np.arange(294,400,30)
t_set_def=np.array([313])
gg_set=np.arange(0.6,1,0.1)
gg_set_def=np.array([0.8])
go_set=np.arange(0.8,1,0.05)
go_set_def=np.array([0.86])
# функция для автоматизации построения графиков по давлению насыщения
def plot_rs_p(plt, func,
              tset, goset, ggset,
              plot_title, plot_xlab, plot_ylab):
    for t in tset:
        for gg in ggset:
            for go in goset:
                plt.plot(p_set, func(p_set, t_K = t, gamma_gas = gg, gamma_oil = go),
                         label='t = %1.0f $ ^{\circ}\mathrm{K}$'%t +
                               ' $\gamma_g$ = %1.2f'%gg +
                               ' $\gamma_o$ = %1.2f'%go
                        )
    plt.title(plot_title)
    plt.ylabel(plot_ylab, color = 'black')
    plt.xlabel(plot_xlab, color = 'black')
    plt.legend()
```

```python
# код для построения графиков
plt.figure(figsize=(15,8))
f = pvt.unf_rs_Standing_m3m3
# рисуем первый график
plt.subplot(221)
plot_rs_p(plt, pvt.unf_rs_Standing_m3m3,
          t_set,go_set_def,gg_set_def,
          'Газосодержание от давления',
          '$P, MPa$',
          '$R_s, м^3/м^3$')
plt.grid()
# рисуем второй график
plt.subplot(222)
plot_rs_p(plt, pvt.unf_rs_Standing_m3m3,
          t_set_def,go_set,gg_set_def,
              'Газосодержание от давления',
               '$P, MPa$',
               '$R_s, м^3/м^3$')
plt.grid()
# рисуем третий график
plt.subplot(223)
plot_rs_p(plt, pvt.unf_rs_Standing_m3m3,
          t_set_def,go_set_def,gg_set,
              'Газосодержание от давления',
               '$P, MPa$',
               '$R_s, м^3/м^3$')

plt.subplots_adjust(top=0.92, bottom=0.08, left=0.10, right=0.95, hspace=0.25, wspace=0.35)
# рисуем все
plt.grid()
plt.show()
```

###  1.2.2. Газосодержание, корреляция Веларде-Маккейна

Корреляция Веларде-Маккейна (1999) для газосодержания базируется на 718 лабораторных исследованиях разгазирования различных нефтей со всего мира.

$$ R_s = R_{sb}R_{sr}  \tag{1.2.2.1} $$

где:

$R_s$ - газосодержание, $м^3/м^3$

$R_{sb}$ - газосодержание при давлении насыщения, $м^3/м^3$

$R_{sr}$ - приведенное газосодержание

$$R_{sr}=a_1p_r^{a_2} + (1-a_1)P_r^{a_3} \tag{1.2.2.2}$$

где $P_r$ - степень превышения давления, $psig/psig$

$$P_r=\dfrac{(P-14,7)}{(P_b-14,7)} \tag{1.2.2.3} $$

$P$ - давление, psia

$P_b$ - давление насыщения, psia

$$a_1=9.73 \cdot 10^{-7}\gamma_{gSP}^{1.672608}API^{0.929870}T^{0.247235}(P_b-14.7)^{1.056052}  \tag{1.2.2.4}$$

$$a_2=0.022339 \gamma_{gSP}^{-1.004750}API^{0.337711}T^{0.132795}(P_b-14.7)^{0.302065} \tag{1.2.2.5}$$

$$a_3=0.725167 \gamma_{gSP}^{-1.485480}API^{-0.164741}T^{-0.091330}(P_b-14.7)^{0.047094} \tag{1.2.2.6}$$

где в свою очередь

$\gamma_{gSP}$ - относительная плотность газа на сепараторе

$API$ - плотность нефти в градусах API

$T$ - температура, F

В приведенной таблице представлены диапазоны значений использованных для создания корреляции:

|
 Параметр  | Мин|Среднее|Макс|
| :--- | :---: |:---:|:---:|
|
 давление насыщения,$P_b$ , $ МПа $  | 2.861  |15.706|53.434|
|
 температура, $^{\circ}\mathrm{С} $ | 21 |86|160|
|
 относительная плотность газа на сепараторе, $\gamma_g$  | 0.555  |0.793|1.472|
|
 объемный коэффициент нефти при давлении насыщения, $B_{ob}$ , $ м^3/м^3 $ | 1.012  |1.358|2.042|

>"Correlation of Black Oil Properties at Pressures Below Bubblepoint Pressure—A New Approach",
    J. VELARDE, T.A. BLASINGAME Texas A&M University, W.D. MCCAIN, JR. S.A. Holditch & Associates, Inc 1999

```python
uc.MPa_2_psi(1)
```

```python
uc.atm_2_bar(1)
```

```python
14.6959 * 10 / 1.01325
```

```python
A = np.array([9.73 * 10 ** (-7), 1.672608, 0.929870, 0.247235, 1.056052])
B = np.array([0.022339, -1.004750, 0.337711, 0.132795, 0.302065])
C = np.array([0.725167, -1.485480, -0.164741, -0.091330, 0.047094])

a1, a2, a3 = symbols('a1 a2 a3')
api = symbols('API')
gamma_gas = symbols('gamma_gas')
gamma_o = symbols('gamma_o')
t_F,t_K = symbols('T_[F] T_[K]')
pb_psia, p_bMPaa = symbols('p_b[psia],p_b[MPaa]')

eq1 = Eq(a1, A[0] * gamma_gas ** A[1] * api ** A[2] * t_F ** A[3] * (pb_psia - 14.7) ** A[4])
eq2 = Eq(a2, B[0] * gamma_gas ** B[1] * api ** B[2] * t_F ** B[3] * (pb_psia - 14.7) ** B[4])
eq3 = Eq(a3, C[0] * gamma_gas ** C[1] * api ** C[2] * t_F ** C[3] * (pb_psia - 14.7) ** C[4])

display(eq1)
display(eq2)
display(eq3)

# выражения для преобразования единиц измерения из американских промысловых в практические метрические
api_to_gamma_o = 141.5 / gamma_o - 131.5
F_to_K = t_K * 9 / 5 - 459.67
psi_to_MPa = p_bMPaa * 145.037737730209#14.6959 #* 10.1325
# покажем выражения в печатном виде
display(Eq(api, api_to_gamma_o))
display(Eq(t_F, F_to_K))
display(Eq(pb_psia, psi_to_MPa))

eq1_m=simplify(eq1.subs(api, api_to_gamma_o)
                  .subs(t_F, F_to_K)
                  .subs(pb_psia, psi_to_MPa)
              )
display(eq1_m)

eq2_m=simplify(eq2.subs(api, api_to_gamma_o)
                  .subs(t_F, F_to_K)
                  .subs(pb_psia, psi_to_MPa)
              )
display(eq2_m)

eq3_m=simplify(eq3.subs(api, api_to_gamma_o)
                  .subs(t_F, F_to_K)
                  .subs(pb_psia, psi_to_MPa)
              )
display(eq3_m)
```

```python
# объявления переменных необходимых для преобразования единиц в вырожении
b_o = symbols('b_o')
rho_sto_lbft3, rho_or_lbft3 = symbols('rho_sto[lbft3] rho_or[lbft3]')
rs_scfstb, gamma_g = symbols('r_s[scfstb] gamma_g')
rs_m3m3 = symbols('r_s[m3m3]')
rho_sto_kgm3, rho_or_kgm3 = symbols('rho_sto[kgm3] rho_or[kgm3]')

# определение алгоритма расчета в американских промысловых единицах
eq1 = Eq(b_o, (rho_sto_lbft3 + 0.01357 * rs_scfstb * gamma_g)/rho_or_lbft3)
# покажем выражения в печатном виде
print('исходное выражение')
display(eq1)

# выражения для преобразования единиц измерения из американских промысловых в практические метрические
# для работы с коэффициентами преобразования используем модуль neftpy.uconvert
scfstb_to_m3m3 = rs_m3m3 * uc.m3m3_2_scfstb(1)
sto_lbft3_to_kgm3 = rho_sto_kgm3 * uc.kgm3_2_lbft3(1)
or_lbft3_to_kgm3 = rho_or_kgm3 * uc.kgm3_2_lbft3(1)

# покажем выражения в печатном виде
print('коэффициенты преобразования')
display(Eq(rs_scfstb , scfstb_to_m3m3))
display(Eq(rho_sto_lbft3 , sto_lbft3_to_kgm3))

# преобразование алгоритма в метрические единицы с использованием символьных вычислений
eq1_m=simplify(eq1.subs(rs_scfstb, scfstb_to_m3m3)
                  .subs(rho_sto_lbft3, sto_lbft3_to_kgm3)
                  .subs(rho_or_lbft3, or_lbft3_to_kgm3)
              )
# вывод результатов преобразований
print('преобразованное выражение')
display(eq1_m)
```

```python
pvt._unf_rs_Velarde_m3m3_(1.1)
```

    array(15.86745211)

```python
pvt.unf_rs_Velarde_m3m3(1.1)
```

    array(15.86745211)

```python
# код для построения графиков
plt.figure(figsize=(15,8))
f = pvt.unf_rs_Velarde_m3m3
# рисуем первый график
plt.subplot(221)
plt.grid()
plot_rs_p(plt, pvt.unf_rs_Velarde_m3m3,
          t_set,go_set_def,gg_set_def,
              'Газосодержание от давления',
               '$P, MPa$',
               '$R_s, м^3/м^3$')
# рисуем второй график
plt.subplot(222)
plt.grid()
plot_rs_p(plt, pvt.unf_rs_Velarde_m3m3,
          t_set_def,go_set,gg_set_def,
              'Газосодержание от давления',
               '$P, MPa$',
               '$R_s, м^3/м^3$')
# рисуем третий график
plt.subplot(223)
plot_rs_p(plt, pvt.unf_rs_Velarde_m3m3,
          t_set_def,go_set_def,gg_set,
              'Газосодержание от давления',
               '$P, MPa$',
               '$R_s, м^3/м^3$')

plt.subplots_adjust(top=0.92, bottom=0.08, left=0.10, right=0.95, hspace=0.25, wspace=0.35)
# рисуем все
plt.grid()
plt.show()
```

###   1.2.3. Оценка объема растворенного газа в сепараторе, для уточнения газосодержания по замерам на сепараторе. Корреляция Маккейна.

Во многих корреляциях используется в качестве исходного параметра - газосодержание при давлении насыщения. Значение этого параметра может быть получено с помощью полевых данных как сумма отделяемого газового фактора в сепараторе и резервуаре для товарной нефти.

$$ R_{sb} = R_{sp} + R_{st}  \tag{1.2.3.1} $$

где:

$R_{sb}$ - газосодержание при давлении насыщения, $м^3/м^3$

$R_{sp}$ - газовый фактор, отделяемый в сепараторе, $м^3/м^3$

$R_{st}$ -  газовый фактор в резервуаре для товарной нефти, $м^3/м^3$

Данное уравнение справедливо только если пластовое давление выше давления насыщения.

---

Расход газа и дебит нефти в сепараторе обычно измеряются, когда как в резервуаре газ обычно выпускается и не замеряется. Поэтому для более точной оценки газосодержания пластовой нефти при давлении насыщения необходимо оценить газовый фактор в резервуаре.
Таким образом, был разработана формула на основе GRACE-алгоритма на базе 898 исследований пластового флюида. В таблице приведены набор данных для исследования.

$$ ln R_{st} = 3.955 + 0.83z - 0.024z^2 + 0.075z^3  \tag{1.2.3.2} $$

где

$$ z =\sum_{n=1}^3 z_n $$

$$ z_n = C0_n + C1_nV_n + C2_nV_n^2 $$

|
 $n$  | $V$|$CO$|$C1$|$C2$|
| :--- | :---: |:---:|:---:|:---:|
|
 $1$  | $ln P_{sp} $ |$-8.005$| $2.7$|$-0.161$|
|
 $2$  | $ln T_{sp}$  |$1.224$|$-0.5$|$0$|
|
 $3$ | $API$  |$-1.587$|$0.0441$|$-2.29 \cdot  10 ^{-5}$|

$T_{sp}$  - температура на сепараторе,  °F

$P_{sp}$  - давление на сепаратора, psia

Выражение для вычисления газового фактора в резервуаре требует знать температуру и давление в сепараторе, которые обычно не всегда бывают известны. Поэтому в этом случае можно использовать следующее уравнение.

$$ R_{sb} = 1.1618 R_{sp}   \tag{1.2.3.3}  $$

> "Reservoir oil bubblepoint pressures revisited; solution gas–oil ratios and surface gas specific gravities",
    J. VELARDE, W.D. MCCAIN, 2002

```python
# параметры определяющие диапазоны значений для построения графиков
p_set=np.arange(1,11,0.25)
t_set=np.arange(294,400,30)
t_set_def=np.array([313])
go_set=np.arange(0.8,1,0.05)
go_set_def=np.array([0.86])
r_sp = 50
# функция для автоматизации построения графиков по давлению насыщения
def plot_rsb_psep(plt,func,
                  tset, goset,
                  plot_title, plot_xlab, plot_ylab):
    for t in tset:
            for go in goset:
                plt.plot(p_set, func(r_sp,go,p_set,t),
                         label='t = %1.0f $ ^{\circ}\mathrm{K}$'%t +
                                                  ' $\gamma_o$ = %1.2f'%go)
    plt.title(plot_title)
    plt.ylabel(plot_ylab, color = 'black')
    plt.xlabel(plot_xlab, color = 'black')
    plt.legend()
```

```python
# код для построения графиков
plt.figure(figsize=(15,8))
f = pvt.unf_rsb_Mccain_m3m3
# рисуем первый график
plt.subplot(221)
plt.grid()
plot_rsb_psep(plt, f,
              t_set, go_set_def,
              'Газосодержание от давления в сепараторе',
              '$P, MPa$',
              '$R_s, м^3/м^3$')
# рисуем второй график
plt.subplot(222)
plt.grid()
plot_rsb_psep(plt, f,
              t_set_def,go_set,
              'Газосодержание от давления в сепараторе',
              '$P, MPa$',
              '$R_s, м^3/м^3$')

plt.subplots_adjust(top=0.92, bottom=0.08, left=0.10, right=0.95, hspace=0.25, wspace=0.35)
# рисуем все
plt.show()
```

##  1.3. Объемный коэффициент нефти

###  1.3.1. Корреляция при давлении выше давления насыщения

Корреляция для объемного коэффициента нефти при давлении выше давления насыщения в некоторых источниках указывается, что она принадлежит Стендингу, в некоторых Vasquez & Beggs. На самом деле это не корреляция, так как природа ее происхождения не статистическая, а вполне себе физическое уравнение.

$$ B_o = B_{ob} \cdot \exp(c_o(p_b - p)) $$

где:

$ B_o $ - объемный коэффициент нефти при давлении p, м3/м3

$ B_{ob} $ - объемный коэффициент нефти при давлении насыщения, м3/м3

$ c_o $ - сжимаемость нефти, 1/МПа

$ P $ - давление, МПа

$ P_b $ - давление насыщения, МПа

### Корреляция Маккейна при давлении меньше или равном давлению насыщения

Уравнение выводится из материального баланса и не является корреляцией.

$$ b_o = \left( \frac{ \rho_{STO} + 0.01357 R_s \gamma_g}{\rho_{or}}\right) $$

где:

$ b_o $ - объемный коэффициент нефти при давлении $P$, м3/м3

$ \rho_{STO} $ - плотность дегазированной нефти, фунт/фт3 (кг/м3)

$ R_s $ - газосодержание при давлении p, фт3/баррель (м3/м3)

$ \gamma_g $ - плотность газа относительно воздуха

$ \rho_{or} $ - плотность пластовой нефти, фунт/фт3 (кг/м3)

#### Внутри функции уже реализован перевод величин, единицы измерения в скобках - входные параметры в функцию

```python
# объявления переменных необходимых для преобразования единиц в вырожении
b_o = symbols('b_o')
rho_sto_lbft3, rho_or_lbft3 = symbols('rho_sto[lbft3] rho_or[lbft3]')
rs_scfstb, gamma_g = symbols('r_s[scfstb] gamma_g')
rs_m3m3 = symbols('r_s[m3m3]')
rho_sto_kgm3, rho_or_kgm3 = symbols('rho_sto[kgm3] rho_or[kgm3]')

# определение алгоритма расчета в американских промысловых единицах
eq1 = Eq(b_o, (rho_sto_lbft3 + 0.01357 * rs_scfstb * gamma_g)/rho_or_lbft3)
# покажем выражения в печатном виде
print('исходное выражение')
display(eq1)

# выражения для преобразования единиц измерения из американских промысловых в практические метрические
# для работы с коэффициентами преобразования используем модуль neftpy.uconvert
scfstb_to_m3m3 = rs_m3m3 * uc.m3m3_2_scfstb(1)
sto_lbft3_to_kgm3 = rho_sto_kgm3 * uc.kgm3_2_lbft3(1)
or_lbft3_to_kgm3 = rho_or_kgm3 * uc.kgm3_2_lbft3(1)

# покажем выражения в печатном виде
print('коэффициенты преобразования')
display(Eq(rs_scfstb , scfstb_to_m3m3))
display(Eq(rho_sto_lbft3 , sto_lbft3_to_kgm3))

# преобразование алгоритма в метрические единицы с использованием символьных вычислений
eq1_m = simplify(eq1.subs(rs_scfstb, scfstb_to_m3m3)
                    .subs(rho_sto_lbft3, sto_lbft3_to_kgm3)
                    .subs(rho_or_lbft3, or_lbft3_to_kgm3)
                )
# вывод результатов преобразований
print('преобразованное выражение')
display(eq1_m)
```

# Список литературы

1. "A Pressure-Volume-Temperature Correlation for Mixtures of California Oil and Gases", M.B. Standing, Drill. & Prod. Prac., API, 1947.
2. "Correlation of Black Oil Properties at Pressures Below Bubblepoint Pressure—A New Approach",
    J. VELARDE, T.A. BLASINGAME Texas A&M University, W.D. MCCAIN, JR. S.A. Holditch & Associates, Inc 1999
3. "Reservoir oil bubblepoint pressures revisited; solution gas–oil ratios and surface gas specific gravities",
    J. VELARDE, W.D. MCCAIN, 2002


```
# default_exp resnetx
```

```
#export
from wong.imports import *
from wong.core import *
from wong.config import cfg, assert_cfg

from torchvision.models.utils import load_state_dict_from_url

```

```
from fastcore.all import *  # test_eq
```

# ResNetX
> a folded resnet

The key distinguishing feature of our proposed architecture is the use of concatenation-skip (addition(additive)-skip) connections like DenseNet (ResNet), but with selective long-range and short range skip connections rather than a dense connectivity.

Despite various parameter-efficient depthwise-convolution-based designs, for GPU-based deployment ResNet architecture provide a comparable or better speed-accuracy trade-off.

Ref:

XNect: Real-time Multi-person 3D Human Pose Estimation with a Single RGB Camera

The proposed networks reduces computations by 20% with equivalent or even superior accuary on the ImageNet dataset, and significantly outperforms state-of-the-art approaches in terms of AP_50 on the MS COCO object detection dataset.

Ref:
CSPNet: A new backbone that can enhance learning capability of CNN

is more accurate and more computationally efficient than the state of art ResNets networks.

which achieve much better accuracy and efficiency than previous ConvNets.

A residual network with multiple direct paths

In order to compare ResNetX with ResNet, we using ablation method. As ResNet is an special ResNetX when fold=1, we first express ResNet as ResNetX, then we change fold from 1, 2, 3, 4 to evaluate its performance. We first use transfer learning, we got pre-trained model of resnet152, then we fill the weights of ResNetX model with pretrained model, then fine tuning them, we got an better result ; Second method is training the model from scratch, we

https://petewarden.com/2017/10/29/how-do-cnns-deal-with-position-differences/

As you go deeper into a network, the number of channels will typically increase, but the size of the image will shrink. This shrinking is done using pooling layers, traditionally with average pooling but more commonly using maximum pooling these days.

```
#export
def get_pred(l:int, d:int=1, start_id:int=None, end_id:int=None):
    "get predecessor layer id."
    if start_id is None: start_id = d
    if end_id is None: end_id = l
    assert l >= 1 and start_id >= d and end_id > start_id
    if l < start_id or l > end_id or d == 1:  # if the current layer index is less than the fold depth, or if fold depth == 1
        pred = l - 1
    else:
        remainder = (l-1-(start_id-d)) % (d-1)
        pred = l - 2 * (1+remainder)
    return pred
```

Parameters:
- l : current layer id.
- start_id : index of the starting node
- end_id : index of the ending node
- d : fold depth.

Return:
- The previous layer id that directly link to the current layer.

\begin{equation}\label{eq:resnetx}
   i =
   \left\{
      \begin{array}{ll}
      1 & l < d \lor d=1 ; \\
      2 * (1 + (l-1) \pmod{d-1}) & \textrm{else} .
      \end{array}
      \right.
\end{equation}

```
get_pred(l=17, d=2, start_id=13)
```

    15

```
get_pred(l=50, d=5, start_id=8)
```

    44

```
test_eq(get_pred(l=12, d=1, start_id=1), 11)

test_eq(get_pred(l=8, d=5, start_id=7), 4)
test_eq(get_pred(l=12, d=4, start_id=6), 10)

```

```
#export
def layer_diff(cur:int, pred:int, num_nodes:tuple):
    "layer difference between the current layer and the predecessor layer."
    assert cur > pred
    num_nodes = (1,) + num_nodes
    cumsum = 0  # start with 0
    for i, num in enumerate(num_nodes):
        if cumsum <= cur < cumsum + num:
            cur_layer = i
            if cur == cumsum:
                first = True
            else:
                first = False
        if cumsum <= pred < cumsum + num:
            pred_layer = i
        cumsum += num
    diff = cur_layer - pred_layer
    return diff, first
```

```
num_nodes = (3,4,6,3)
cur, pred = 9,0
layer_diff(cur, pred, num_nodes)
```

    (3, False)

Parameters:
- Stem : the stemming stage, which accept original images, transform them, then input into the backbone network.
- Unit : the operation at nodes.
- Conn : the connections between nodes
- fold : the fold depth
- ni : number of input channels of the backbone network.
- *num_stages : number of stages in the backbone network.*
- num_nodes : number of nodes of every stage in the backbone network.
- start_id : index of starting node of ResNetX
- base : standard width of channels in the backbone network.
- exp : expansion along with the increase of stages.
- bottle_scale : bottleneck scale
- first_downsample: dose down-sample at the start of the first stage.
- deep_stem : using 7x7 or 3 3x3 conv in stemming stage.
- c_in : number of input channels of the Start layer
- c_out : number of classes in the output of the final classifier.
- kwargs : arguments translate into `Unit`

```
#export
class ResNetX(nn.Module):
    "A folded resnet."
    def __init__(self, Stem, Unit, Conn, Tail, fold:int, ni:int, num_nodes:tuple, start_id:int=None, end_id:int=None,
                 base:int=64, exp:int=2, bottle_scale:int=1, first_downsample:bool=False,
                 c_in:int=3, c_out:int=10, **kwargs):
        super(ResNetX, self).__init__()
        # fold depth should be less than the sum length of any two neighboring stages

        if start_id < fold: start_id = fold
        origin_ni = ni
        num_stages = len(num_nodes)
        nhs = [base * exp ** i for i in range(num_stages)]
        nos = [int(nh * bottle_scale) for nh in nhs]
        strides = [1 if i==0 and not first_downsample else 2 for i in range(num_stages)]
#         print('nhs=', nhs, 'nos=', nos, 'nus=', nus, 'strides=', strides)

        self.stem = Stem(c_in, no=ni) # , deep_stem

        units = []
        idmappings = []
        cur = 1
        for i, (nh, no, nu, stride) in enumerate(zip(nhs, nos, num_nodes, strides)):
            for j in range(nu):
                if j == 0: # the first node(layer) of each stage
                    units += [Unit(ni, no, nh, stride=stride, **kwargs)]
                else:
                    units += [Unit(no, no, nh, stride=1, **kwargs)]

                pred = get_pred(cur, fold, start_id, end_id) #
                diff, first = layer_diff(cur, pred, num_nodes)
                assert diff == 0 or diff == 1 or (diff == 2 and pred == 0), \
                       'cur={}, pred={}, diff={} is not allowed.'.format(cur, pred, diff)
#                 print('fold = {} , cur = {} , pred = {} ,diff = {}'.format(fold, cur, pred, diff))
                if diff == 0:
                    idmappings += [Conn(no, no, stride=1)]
                elif diff == 1:
#                     if first:
                    idmappings += [Conn(ni, no, stride=stride)]
#                     else:
#                         idmappings += [Conn(no, no, stride=1)]
                elif diff == 2:
                    idmappings += [Conn(origin_ni, no, stride=stride)]
                cur += 1
            ni = no
        self.units = nn.ModuleList(units)
        self.idmappings = nn.ModuleList(idmappings)

        self.classifier = Tail(nos[-1], c_out)
        self.fold, self.start_id, self.end_id = fold, start_id, end_id
        self.num_nodes = num_nodes
        init_cnn(self)

    def forward(self, x):
        results = {}
        results[0] = self.stem(x)
        cur = 0
        for i, (unit, idmapping) in enumerate(zip(self.units, self.idmappings)):
            cur += 1
            pred = get_pred(cur, self.fold, self.start_id, self.end_id)
            diff, first = layer_diff(cur, pred, self.num_nodes)
#             if diff == 0:
            results[cur % (2*self.fold-1)] = unit(results[(cur-1) % (2*self.fold-1)]) + idmapping(results[pred % (2*self.fold-1)])
#             else:
#                 results[cur % (2*self.fold-1)] = unit(results[(cur-1) % (2*self.fold-1)]) + idmapping(results[(cur-1) % (2*self.fold-1)])
        x = results[cur % (2*self.fold-1)]

        x = self.classifier(x)
        return x

    def my_load_state_dict(self, state_dict, local_to_pretrained):
        error_msgs = []
        def load(module, prefix=''):
            local_name_params = itertools.chain(module._parameters.items(), module._buffers.items())
            local_state = {k: v.data for k, v in local_name_params if v is not None}

            new_prefix = local_to_pretrained.get(prefix, 'none')
            for name, param in local_state.items():
                key = new_prefix + name
                if key in state_dict:
#                     print(key)
                    input_param = state_dict[key]

                    if input_param.shape != param.shape:
                        # local shape should match the one in checkpoint
                        error_msgs.append('size mismatch for {}: copying a param with shape {} from checkpoint, '
                                          'the shape in current model is {}.'
                                          .format(key, input_param.shape, param.shape))
                        continue

                    try:
                        param.copy_(input_param)
                    except Exception:
                        error_msgs.append('While copying the parameter named "{}", '
                                          'whose dimensions in the model are {} and '
                                          'whose dimensions in the checkpoint are {}.'
                                          .format(key, param.size(), input_param.size()))

            for name, child in module._modules.items():
                if child is not None:
                    load(child, prefix + name + '.')
        load(self)
        load = None # break load->load reference cycle

```

```
#export
def resnet_local_to_pretrained(num_nodes, fold, start_id, end_id):
    "mapping from local state_dict to pretrained state_dict. the pretrained model is restricted to torchvision.models.resnet."
    local_to_pretrained = {  # mapping from the names of local modules to the names of pretrained modules
        'stem.0.': 'conv1.',
        'stem.1.': 'bn1.',
    }

    cumsum = 0
    for i, num in enumerate(num_nodes):
        for j in range(num):
            key = 'units.' + str(cumsum + j) + '.'
            value = 'layer' + str(i+1) + '.' + str(j) + '.'
            downsample0 = 'layer' + str(i+1) + '.0.' + 'downsample.0.'
            downsample1 = 'layer' + str(i+1) + '.0.' + 'downsample.1.'

            pred = get_pred(cumsum + j + 1, fold, start_id, end_id) #
            diff = layer_diff(cumsum + j + 1, pred, num_nodes)
            if diff == 1:
                idmapping0 = 'idmappings.' + str(cumsum + j) + '.unit.0.'
                idmapping1 = 'idmappings.' + str(cumsum + j) + '.unit.1.'
#                     print(idmapping0, downsample0)
#                     print(idmapping1, downsample1)
                local_to_pretrained[idmapping0] = downsample0
                local_to_pretrained[idmapping1] = downsample1

            for a, b in zip(['1.','2.','4.','5.','7.','8.'], ['conv1.','bn1.','conv2.','bn2.','conv3.','bn3.']):
#                     print (key + a, value + b)
                local_to_pretrained[key + a] = value + b

        cumsum += num

    return local_to_pretrained

```

Three priority levels to set configuration:
- `default_cfg` the default configuration, which set all the option names and their default values
- `cfg_file` the configuration file, which will override the default configuration
- `cfg_list` the configuration list, which will override all the previous configurations.

```
#export
def resnetx(default_cfg:dict, cfg_file:str=None, cfg_list:list=None, pretrained:bool=False, **kwargs):
    "wrapped resnetx"
    assert default_cfg.__class__.__module__ == 'yacs.config' and default_cfg.__class__.__name__ == 'CfgNode'
    cfg = default_cfg
    if cfg_file is not None: cfg.merge_from_file(cfg_file)
    if cfg_list is not None: cfg.merge_from_list(cfg_list)
    assert_cfg(cfg)
    cfg.freeze()

    Stem = getattr(sys.modules[__name__], cfg.GRAPH.STEM)
    Unit = getattr(sys.modules[__name__], cfg.GRAPH.UNIT)
    Conn = getattr(sys.modules[__name__], cfg.GRAPH.CONN)
    Tail = getattr(sys.modules[__name__], cfg.GRAPH.TAIL)
    # start_id >= fold + 1, fold <= 6
    model = ResNetX(Stem=Stem, Unit=Unit, Conn=Conn, Tail=Tail, fold=cfg.GRAPH.FOLD, ni=cfg.GRAPH.NI, num_nodes=cfg.GRAPH.NUM_NODES,
                    start_id=cfg.GRAPH.START_ID, end_id=cfg.GRAPH.END_ID, base=cfg.GRAPH.BASE, exp=cfg.GRAPH.EXP, bottle_scale=cfg.GRAPH.BOTTLE_SCALE,
                    first_downsample=cfg.GRAPH.FIRST_DOWNSAMPLE, **kwargs)
    if pretrained:
        state_dict = load_state_dict_from_url(cfg.URL)
        local_to_pretrained = resnet_local_to_pretrained(cfg.GRAPH.NUM_NODES, cfg.GRAPH.FOLD,cfg.GRAPH.START_ID,cfg.GRAPH.END_ID)
        model.my_load_state_dict(state_dict, local_to_pretrained)
        for param in model.parameters(): # freeze all
            param.requires_grad = False
    return model
```

```
cfg
```

    CfgNode({'URL': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth', 'GRAPH': CfgNode({'NUM_STAGES': 4, 'NUM_NODES': (3, 8, 36, 3), 'NUM_CHANNELS': (64, 128, 256, 512), 'STEM': 'resnet_stem', 'UNIT': 'mbconv', 'CONN': 'IdentityMapping', 'TAIL': 'Classifier', 'FOLD': 3, 'START_ID': 15, 'END_ID': 47, 'NI': 64, 'BASE': 64, 'EXP': 2, 'BOTTLE_SCALE': 0.5, 'FIRST_DOWNSAMPLE': False, 'DEEP_STEM': False})})

```
num_nodes = (3, 8, 36, 3)
num_all_nodes = sum(num_nodes)
fold = 3
start_id = num_nodes[0] + num_nodes[1] + fold + 1
end_id = num_nodes[0] + num_nodes[1] + num_nodes[0] + num_nodes[2] - 3
cfg_list = ["GRAPH.STEM", "resnet_stem",
            "GRAPH.UNIT", "mbconv",  # resnet_bottleneck
            "GRAPH.CONN", "IdentityMapping",
            "GRAPH.TAIL", "Classifier",
            "GRAPH.NUM_NODES", num_nodes,
            "GRAPH.FOLD", fold,
            "GRAPH.START_ID", start_id,
            "GRAPH.END_ID", end_id,
            "GRAPH.NI", 64,
            "GRAPH.BASE", 64,
            "GRAPH.EXP", 2,
            "GRAPH.BOTTLE_SCALE", 0.5, # 4
            "URL", 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',
           ]
model = resnetx(cfg, cfg_list=cfg_list, pretrained=False, c_out=100, ks=5)
```

```
num_nodes = (24, 24, 24, 24)
num_all_nodes = sum(num_nodes)
fold = 4
start_id = fold
end_id = num_all_nodes
cfg_list = ["GRAPH.STEM", "conv_bn",
            "GRAPH.UNIT", "mbconv",  # resnet_bottleneck
            "GRAPH.CONN", "IdentityMappingMaxPool",
            "GRAPH.TAIL", "Classifier",
            "GRAPH.NUM_NODES", num_nodes,
            "GRAPH.FOLD", fold,
            "GRAPH.START_ID", start_id,
            "GRAPH.END_ID", end_id,
            "GRAPH.NI", 64,
            "GRAPH.BASE", 64,
            "GRAPH.EXP", 1,
            "GRAPH.BOTTLE_SCALE", 1., # 4
            "URL", 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',
           ]
model = resnetx(cfg, cfg_list=cfg_list, pretrained=False, c_out=100, ks=3)
num_params(model)
```

    tensor(886948)

**Tip** : Three methods to get `class` or `function` object from its string name:

- `getattr(sys.modules[__name__], cfg.GRAPH.STEM)`
- `globals()[cfg.GRAPH.STEM]`
- `eval(cfg.GRAPH.STEM)`

```
x = torch.randn(2,3,64,64)
```

```
with torch.autograd.set_detect_anomaly(True):
    out = model(x)
    out.mean().backward()
```

```
"{:,}".format(num_params(model))
```

    '60,225,700'

```

```