Fitting and forecasting

To use bluebonnet.forecast in a project:

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

from bluebonnet.flow import (
    FlowProperties,
    IdealReservoir,
    RelPermParams,
    SinglePhaseReservoir,
)
from bluebonnet.forecast import ForecasterOnePhase

Forecasting with an ideal gas

[2]:
# Get ideal gas recovery
t_end = 6
nx = 50
pressure_initial = 9000
pressure_bh = 1000
time = np.linspace(0, np.sqrt(t_end), 10_000) ** 2
res_ideal = IdealReservoir(nx, pressure_bh, pressure_initial, None)
res_ideal.simulate(time)
rf_ideal = res_ideal.recovery_factor()
rf_func = interp1d(time, rf_ideal, bounds_error=False, fill_value=(0, rf_ideal[-1]))
[3]:
# Create test production to fit to
m_factor = 1e3
fake_tau = 2
time_on_production = np.linspace(1e-2, t_end, 500) * fake_tau

# Add some randomness to make this interesting
randomness = np.random.default_rng(42).normal(0, 2e-3, size=len(time_on_production)) / np.sqrt(
    time_on_production + 0.1
)
fake_rf = rf_func(time_on_production / fake_tau)
fake_rate = m_factor * np.maximum(np.gradient(fake_rf) + randomness, 0)
cum_production = cumulative_trapezoid(fake_rate, time_on_production, initial=0)

fig, ax = plt.subplots()
ax.plot(time_on_production, fake_rate, color="peru", label="Rate")
ax.plot(time_on_production, cum_production, color="steelblue", label="Cumulative")
ax.set(
    xlabel="Time on production",
    ylabel="Rate or cumulative prod",
    xlim=(0, None),
    ylim=(0, None),
)
ax.legend()
[3]:
<matplotlib.legend.Legend at 0x787b415b2c30>
_images/forecast_4_1.png

After that setup, the actual fitting is fairly simple:

[4]:
# Fit the test production
scaling_curve = ForecasterOnePhase(rf_func)

scaling_curve.fit(time_on_production, cum_production)
M_fit = scaling_curve.M_
tau_fit = scaling_curve.tau_
print(f"{tau_fit=:.2f}, and should be {fake_tau}")
tau_fit=2.84, and should be 2
[5]:
# Forecast cumulative production and compare fit
cum_bestfit = scaling_curve.forecast_cum(time_on_production)

fig, ax = plt.subplots()
ax.plot(time_on_production, cum_production, label="'Real' production")
ax.plot(time_on_production, cum_bestfit, label="Fit")
ax.legend()
ax.set(
    xlim=(0, None),
    ylim=(0, None),
    xlabel="Time on production",
    ylabel="Cumulative production",
);
_images/forecast_7_0.png

The forecaster also allows you to fit production with a known value for \(\tau\). This is important when the well has not yet ended transient flow, usually around \(t < 0.5\tau\). Until that point, setting \(\tau\) to any value above a certain point will achieve the same fit but different in-place and ultimate recovery estimates.

A few example fits:

[6]:
from itertools import cycle

cum_production = cum_production[time_on_production < 1] + 2.1
time_on_production = time_on_production[time_on_production < 1]

fig, ax = plt.subplots()

lines = ["--", "-.", ":"]
linecycler = cycle(lines)
ax.plot(time_on_production, cum_production, label="'Real' production")
for tau in [2, 3, 4]:
    scaling_curve.fit(time_on_production, cum_production, tau=tau)
    M_fit = scaling_curve.M_
    print(f"{tau=:.2f}, and {M_fit=:.2f}")

    # Forecast cumulative production and compare fit
    cum_bestfit = scaling_curve.forecast_cum(time_on_production)
    ax.plot(time_on_production, cum_bestfit, next(linecycler), label=f"Fit for {tau=}")
ax.legend()
ax.set(
    xlim=(0, None),
    ylim=(0, None),
    xlabel="Time on production",
    ylabel="Cumulative production",
    xscale="squareroot",
);
tau=2.00, and M_fit=24.05
tau=3.00, and M_fit=29.37
tau=4.00, and M_fit=34.06
_images/forecast_9_1.png

Forecasting with a real gas

[7]:
pvt_gas = pd.read_csv(
    "https://raw.githubusercontent.com/frank1010111/bluebonnet/main/tests/data/pvt_gas.csv"
).rename(
    columns={
        "P": "pressure",
        "Z-Factor": "z-factor",
        "Cg": "compressibility",
        "Viscosity": "viscosity",
        "Density": "density",
    }
)
t_end = 6
nx = 60
pressure_initial = 12_000
pressure_bh = 1000
real_gas_flow_props = FlowProperties(pvt_gas, p_i=pressure_initial)
time = np.linspace(0, np.sqrt(t_end), 10_000) ** 2
res_real = SinglePhaseReservoir(nx, pressure_bh, pressure_initial, real_gas_flow_props)
res_real.simulate(time)
rf_real = res_real.recovery_factor()
[8]:
fake_ogip = 300
fake_tau = 150
time_on_production = time[: len(time) // 2] * fake_tau
randomness = np.maximum(
    0, np.random.default_rng(42).normal(0, 0.02, size=len(time_on_production))
).cumsum()
fake_cum = rf_real[: len(time) // 2] * fake_ogip + randomness
[9]:
rf_func = interp1d(time, rf_real, bounds_error=False, fill_value=(0, rf_real[-1]))
scaling_curve = ForecasterOnePhase(rf_func)
scaling_curve.fit(time_on_production, fake_cum)
print(
    f"The fitted OOIP is {scaling_curve.M_:.2f}, and it should be over {fake_ogip} "
    "(because all the randomness is positive)\n"
    f"The fitted tau is {scaling_curve.tau_:.2f}, and it should be near {fake_tau}"
)

cum_bestfit = scaling_curve.forecast_cum(time_on_production)

fig, ax = plt.subplots()
ax.plot(time_on_production, fake_cum, label="'Real' production")
ax.plot(time_on_production, cum_bestfit, "--", label="Fit")
ax.legend()
ax.set(
    xlim=(0, None),
    ylim=(0, None),
    xlabel="Time on production",
    ylabel="Cumulative production",
);
The fitted OOIP is 389.75, and it should be over 300 (because all the randomness is positive)
The fitted tau is 175.52, and it should be near 150
_images/forecast_13_1.png

Forecasting oil

Oil wells tend to be producing three phases at a time. Let’s say that the water phase is constant, and we can just worry about matching hydrocarbon production. These wells are almost all producing at below the bubble point, so a two-phase model is most appropriate.

[10]:
from bluebonnet.flow import (
    FlowPropertiesTwoPhase,
    relative_permeabilities,
)

# set conditions
Sw = 0.1
p_frac = 1000
p_res = 6_000
phi = 0.1

df_pvt_mp = pd.read_csv("../tests/data/pvt_multiphase_oil.csv")
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
)

# get recovery factor curve
res = SinglePhaseReservoir(50, p_frac, p_res, flow_props)
res.simulate(time)
rf_twophase = res.recovery_factor()

At this point, you have a recovery factor curve and can proceed just like real gas or ideal gas

[11]:
fake_ooip = 700
fake_tau = 200
time_on_production = time[: round(len(time) / 1.5)] * fake_tau
randomness = np.maximum(
    0, np.random.default_rng(42).normal(0, 0.02, size=len(time_on_production))
).cumsum()
fake_cum = rf_twophase[: round(len(time) / 1.5)] * fake_ooip + randomness
[12]:
rf_func = interp1d(time, rf_twophase, bounds_error=False, fill_value=(0, rf_twophase[-1]))
scaling_curve = ForecasterOnePhase(rf_func)
scaling_curve.fit(time_on_production, fake_cum)
print(
    f"The fitted OOIP is {scaling_curve.M_:.2f}, and it should be over {fake_ooip} "
    "(because all the randomness is positive)\n"
    f"The fitted tau is {scaling_curve.tau_:.2f}, and it should be near {fake_tau}"
)

cum_bestfit = scaling_curve.forecast_cum(time_on_production)

fig, ax = plt.subplots()
ax.plot(time_on_production, fake_cum, label="'Real' production")
ax.plot(time_on_production, cum_bestfit, "--", label="Fit")
ax.legend()
ax.set(
    xlim=(0, None),
    ylim=(0, None),
    xlabel="Time on production",
    ylabel="Cumulative production",
);
The fitted OOIP is 751.99, and it should be over 700 (because all the randomness is positive)
The fitted tau is 217.50, and it should be near 200
_images/forecast_18_1.png