Exercise 8.6
Correlation Tuning
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) |
|---|---|
| 200 | 1150 |
| 400 | 2340 |
| 600 | 3480 |
| 800 | 4650 |
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.
Stuck? Reveal hints one at a time — they progress from nudge to near-solution.
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.