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));
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),
);
[6]:
ax = plot_pseudopressure(res_oil, 20)
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')]
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",
);
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)",
);
[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),
);