Simulating tight oil flow

With bluebonnet.flow, you can also solve for recovery over time for tight oil wells. First, a few imports:

[1]:
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy.interpolate import interp1d

from bluebonnet.flow import (
    FlowProperties,
    FlowPropertiesTwoPhase,
    RelPermParams,
    SinglePhaseReservoir,
    relative_permeabilities,
)
from bluebonnet.plotting import (
    plot_pseudopressure,
)

plt.style.use("ggplot")

Single-phase oil simulation

Let’s load PVT properties for oil

[ ]:

[2]:
pvt_oil = pd.read_csv("../tests/data/pvt_oil.csv").rename(
    columns={
        "P": "pressure",
        "Z-Factor": "z-factor",
        "Co": "compressibility",
        "Oil_Viscosity": "viscosity",
        "Oil_Density": "density",
    }
)

For oil, there is a large change in diffusivity at the bubble-point pressure. Single-phase flow only occurs above that pressure. See the following plot:

[3]:
pvt_oil.plot(x="pressure", y=["Bo", "z-factor", "viscosity"]).set(ylabel="value", xlim=(0, 9000));
_images/oil_flow_6_0.png

If pressure remains above the bubble point, then generate a FlowProperties instance, which becomes an argument passed to the SinglePhaseReservoir class. It’s just like gas!

[4]:
t_end = 10
time = np.linspace(0, np.sqrt(t_end), 1_000) ** 2
flow_properties = FlowProperties(pvt_oil, p_i=6000)
res_oil = SinglePhaseReservoir(40, 3000, 6000, flow_properties)

%time res_oil.simulate(time)
rf = res_oil.recovery_factor()
CPU times: user 689 ms, sys: 434 μs, total: 690 ms
Wall time: 690 ms
[5]:
fig, ax = plt.subplots()
ax.plot(time, res_oil.recovery_factor(True), label="Recovery from density")
ax.plot(time, rf, "--", label="Recovery factor from frac face")
ax.legend()
ax.set(
    xlabel="Time",
    title="Oil recovery factor",
    ylabel="RF",
    ylim=(0, None),
    xscale="squareroot",
    xlim=(0, None),
);
_images/oil_flow_9_0.png
[6]:
ax = plot_pseudopressure(res_oil, 20)
_images/oil_flow_10_0.png

Multiphase flow

In most cases, two phases are necessary to model tight oil wells. There is a gas phase that, at lower pressures, is no longer miscible with the oil phase. Water can also be present.

Set up PVT

First we gather pvt data for the oil-gas system and water. Then we calculate the oil saturation as a function of pressure and perform the pseudopressure transform.

[7]:
# set conditions
Sw = 0.1
p_frac = 1000
p_res = 6_000
phi = 0.1

# get pvt tables
pvt_oil = pd.read_csv("../tests/data/pvt_oil.csv")
pvt_water = pd.read_csv("../tests/data/pvt_water.csv").rename(
    columns={"T": "temperature", "P": "pressure", "Viscosity": "mu_w"}
)
df_pvt = (
    pvt_water.drop(columns=["temperature"])
    .merge(
        pvt_oil.rename(
            columns={
                "T": "temperature",
                "P": "pressure",
                "Oil_Viscosity": "mu_o",
                "Gas_Viscosity": "mu_g",
                "Rso": "Rs",
            }
        ),
        on="pressure",
    )
    .assign(Rv=0)
)

# calculate So, Sg assuming no mobile water
df_pvt_mp = df_pvt.copy()
df_pvt_mp["So"] = (1 - Sw) / (
    (df_pvt["Rs"].max() - df_pvt["Rs"]) * df_pvt["Bg"] / df_pvt["Bo"] / 5.61458 + 1
)

# scale pseudopressure
pseudopressure = interp1d(df_pvt.pressure, df_pvt.pseudopressure)
df_pvt_mp["pseudopressure"] = (pseudopressure(df_pvt_mp["pressure"]) - pseudopressure(p_frac)) / (
    pseudopressure(p_res) - pseudopressure(p_frac)
)

fig, ax = plt.subplots()
df_pvt_mp.plot(x="pressure", y="pseudopressure", ax=ax)
df_pvt_mp.plot(x="pressure", y="So", ax=ax)
ax.set(xlim=(p_frac, p_res), ylim=(0, 1.0), ylabel="Value")
[7]:
[(1000.0, 6000.0), (0.0, 1.0), Text(0, 0.5, 'Value')]
_images/oil_flow_12_1.png

Set up relative permeabilities

The next step involves declaring relative permeability curves. Here, we use the Brooks-Corey method, made available in this library with the relative_permeabilities function.

[8]:
relperm_params = RelPermParams(
    n_o=1, n_g=1, n_w=1, S_or=0, S_gc=0, S_wc=0.1, k_ro_max=1, k_rw_max=1, k_rg_max=1
)
saturations_test = pd.DataFrame(
    {"So": np.linspace(0, 0.9), "Sw": np.full(50, 0.1), "Sg": np.linspace(0.9, 0)}
)
kr_matrix = pd.DataFrame(
    relative_permeabilities(saturations_test.to_records(index=False), relperm_params)
)
df_kr = pd.concat([saturations_test, kr_matrix], axis=1)
reference_densities = {"rho_o0": 141.5 / (45 + 131.5), "rho_g0": 1.03e-3, "rho_w0": 1}
flow_props = FlowPropertiesTwoPhase.from_table(
    df_pvt_mp, df_kr, reference_densities, phi, Sw, p_res
)
m_scaled = np.linspace(0, 1)
fig, ax = plt.subplots()
ax.plot(m_scaled, flow_props.alpha(m_scaled) / flow_props.alpha(1))
ax.set(
    xlim=(0, 1),
    ylim=(0, None),
    xlabel="scaled pseudopressure",
    ylabel=r"$\alpha/\alpha_i$",
    title="Fluid diffusivity",
);
_images/oil_flow_14_0.png

Simulate

Calculating the pseudopressure profiles and recovery factor looks very similar to the functions used above. The main difference is a more complicated flow_props that came from FlowPropertiesTwoPhase.

[9]:
res = SinglePhaseReservoir(50, p_frac, p_res, flow_props)
t_end = 3
time_twophase = np.linspace(0, np.sqrt(t_end), 10_000) ** 2
res.simulate(time_twophase)
rf_twophase = res.recovery_factor()
[10]:
fig, ax = plt.subplots()
ax.plot(time_twophase, rf_twophase)
ax.set(
    xscale="squareroot",
    xlim=(0, t_end),
    ylim=(0, None),
    ylabel="RF",
    title="Recovery factor",
    xlabel="Scaled time (square root scale)",
);
_images/oil_flow_17_0.png
[11]:
fig, ax = plt.subplots()
ax.plot(time, rf, label="Single phase flow")
ax.plot(time_twophase, rf_twophase, "--", label="Multiphase flow")
ax.legend()
ax.set(
    xlabel="Time",
    ylabel="Recovery factor",
    ylim=(0, None),
    xscale="squareroot",
    xlim=(0, 3),
);
_images/oil_flow_18_0.png