Exerciseschevron_rightChapter 8chevron_right8.6
fitness_center

Exercise 8.6

Correlation Tuning

Level 2
Chapter 8: PVT Correlations
descriptionProblem

Standing's bubble-point correlation was fit to California crudes in 1947. The OML 58 lab in Port Harcourt just sent back a fresh PVT report on a 30-API crude (gamma_g = 0.75, reservoir T_F = 200), and the raw Standing numbers run low against their measured separator-flash bubble points. Before you trust the correlation to size the gas cap, you tune it with a single multiplicative correction factor and prove the tuned model actually fits better.

This is the lab table the PVT group handed you:

Rs (scf/STB)Pb_measured (psia)
2001150
4002340
6003480
8004650

Standing's correlation (embedded as standing_bubble_point) is:

exponent = 0.00091 * T_F - 0.0125 * API
Pb = 18.2 * ((Rs / gamma_g) ** 0.83 * 10 ** exponent - 1.4)

Write three things:

  • predicted_pb(rs_list, gamma_g, T_F, API): return a NumPy array of Standing

pb_psia for each Rs, i.e. [standing_bubble_point(rs, gamma_g, T_F, API) for rs in rs_list].

  • tuning_factor(rs_list, pb_measured, gamma_g, T_F, API): return the single

factor C that best scales Standing onto the lab data, taken as the mean of the per-point ratios: C = mean(pb_measured[i] / predicted[i]).

  • mean_abs_error(pb_measured, pb_model): the mean absolute error (psia)

between two equal-length arrays.

Then, for the lab crude above, store:

  • C = tuning_factor(...): the tuned correction factor,
  • mae_uncorrected = mean_abs_error(pb_measured, predicted),
  • mae_corrected = mean_abs_error(pb_measured, C * predicted).

The whole point: applying C must lower the mean absolute error. If your tuned model isn't tighter than raw Standing, the correction isn't earning its keep.

lightbulbHints (0/3)

Stuck? Reveal hints one at a time — they progress from nudge to near-solution.

codeYour solution
main.py
visibilityReveal reference solutionexpand_more

Try solving it yourself first — the hints walk you through it. The solution below is one valid approach; yours may differ and still be correct.

import numpy as np


def standing_bubble_point(Rs, gamma_g, T_F, API):
    exponent = 0.00091 * T_F - 0.0125 * API
    return 18.2 * ((Rs / gamma_g) ** 0.83 * 10 ** exponent - 1.4)


# OML 58 lab crude (Port Harcourt PVT report)
API = 30.0
gamma_g = 0.75
T_F = 200.0
rs_scf_stb = np.array([200.0, 400.0, 600.0, 800.0])
pb_measured = np.array([1150.0, 2340.0, 3480.0, 4650.0])


def predicted_pb(rs_list, gamma_g, T_F, API):
    """Standing bubble-point pb_psia for each Rs in rs_list (NumPy array)."""
    return np.array(
        [standing_bubble_point(rs, gamma_g, T_F, API) for rs in rs_list],
        dtype=float,
    )


def tuning_factor(rs_list, pb_measured, gamma_g, T_F, API):
    """Mean multiplicative correction C = mean(pb_measured / predicted)."""
    predicted = predicted_pb(rs_list, gamma_g, T_F, API)
    pb_measured = np.asarray(pb_measured, dtype=float)
    return float(np.mean(pb_measured / predicted))


def mean_abs_error(pb_measured, pb_model):
    """Mean absolute error (psia) between measured and modelled Pb."""
    pb_measured = np.asarray(pb_measured, dtype=float)
    pb_model = np.asarray(pb_model, dtype=float)
    return float(np.mean(np.abs(pb_measured - pb_model)))


predicted = predicted_pb(rs_scf_stb, gamma_g, T_F, API)
C = tuning_factor(rs_scf_stb, pb_measured, gamma_g, T_F, API)
mae_uncorrected = mean_abs_error(pb_measured, predicted)
mae_corrected = mean_abs_error(pb_measured, C * predicted)

print("predicted Pb (psia):", predicted)
print("tuning factor C:", C)
print("MAE uncorrected / corrected:", mae_uncorrected, mae_corrected)

lockCopying code is a Full Access feature.