welleng.kick_tolerance package
Tier-0 single-bubble kick-tolerance subpackage (clean-room).
Public single-bubble kick-tolerance closed form (Eqs. A-1…A-9 of the public SPE paper SPE-208788-PA) with a clean-room Hall & Yarborough (1973) real-gas Z-factor backend for the Tier-0 pure-methane influx.
Model class & scope (what this is, and is NOT)
This is a single-bubble, quasi-static, deterministic kick-tolerance calculator. One coherent (insoluble) gas influx is sized by the shoe / weak-zone pressure balance (the closed form) and marched up the annulus with Boyle + real-gas Z + optional temperature expansion (the migration engine). It is CONSERVATIVE (safe-side): a single coherent slug imposes the GREATEST shoe pressure, so the tolerable influx it returns is a LOWER BOUND on what a real dispersed / multiphase influx would allow. The literature confirms the direction – dynamic multiphase simulators return a HIGHER kick tolerance (SPE-208788-PA; Kiani Nassab et al., SPE-202426-PA, Fig. 12: static single-bubble 13-16 bbl vs dynamic ~17 bbl).
It is NOT a transient two-phase flow simulator: no gas/liquid slip or holdup, no flow-regime transitions, no gas dissolution into the mud, no transient annulus temperature (that is OLGA / Drillbench / “Simulator D” territory). Use this as a fast, reproducible, conservative well-control barrier check. A full multiphase model would only RELAX (raise) the tolerance – a casing-design margin-recovery tool, not a safety improvement – and its transient two-phase hydraulics belong with a hydraulics kernel, not here.
WORST-CASE PORE PRESSURE – the stated convention (READ THIS)
welleng uses PP + kick intensity, floored at the mud hydrostatic.
scenario_P_td = max( (PP + kick_intensity) * g * D_td , rho_mud * g * D_td )
This is a CHOICE, and it is the single biggest source of disagreement between kick-tolerance numbers in the industry. Santos & Sonnemann, Transitional Kick Tolerance, SPE-159175-MS (2012), names the two families in common use:
PP+KI – “Predicted pore pressure + kick intensity”. What we implement.
MW+KI – “Current Mud Density + kick intensity”, i.e. the worst case is referenced to the fluid in the hole rather than to the predicted pore pressure.
They give different answers, and the paper is blunt about the consequence: “one big issue in the industry today is the lack of consistency about KT calculation, leading to significant confusion and increased risk”, from “methods which provide widely diverging results”. It also warns that “several, slightly different ways” exist WITHIN each family – our mud-hydrostatic floor is one such variation, so quoting “PP+KI” alone does not pin the number either.
Quote the convention with the number. Two engineers can both be right and disagree by a wide margin, and neither will know why unless it is stated.
Not implemented: Transitional Kick Tolerance (the same paper) – KT as a curve over the whole hole section rather than a single value at section TD. The paper’s criticism that the industry focuses “almost exclusively on the KT at the end of a hole section” applies to this package as it stands.
STRING POSITION – the stated convention (READ THIS)
A kick tolerance is a number about a stated configuration, and this is ours. The engine can place the string anywhere; the convention below is what the REPORTED quantity means, not what the code is capable of. Quote it with the number.
Drilling case (
drill_kick, and the sectioned solvers used for it) – the BHA is ON BOTTOM. There is no string-position variable: the geometry is fixed and the kick margin is carried in the bottom-hole pressure. Nothing is searched over, because nothing moves.Swab case (
swab_kick, tripping) – the string position IS a variable, because the BHA can intercept the bubble at any depth as it is run in. The worst interception is COMPUTED, not assumed.Industry commonly assumes the worst case is bubble top at the shoe with the bit at the bubble’s bottom. We do not assume it, because we can afford to solve for it – and it is NOT conservative. The worst case has the bubble top at the BINDING depth, which is the shoe only when the shoe is what binds. Measured against the true worst: the shoe assumption is +0.68%, +0.79% and +25.42% high on a well whose weak zone sits below the shoe. It has the right shape and the wrong depth.
The true worst is determinate, not a search: the gas length
L(d)is density-driven and capacity-independent, so for each candidate binding depthdthe worst string position isd + L(d).
MAASP – the stated convention (READ THIS)
maasp() reports MAASP at the casing shoe, the industry definition:
MAASP = P_frac(shoe) - g.rho_mud.shoe
That is what a driller computes by hand and what belongs under a field labelled
“MAASP”. It is deliberately NOT redefined. limiting_psi carries the
generalisation – the same quantity minimised over every exposed depth. The two
are identical for a constant fracture gradient, because g.d.(FP_emw - rho_mud)
grows with depth so the shallowest exposed point governs; they separate only when a
weak zone sits below the shoe, and then the conventional number is the higher, less
safe one (governed_by_shoe tells you which case you are in).
Three properties that are easy to assume wrongly:
Shut in. Annular pressure loss is NOT deducted – APL is a circulating term, and subtracting it would understate the closed-in limit.
Mud-filled annulus. MAASP is a PLANNING number, not the shut-in limit during a kick: with influx in the annulus the column is lighter than mud and the surface pressure that breaks the shoe down is a different quantity – the one the kick-tolerance solve computes. Do not compare a live SICP against MAASP and conclude the shoe is safe.
``g`` does NOT cancel, unlike the kick tolerance itself. MAASP is a difference of two large pressures rather than an equivalent-mud-weight ratio, so a source quoting
g = 0.052differs from welleng’s0.0521by ~0.7% on a typical shoe. Expect that when reconciling against hand calcs and textbook examples.
A fully cased hole RAISES: nothing is exposed, so MAASP is undefined and the governing limit is casing burst instead.
Related shut-in quantities sidp_psi and sicp_psi are reported per
MigrationStep. SICP is a SCHEDULE, not a single value – it rises as the
bubble expands and migrates, so any displayed SICP must say which position it is.
INFLUX DENSITY <-> GRADIENT (READ THIS if you compare against a standard)
ppg_to_gradient(rho_ppg) and gradient_to_ppg(psi_per_ft), and the influx
gradient is reported on the result as rho_influx_gradient_psi_per_ft beside
rho_influx.
Why it exists. The well-control literature quotes influx gradients, not densities – NOGEPA-50 states a gas range of 0.05-0.15 psi/ft – so this is how a practitioner checks welleng’s computed density against the standard they work to.
Which constant, and why it matters. In SI a gradient needs no constant at all: it
is rho * g [Pa/m]. The field form folds g into a ppg->psi/ft factor and there
are THREE in circulation, differing by ~0.3%:
0.05210 G_PSI_PER_PPG_FT -- what THIS engine weights columns with
0.05200 NOGEPA_G -- the mandated NOGEPA-50 constant
0.0519481 units.PSI_PER_PPG_PER_FT -- exact, from standard gravity
These functions use the ENGINE’s constant, deliberately, so a reported gradient reproduces the column weight actually applied. Unlike a kick tolerance expressed in equivalent mud weights, the constant does NOT divide out here – it is an absolute gradient, so the full 0.3% is present. Want another convention? Multiply explicitly, so the choice is visible at the call site.
Comparison only – NOT an input path. Converting a quoted gradient to a density and
feeding it to the engine is hand-injected gas properties arriving through the front
door. That is what the gas-property overrides Z_s / Z_td / rho_gas_s
are for, and they exist for ONE purpose: reproducing a published worked example
against the paper’s own tabulated gas properties, independently of our Z backend
(tests/test_spe208788_worked_example.py does exactly this). They are not a way
to describe a real influx – a non-methane influx goes in as a composition and
welleng computes the density itself. Use this to check agreement, not to override it.
UNITS – the field-units contract (READ THIS)
Every input and output of this subpackage is in US oilfield field units,
deliberately, to match the validation papers (SPE-208788, NOGEPA, API RP 59) and
industry KT practice. This diverges from welleng’s SI convention: it is the ONE
field-units subpackage. Unit conversion is a BOUNDARY concern – a caller (API, GUI,
notebook) converts to/from these units with Pint (welleng.units = a pint
registry), the engine itself never converts. The canonical units are:
quantity unit engine parameters (examples) ——————– ———- —————————————– density / mud weight ppg rho_mud, rho_mud_ppg, rho_gas_s pressure (equiv. MW) ppg PP, kick_intensity, P_lot, pp/fp profiles pressure (absolute) psi P_apl, bhp_psi, and all returned pressures depth / length ft D_td, D_lot, section top/bottom, profile depths
– ALL depths are TRUE VERTICAL DEPTH (TVD)
temperature degF T_s, T_td (KickInputs) temperature degR T_bh in gas_bh_state; temp-profile callables volume bbl influx, kick tolerance, kt_threshold annular capacity bbl/ft V_dpa, WellSection.annular_capacity_bbl_per_ft inclination deg inc_shoe Z factor / fractions - Z_s, Z_td, gas mole fractions (dimensionless)
gas_bh_state tuple = (P_bh [psi], T_bh [degR], Z [-], rho_gas [ppg]) (any
element may be None -> computed). PP / fracture / kick-intensity are
mud-weight-EQUIVALENT densities (ppg), NOT absolute pressures. Temperatures are
degF on KickInputs but degR inside gas_bh_state and the temp-profile
callables – note the difference. Per-parameter units are also restated in each
function/dataclass docstring.
Layout
core– the margin logic (KickInputs/KickResult, drill/swab cases).gas_z– clean-room Hall & Yarborough (1973) Z-factor backend.envelope– deterministic worst-credible-case barrier envelope.monotonicity– per-case SymPy monotonicity analysis feeding the envelope.
core and gas_z depend only on the standard library plus numpy/scipy, so
they import with no optional dependencies. envelope and monotonicity
require SymPy (the optional kick extra); when SymPy is absent the envelope
API is exposed as a stub that raises a helpful ImportError on use, so that
import welleng (and the core kick-tolerance API) still works without it.
- class welleng.kick_tolerance.AnalyticalKickTolerance(max_influx_bbl: float, binding_gas_top_tvd: float, binding_gas_bottom_tvd: float, binding_depth_tvd: float, open_hole_unconstrained: bool, breakpoints: dict, surface_containment_bbl: float | None = None, casing_binds: bool = False, already_fractured: bool = False, maasp_psi: float = nan, maasp_limiting_psi: float = nan, maasp_governing_tvd: float = nan, maasp_governed_by_shoe: bool = True, rho_influx_bh_ppg: float = nan, rho_influx_bh_gradient_psi_per_ft: float = nan)[source]
Bases:
objectResult of
analytical_kick_tolerance().- __init__(max_influx_bbl: float, binding_gas_top_tvd: float, binding_gas_bottom_tvd: float, binding_depth_tvd: float, open_hole_unconstrained: bool, breakpoints: dict, surface_containment_bbl: float | None = None, casing_binds: bool = False, already_fractured: bool = False, maasp_psi: float = nan, maasp_limiting_psi: float = nan, maasp_governing_tvd: float = nan, maasp_governed_by_shoe: bool = True, rho_influx_bh_ppg: float = nan, rho_influx_bh_gradient_psi_per_ft: float = nan) None
- already_fractured: bool = False
- binding_depth_tvd: float
- binding_gas_bottom_tvd: float
- binding_gas_top_tvd: float
- breakpoints: dict
- casing_binds: bool = False
- maasp_governed_by_shoe: bool = True
- maasp_governing_tvd: float = nan
- maasp_limiting_psi: float = nan
- maasp_psi: float = nan
- max_influx_bbl: float
- open_hole_unconstrained: bool
- rho_influx_bh_gradient_psi_per_ft: float = nan
- rho_influx_bh_ppg: float = nan
- surface_containment_bbl: float | None = None
- class welleng.kick_tolerance.GasState(p_psia: float, t_rankine: float, z: float, rho_ppg: float, gradient_psi_per_ft: float, solved_for: str, reference_t_rankine: float | None = None, t_discrepancy_rankine: float | None = None)[source]
Bases:
objectA resolved influx gas state — see
gas_state_from_density().- __init__(p_psia: float, t_rankine: float, z: float, rho_ppg: float, gradient_psi_per_ft: float, solved_for: str, reference_t_rankine: float | None = None, t_discrepancy_rankine: float | None = None) None
- gradient_psi_per_ft: float
- p_psia: float
- reference_t_rankine: float | None = None
- rho_ppg: float
- solved_for: str
- t_discrepancy_rankine: float | None = None
- t_rankine: float
- z: float
- class welleng.kick_tolerance.KickInputs(rho_mud: float, PP: float, kick_intensity: float, P_lot: float, P_apl: float, D_td: float, D_lot: float, V_dpa: float, T_s: float | None = None, T_td: float | None = None, Z_s: float | None = None, Z_td: float | None = None, rho_gas_s: float | None = None, fluid: dict | None = None, kt_threshold: float = 25.0, inc_shoe: float = 0.0, model_revision: str = 'bubble-state', ideal_gas: bool = False)[source]
Bases:
objectOperational + gas-property inputs for a single-bubble kick-tolerance case.
- Operational (design-box) inputs:
rho_mud : drilling fluid density [ppg] PP : formation pore pressure (mud-weight equiv.) [ppg] kick_intensity : kick intensity added to PP for max-credible [ppg]
DRILL CASE ONLY. The swab case IGNORES kick_intensity by design: its bottom-hole pressure is the mud hydrostatic, independent of PP and KI (Nassab SPE-202426-PA, Eqs 8-9). See
swab_kick.P_lot : formation strength / LOT at the shoe [ppg] P_apl : surface-side pressure margin at the shoe [psi]
(applied choke/back pressure + safety margin). Annular friction is conventionally ZERO here (well killing is circulated at a reduced/kill rate, so annular dP is small); a user who wants to include it simply ADDS it to this term.
D_td : measured/true depth at TD [ft] D_lot : shoe (casing / LOT) depth [ft] T_s : temperature at the shoe [degF] T_td : temperature at TD [degF] V_dpa : annular capacity, drillpipe-in-hole [bbl/ft]
Gas-property inputs (COMPUTED when left as
None; a numeric value overrides). Default backend = clean-room Hall-Yarborough for pure methane; setfluidto use the CoolProp real-EOS mixture backend instead:Z_s : real-gas Z factor at the shoe station [-] Z_td : real-gas Z factor at the TD station [-] rho_gas_s : influx gas density at the shoe station [ppg] fluid : optional gas COMPOSITION (mole fractions), e.g.
{“Methane”: 0.9, “CO2”: 0.1}. When set, Z/density are computed via CoolProp (real EOS, CO2/CCUS mixtures) – requires welleng[kick]. None => pure-methane Hall-Yarborough.
- Design threshold:
kt_threshold : required tolerable-kick volume (margin datum) [bbl]
- Trajectory (deviated wells):
- inc_shoehole inclination at the shoe [deg]; 0.0 = vertical (the
SPE-208788 Table-1 case). Converts the gas column’s VERTICAL height H_gas to an along-hole length L_gas = H_gas / cos(inc) before it multiplies the per-MD annular capacity V_dpa (Nassab et al., SPE-202426-PA). Net effect: A -> A / cos(inc_shoe). D_td / D_lot are TRUE VERTICAL depths; all hydrostatic terms are TVD-referenced.
- D_lot: float
- D_td: float
- PP: float
- P_apl: float
- P_lot: float
- T_s: float | None = None
- T_td: float | None = None
- V_dpa: float
- Z_s: float | None = None
- Z_td: float | None = None
- __init__(rho_mud: float, PP: float, kick_intensity: float, P_lot: float, P_apl: float, D_td: float, D_lot: float, V_dpa: float, T_s: float | None = None, T_td: float | None = None, Z_s: float | None = None, Z_td: float | None = None, rho_gas_s: float | None = None, fluid: dict | None = None, kt_threshold: float = 25.0, inc_shoe: float = 0.0, model_revision: str = 'bubble-state', ideal_gas: bool = False) None
- fluid: dict | None = None
- classmethod from_survey(survey, shoe_md: float, td_md: float, **params)[source]
Build inputs from a welleng
Survey, reading the trajectory-derived quantities off the survey so callers don’t hand-transcribe them.D_lot/D_td(true vertical depths at the shoe and TD) andinc_shoe(hole inclination at the shoe) are interpolated from the survey atshoe_md/td_mdvia min-curvature (interpolate_md); every other input (rho_mud,PP,P_lot,V_dpa, …) is passed through inparams. The survey is duck-typed — any object withinterpolate_md(md) -> nodeexposingpos_nevandinc_degworks.Nodehas no direct.tvd; its position ispos_nev = [North, East, Vertical](welleng NEV convention), sopos_nev[2]IS the TVD. This is the GLOBAL (datum-referenced,start_nev-inclusive) TVD – which is the correct depth for the hydrostatic terms (the mud column is referenced to the surface datum, not the survey start). It equalssurvey.tvdonly at a zero datum (survey.tvdis the LOCAL depth, per theSurveydocstring); on a datum-shifted / sidetrack survey they differ bystart_nev, and the globalpos_nev[2]is the one KT wants. Reduces to MD for a vertical well.
- ideal_gas: bool = False
- inc_shoe: float = 0.0
- kick_intensity: float
- kt_threshold: float = 25.0
- model_revision: str = 'bubble-state'
- rho_gas_s: float | None = None
- rho_mud: float
- class welleng.kick_tolerance.KickResult(case: str, P_td: float, A: float, B: float, capacity: float, threshold: float, margin: float, passed: bool, pp_at_threshold: float, model_revision: str = 'bubble-state', T_influx: float | None = None, rho_influx: float | None = None, H_gas: float | None = None, bubble_length_limited: bool = False, capacity_negative: bool = False, maasp_psi: float = nan, rho_influx_gradient_psi_per_ft: float = nan)[source]
Bases:
objectResult of a single kick-tolerance case.
- A: float
- B: float
- H_gas: float | None = None
- P_td: float
- T_influx: float | None = None
- __init__(case: str, P_td: float, A: float, B: float, capacity: float, threshold: float, margin: float, passed: bool, pp_at_threshold: float, model_revision: str = 'bubble-state', T_influx: float | None = None, rho_influx: float | None = None, H_gas: float | None = None, bubble_length_limited: bool = False, capacity_negative: bool = False, maasp_psi: float = nan, rho_influx_gradient_psi_per_ft: float = nan) None
- bubble_length_limited: bool = False
- capacity: float
- capacity_negative: bool = False
- case: str
- maasp_psi: float = nan
- margin: float
- model_revision: str = 'bubble-state'
- passed: bool
- pp_at_threshold: float
- rho_influx: float | None = None
- rho_influx_gradient_psi_per_ft: float = nan
- threshold: float
- class welleng.kick_tolerance.KickToleranceResult(max_influx_bbl: float, binding_tvd: float, binding_step: int, limited_by: str, min_fp_margin_psi: float, open_hole_unconstrained: bool = False)[source]
Bases:
objectMigration kick tolerance V* AND where/why it is limited.
max_influx_bblis the largest influx that can be circulated out. The binding fields describe the breach that limits it.binding_tvdis the governing depth – which may be a WEAK FORMATION deeper than the casing shoe, NOT the shoe assumed by the static single-shoe check: the migration checks every exposed depth at every bubble position, so it reports the true limit.- __init__(max_influx_bbl: float, binding_tvd: float, binding_step: int, limited_by: str, min_fp_margin_psi: float, open_hole_unconstrained: bool = False) None
- binding_step: int
- binding_tvd: float
- limited_by: str
- max_influx_bbl: float
- min_fp_margin_psi: float
- open_hole_unconstrained: bool = False
- class welleng.kick_tolerance.MaaspResult(maasp_psi: float, shoe_tvd: float, limiting_psi: float, governing_tvd: float, governed_by_shoe: bool)[source]
Bases:
objectResult of
maasp().- __init__(maasp_psi: float, shoe_tvd: float, limiting_psi: float, governing_tvd: float, governed_by_shoe: bool) None
- governed_by_shoe: bool
- governing_tvd: float
- limiting_psi: float
- maasp_psi: float
- shoe_tvd: float
- class welleng.kick_tolerance.MigrationResult(steps: list, within_envelope: bool, min_fp_margin_psi: float, binding_tvd: float, binding_step: int, bha_length_exceeded: bool, _ctx: dict = <factory>)[source]
Bases:
objectOutcome of a bubble migration sweep.
stepsis the ordered per-step animation trajectory (bottom -> surface). The scalar fields summarise the worst (binding) point over the whole sweep.- __init__(steps: list, within_envelope: bool, min_fp_margin_psi: float, binding_tvd: float, binding_step: int, bha_length_exceeded: bool, _ctx: dict = <factory>) None
- bha_length_exceeded: bool
- binding_step: int
- binding_tvd: float
- min_fp_margin_psi: float
- steps: list
- within_envelope: bool
- class welleng.kick_tolerance.MigrationStep(gas_top_tvd: float, gas_bottom_tvd: float, gas_length_ft: float, min_fp_margin_psi: float, binding_tvd: float, p_at_binding_psi: float, sidp_psi: float, sicp_psi: float)[source]
Bases:
objectOne migration step – one frame of the animation trajectory.
- __init__(gas_top_tvd: float, gas_bottom_tvd: float, gas_length_ft: float, min_fp_margin_psi: float, binding_tvd: float, p_at_binding_psi: float, sidp_psi: float, sicp_psi: float) None
- binding_tvd: float
- gas_bottom_tvd: float
- gas_length_ft: float
- gas_top_tvd: float
- min_fp_margin_psi: float
- p_at_binding_psi: float
- sicp_psi: float
- sidp_psi: float
- class welleng.kick_tolerance.NogepaResult(kick_tolerance_bbl: float, gas_height_ft: float, influx_at_shoe_bbl: float, p_frac_shoe_psi: float)[source]
Bases:
objectNOGEPA-50 static drilling-kick tolerance and its intermediates.
- __init__(kick_tolerance_bbl: float, gas_height_ft: float, influx_at_shoe_bbl: float, p_frac_shoe_psi: float) None
- gas_height_ft: float
- influx_at_shoe_bbl: float
- kick_tolerance_bbl: float
- p_frac_shoe_psi: float
- class welleng.kick_tolerance.WellSection(top_tvd: float, bottom_tvd: float, annular_capacity_bbl_per_ft: float, is_open_hole: bool, top_md: float | None = None, bottom_md: float | None = None, burst_pressure_psi: float | None = None)[source]
Bases:
objectOne annular section, described against TVD.
- Parameters:
top_tvd – Section extent in true vertical depth [ft] (top shallower than bottom).
bottom_tvd – Section extent in true vertical depth [ft] (top shallower than bottom).
annular_capacity_bbl_per_ft (float) – Annular capacity of the section [bbl/ft] – the gas length in this section is volume / this capacity. Capacity changes discretely at section boundaries (e.g. casing shoe, BHA top).
is_open_hole (bool) – True for an exposed open-hole formation (subject to the PP-FP envelope check); False for a cased/protected interval (not checked).
top_md – Optional along-hole extent of the same section [ft]. Annular capacity is a volume per unit of ALONG-HOLE length, so in a deviated well the volume held between two TVDs is
capacity * dMD, notcapacity * dTVD. Supply these (from a survey) and the section reportscapacity_per_tvd_ftaccordingly. Leave themNoneand the section is treated as vertical (dMD == dTVD), which is the pre-0.27 behaviour exactly.bottom_md – Optional along-hole extent of the same section [ft]. Annular capacity is a volume per unit of ALONG-HOLE length, so in a deviated well the volume held between two TVDs is
capacity * dMD, notcapacity * dTVD. Supply these (from a survey) and the section reportscapacity_per_tvd_ftaccordingly. Leave themNoneand the section is treated as vertical (dMD == dTVD), which is the pre-0.27 behaviour exactly.burst_pressure_psi (float | None) –
Optional ALLOWABLE internal pressure for a cased section [psi] – the published minimum internal yield pressure already reduced by a design factor. Supply it and the imposed pressure is checked against it over the cased interval, alongside the pore/fracture check in open hole; leave it
None(the default) and cased intervals are unchecked, as they were before 0.27.INDICATIVE ONLY – this is not a casing design tool. It credits NO external backup (the full internal pressure is resisted by the pipe, which is the safe-side worst case), and it accounts for no axial load, bending, temperature derating, wear or connection rating. A real burst design is a differential calculation against a backup profile over the load cases. Use this to notice that the casing may bind before the formation does, not to size a string.
section (Pressure is a function of TVD and volume is a function of MD; a)
The (carries both extents so the two integrals stay in their own domains.)
sec(inc) (ratio dMD/dTVD is the section-mean)
be (so a section should)
with (short enough that inclination is near-constant across it -- build them)
:param
welleng.kick_tolerance.geometry.sections_from_architecture(): :param which: :param splits at the union of geometry changes and survey stations.:- __init__(top_tvd: float, bottom_tvd: float, annular_capacity_bbl_per_ft: float, is_open_hole: bool, top_md: float | None = None, bottom_md: float | None = None, burst_pressure_psi: float | None = None) None
- annular_capacity_bbl_per_ft: float
- bottom_md: float | None = None
- bottom_tvd: float
- burst_pressure_psi: float | None = None
- property capacity_per_tvd_ft: float
Annular capacity per foot of TVD [bbl/ft].
annular_capacity_bbl_per_ft * (dMD / dTVD)– the along-hole capacity scaled by the section-meansec(inc). Equals the raw capacity for a vertical section. Every volume in the TVD-domain engines isthis * dTVD.- Raises:
ValueError – If the section has zero TVD extent (a horizontal section holds volume across no TVD at all, which the TVD-domain formulation cannot represent).
- is_open_hole: bool
- property md_extent: float
Along-hole length of the section [ft]; the TVD extent if unset.
- top_md: float | None = None
- top_tvd: float
- welleng.kick_tolerance.analytical_kick_tolerance(sections: Sequence[WellSection], pp: Callable[[float], float] | tuple[Sequence[float], Sequence[float]], fp: Callable[[float], float] | tuple[Sequence[float], Sequence[float]], *, bhp_psi: float, rho_mud_ppg: float, gas_bh_state, gas_density_mode: str = 'conservative', temp_profile: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, geothermal: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, gas_composition=None, fluid_table=None, gas_model: str = 'real', check_depths: Sequence[float] | None = None, case: str | None = None) AnalyticalKickTolerance[source]
Max bottom-hole influx tolerable over the whole migration, by breakpoints.
Breakpoint alternative to
_max_influx_circulated(): evaluates the imposed pressure only at the breakpoints ofP(gas position)(§module docstring) and bisects the influx to the first fracture.gas_density_modeselects the safe-side numeric bound ("conservative", default) or the more accurate true-density value ("exact") – see the module docstring for the density convention, the borderline caveat, the geometry assumption and the breakpoint families.Real-gas backend (optional): pass
gas_composition(mole fractions, e.g.{"methane": 0.9, "co2": 0.1}) or a prebuiltfluid_table(ZTable) to use CoolProp real-EOS Z(P,T) for a mixture / CO2 / CCUS influx (built once over the case P,T box, then interpolated). Default (neither) = the clean-room Hall-Yarborough methane backend.- Units (US oilfield – see the subpackage docstring’s UNITS contract):
- sections
WellSectionwith TVDtop/bottom[ft] and annular_capacity_bbl_per_ft[bbl/ft].- pp, fppore / fracture profile
(tvd_ft, ppg)tables or callables – mud-weight-EQUIVALENT density [ppg] vs TVD [ft].
bhp_psi : bottom-hole (kill) pressure [psi]. rho_mud_ppg : drilling-fluid density [ppg]. gas_bh_state :
(P_bh [psi], T_bh [degR], Z [-], rho_gas [ppg])(any None-> computed). Returns influx / kick tolerance in [bbl], binding depths in [ft].
- case‘drill’ | ‘swab’ | None. Selects the TEMPERATURE convention when no
explicit
temp_profileis given – see_temperature_for_case(). ‘drill’ is a circulated kill (isothermal at T_bh, the TD-temperature limit) and REFUSES a geothermal profile, which reads 7-12% high; ‘swab’ is a trip and REQUIRES one.Nonekeeps the pre-0.27 precedence temp_profile > geothermal > isothermal.- check_depthsoptional explicit TVDs [ft] at which to enforce the
pore-fracture (PP-FP) envelope, OVERRIDING the auto-enumerated exposed depths (open-hole boundaries + PP/FP breakpoints + gas faces). Use it to pin the checked constraint set: e.g.
[shoe_tvd]gives SINGLE-SHOE semantics (only the casing shoe strength-checked; deeper FP jumps are not binding), the free-tier convention.None(default) = the full sections-aware multi-depth check. Gas-position enumeration is unaffected – the gas may still sit anywhere; the envelope is simply enforced only at these depths. NOTE: the constraint checked at each depth is the full envelopemin(FP - P, P - PP)– the fracture bound AND the pore bound. For a sane single-shoe well the imposed pressure P exceeds PP at the shoe, so the fracture bound is the active (binding) one there; the pore bound rides along and is slack. If you need a strictly fracture-only check, filter on the returned binding mechanism.
- sections
- welleng.kick_tolerance.annular_capacity(inner_id_in: float, pipe_od_in: float = 0.0) float[source]
Annulus capacity [bbl/ft] between a bore
inner_id_in(casing ID or open-hole diameter) and the stringpipe_od_ininside it:capacity = (inner_id^2 - pipe_od^2) / 1029.4
pipe_od_in = 0gives the full-bore capacity (no string).
- welleng.kick_tolerance.cased_section(top_tvd: float, bottom_tvd: float, *, casing_od_in: float, casing_weight_ppf: float, pipe_od_in: float, grade: str | None = None, burst_design_factor: float = 0.8) WellSection[source]
A cased WellSection with true annular capacity (casing ID from the API-5CT catalogue, minus the string OD).
is_open_hole=False– its formation is protected and is not exposed to fracture.When a
gradeis given the catalogue also yields the minimum internal yield pressure (API TR 5C3 Eq. 10, Barlow, already derated by minimum wall), and the section carriesburst_design_factortimes that as its allowable internal pressure. Without a grade there is no rating and the section is left unchecked – the catalogue flags an absent grade rather than guessing, and so does this.The burst check is INDICATIVE, not a casing design: no external backup is credited, and no axial, bending, temperature, wear or connection effects. See
WellSection.
- welleng.kick_tolerance.drill_kick(inp: KickInputs) KickResult[source]
Drillability gate (section PASS/FAIL).
A-7 tolerable-influx capacity at the MAXIMUM-CREDIBLE pore pressure, P_pp = (PP + kick_intensity) expressed as a pressure at D_td, per A-1.
Temperature is OPTIONAL here: omit
T_s/T_tdand supply an influx densityrho_gas_sto run the ideal-isothermal, temperature-cancels path (matches a simple density-based well-control tool). See_with_resolved_temperature().
- welleng.kick_tolerance.evaluate_envelope(*args, **kwargs)[source]
Stub raised when the optional
kickextra (SymPy) is missing.
- welleng.kick_tolerance.fluid_aliases() Dict[str, str][source]
Friendly component name -> CoolProp fluid name (e.g.
co2->CarbonDioxide). Case-insensitive keys; the same map used to resolve a composition before it reaches CoolProp.
- welleng.kick_tolerance.fluid_presets() list[source]
Curated influx-fluid shortlist:
[{label, composition}, ...].compositionis a mole-fraction dict in CoolProp canonical names, consumed as-is byKickInputs.fluid/analytical_kick_tolerance(gas_composition=). The natural-gas / sour entries are representative examples, not standards. For the full fluid list use CoolProp’sget_global_param_string("FluidsList").
- welleng.kick_tolerance.fluid_z_density(composition: Dict[str, float], p_psia: float, t_rankine: float) Tuple[float, float][source]
Real-gas (Z, density_ppg) for a gas
compositionat (P, T) via CoolProp.- Parameters:
composition (dict) – Component mole fractions, e.g.
{"Methane": 0.9, "CO2": 0.1}(names are case-insensitive; common aliases likeCO2/N2/H2Sare mapped).p_psia (float) – Pressure [psia] and absolute temperature [degR].
t_rankine (float) – Pressure [psia] and absolute temperature [degR].
- Returns:
Compressibility factor [-] and gas density [ppg mud-weight equivalent].
- Return type:
(z, rho_ppg)
- welleng.kick_tolerance.gas_density_ppg(p_psia: float, t_rankine: float, z: float, molar_mass_lbm: float = 16.043) float[source]
Real-gas density in ppg (mud-weight equivalent).
rho = P M / (Z R T) [lbm/ft^3], converted to ppg via 7.4805 gal/ft^3.
- welleng.kick_tolerance.gas_state_from_density(*, rho_ppg: float | None = None, gradient_psi_per_ft: float | None = None, p_psia: float | None = None, t_rankine: float | None = None, gas_gravity: float | None = None, molar_mass_lbm: float | None = None, t_pc_rankine: float | None = None, p_pc_psia: float | None = None, reference_t_rankine: float | None = None, z_fn=None, ideal: bool = False) GasState[source]
Given an influx DENSITY (or gradient), solve for the (P, T) that produces it.
A density does not determine a state.
rho = P.M / (Z.R.T)is one equation in two unknowns, so a density is a CURVE in (P, T), not a point. This therefore requires exactly ONE ofp_psiaort_rankineand solves for the other.The intended use is the READ-ONLY comparison a practitioner needs: they arrive with a published case that quotes a gas gradient and no temperature, and the well already has a BHP. Pin
p_psiato that BHP, solve for the temperature the quoted density implies, and show it beside the well’s actual bottom-hole temperature. When those two disagree, the quoted density is not describing this well’s gas – and that is the useful output, not a number to feed back in.Worked example of exactly that. A published case quotes 2.00 ppg at 5400 psi. Pin P and solve: it needs ~80 degF. At 10,000 ft TVD that is absurd, so either the influx is not pure methane (2.00 ppg at a realistic 180 degF needs a gas gravity of ~0.69) or the figure is a round number with no state behind it. Either way the user now knows, instead of having silently injected it.
Never write the result back into a design input. Solving a temperature to hit a density and then computing with it is hand-injected gas properties arriving through the front door. The overrides
Z_s/Z_td/rho_gas_sexist for ONE purpose – reproducing a published worked example against the paper’s own tabulated gas properties, independently of our Z backend – and NOT for describing a real influx: that goes in as a COMPOSITION and welleng computes the density itself. This function is a diagnostic.- Parameters:
rho_ppg – The target, as a density [ppg] OR a gradient [psi/ft]. Exactly one. The gradient is converted with THIS engine’s constant (see
ppg_to_gradient()).gradient_psi_per_ft – The target, as a density [ppg] OR a gradient [psi/ft]. Exactly one. The gradient is converted with THIS engine’s constant (see
ppg_to_gradient()).p_psia – Exactly one. The KNOWN one; the other is solved.
t_rankine – Exactly one. The KNOWN one; the other is solved.
molar_mass_lbm – Gas molar mass [lbm/lbmol]. Default methane (16.043). A heavier natural gas is ~19-21, i.e. gas gravity 0.65-0.73.
z_fn – Optional real-gas
(P, T) -> Zprovider (e.g. a CoolProp table). Default is the clean-room Hall & Yarborough methane backend.ideal –
TrueforcesZ = 1. Use for reproducing a textbook case that assumes it; do not use for engineering.
- Return type:
- Raises:
ValueError – If not exactly one target is given, or not exactly one known variable, or the target density is unreachable — in which case the message states the range that IS achievable, because that is the diagnostic.
Notes
Density is monotone in both variables (falling with T at fixed P, rising with P at fixed T), verified across 60-400 degF and 500-12,000 psia, so the root is unique and the bracketed solve cannot land on a second branch.
- welleng.kick_tolerance.gas_z(p_psia: float, t_rankine: float, gas_gravity: float = None, composition: dict = None, t_pc_rankine: float = None, p_pc_psia: float = None, method: str = 'auto', pc_correlation: str = 'standing') float[source]
Unified classic real-gas Z-factor — Hall-Yarborough or CoolProp EOS.
A single entry point that switches between the two classic/public backends:
Hall & Yarborough (fast correlation) for a hydrocarbon gas specified by
gas_gravity(or explicitt_pc_rankine/p_pc_psia);CoolProp real-EOS (accurate) for a
compositionmole-fraction dict (mixtures / non-hydrocarbons like CO2 / N2 / H2S).
- Parameters:
p_psia (float) – Absolute pressure [psia] and temperature [degR].
t_rankine (float) – Absolute pressure [psia] and temperature [degR].
gas_gravity (float, optional) – Gas gravity relative to air; pseudo-criticals are derived via
pc_correlation. Ignored ift_pc_rankine/p_pc_psiaare given.composition (dict, optional) – Mole-fraction composition (e.g.
{"Methane": 0.9, "CO2": 0.1}) -> the CoolProp path.t_pc_rankine (float, optional) – Explicit pseudo-criticals for the Hall-Yarborough path (overrides
gas_gravity). Default to Tier-0 methane if nothing is supplied.p_pc_psia (float, optional) – Explicit pseudo-criticals for the Hall-Yarborough path (overrides
gas_gravity). Default to Tier-0 methane if nothing is supplied.method ({"auto", "hall_yarborough", "coolprop"}) –
"auto"picks CoolProp when acompositionis given, else Hall-Yarborough. An explicit choice forces the backend.pc_correlation ({"standing", "sutton"}) – Gas-gravity -> pseudo-critical correlation for the Hall-Yarborough path.
- Returns:
The real-gas Z-factor [-].
- Return type:
float
Notes
CoolProp is an optional extra. If the CoolProp path is requested (or auto- selected) but CoolProp is not installed, this falls back to Hall-Yarborough when a
gas_gravity/pseudo-criticals are available (so the correlation path always works), and raises only if it has nothing to fall back on. This is the classic/public oracle; the fast analytical (Papay) form lives in welleng-api.
- welleng.kick_tolerance.gradient_to_ppg(gradient_psi_per_ft: float | ndarray)[source]
Pressure gradient [psi/ft] -> density [ppg]. Inverse of
ppg_to_gradient().Useful because the well-control literature quotes influx GRADIENTS, not densities – NOGEPA-50 states a gas range of 0.05-0.15 psi/ft – so this is how a quoted gradient is compared against what welleng computed from (P, T, composition).
Read-only comparison, not an input path. Converting a quoted gradient to a density and feeding it to the engine is hand-injected gas properties arriving through the front door. Z_s/Z_td/rho_gas_s exist ONLY to reproduce a published worked example against the paper’s own tabulated gas properties; a real influx goes in as a COMPOSITION and welleng computes the density itself. Use this to CHECK agreement, not to override it.
- welleng.kick_tolerance.hall_yarborough_z(p_psia: float, t_rankine: float, t_pc_rankine: float = 343.0, p_pc_psia: float = 667.0, y0: float = 0.001) float[source]
Real-gas Z-factor by the Hall & Yarborough (1973) correlation.
- Parameters:
p_psia – Absolute pressure [psia].
t_rankine – Absolute temperature [degR].
t_pc_rankine – Pseudo-critical temperature [degR] and pressure [psia]. Default to the Tier-0 pure-methane values.
p_pc_psia – Pseudo-critical temperature [degR] and pressure [psia]. Default to the Tier-0 pure-methane values.
- Returns:
Compressibility factor Z = 0.06125 Ppr t exp[-1.2 (1 - t)^2] / y.
- Return type:
float
- welleng.kick_tolerance.influx_column(inp: KickInputs, P_td: float | None = None) tuple[float, float, float][source]
The limiting influx column:
(T_influx [degF], H_gas [ft], rho_influx).The three quantities A-5 was actually evaluated with. Use these to draw or check the limiting pressure profile – they are the only set that closes.
THE IDENTITY. Reconstructing the shoe pressure from this column,
P_shoe = P_td - g [ rho_influx * H + rho_mud * (D_td - D_lot - H) ]
returns the binding fracture pressure
g * P_lot * D_lot - P_aplEXACTLY, for anyrho_influx, because A-2 defines H as(B - P_td) / [g (rho_mud - rho_influx)]and B carries the mud column over the same interval. That is the definition of the limiting condition: at the tolerable influx the shoe sits ON the fracture pressure. Verified to 0.00 psi under both revisions bytest_kick_model_revision.py.DO NOT back-derive H from
KickResult.capacityinstead. Expanding the capacity to the shoe goes through A-4, whose pressure bookkeeping is not algebraically identical to A-5/A-7 as printed (A-4 pairsP_lot - P_apl + P_atmwithP_td, while A-7 pairsP_lot - P_aplwithP_td + P_atm). On the Table-1 case that route misses the fracture pressure by -3.93 psi – IDENTICALLY under both revisions, so it is the paper’s own bookkeeping and not the influx-temperature correction. A profile built that way was already off before 0.26.0; it just did not move.rho_influxrespects an injectedrho_gas_s, so the identity holds for a caller supplying its own gas properties too.
- welleng.kick_tolerance.linear_temp_profile(shoe_tvd: float, shoe_temp_rankine: float, td_tvd: float, td_temp_rankine: float) Callable[[ndarray], ndarray][source]
Two-point (shoe + TD) linear temperature gradient – the BASIC case.
Returns a callable
tvd -> T_rankinefor a straight-line geothermal gradient anchored at the casing shoe and at TD. Extrapolated linearly outside[shoe_tvd, td_tvd](so the gradient continues to surface, TVD=0, as the bubble rises). For a full / field temperature survey pass a(tvd_array, T_rankine_array)table instead (the ADVANCED case).- Parameters:
shoe_tvd – Anchor depths [ft TVD];
td_tvdmust differ fromshoe_tvd.td_tvd – Anchor depths [ft TVD];
td_tvdmust differ fromshoe_tvd.shoe_temp_rankine – Temperatures at those depths [degR]. A geothermal gradient has
td_temp_rankine > shoe_temp_rankine(hotter with depth).td_temp_rankine – Temperatures at those depths [degR]. A geothermal gradient has
td_temp_rankine > shoe_temp_rankine(hotter with depth).
- welleng.kick_tolerance.maasp(sections: Sequence[WellSection], fp: Callable[[float], float] | tuple[Sequence[float], Sequence[float]], *, rho_mud_ppg: float, check_depths: Sequence[float] | None = None) MaaspResult[source]
Maximum allowable annular surface pressure, over the WHOLE exposed hole.
MAASP is the surface pressure that just brings the weakest exposed formation to its fracture pressure with the well shut in:
MAASP = min over exposed d of [ P_frac(d) - g.rho_mud.d ]
Shut-in, so no annular friction is deducted. Annular pressure loss is a CIRCULATING term; subtracting it here would understate the closed-in limit and is a different quantity. If a back-pressure or choke margin applies, subtract it from the returned value at the point of use, where its sign is unambiguous.
``maasp_psi`` is the CONVENTION and is not redefined: it is evaluated at the casing shoe, which is what a driller computes by hand and what belongs under a field labelled “MAASP”.
limiting_psicarries the generalisation – the same quantity minimised over every exposed depth.With a CONSTANT fracture gradient the two are identical, because
P_frac(d) - g.rho_mud.d = g.d.(FP_emw - rho_mud)grows with depth and the shallowest exposed point – the shoe – governs. They separate only when a weak zone sits BELOW the shoe, and then the conventional number is the higher, less safe one.governed_by_shoesays which case you are in.MAASP assumes a MUD-FILLED annulus. It is a planning number, not the shut-in limit during a kick: once influx is in the annulus the column is lighter than mud and the surface pressure at which the shoe breaks down is a different quantity (that is what the kick-tolerance solve computes). Do NOT compare a live SICP against MAASP and conclude the shoe is safe.
MAASP depends on the CURRENT mud weight, so it changes as the well is weighted up; it is a property of (hole, fracture profile, mud), not of an influx.
- Parameters:
sections (sequence of WellSection) – Only
is_open_holesections are exposed. Cased formations are behind pipe and are not assessed here (casing BURST is a separate limit – seemax_influx_contained_at_surface()).fp (ProfileLike) – Fracture pressure as a
(tvd, ppg)table or a callable [ppg EMW].rho_mud_ppg (float) – Current mud weight [ppg].
check_depths (sequence of float, optional) – Explicit depths to evaluate. Defaults to the open-hole boundaries plus the fracture-profile breakpoints that fall inside the open hole – the depths where the governing constraint can turn. A CALLABLE
fpexposes no breakpoints, so passcheck_depthsfor one or a narrow weak zone can be stepped over entirely.
- Return type:
- Raises:
ValueError – If no section is open hole – nothing is exposed, so MAASP is undefined.
- welleng.kick_tolerance.max_influx_contained_at_surface(sections: Sequence[WellSection], *, burst_pressure_psi: float, bhp_psi: float, rho_mud_ppg: float, gas_bh_state, bottom_tvd: float | None = None, temp_profile: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, z_fn=None) float[source]
Largest bottom-hole influx whose migration to surface still keeps the surface pressure within
burst_pressure_psi[bbl].The worst case for the casing is the whole influx arrived at surface: the gas has expanded, it has displaced the mud that was carrying the load, and the surface pressure is at its highest. With the bubble occupying
[0, L]and mud beneath it to TD, and the bottom pinned at the bottom-hole pressure:P_s * exp(k * L) + rho_mud * g * (TD - L) = BHP
One scalar equation in the gas length
L, solved directly – setP_sto the allowable and the equation gives the longest tolerable column, which the per-section capacities then turn into an influx. Surface pressure rises monotonically withL(gas replacing mud), so the root is unique.Compare the result with the kick tolerance from
analytical_kick_tolerance(): if it is SMALLER, the casing is the binding barrier rather than the formation, and the open-hole answer alone overstates what the well can take.INDICATIVE ONLY – this is not a casing design calculation. It credits no external backup, assumes the influx arrives coherently at surface with mud beneath it, and ignores axial load, bending, temperature derating, wear and connection ratings.
- Returns:
The influx [bbl at bottom-hole conditions].
0.0if the well cannot be shut in within the allowable even with no influx at all (i.e. the mud column alone already puts surface above the rating).- Return type:
float
- welleng.kick_tolerance.methane_properties(p_psia: float, t_rankine: float) tuple[float, float][source]
Convenience: (Z, density_ppg) for pure methane at (P, T).
- Returns:
Z-factor [-] and gas density [ppg] for Tier-0 methane.
- Return type:
(z, rho_ppg)
- welleng.kick_tolerance.migrate(sections: Sequence[WellSection], pp: Callable[[float], float] | tuple[Sequence[float], Sequence[float]], fp: Callable[[float], float] | tuple[Sequence[float], Sequence[float]], *, bhp_psi: float, influx_bbl_bh: float, rho_mud_ppg: float, gas_bh_state, gas_density_mode: str = 'conservative', temp_profile: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, geothermal: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, n_steps: int = 100, mode: str = 'thorough', ideal_gas: bool = False) MigrationResult[source]
March a single gas bubble up the annulus under constant BHP.
Holds the bottom-hole pressure constant at the kill value (Driller’s method; API RP 59 Sec. 4.10; SPE-202426) and steps the bubble top from the bottom to surface. At each step the gas expands by the real-gas law (Boyle + Z + T from the bottom-hole state; API RP 59 Sec. 4.8.7.3), its length follows from the annular capacity of the section(s) it occupies, and the imposed pressure is checked against the pore/fracture window
PP(d) <= P(d) <= FP(d)at every EXPOSED open-hole depth (barrier envelope; API RP 59 Sec. 12.5). Cased intervals are protected and not checked.This is an EXTENSION BEYOND the static NOGEPA single-shoe kick-tolerance method (NOGEPA-50) – a defensible assembly of the cited pieces. The single coherent bubble is the safe-side idealisation; OBM gas solubility is out of scope. See the module docstring.
- Parameters:
sections – Annular sections (
WellSection) covering surface (0) to bottom.pp – Pore and fracture pressure profiles [ppg], each a callable tvd->ppg OR a (tvd_array, ppg_array) table (numpy-interpolated).
fp – Pore and fracture pressure profiles [ppg], each a callable tvd->ppg OR a (tvd_array, ppg_array) table (numpy-interpolated).
bhp_psi – Constant bottom-hole pressure held during the kill [psi].
influx_bbl_bh – Influx volume at bottom-hole conditions V_bh [bbl].
rho_mud_ppg – Mud density [ppg].
gas_bh_state – (P_bh, T_bh_rankine, Z_bh, rho_gas_ppg).
P_bhNone ->bhp_psi;Z_bh/rho_gas_ppgNone -> computed by the Hall-Yarborough methane backend (gas_z.py);T_bh_rankinerequired.gas_density_mode ({"conservative", "exact"}) – Gas-column density treatment, passed through to
pressure_at_depth. Default"conservative"(gas-TOP lightest density, safe-side bound);"exact"integrates the true pressure-dependent local density. Seepressure_at_depth.temp_profile (None | callable | (tvd_array, T_rankine_array)) – Temperature profile T(d) [degR], passed through to
pressure_at_depthAND used in the Boyle expansion (the gas expands at the LOCAL temperature at its top, not the bottom-hole temperature).None(DEFAULT) is ISOTHERMAL atT_bh_rankine– reproduces the previous result exactly. Seelinear_temp_profilefor the basic two-point gradient.geothermal (None | callable | (tvd_array, T_rankine_array)) – Field geothermal gradient, the DEFAULT temperature when
temp_profileis not given (explicittemp_profile>geothermal> isothermal).n_steps – Number of migration steps (bottom -> surface).
- Returns:
.stepsis the per-step animation trajectory; scalar fields summarise the binding (worst) constraint over the whole sweep.- Return type:
- welleng.kick_tolerance.nogepa_drilling_kick_tolerance(*, formation_pressure_psi: float, z_oh_ft: float, z_csg_ft: float, lot_ppg: float, mud_ppg: float, cap_ann_bbl_per_ft: float, g_gas_psi_per_ft: float = 0.1) NogepaResult[source]
NOGEPA-50 §3.2 static drilling-kick tolerance [bbl] (the
V2value).Reproduces the mandated formula exactly (T and Z ignored, per the standard). The migration engine reduces to this when the casing shoe genuinely governs.
- welleng.kick_tolerance.open_hole_section(top_tvd: float, bottom_tvd: float, *, hole_size_in: float, pipe_od_in: float) WellSection[source]
An open-hole WellSection with true annular capacity (bit diameter minus the string OD).
is_open_hole=True– its formation is fracture-exposed.
- welleng.kick_tolerance.ppg_to_gradient(rho_ppg: float | ndarray) float | ndarray[source]
Density [ppg] -> pressure gradient [psi/ft], with THIS engine’s constant.
In SI this needs no constant at all – a gradient is just
rho * g[Pa/m]. The field-unit form carriesgfolded into a ppg->psi/ft factor, and there are three in circulation which differ by ~0.3%:0.05210 G_PSI_PER_PPG_FT -- what THIS engine weights columns with 0.05200 NOGEPA_G -- the mandated NOGEPA-50 constant 0.0519481 units.PSI_PER_PPG_PER_FT -- exact, from standard gravity
This function deliberately uses the ENGINE’s constant. A gradient reported with any other one does not reproduce the column weight the engine actually applied, and a consumer reconciling the two would be chasing a discrepancy that is purely a choice of constant. (welleng-api lost a morning to exactly that class of mismatch on 2026-07-28.) If you want the gradient under a DIFFERENT convention – to compare against a standard that mandates its own – multiply the density by that constant explicitly, so the choice is visible at the call site.
Note the constant does NOT divide out here, unlike a kick tolerance expressed in equivalent mud weights: this is an absolute gradient, so the ~0.3% spread above is fully present in the answer.
- Parameters:
rho_ppg – Density [ppg]. Scalar or array.
- Returns:
Pressure gradient [psi/ft].
- Return type:
float or ndarray
Examples
>>> round(ppg_to_gradient(1.70133), 6) # a real-gas methane influx 0.088639
- welleng.kick_tolerance.pressure_at_depth(depth_ft: float | ndarray, *, gas_top_tvd: float, gas_bottom_tvd: float, bottom_tvd: float, bhp_psi: float, rho_mud_ppg: float, gas_bh, gas_density_mode: str = 'conservative', temp_profile: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, geothermal: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, g: float = 0.0521, n_sub: int = 40, z_fn=None) float | ndarray[source]
Imposed pressure at
depth_ftfor BHP held constant at the bottom.z_fn(optional): a real-gas Z provider(P_psi, T_rankine) -> Z– e.g. a precomputed CoolPropZTablefor a mixture / CO2 / CCUS influx. WhenNone(default) the clean-room Hall-Yarborough methane backend is used with a warm-started Newton (behaviour unchanged, bit-for-bit).Marching UP from the bottom, hydrostatic is removed: a mud gradient outside the gas interval and a gas gradient inside [gas_top, gas_bottom]. The local gas density is NON-ISOTHERMAL:
rho_gas(P, d) = rho_gas_bh * (P * Z_bh * T_bh) / (P_bh * Z(P, T(d)) * T(d))
with
Z(P, T(d))from the clean-room Hall & Yarborough (1973) methane backend at the LOCAL temperatureT(d).gas_bh = (P_bh, T_bh_rankine, Z_bh, rho_gas_bh). Whentemp_profileisNonethe local temperature equalsT_bheverywhere, T cancels, and this reduces EXACTLY to the previous isothermal densityrho_gas_bh * (P * Z_bh) / (P_bh * Z(P)). Vectorised overdepth_ft.- Parameters:
temp_profile (None | callable | (tvd_array, T_rankine_array)) – Temperature profile T(d) [degR].
None(DEFAULT) is ISOTHERMAL at the bottom-hole temperatureT_bh_rankine(fromgas_bh) – reproduces the previous behaviour exactly. A callabletvd -> T_rankineor a(tvd, T_rankine)table sets a depth-varying temperature (seelinear_temp_profilefor the basic two-point gradient). Hotter gas up-hole is lighter (lower density) -> a HIGHER pressure at/above the gas.geothermal (None | callable | (tvd_array, T_rankine_array)) – The field geothermal gradient, used as the DEFAULT temperature when
temp_profileis not given. Resolution is three-tier: an explicittemp_profilewins; elsegeothermal(the go-to default when a gradient is known); else isothermal atT_bh_rankine.gas_density_mode ({"conservative", "exact"}) –
Gas-column density treatment (each evaluated at the local T(d)):
"conservative"(DEFAULT, safe-side) – use the gas density at the gas TOP (the lowest pressure -> lightest gas) as a CONSTANT for the whole gas column. The lightest-possible column gives the smallest pressure drop across the gas and hence the HIGHEST pressure at and above the gas top (and the shoe). This is the safe-side bound for a fracture barrier: it never under-states the shallow loading. Slightly over-conservative vs. the true integrated column."exact"– integrate the TRUE pressure-dependent local density down the column (forward Euler). This is the technically-correct / true- average result; it lies BELOW the conservative bound.
Ordering at any depth at/above the gas:
P(conservative) >= P(exact) >= P(bottom-hole-constant).n_sub – Sub-steps for the exact forward-Euler integration down the gas interval (also used to seed the gas-top pressure for the conservative mode).
- welleng.kick_tolerance.resolve_gas_properties(inp: KickInputs, P_td: float | None = None) tuple[float, float, float][source]
Return (Z_s, Z_td, rho_gas_s), computing any left as
None.TD station: (P_td, T_td) -> Z_td. Shoe station: (P_shoe, T_s) -> Z_s, rho_gas_s.
P_tdis the bottom-hole pressure the influx gas is stationed at. DefaultNoneuses the DRILL scenario pressurescenario_P_td(inp); the swab case passes its own P_td (mud hydrostatic) so kick_intensity does not leak into the swab gas properties (Nassab SPE-202426-PA Eqs 8-9).Backend: pure-methane Hall-Yarborough by default; if
inp.fluidis a composition dict, the CoolProp real-EOS mixture backend (CO2 / CCUS) is used. Injected numeric Z_s/Z_td/rho_gas_s values are used verbatim (override).
- welleng.kick_tolerance.standing_pseudo_criticals(gas_gravity: float) tuple[source]
Natural-gas pseudo-criticals from gas gravity — Standing (1977):
T_pc = 168 + 325.g - 12.5.g^2 [degR] P_pc = 677 + 15.0.g - 37.5.g^2 [psia]
gas_gravityis relative to AIR (air = 1.0), i.e.M_gas / 28.9647— a COMPOSITION, not a density. Pure methane is 0.5539.Why this matters and is not a refinement. Molar mass alone scales density but says nothing about Z; the Hall & Yarborough correlation needs pseudo-criticals. Using methane’s for a heavier gas is internally inconsistent, and not by a little: at 5400 psi, the temperature at which a 0.686-gravity gas reaches 2.00 ppg is 179.9 degF on methane’s pseudo-criticals and 194.6 degF on Standing’s — a 14.7 degF error, in a diagnostic whose entire purpose is to expose an implausible temperature.
Valid for sweet natural gases roughly 0.55-1.0. Sour gases (H2S, CO2) need a Wichert-Aziz correction, which is NOT applied here — pass explicit pseudo-criticals for those.
- welleng.kick_tolerance.sutton_pseudo_criticals(gas_gravity: float) tuple[source]
Natural-gas pseudo-criticals from gas gravity — Sutton (1985):
T_pc = 169.2 + 349.5.g - 74.0.g^2 [degR] P_pc = 756.8 - 131.0.g - 3.6.g^2 [psia]
An alternative to
standing_pseudo_criticals()fitted to a wider gravity range; preferred for heavier/associated gases (roughlyg0.57-1.68). Like Standing’s, it carries no sour-gas (H2S/CO2) correction – pass explicit pseudo-criticals (Wichert-Aziz corrected) for sour gases.
- welleng.kick_tolerance.swab_kick(inp: KickInputs) KickResult[source]
Unmitigated free-trip limit (A-8) – a MITIGABLE operational limit.
A-8 is A-7 with the bottom-hole pressure set to the mud hydrostatic, P_td = g * rho_mud * D_td (pore pressure <= mud hydrostatic). A swab failure means “pump out of the hole” – which is the drill case, assessed separately. Swab NEVER overrides drill and is never blind-min’d with it.
kick_intensityis DELIBERATELY UNUSED here. For a swabbed kick the bottom-hole pressure is the mud hydrostatic, independent of formation PP (and therefore of any intensity margin added to PP): Nassab SPE-202426-PA, Eqs 8-9 – “Ptd … is independent of formation PP and is equal to mud hydrostatic pressure. This concept is misunderstood … in many KT models that assume Ptd is equal to PP for both underbalanced and swabbed kicks[,] … [leading] to an overestimated KT value in swabbing conditions.” Carrying PP or PP+KI into the swab bottom-hole pressure would over-report swab KT (unsafe); dropping KI for swab is the correct, conservative default. This includes the influx gas STATIONING: A is evaluated with the swab P_td (mud hydrostatic), not the drill scenario pressure, so kick_intensity does not leak into Z/rho either.CONSCIOUS DIVERGENCE FROM SPE-208788-PA. That paper’s worked example shares ONE A constant across A-7 (drill) and A-8 (swab) – so its published swab figure (43.79 bbl, Table-1) stations the swab gas at the drill max-credible pressure (PP + KI), carrying kick intensity into a swab number. We reproduce that figure under its own convention in the validation suite, but our MODEL does not adopt it: a swab kick is not a drill kick, so it carries no kick intensity (Nassab SPE-202426-PA, Eqs 8-9). The difference is ~1.5% and only when PP+KI > mud (outside the swab model’s PP <= mud domain).
- welleng.kick_tolerance.swab_worst_bit(sections_for_bit, pp: Callable[[float], float] | tuple[Sequence[float], Sequence[float]], fp: Callable[[float], float] | tuple[Sequence[float], Sequence[float]], *, bhp_psi: float, rho_mud_ppg: float, gas_bh_state, bottom_tvd: float, shoe_tvd: float, gas_density_mode: str = 'conservative', temp_profile: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, geothermal: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, **kwargs)[source]
Worst string position for the SWAB case, and the kick tolerance there.
Tripping in, the BHA can intercept the bubble at any depth, so unlike the drilling case (BHA on bottom, geometry fixed) the string position is a variable. This finds the worst one WITHOUT searching over it.
Industry commonly assumes the worst case is bubble top at the shoe with the bit at the bubble’s bottom. That is not conservative – it hard-codes the shoe as the binding depth. Measured against the true worst it is +0.68%, +0.79% and +25.42% high, the last on a well whose weak zone sits below the shoe. Right shape, wrong depth.
The worst position is DETERMINATE. The gas length
L(d)is density-driven and capacity-independent, so with the bubble top pinned at a candidate binding depthd:bit* = d + L(d), L(d) from FP(d)*exp(k*L) = A + b*L
one closed-form solve per candidate depth, no search.
bit*is itself a breakpoint – it is exactly where the gas top lands on the pressure knot, so the binding candidate switches there – hence both sides of each are evaluated and the worse taken.Do NOT be tempted by the fixed point
bit := gas_bottom(bit): once the bit is deep enough the gas bottom equals the bit identically, so every position satisfies it and any iteration “converges” to wherever it started.- Parameters:
sections_for_bit (callable) –
bit_tvd -> list[WellSection]. Builds the annulus for a string position: the BHA/collar annulus above the bit, open hole below it.pp (ProfileLike) – Pore and fracture profiles as
(tvd, ppg)tables.fp (ProfileLike) – Pore and fracture profiles as
(tvd, ppg)tables.bhp_psi – As
analytical_kick_tolerance().rho_mud_ppg – As
analytical_kick_tolerance().gas_bh_state – As
analytical_kick_tolerance().bottom_tvd – Well TD and casing shoe [ft TVD].
shoe_tvd – Well TD and casing shoe [ft TVD].
**kwargs – Passed through to
analytical_kick_tolerance().
- Returns:
The result at the worst string position, and that position [ft TVD].
- Return type:
(AnalyticalKickTolerance, float)
Submodules
welleng.kick_tolerance.analytical module
Analytical (breakpoint) kick-tolerance solver.
The migration _max_influx_circulated()
marches a single gas bubble up the annulus and bisects the influx. That is exact
in the limit but (a) costs a fine march and (b) can UNDER-sample a narrow
breakpoint of the imposed-pressure-vs-position curve. This module computes the
kick tolerance by evaluating the imposed pressure only at the BREAKPOINTS of that
curve – the positions where the binding constraint can turn – and taking the
worst. It evaluates the true worst gas position (no sampling gap) in
O(breakpoints).
Density convention (the gas_density_mode switch, shared with the engine):
"conservative"– the whole influx is expanded to the gas-TOP pressure and placed at that lightest, constant density (longest column -> highest shallow loading -> lowest KT). This is the INHERENT CONSERVATISM of the numeric single-bubble model; the analytical solver reproduces that safe-side bound exactly. Default."exact"– integrate the TRUE (pressure-dependent) gas density up the column. This is MORE ACCURATE – it recovers the real tolerance the conservative bound forfeits. It is not “less conservative”; it is the accurate value, without the numeric model’s built-in cushion. CAVEAT: on a BORDERLINE result (computed max KT ~ the prescribed/regulatory max), the operator must apply their own margin, precisely because the accurate value carries no hidden slack.
The breakpoints of P(gas position) (for a fixed influx) are the configs where
either gas face crosses a discrete change in the problem:
gas TOP at a section boundary or a PP/FP breakpoint (classic shoe case)
gas BOTTOM at a section boundary – the fill alignments (was the missed set)
the DEEPEST bubble: gas bottom at TD (initial position)
the binding exposed depth switches (a PP/FP breakpoint under the gas/mud)
Evaluating only (1) – the naive “min over interfaces” – is NOT conservative: on a tight/BHA bottom section the worst position is (2)/(3), where the gas bottom crosses the BHA-top capacity discontinuity and fills the tight section to TD, so the gas top sits INTERIOR to the open hole (not on any boundary).
Geometry assumption (single-bubble model class): the bubble caps are
PERPENDICULAR to the well centerline (an annular slug), NOT horizontal
gravity-segregated interfaces. Gas geometry is 1-D along the wellbore axis; in a
deviated well the along-hole (MD) length maps to a TVD extent by cos(inc) (a
later refinement – this module is TVD-native like the rest of the engine).
Each candidate’s breach influx is solved DIRECTLY (no marching, no outer influx bisection):
families 1/4 (gas TOP at a boundary) – CLOSED FORM. At breach the gas-top pressure equals FP(d), so the gas length follows from the column pressure balance: conservative = a constant-density (linear) column -> a linear solve; exact = an exponential column
FP.exp(k.L) = A + b.L-> a Lambert-W-form root (a 3-iter Newton, dependency-free). The length is density-driven hence CAP-independent; only the bbl total walks the per-section capacities.families 2/3 (gas BOTTOM at a boundary + TD – the interior/tight-BHA binding a gas-top pin can’t express) – ALSO CLOSED FORM. Pinning the binding depth makes the bit depth cancel out of the balance, leaving
rho_mud.g.L - dP_gas(L) = C(d)withCa function of the binding depth alone: the same Lambert-W form as above, once per (gas-bottom pin, envelope depth) pair. The influx never needed bisecting._breach_v_gas_bottom()retains the bracketed secant as the REFERENCE implementation the closed form is tested against; it is not on the solve path.
The kick tolerance is the MIN breach influx over all candidates.
- class welleng.kick_tolerance.analytical.AnalyticalKickTolerance(max_influx_bbl: float, binding_gas_top_tvd: float, binding_gas_bottom_tvd: float, binding_depth_tvd: float, open_hole_unconstrained: bool, breakpoints: dict, surface_containment_bbl: float | None = None, casing_binds: bool = False, already_fractured: bool = False, maasp_psi: float = nan, maasp_limiting_psi: float = nan, maasp_governing_tvd: float = nan, maasp_governed_by_shoe: bool = True, rho_influx_bh_ppg: float = nan, rho_influx_bh_gradient_psi_per_ft: float = nan)[source]
Bases:
objectResult of
analytical_kick_tolerance().- __init__(max_influx_bbl: float, binding_gas_top_tvd: float, binding_gas_bottom_tvd: float, binding_depth_tvd: float, open_hole_unconstrained: bool, breakpoints: dict, surface_containment_bbl: float | None = None, casing_binds: bool = False, already_fractured: bool = False, maasp_psi: float = nan, maasp_limiting_psi: float = nan, maasp_governing_tvd: float = nan, maasp_governed_by_shoe: bool = True, rho_influx_bh_ppg: float = nan, rho_influx_bh_gradient_psi_per_ft: float = nan) None
- already_fractured: bool = False
- binding_depth_tvd: float
- binding_gas_bottom_tvd: float
- binding_gas_top_tvd: float
- breakpoints: dict
- casing_binds: bool = False
- maasp_governed_by_shoe: bool = True
- maasp_governing_tvd: float = nan
- maasp_limiting_psi: float = nan
- maasp_psi: float = nan
- max_influx_bbl: float
- open_hole_unconstrained: bool
- rho_influx_bh_gradient_psi_per_ft: float = nan
- rho_influx_bh_ppg: float = nan
- surface_containment_bbl: float | None = None
- welleng.kick_tolerance.analytical.analytical_kick_tolerance(sections: Sequence[WellSection], pp: Callable[[float], float] | tuple[Sequence[float], Sequence[float]], fp: Callable[[float], float] | tuple[Sequence[float], Sequence[float]], *, bhp_psi: float, rho_mud_ppg: float, gas_bh_state, gas_density_mode: str = 'conservative', temp_profile: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, geothermal: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, gas_composition=None, fluid_table=None, gas_model: str = 'real', check_depths: Sequence[float] | None = None, case: str | None = None) AnalyticalKickTolerance[source]
Max bottom-hole influx tolerable over the whole migration, by breakpoints.
Breakpoint alternative to
_max_influx_circulated(): evaluates the imposed pressure only at the breakpoints ofP(gas position)(§module docstring) and bisects the influx to the first fracture.gas_density_modeselects the safe-side numeric bound ("conservative", default) or the more accurate true-density value ("exact") – see the module docstring for the density convention, the borderline caveat, the geometry assumption and the breakpoint families.Real-gas backend (optional): pass
gas_composition(mole fractions, e.g.{"methane": 0.9, "co2": 0.1}) or a prebuiltfluid_table(ZTable) to use CoolProp real-EOS Z(P,T) for a mixture / CO2 / CCUS influx (built once over the case P,T box, then interpolated). Default (neither) = the clean-room Hall-Yarborough methane backend.- Units (US oilfield – see the subpackage docstring’s UNITS contract):
- sections
WellSectionwith TVDtop/bottom[ft] and annular_capacity_bbl_per_ft[bbl/ft].- pp, fppore / fracture profile
(tvd_ft, ppg)tables or callables – mud-weight-EQUIVALENT density [ppg] vs TVD [ft].
bhp_psi : bottom-hole (kill) pressure [psi]. rho_mud_ppg : drilling-fluid density [ppg]. gas_bh_state :
(P_bh [psi], T_bh [degR], Z [-], rho_gas [ppg])(any None-> computed). Returns influx / kick tolerance in [bbl], binding depths in [ft].
- case‘drill’ | ‘swab’ | None. Selects the TEMPERATURE convention when no
explicit
temp_profileis given – see_temperature_for_case(). ‘drill’ is a circulated kill (isothermal at T_bh, the TD-temperature limit) and REFUSES a geothermal profile, which reads 7-12% high; ‘swab’ is a trip and REQUIRES one.Nonekeeps the pre-0.27 precedence temp_profile > geothermal > isothermal.- check_depthsoptional explicit TVDs [ft] at which to enforce the
pore-fracture (PP-FP) envelope, OVERRIDING the auto-enumerated exposed depths (open-hole boundaries + PP/FP breakpoints + gas faces). Use it to pin the checked constraint set: e.g.
[shoe_tvd]gives SINGLE-SHOE semantics (only the casing shoe strength-checked; deeper FP jumps are not binding), the free-tier convention.None(default) = the full sections-aware multi-depth check. Gas-position enumeration is unaffected – the gas may still sit anywhere; the envelope is simply enforced only at these depths. NOTE: the constraint checked at each depth is the full envelopemin(FP - P, P - PP)– the fracture bound AND the pore bound. For a sane single-shoe well the imposed pressure P exceeds PP at the shoe, so the fracture bound is the active (binding) one there; the pore bound rides along and is slack. If you need a strictly fracture-only check, filter on the returned binding mechanism.
- sections
- welleng.kick_tolerance.analytical.max_influx_contained_at_surface(sections: Sequence[WellSection], *, burst_pressure_psi: float, bhp_psi: float, rho_mud_ppg: float, gas_bh_state, bottom_tvd: float | None = None, temp_profile: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, z_fn=None) float[source]
Largest bottom-hole influx whose migration to surface still keeps the surface pressure within
burst_pressure_psi[bbl].The worst case for the casing is the whole influx arrived at surface: the gas has expanded, it has displaced the mud that was carrying the load, and the surface pressure is at its highest. With the bubble occupying
[0, L]and mud beneath it to TD, and the bottom pinned at the bottom-hole pressure:P_s * exp(k * L) + rho_mud * g * (TD - L) = BHP
One scalar equation in the gas length
L, solved directly – setP_sto the allowable and the equation gives the longest tolerable column, which the per-section capacities then turn into an influx. Surface pressure rises monotonically withL(gas replacing mud), so the root is unique.Compare the result with the kick tolerance from
analytical_kick_tolerance(): if it is SMALLER, the casing is the binding barrier rather than the formation, and the open-hole answer alone overstates what the well can take.INDICATIVE ONLY – this is not a casing design calculation. It credits no external backup, assumes the influx arrives coherently at surface with mud beneath it, and ignores axial load, bending, temperature derating, wear and connection ratings.
- Returns:
The influx [bbl at bottom-hole conditions].
0.0if the well cannot be shut in within the allowable even with no influx at all (i.e. the mud column alone already puts surface above the rating).- Return type:
float
- welleng.kick_tolerance.analytical.swab_worst_bit(sections_for_bit, pp: Callable[[float], float] | tuple[Sequence[float], Sequence[float]], fp: Callable[[float], float] | tuple[Sequence[float], Sequence[float]], *, bhp_psi: float, rho_mud_ppg: float, gas_bh_state, bottom_tvd: float, shoe_tvd: float, gas_density_mode: str = 'conservative', temp_profile: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, geothermal: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, **kwargs)[source]
Worst string position for the SWAB case, and the kick tolerance there.
Tripping in, the BHA can intercept the bubble at any depth, so unlike the drilling case (BHA on bottom, geometry fixed) the string position is a variable. This finds the worst one WITHOUT searching over it.
Industry commonly assumes the worst case is bubble top at the shoe with the bit at the bubble’s bottom. That is not conservative – it hard-codes the shoe as the binding depth. Measured against the true worst it is +0.68%, +0.79% and +25.42% high, the last on a well whose weak zone sits below the shoe. Right shape, wrong depth.
The worst position is DETERMINATE. The gas length
L(d)is density-driven and capacity-independent, so with the bubble top pinned at a candidate binding depthd:bit* = d + L(d), L(d) from FP(d)*exp(k*L) = A + b*L
one closed-form solve per candidate depth, no search.
bit*is itself a breakpoint – it is exactly where the gas top lands on the pressure knot, so the binding candidate switches there – hence both sides of each are evaluated and the worse taken.Do NOT be tempted by the fixed point
bit := gas_bottom(bit): once the bit is deep enough the gas bottom equals the bit identically, so every position satisfies it and any iteration “converges” to wherever it started.- Parameters:
sections_for_bit (callable) –
bit_tvd -> list[WellSection]. Builds the annulus for a string position: the BHA/collar annulus above the bit, open hole below it.pp (ProfileLike) – Pore and fracture profiles as
(tvd, ppg)tables.fp (ProfileLike) – Pore and fracture profiles as
(tvd, ppg)tables.bhp_psi – As
analytical_kick_tolerance().rho_mud_ppg – As
analytical_kick_tolerance().gas_bh_state – As
analytical_kick_tolerance().bottom_tvd – Well TD and casing shoe [ft TVD].
shoe_tvd – Well TD and casing shoe [ft TVD].
**kwargs – Passed through to
analytical_kick_tolerance().
- Returns:
The result at the worst string position, and that position [ft TVD].
- Return type:
(AnalyticalKickTolerance, float)
welleng.kick_tolerance.core module
Single-bubble kick-tolerance margin module (clean-room).
Implements the public single-bubble kick-tolerance closed form, Eqs. A-1…A-9, as derived in Appendix A of the public SPE paper SPE-208788-PA (Thorogood et al., “Kick Tolerance …”, SPE Drilling & Completion, September 2022, pp. 242-243). The equations are transcribed faithfully from that public source only.
Clean-room scope
Gas properties (the real-gas compressibility factors Z_s, Z_td and the
influx gas density at the shoe rho_gas_s) are COMPUTED by the clean-room
Hall & Yarborough (1973) backend in gas_z.py from the influx composition
(Tier 0 = pure methane) and the model’s own pressure/temperature conditions.
They may still be INJECTED as overrides (any of Z_s, Z_td,
rho_gas_s left as None is computed; a numeric value is used verbatim),
which keeps mixtures / CO2-bearing systems (e.g. a CoolProp HEOS backend)
pluggable. No external, third-party, or private gas-property data source is
referenced.
Where the gas properties are evaluated
The influx gas is a single migrating bubble. Its properties are evaluated once per scenario at two physical stations and shared by both the drill and swab cases (as in the reference derivation):
TD station – (P_td, T_td), with P_td the A-1 bottom-hole pressure at the maximum-credible pore pressure. Gives
Z_tdand (unused)rho_gas_td.Influx station – (P_shoe, T_influx).
P_shoeis the influx-gas pressure at the casing shoe: the bottom-hole pressure reduced by the static influx-gas column over the open-hole section (a standard, source-free gas-gradient step). GivesZ_sandrho_gas_s.
T_influxis REVISION-DEPENDENT (seeMODEL_REVISIONS). As published it is the shoe temperatureT_s; from welleng 0.26.0 the default is the INFLUX COLUMN’s mean temperature, because the column hangs BELOW the shoe and the shoe is its cool end. Evaluating at the shoe made the gas denser than the column, inflating A and OVERSTATING the tolerable influx by ~3% on the Table-1 case (anti-conservative). See_influx_temperature.
Note on the reference case: the reference paper’s printed shoe Z (1.123) sits above the value at the shoe fracture pressure P_lot - P_apl (~1.04); it corresponds to a near-bottom-hole influx-gas pressure, which is what the static gas column recovers. Reproducing the paper’s Table-1 gas properties therefore lands the kick volumes within ~1-1.3% – the paper’s own Table-1 inconsistency, not a backend error.
Two first-class cases (kept SEPARATE by design)
drill_kick– the drillability gate. A-7 tolerable-influx capacity evaluated at the maximum-credible pore pressure (PP + kick intensity). This is the section PASS/FAIL.
swab_kick– the unmitigated free-trip limit (A-8), i.e. A-7 with the bottom-hole pressure set to the mud hydrostatic. This is a MITIGABLE operational limit: a swab failure flags “pump out of the hole” – and pumping out is itself the drill case, which is already assessed. Swab NEVER overrides drill; the two are reported separately and are never blind-min’d / AND-gated.
Units
Depths ft; densities ppg; pressures psi; temperatures input in degF and
converted to absolute degR internally; annular capacity V_dpa in bbl/ft so
that capacities come out in bbl. Gravitational constant g = 0.0521
psi.ppg^-1.ft^-1 (oilfield units); atmospheric pressure P_atm = 14.7 psi.
Note (from the derivation): the gas density at TD, rho_gas_td, appears in NO equation of the closed form – only rho_gas_s does (via the A-5 constant A).
- class welleng.kick_tolerance.core.KickInputs(rho_mud: float, PP: float, kick_intensity: float, P_lot: float, P_apl: float, D_td: float, D_lot: float, V_dpa: float, T_s: float | None = None, T_td: float | None = None, Z_s: float | None = None, Z_td: float | None = None, rho_gas_s: float | None = None, fluid: dict | None = None, kt_threshold: float = 25.0, inc_shoe: float = 0.0, model_revision: str = 'bubble-state', ideal_gas: bool = False)[source]
Bases:
objectOperational + gas-property inputs for a single-bubble kick-tolerance case.
- Operational (design-box) inputs:
rho_mud : drilling fluid density [ppg] PP : formation pore pressure (mud-weight equiv.) [ppg] kick_intensity : kick intensity added to PP for max-credible [ppg]
DRILL CASE ONLY. The swab case IGNORES kick_intensity by design: its bottom-hole pressure is the mud hydrostatic, independent of PP and KI (Nassab SPE-202426-PA, Eqs 8-9). See
swab_kick.P_lot : formation strength / LOT at the shoe [ppg] P_apl : surface-side pressure margin at the shoe [psi]
(applied choke/back pressure + safety margin). Annular friction is conventionally ZERO here (well killing is circulated at a reduced/kill rate, so annular dP is small); a user who wants to include it simply ADDS it to this term.
D_td : measured/true depth at TD [ft] D_lot : shoe (casing / LOT) depth [ft] T_s : temperature at the shoe [degF] T_td : temperature at TD [degF] V_dpa : annular capacity, drillpipe-in-hole [bbl/ft]
Gas-property inputs (COMPUTED when left as
None; a numeric value overrides). Default backend = clean-room Hall-Yarborough for pure methane; setfluidto use the CoolProp real-EOS mixture backend instead:Z_s : real-gas Z factor at the shoe station [-] Z_td : real-gas Z factor at the TD station [-] rho_gas_s : influx gas density at the shoe station [ppg] fluid : optional gas COMPOSITION (mole fractions), e.g.
{“Methane”: 0.9, “CO2”: 0.1}. When set, Z/density are computed via CoolProp (real EOS, CO2/CCUS mixtures) – requires welleng[kick]. None => pure-methane Hall-Yarborough.
- Design threshold:
kt_threshold : required tolerable-kick volume (margin datum) [bbl]
- Trajectory (deviated wells):
- inc_shoehole inclination at the shoe [deg]; 0.0 = vertical (the
SPE-208788 Table-1 case). Converts the gas column’s VERTICAL height H_gas to an along-hole length L_gas = H_gas / cos(inc) before it multiplies the per-MD annular capacity V_dpa (Nassab et al., SPE-202426-PA). Net effect: A -> A / cos(inc_shoe). D_td / D_lot are TRUE VERTICAL depths; all hydrostatic terms are TVD-referenced.
- D_lot: float
- D_td: float
- PP: float
- P_apl: float
- P_lot: float
- T_s: float | None = None
- T_td: float | None = None
- V_dpa: float
- Z_s: float | None = None
- Z_td: float | None = None
- __init__(rho_mud: float, PP: float, kick_intensity: float, P_lot: float, P_apl: float, D_td: float, D_lot: float, V_dpa: float, T_s: float | None = None, T_td: float | None = None, Z_s: float | None = None, Z_td: float | None = None, rho_gas_s: float | None = None, fluid: dict | None = None, kt_threshold: float = 25.0, inc_shoe: float = 0.0, model_revision: str = 'bubble-state', ideal_gas: bool = False) None
- fluid: dict | None = None
- classmethod from_survey(survey, shoe_md: float, td_md: float, **params)[source]
Build inputs from a welleng
Survey, reading the trajectory-derived quantities off the survey so callers don’t hand-transcribe them.D_lot/D_td(true vertical depths at the shoe and TD) andinc_shoe(hole inclination at the shoe) are interpolated from the survey atshoe_md/td_mdvia min-curvature (interpolate_md); every other input (rho_mud,PP,P_lot,V_dpa, …) is passed through inparams. The survey is duck-typed — any object withinterpolate_md(md) -> nodeexposingpos_nevandinc_degworks.Nodehas no direct.tvd; its position ispos_nev = [North, East, Vertical](welleng NEV convention), sopos_nev[2]IS the TVD. This is the GLOBAL (datum-referenced,start_nev-inclusive) TVD – which is the correct depth for the hydrostatic terms (the mud column is referenced to the surface datum, not the survey start). It equalssurvey.tvdonly at a zero datum (survey.tvdis the LOCAL depth, per theSurveydocstring); on a datum-shifted / sidetrack survey they differ bystart_nev, and the globalpos_nev[2]is the one KT wants. Reduces to MD for a vertical well.
- ideal_gas: bool = False
- inc_shoe: float = 0.0
- kick_intensity: float
- kt_threshold: float = 25.0
- model_revision: str = 'bubble-state'
- rho_gas_s: float | None = None
- rho_mud: float
- class welleng.kick_tolerance.core.KickResult(case: str, P_td: float, A: float, B: float, capacity: float, threshold: float, margin: float, passed: bool, pp_at_threshold: float, model_revision: str = 'bubble-state', T_influx: float | None = None, rho_influx: float | None = None, H_gas: float | None = None, bubble_length_limited: bool = False, capacity_negative: bool = False, maasp_psi: float = nan, rho_influx_gradient_psi_per_ft: float = nan)[source]
Bases:
objectResult of a single kick-tolerance case.
- A: float
- B: float
- H_gas: float | None = None
- P_td: float
- T_influx: float | None = None
- __init__(case: str, P_td: float, A: float, B: float, capacity: float, threshold: float, margin: float, passed: bool, pp_at_threshold: float, model_revision: str = 'bubble-state', T_influx: float | None = None, rho_influx: float | None = None, H_gas: float | None = None, bubble_length_limited: bool = False, capacity_negative: bool = False, maasp_psi: float = nan, rho_influx_gradient_psi_per_ft: float = nan) None
- bubble_length_limited: bool = False
- capacity: float
- capacity_negative: bool = False
- case: str
- maasp_psi: float = nan
- margin: float
- model_revision: str = 'bubble-state'
- passed: bool
- pp_at_threshold: float
- rho_influx: float | None = None
- rho_influx_gradient_psi_per_ft: float = nan
- threshold: float
- welleng.kick_tolerance.core.P_td_from_A1(inp: KickInputs, P_pp_psi: float) float[source]
A-1: P_td = max{P_pp, D_td * g * rho_mud} [psi].
- welleng.kick_tolerance.core.annular_capacity_dpa(hole_id_in: float, pipe_od_in: float) float[source]
Convenience: annular capacity V_dpa = (hole^2 - pipe^2) / 1029.4 [bbl/ft].
Per the A-3 simplification the influx occupies the drillpipe-in-hole annulus at the shoe (pipe_od = drillpipe OD). This is a geometry helper only; V_dpa may equally be supplied directly as an input.
- welleng.kick_tolerance.core.constant_A(inp: KickInputs, P_td: float | None = None) float[source]
- A-5: A = (P_lot - P_apl) * T_td * Z_td * V_dpa
/ [ g * T_s * Z_s * (rho_mud - rho_gas_s) ] [bbl]
P_lot is converted to a pressure at the shoe depth (g * P_lot * D_lot). Temperatures are absolute (degR). Gas properties (Z_s, Z_td, rho_gas_s) are computed by the clean-room Hall-Yarborough backend unless injected, stationed at
P_td(default: the drill scenario pressure; swab passes mud hydrostatic – seeresolve_gas_properties).Deviated wells (Nassab et al., SPE-202426-PA): the gas column’s vertical height H_gas occupies an along-hole length L_gas = H_gas / cos(inc_shoe), which is what multiplies the per-MD annular capacity V_dpa. This scales A by 1 / cos(inc_shoe). inc_shoe = 0 (vertical) recovers the SPE-208788 Table-1 form exactly. (Exact survey-integral form – absorbing BHA change-of-section – is a documented follow-up; this constant-inclination form is the standard published convention.)
- welleng.kick_tolerance.core.constant_B(inp: KickInputs) float[source]
A-6: B = P_lot - P_apl + g * rho_mud * (D_td - D_lot) [psi]
(the maximum pore pressure at which the tolerable influx is zero).
- welleng.kick_tolerance.core.drill_kick(inp: KickInputs) KickResult[source]
Drillability gate (section PASS/FAIL).
A-7 tolerable-influx capacity at the MAXIMUM-CREDIBLE pore pressure, P_pp = (PP + kick_intensity) expressed as a pressure at D_td, per A-1.
Temperature is OPTIONAL here: omit
T_s/T_tdand supply an influx densityrho_gas_sto run the ideal-isothermal, temperature-cancels path (matches a simple density-based well-control tool). See_with_resolved_temperature().
- welleng.kick_tolerance.core.fahrenheit_to_rankine(t_degf: float) float[source]
Absolute temperature in degR (paper uses a +460 offset).
- welleng.kick_tolerance.core.influx_column(inp: KickInputs, P_td: float | None = None) tuple[float, float, float][source]
The limiting influx column:
(T_influx [degF], H_gas [ft], rho_influx).The three quantities A-5 was actually evaluated with. Use these to draw or check the limiting pressure profile – they are the only set that closes.
THE IDENTITY. Reconstructing the shoe pressure from this column,
P_shoe = P_td - g [ rho_influx * H + rho_mud * (D_td - D_lot - H) ]
returns the binding fracture pressure
g * P_lot * D_lot - P_aplEXACTLY, for anyrho_influx, because A-2 defines H as(B - P_td) / [g (rho_mud - rho_influx)]and B carries the mud column over the same interval. That is the definition of the limiting condition: at the tolerable influx the shoe sits ON the fracture pressure. Verified to 0.00 psi under both revisions bytest_kick_model_revision.py.DO NOT back-derive H from
KickResult.capacityinstead. Expanding the capacity to the shoe goes through A-4, whose pressure bookkeeping is not algebraically identical to A-5/A-7 as printed (A-4 pairsP_lot - P_apl + P_atmwithP_td, while A-7 pairsP_lot - P_aplwithP_td + P_atm). On the Table-1 case that route misses the fracture pressure by -3.93 psi – IDENTICALLY under both revisions, so it is the paper’s own bookkeeping and not the influx-temperature correction. A profile built that way was already off before 0.26.0; it just did not move.rho_influxrespects an injectedrho_gas_s, so the identity holds for a caller supplying its own gas properties too.
- welleng.kick_tolerance.core.influx_volume_A7(A: float, B: float, P_td: float) float[source]
A-7: V_gas_td = A * (B - P_td) / (P_td + P_atm) [bbl].
Tolerable single-bubble influx volume (zero when P_pp == B).
- welleng.kick_tolerance.core.pp_at_threshold_A9(A: float, B: float, V_threshold: float) float[source]
A-9: P_td = (A * B - V_gas_td * P_atm) / (V_gas_td + A) [psi],
the bottom-hole pressure corresponding to a chosen kick-tolerance volume, returned as an equivalent pore-pressure mud weight [ppg] at D_td.
- welleng.kick_tolerance.core.ppg_to_psi(rho_ppg: float, depth_ft: float) float[source]
Hydrostatic / gradient pressure: g * rho * depth [psi].
- welleng.kick_tolerance.core.resolve_gas_properties(inp: KickInputs, P_td: float | None = None) tuple[float, float, float][source]
Return (Z_s, Z_td, rho_gas_s), computing any left as
None.TD station: (P_td, T_td) -> Z_td. Shoe station: (P_shoe, T_s) -> Z_s, rho_gas_s.
P_tdis the bottom-hole pressure the influx gas is stationed at. DefaultNoneuses the DRILL scenario pressurescenario_P_td(inp); the swab case passes its own P_td (mud hydrostatic) so kick_intensity does not leak into the swab gas properties (Nassab SPE-202426-PA Eqs 8-9).Backend: pure-methane Hall-Yarborough by default; if
inp.fluidis a composition dict, the CoolProp real-EOS mixture backend (CO2 / CCUS) is used. Injected numeric Z_s/Z_td/rho_gas_s values are used verbatim (override).
- welleng.kick_tolerance.core.scenario_P_td(inp: KickInputs) float[source]
A-1 DRILL bottom-hole pressure at the maximum-credible pore pressure [psi].
The pressure at which the DRILL influx gas properties are evaluated. This is the drill scenario (PP + kick intensity, floored at mud hydrostatic); the SWAB case stations its gas at its own bottom-hole pressure (mud hydrostatic) instead – see
resolve_gas_properties/constant_AP_tdandswab_kick. (Sharing this pressure with swab leaked kick_intensity into the swab number, anti-conservatively – Nassab SPE-202426-PA Eqs 8-9.)
- welleng.kick_tolerance.core.swab_kick(inp: KickInputs) KickResult[source]
Unmitigated free-trip limit (A-8) – a MITIGABLE operational limit.
A-8 is A-7 with the bottom-hole pressure set to the mud hydrostatic, P_td = g * rho_mud * D_td (pore pressure <= mud hydrostatic). A swab failure means “pump out of the hole” – which is the drill case, assessed separately. Swab NEVER overrides drill and is never blind-min’d with it.
kick_intensityis DELIBERATELY UNUSED here. For a swabbed kick the bottom-hole pressure is the mud hydrostatic, independent of formation PP (and therefore of any intensity margin added to PP): Nassab SPE-202426-PA, Eqs 8-9 – “Ptd … is independent of formation PP and is equal to mud hydrostatic pressure. This concept is misunderstood … in many KT models that assume Ptd is equal to PP for both underbalanced and swabbed kicks[,] … [leading] to an overestimated KT value in swabbing conditions.” Carrying PP or PP+KI into the swab bottom-hole pressure would over-report swab KT (unsafe); dropping KI for swab is the correct, conservative default. This includes the influx gas STATIONING: A is evaluated with the swab P_td (mud hydrostatic), not the drill scenario pressure, so kick_intensity does not leak into Z/rho either.CONSCIOUS DIVERGENCE FROM SPE-208788-PA. That paper’s worked example shares ONE A constant across A-7 (drill) and A-8 (swab) – so its published swab figure (43.79 bbl, Table-1) stations the swab gas at the drill max-credible pressure (PP + KI), carrying kick intensity into a swab number. We reproduce that figure under its own convention in the validation suite, but our MODEL does not adopt it: a swab kick is not a drill kick, so it carries no kick intensity (Nassab SPE-202426-PA, Eqs 8-9). The difference is ~1.5% and only when PP+KI > mud (outside the swab model’s PP <= mud domain).
welleng.kick_tolerance.envelope module
welleng.kick_tolerance.gas_z module
Clean-room Hall & Yarborough (1973) real-gas Z-factor backend.
Implements the Hall-Yarborough compressibility-factor correlation from the public source:
Hall, K. R. & Yarborough, L. (1973). “A new equation of state for Z-factor calculations.” Oil & Gas Journal 71(25): 82-92.
The correlation is a public, widely-published equation of state. It is implemented here from the standard public formulation only – no external, third-party, or private data source is referenced.
Formulation
Given pseudo-reduced temperature/pressure
Tpr = T / Tpc , Ppr = P / Ppc , t = 1 / Tpr ,
the reduced density y solves the implicit Hall-Yarborough equation
- F(y) = -A
(y + y^2 + y^3 - y^4) / (1 - y)^3
(14.76 t - 9.76 t^2 + 4.58 t^3) y^2
(90.7 t - 242.2 t^2 + 42.4 t^3) y^(2.18 + 2.82 t)
= 0 ,
with the leading constant
A = 0.06125 Ppr t exp[-1.2 (1 - t)^2] .
F(y) = 0 is solved by Newton-Raphson (analytic derivative). The Z-factor
follows as
Z = A / y = 0.06125 Ppr t exp[-1.2 (1 - t)^2] / y .
The correlation is valid roughly for 0.1 <= Ppr <= ~24 and 1.15 <= Tpr <= 3.0.
Tier 0 gas: pure methane
For Tier 0 the influx is treated as pure methane (gas gravity ~0.5539). The pseudo-criticals are methane’s critical constants (equivalently the single-component Standing-Katz values):
Tpc = 343.0 degR , Ppc = 667.0 psia , M = 16.043 lbm/lbmol .
Units
Pressure psia; temperature degR; density returned in ppg (mud-weight equivalent) using the field-unit gas law rho = P M / (Z R T) with the universal gas constant R = 10.732 psia.ft^3 / (lbmol.degR) and 7.4805 gal/ft^3.
- welleng.kick_tolerance.gas_z.gas_density_ppg(p_psia: float, t_rankine: float, z: float, molar_mass_lbm: float = 16.043) float[source]
Real-gas density in ppg (mud-weight equivalent).
rho = P M / (Z R T) [lbm/ft^3], converted to ppg via 7.4805 gal/ft^3.
- welleng.kick_tolerance.gas_z.gas_z(p_psia: float, t_rankine: float, gas_gravity: float = None, composition: dict = None, t_pc_rankine: float = None, p_pc_psia: float = None, method: str = 'auto', pc_correlation: str = 'standing') float[source]
Unified classic real-gas Z-factor — Hall-Yarborough or CoolProp EOS.
A single entry point that switches between the two classic/public backends:
Hall & Yarborough (fast correlation) for a hydrocarbon gas specified by
gas_gravity(or explicitt_pc_rankine/p_pc_psia);CoolProp real-EOS (accurate) for a
compositionmole-fraction dict (mixtures / non-hydrocarbons like CO2 / N2 / H2S).
- Parameters:
p_psia (float) – Absolute pressure [psia] and temperature [degR].
t_rankine (float) – Absolute pressure [psia] and temperature [degR].
gas_gravity (float, optional) – Gas gravity relative to air; pseudo-criticals are derived via
pc_correlation. Ignored ift_pc_rankine/p_pc_psiaare given.composition (dict, optional) – Mole-fraction composition (e.g.
{"Methane": 0.9, "CO2": 0.1}) -> the CoolProp path.t_pc_rankine (float, optional) – Explicit pseudo-criticals for the Hall-Yarborough path (overrides
gas_gravity). Default to Tier-0 methane if nothing is supplied.p_pc_psia (float, optional) – Explicit pseudo-criticals for the Hall-Yarborough path (overrides
gas_gravity). Default to Tier-0 methane if nothing is supplied.method ({"auto", "hall_yarborough", "coolprop"}) –
"auto"picks CoolProp when acompositionis given, else Hall-Yarborough. An explicit choice forces the backend.pc_correlation ({"standing", "sutton"}) – Gas-gravity -> pseudo-critical correlation for the Hall-Yarborough path.
- Returns:
The real-gas Z-factor [-].
- Return type:
float
Notes
CoolProp is an optional extra. If the CoolProp path is requested (or auto- selected) but CoolProp is not installed, this falls back to Hall-Yarborough when a
gas_gravity/pseudo-criticals are available (so the correlation path always works), and raises only if it has nothing to fall back on. This is the classic/public oracle; the fast analytical (Papay) form lives in welleng-api.
- welleng.kick_tolerance.gas_z.hall_yarborough_z(p_psia: float, t_rankine: float, t_pc_rankine: float = 343.0, p_pc_psia: float = 667.0, y0: float = 0.001) float[source]
Real-gas Z-factor by the Hall & Yarborough (1973) correlation.
- Parameters:
p_psia – Absolute pressure [psia].
t_rankine – Absolute temperature [degR].
t_pc_rankine – Pseudo-critical temperature [degR] and pressure [psia]. Default to the Tier-0 pure-methane values.
p_pc_psia – Pseudo-critical temperature [degR] and pressure [psia]. Default to the Tier-0 pure-methane values.
- Returns:
Compressibility factor Z = 0.06125 Ppr t exp[-1.2 (1 - t)^2] / y.
- Return type:
float
- welleng.kick_tolerance.gas_z.hall_yarborough_z_and_y(p_psia: float, t_rankine: float, t_pc_rankine: float = 343.0, p_pc_psia: float = 667.0, y0: float = 0.001) tuple[source]
Z factor AND the reduced density
ythat produced it.yis returned so a caller stepping through nearby pressures (a gas-column integration) can pass it as they0warm-start of the next solve, cutting Newton iterations without changing the converged result.
- welleng.kick_tolerance.gas_z.methane_properties(p_psia: float, t_rankine: float) tuple[float, float][source]
Convenience: (Z, density_ppg) for pure methane at (P, T).
- Returns:
Z-factor [-] and gas density [ppg] for Tier-0 methane.
- Return type:
(z, rho_ppg)
- welleng.kick_tolerance.gas_z.reduced_density(p_psia: float, t_rankine: float, t_pc_rankine: float = 343.0, p_pc_psia: float = 667.0, y0: float = 0.001) float[source]
Solve the Hall-Yarborough implicit equation for reduced density
y.Newton-Raphson from the seed
y0(default a small positive value);yis confined to (0, 1).y0lets a caller WARM-START from a nearby prior solve (consecutive sub-steps in a gas-column integration have close pressures, so the previousyconverges in ~2 iterations instead of ~5) – the converged root is identical to 1e-12 regardless of the seed.The residual and its analytic derivative are inlined here and the temperature-dependent coefficient groups are precomputed once (not per Newton iteration): this is the same arithmetic as
_hy_residual()/_hy_residual_derivative()but avoids ~5 Python function calls and repeated power evaluations per solve, which dominate the migration-engine hot path.- Raises:
ValueError – If inputs are non-physical or the iteration fails to converge.
- welleng.kick_tolerance.gas_z.standing_pseudo_criticals(gas_gravity: float) tuple[source]
Natural-gas pseudo-criticals from gas gravity — Standing (1977):
T_pc = 168 + 325.g - 12.5.g^2 [degR] P_pc = 677 + 15.0.g - 37.5.g^2 [psia]
gas_gravityis relative to AIR (air = 1.0), i.e.M_gas / 28.9647— a COMPOSITION, not a density. Pure methane is 0.5539.Why this matters and is not a refinement. Molar mass alone scales density but says nothing about Z; the Hall & Yarborough correlation needs pseudo-criticals. Using methane’s for a heavier gas is internally inconsistent, and not by a little: at 5400 psi, the temperature at which a 0.686-gravity gas reaches 2.00 ppg is 179.9 degF on methane’s pseudo-criticals and 194.6 degF on Standing’s — a 14.7 degF error, in a diagnostic whose entire purpose is to expose an implausible temperature.
Valid for sweet natural gases roughly 0.55-1.0. Sour gases (H2S, CO2) need a Wichert-Aziz correction, which is NOT applied here — pass explicit pseudo-criticals for those.
- welleng.kick_tolerance.gas_z.sutton_pseudo_criticals(gas_gravity: float) tuple[source]
Natural-gas pseudo-criticals from gas gravity — Sutton (1985):
T_pc = 169.2 + 349.5.g - 74.0.g^2 [degR] P_pc = 756.8 - 131.0.g - 3.6.g^2 [psia]
An alternative to
standing_pseudo_criticals()fitted to a wider gravity range; preferred for heavier/associated gases (roughlyg0.57-1.68). Like Standing’s, it carries no sour-gas (H2S/CO2) correction – pass explicit pseudo-criticals (Wichert-Aziz corrected) for sour gases.
welleng.kick_tolerance.gas_z_coolprop module
CoolProp real-EOS gas-property backend for kick tolerance (mixtures, CO2 / CCUS).
Optional – requires the coolprop extra (pip install 'welleng[kick]'). The
Tier-0 default backend (welleng.kick_tolerance.gas_z) is a clean-room
Hall & Yarborough (1973) correlation for pure methane; that correlation cannot
represent CO2 + impurity influx. This module computes the real-gas Z-factor and
density for an arbitrary gas COMPOSITION (mole fractions) via CoolProp’s HEOS
reference EOS (GERG-2008 for natural-gas mixtures), enabling CCUS / CO2 kicks.
CoolProp is MIT-licensed; use the native HEOS backend only (never the paid NIST REFPROP backend). Cite: Bell, I. H. et al. (2014), Ind. Eng. Chem. Res. 53(6):2498.
- class welleng.kick_tolerance.gas_z_coolprop.ZTable(composition: Dict[str, float], p_psia_range: Tuple[float, float], t_rankine_range: Tuple[float, float], n_p: int = 40, n_t: int = 10, cache: bool = True)[source]
Bases:
objectPrecomputed real-gas
Z(P,T)and density surface for a fixed gas composition – the fast path for the CoolProp backend.A single CoolProp GERG-2008 mixture flash costs ~15 ms, so calling it inside the ~700-evaluation kick-tolerance solve is ~12 s / solve (unusable). Instead grid the (P, T) box ONCE (Z and density are smooth), then bilinearly interpolate in the hot loop (~2 us/lookup, interp error ~1e-6 vs CoolProp). The grid is disk-cached per (composition, box, resolution), so a project’s fixed composition is built once and reused forever; pure methane stays on the fast Hall-Yarborough backend and does not need this.
- Parameters:
composition (dict) – Mole fractions (same as
fluid_z_density()).p_psia_range ((lo, hi)) – The pressure [psia] and temperature [degR] box to tabulate. Pad it beyond the case’s surface..bottom-hole range so lookups never extrapolate.
t_rankine_range ((lo, hi)) – The pressure [psia] and temperature [degR] box to tabulate. Pad it beyond the case’s surface..bottom-hole range so lookups never extrapolate.
n_p (int) – Grid resolution (default 40 x 10 – Z is smooth, so this is ample).
n_t (int) – Grid resolution (default 40 x 10 – Z is smooth, so this is ample).
- welleng.kick_tolerance.gas_z_coolprop.fluid_aliases() Dict[str, str][source]
Friendly component name -> CoolProp fluid name (e.g.
co2->CarbonDioxide). Case-insensitive keys; the same map used to resolve a composition before it reaches CoolProp.
- welleng.kick_tolerance.gas_z_coolprop.fluid_presets() list[source]
Curated influx-fluid shortlist:
[{label, composition}, ...].compositionis a mole-fraction dict in CoolProp canonical names, consumed as-is byKickInputs.fluid/analytical_kick_tolerance(gas_composition=). The natural-gas / sour entries are representative examples, not standards. For the full fluid list use CoolProp’sget_global_param_string("FluidsList").
- welleng.kick_tolerance.gas_z_coolprop.fluid_z_density(composition: Dict[str, float], p_psia: float, t_rankine: float) Tuple[float, float][source]
Real-gas (Z, density_ppg) for a gas
compositionat (P, T) via CoolProp.- Parameters:
composition (dict) – Component mole fractions, e.g.
{"Methane": 0.9, "CO2": 0.1}(names are case-insensitive; common aliases likeCO2/N2/H2Sare mapped).p_psia (float) – Pressure [psia] and absolute temperature [degR].
t_rankine (float) – Pressure [psia] and absolute temperature [degR].
- Returns:
Compressibility factor [-] and gas density [ppg mud-weight equivalent].
- Return type:
(z, rho_ppg)
welleng.kick_tolerance.geometry module
Catalogue-backed well geometry for the gas-migration kick-tolerance engine.
Builds WellSection list with the TRUE
ANNULAR capacity – the annulus between the bore (casing ID or open-hole
diameter) and the drill/work string inside it – rather than a full-bore
capacity. Casing IDs are resolved from the API-5CT catalogue
(welleng.catalog) from (OD, nominal weight[, grade]); the string OD is
subtracted. This fixes the over-capacity error you get from an EDM
linear_capacity (which is full-bore): a full-bore capacity makes the gas
bubble shorter than reality and UNDER-states the pressure it imposes on a
shallow formation – the wrong direction for a fracture barrier.
welleng.catalog is imported lazily so the kick-tolerance package stays
importable without it; the builders raise a clear error if it is unavailable.
- welleng.kick_tolerance.geometry.BURST_DESIGN_FACTOR = 0.8
Default burst design factor – the published minimum internal yield pressure is reduced to this fraction to get the allowable. 0.80 is the conventional setting and is what Volve’s own kill sheets carry (
WP_KILL_SHEET_GEN.casing_burst_safety_factor = 80.0, on all 362 records).
- welleng.kick_tolerance.geometry.annular_capacity(inner_id_in: float, pipe_od_in: float = 0.0) float[source]
Annulus capacity [bbl/ft] between a bore
inner_id_in(casing ID or open-hole diameter) and the stringpipe_od_ininside it:capacity = (inner_id^2 - pipe_od^2) / 1029.4
pipe_od_in = 0gives the full-bore capacity (no string).
- welleng.kick_tolerance.geometry.cased_section(top_tvd: float, bottom_tvd: float, *, casing_od_in: float, casing_weight_ppf: float, pipe_od_in: float, grade: str | None = None, burst_design_factor: float = 0.8) WellSection[source]
A cased WellSection with true annular capacity (casing ID from the API-5CT catalogue, minus the string OD).
is_open_hole=False– its formation is protected and is not exposed to fracture.When a
gradeis given the catalogue also yields the minimum internal yield pressure (API TR 5C3 Eq. 10, Barlow, already derated by minimum wall), and the section carriesburst_design_factortimes that as its allowable internal pressure. Without a grade there is no rating and the section is left unchecked – the catalogue flags an absent grade rather than guessing, and so does this.The burst check is INDICATIVE, not a casing design: no external backup is credited, and no axial, bending, temperature, wear or connection effects. See
WellSection.
- welleng.kick_tolerance.geometry.open_hole_section(top_tvd: float, bottom_tvd: float, *, hole_size_in: float, pipe_od_in: float) WellSection[source]
An open-hole WellSection with true annular capacity (bit diameter minus the string OD).
is_open_hole=True– its formation is fracture-exposed.
- welleng.kick_tolerance.geometry.sections_from_architecture(wellbore, string, survey, *, shoe_md: float, top_md: float | None = None, bottom_md: float | None = None, max_piece_md: float | None = None) list[source]
Build the elementary
WellSectionlist from an MD-domain well architecture and a survey.Annular volume lives in the MD domain (capacity is bbl per foot of along-hole length) and pressure lives in the TVD domain. The two are coupled by the survey. This builder cuts the well at the UNION of every depth at which either domain changes character:
every hole-geometry change (
wellboresection tops/bottoms),every string-geometry change (
stringsection tops/bottoms – BHA component boundaries, the bit),every survey station,
the casing shoe.
Each resulting piece therefore has a CONSTANT annular capacity and lies inside a SINGLE minimum-curvature leg, so its along-hole and vertical extents are both exact and their ratio is the piece’s mean
sec(inc). No fixed-step march and no chunk size: an interface that lands inside a piece is placed exactly withsplit_at_tvd().- Parameters:
wellbore (welleng.architecture.WellBore) – Hole geometry, MD [m]; each section carries
'id'[in].string (welleng.architecture.BHA or welleng.architecture.String or None) – Drill/work string, MD [m]; each section carries
'od'[in]. Where the string is absent the annulus is taken as full bore.survey (welleng.survey.Survey) – The trajectory, supplying MD [m] -> TVD [m] and the station set.
shoe_md (float) – Casing-shoe MD [m]. Pieces below it are
is_open_hole=Trueand are checked against the pore/fracture envelope; pieces above it are cased and protected.top_md (float, optional) – Restrict the build to this along-hole interval [m]. Defaults to the wellbore’s own extent.
bottom_md (float, optional) – Restrict the build to this along-hole interval [m]. Defaults to the wellbore’s own extent.
max_piece_md (float, optional) – Subdivide any piece longer than this [m]. A piece reports one mean
sec(inc), so a partial span taken inside it – which is what happens where a gas face lands – carries that mean rather than the local value. The error is 0.12% of a 1000 ft bubble at 30 m survey stations and 25% at 1000 m stations, so this is only worth setting on a COARSELY surveyed well. Subdividing costs nothing but a longer list: the extra cuts get their TVDs from the same batch interpolation as every other cut, so they are exact, not interpolated approximations.
- Returns:
Ordered shallow to deep, each carrying both its MD and TVD extent.
- Return type:
list of WellSection
- Raises:
ValueError – If a piece has a non-increasing TVD extent – a TVD reversal (a well that drops then builds back up). The TVD-domain engines are not formulated for a depth that is reached twice.
- welleng.kick_tolerance.geometry.split_at_tvd(section: WellSection, tvd: float, survey) tuple[source]
Split
sectionattvd[ft], placing the cut EXACTLY.The along-hole depth of the cut comes from the survey’s closed-form TVD interpolation (Sawaryn & Thorogood 2005, SPE-84246-PA), so both halves carry exact MD and TVD extents and neither is a linearisation of the parent. This is what removes the need for a fine march: an interface is not stepped up to, it is solved for and the piece it lands in is split.
- Parameters:
section (WellSection) – The piece the interface lands in. Must carry an MD extent.
tvd (float) – The true vertical depth of the interface [ft], strictly inside the section.
survey (welleng.survey.Survey) – The trajectory the section was built against.
- Returns:
The upper and lower halves.
- Return type:
tuple of (WellSection, WellSection)
- Raises:
ValueError – If
tvdis not inside the section, if the section carries no MD extent, or if the survey crossestvdmore than once within it (a TVD reversal inside a single piece).
welleng.kick_tolerance.migration module
Gas-migration kick-tolerance engine (single migrating bubble, whole open hole).
WHAT THIS IS
The Tier-0 single-bubble kick-tolerance check (kick_tolerance.py, transcribed
from SPE-208788-PA Appendix A) tests the tolerable influx at ONE station – the
casing shoe. This engine is an extension BEYOND that static NOGEPA single-shoe
method: it tracks the single gas bubble as it MIGRATES up the annulus and
requires the imposed pressure to stay inside the pore/fracture (PP-FP) window at
EVERY exposed open-hole depth, at every migration step.
It is a defensible assembly of referenced pieces – no single public source packages the whole thing, so each piece is cited where it is used:
Hold BHP constant at the kill (circulating) bottom-hole pressure – Driller’s-method well control, API RP 59 (Recommended Practice for Well Control Operations) 2nd ed. Sec. 4.10; SPE-202426 (managed-pressure / well-control constant-BHP kill).
March the single bubble up the annulus, expanding by the real-gas law – single-bubble gas migration, API RP 59 Sec. 4.8.7.3 (gas expansion on the way up); the closed-form single-bubble treatment of SPE-208788-PA.
Keep every EXPOSED open-hole formation between pore and fracture pressure – the barrier envelope, API RP 59 Sec. 12.5 (do not underbalance a permeable zone; do not exceed formation breakdown). Cased intervals are protected and are not checked.
Flag when the bubble grows longer than the open hole / BHA it must migrate past – SPE-140113 (bubble-length vs. open-hole-length limit).
NOGEPA-50 (Netherlands well-control standard) frames the single-shoe kick tolerance this engine extends to the whole exposed open hole.
Modelling stance (safe-side, explicit)
SINGLE BUBBLE, no slip / no dispersion: one contiguous gas interval of conserved mass. Treating the influx as a single coherent bubble is the conservative (safe-side) idealisation – a dispersed/percolating influx imposes LESS peak pressure at any shallow depth than one coherent light column, so the single bubble bounds the fracture risk.
- Real-gas expansion via Boyle + Z + T from the bottom-hole state:
V(P,T,Z) = V_bh * (P_bh * Z * T) / (P * Z_bh * T_bh).
Z(P) is taken from the clean-room Hall & Yarborough (1973) methane backend (
gas_z.py), so the bubble grows correctly as it rises. This is the dominant migration effect and it is modelled pressure-dependently.GAS-COLUMN HYDROSTATIC uses the LOCAL, pressure- AND temperature-dependent gas density, rho_gas(P, d) = rho_gas_bh * (P * Z_bh * T_bh) / (P_bh * Z(P, T(d)) * T(d)), which lightens up-hole as P falls. The temperature enters via
temp_profile(defaultNone= ISOTHERMAL at the bottom-hole T, so T cancels and this is exactly the previous rho_gas_bh * (P * Z_bh) / (P_bh * Z(P))); a supplied profile (linear_temp_profiletwo-point gradient, or a full (tvd, T) table) makes Z and density track the true T(depth). A lighter up-hole column means a SMALLER pressure drop across the gas and hence a HIGHER imposed pressure at and above the gas top (and the shoe) – the safe-side direction for a fracture barrier.pressure_at_depth/migrateexposegas_density_mode:"conservative"(DEFAULT) holds the gas-TOP (lightest) density constant over the whole gas column – the highest-pressure, safe-side BOUND;"exact"integrates the true local density – the correct value, which lies just below the bound. Using the bottom-hole density everywhere would be non-conservative and is deliberately NOT an option.OIL-BASED-MUD gas SOLUBILITY is OUT OF SCOPE – the influx is treated as a free-gas bubble throughout (a water-based-mud / free-gas assumption). In OBM a dissolved influx breaks out shallow and expands abruptly; that is a separate model.
TVD-based, capacity-per-section. MD / deviation coupling (the true annular length a given TVD interval spans in a deviated well) is a LATER refinement; here each section carries an annular capacity [bbl/ft] against TVD directly.
Units (oilfield)
Depths TVD [ft]; densities [ppg]; pressures [psi]; temperature [degR]; annular capacity [bbl/ft]; gas volume [bbl]. Gravitational constant g = 0.0521 psi.ppg^-1.ft^-1.
- class welleng.kick_tolerance.migration.GasState(p_psia: float, t_rankine: float, z: float, rho_ppg: float, gradient_psi_per_ft: float, solved_for: str, reference_t_rankine: float | None = None, t_discrepancy_rankine: float | None = None)[source]
Bases:
objectA resolved influx gas state — see
gas_state_from_density().- __init__(p_psia: float, t_rankine: float, z: float, rho_ppg: float, gradient_psi_per_ft: float, solved_for: str, reference_t_rankine: float | None = None, t_discrepancy_rankine: float | None = None) None
- gradient_psi_per_ft: float
- p_psia: float
- reference_t_rankine: float | None = None
- rho_ppg: float
- solved_for: str
- t_discrepancy_rankine: float | None = None
- t_rankine: float
- z: float
- class welleng.kick_tolerance.migration.KickToleranceResult(max_influx_bbl: float, binding_tvd: float, binding_step: int, limited_by: str, min_fp_margin_psi: float, open_hole_unconstrained: bool = False)[source]
Bases:
objectMigration kick tolerance V* AND where/why it is limited.
max_influx_bblis the largest influx that can be circulated out. The binding fields describe the breach that limits it.binding_tvdis the governing depth – which may be a WEAK FORMATION deeper than the casing shoe, NOT the shoe assumed by the static single-shoe check: the migration checks every exposed depth at every bubble position, so it reports the true limit.- __init__(max_influx_bbl: float, binding_tvd: float, binding_step: int, limited_by: str, min_fp_margin_psi: float, open_hole_unconstrained: bool = False) None
- binding_step: int
- binding_tvd: float
- limited_by: str
- max_influx_bbl: float
- min_fp_margin_psi: float
- open_hole_unconstrained: bool = False
- class welleng.kick_tolerance.migration.MaaspResult(maasp_psi: float, shoe_tvd: float, limiting_psi: float, governing_tvd: float, governed_by_shoe: bool)[source]
Bases:
objectResult of
maasp().- __init__(maasp_psi: float, shoe_tvd: float, limiting_psi: float, governing_tvd: float, governed_by_shoe: bool) None
- governed_by_shoe: bool
- governing_tvd: float
- limiting_psi: float
- maasp_psi: float
- shoe_tvd: float
- class welleng.kick_tolerance.migration.MigrationResult(steps: list, within_envelope: bool, min_fp_margin_psi: float, binding_tvd: float, binding_step: int, bha_length_exceeded: bool, _ctx: dict = <factory>)[source]
Bases:
objectOutcome of a bubble migration sweep.
stepsis the ordered per-step animation trajectory (bottom -> surface). The scalar fields summarise the worst (binding) point over the whole sweep.- __init__(steps: list, within_envelope: bool, min_fp_margin_psi: float, binding_tvd: float, binding_step: int, bha_length_exceeded: bool, _ctx: dict = <factory>) None
- bha_length_exceeded: bool
- binding_step: int
- binding_tvd: float
- min_fp_margin_psi: float
- steps: list
- within_envelope: bool
- class welleng.kick_tolerance.migration.MigrationStep(gas_top_tvd: float, gas_bottom_tvd: float, gas_length_ft: float, min_fp_margin_psi: float, binding_tvd: float, p_at_binding_psi: float, sidp_psi: float, sicp_psi: float)[source]
Bases:
objectOne migration step – one frame of the animation trajectory.
- __init__(gas_top_tvd: float, gas_bottom_tvd: float, gas_length_ft: float, min_fp_margin_psi: float, binding_tvd: float, p_at_binding_psi: float, sidp_psi: float, sicp_psi: float) None
- binding_tvd: float
- gas_bottom_tvd: float
- gas_length_ft: float
- gas_top_tvd: float
- min_fp_margin_psi: float
- p_at_binding_psi: float
- sicp_psi: float
- sidp_psi: float
- class welleng.kick_tolerance.migration.WellSection(top_tvd: float, bottom_tvd: float, annular_capacity_bbl_per_ft: float, is_open_hole: bool, top_md: float | None = None, bottom_md: float | None = None, burst_pressure_psi: float | None = None)[source]
Bases:
objectOne annular section, described against TVD.
- Parameters:
top_tvd – Section extent in true vertical depth [ft] (top shallower than bottom).
bottom_tvd – Section extent in true vertical depth [ft] (top shallower than bottom).
annular_capacity_bbl_per_ft (float) – Annular capacity of the section [bbl/ft] – the gas length in this section is volume / this capacity. Capacity changes discretely at section boundaries (e.g. casing shoe, BHA top).
is_open_hole (bool) – True for an exposed open-hole formation (subject to the PP-FP envelope check); False for a cased/protected interval (not checked).
top_md – Optional along-hole extent of the same section [ft]. Annular capacity is a volume per unit of ALONG-HOLE length, so in a deviated well the volume held between two TVDs is
capacity * dMD, notcapacity * dTVD. Supply these (from a survey) and the section reportscapacity_per_tvd_ftaccordingly. Leave themNoneand the section is treated as vertical (dMD == dTVD), which is the pre-0.27 behaviour exactly.bottom_md – Optional along-hole extent of the same section [ft]. Annular capacity is a volume per unit of ALONG-HOLE length, so in a deviated well the volume held between two TVDs is
capacity * dMD, notcapacity * dTVD. Supply these (from a survey) and the section reportscapacity_per_tvd_ftaccordingly. Leave themNoneand the section is treated as vertical (dMD == dTVD), which is the pre-0.27 behaviour exactly.burst_pressure_psi (float | None) –
Optional ALLOWABLE internal pressure for a cased section [psi] – the published minimum internal yield pressure already reduced by a design factor. Supply it and the imposed pressure is checked against it over the cased interval, alongside the pore/fracture check in open hole; leave it
None(the default) and cased intervals are unchecked, as they were before 0.27.INDICATIVE ONLY – this is not a casing design tool. It credits NO external backup (the full internal pressure is resisted by the pipe, which is the safe-side worst case), and it accounts for no axial load, bending, temperature derating, wear or connection rating. A real burst design is a differential calculation against a backup profile over the load cases. Use this to notice that the casing may bind before the formation does, not to size a string.
section (Pressure is a function of TVD and volume is a function of MD; a)
The (carries both extents so the two integrals stay in their own domains.)
sec(inc) (ratio dMD/dTVD is the section-mean)
be (so a section should)
with (short enough that inclination is near-constant across it -- build them)
:param
welleng.kick_tolerance.geometry.sections_from_architecture(): :param which: :param splits at the union of geometry changes and survey stations.:- __init__(top_tvd: float, bottom_tvd: float, annular_capacity_bbl_per_ft: float, is_open_hole: bool, top_md: float | None = None, bottom_md: float | None = None, burst_pressure_psi: float | None = None) None
- annular_capacity_bbl_per_ft: float
- bottom_md: float | None = None
- bottom_tvd: float
- burst_pressure_psi: float | None = None
- property capacity_per_tvd_ft: float
Annular capacity per foot of TVD [bbl/ft].
annular_capacity_bbl_per_ft * (dMD / dTVD)– the along-hole capacity scaled by the section-meansec(inc). Equals the raw capacity for a vertical section. Every volume in the TVD-domain engines isthis * dTVD.- Raises:
ValueError – If the section has zero TVD extent (a horizontal section holds volume across no TVD at all, which the TVD-domain formulation cannot represent).
- is_open_hole: bool
- property md_extent: float
Along-hole length of the section [ft]; the TVD extent if unset.
- top_md: float | None = None
- top_tvd: float
- welleng.kick_tolerance.migration.gas_state_from_density(*, rho_ppg: float | None = None, gradient_psi_per_ft: float | None = None, p_psia: float | None = None, t_rankine: float | None = None, gas_gravity: float | None = None, molar_mass_lbm: float | None = None, t_pc_rankine: float | None = None, p_pc_psia: float | None = None, reference_t_rankine: float | None = None, z_fn=None, ideal: bool = False) GasState[source]
Given an influx DENSITY (or gradient), solve for the (P, T) that produces it.
A density does not determine a state.
rho = P.M / (Z.R.T)is one equation in two unknowns, so a density is a CURVE in (P, T), not a point. This therefore requires exactly ONE ofp_psiaort_rankineand solves for the other.The intended use is the READ-ONLY comparison a practitioner needs: they arrive with a published case that quotes a gas gradient and no temperature, and the well already has a BHP. Pin
p_psiato that BHP, solve for the temperature the quoted density implies, and show it beside the well’s actual bottom-hole temperature. When those two disagree, the quoted density is not describing this well’s gas – and that is the useful output, not a number to feed back in.Worked example of exactly that. A published case quotes 2.00 ppg at 5400 psi. Pin P and solve: it needs ~80 degF. At 10,000 ft TVD that is absurd, so either the influx is not pure methane (2.00 ppg at a realistic 180 degF needs a gas gravity of ~0.69) or the figure is a round number with no state behind it. Either way the user now knows, instead of having silently injected it.
Never write the result back into a design input. Solving a temperature to hit a density and then computing with it is hand-injected gas properties arriving through the front door. The overrides
Z_s/Z_td/rho_gas_sexist for ONE purpose – reproducing a published worked example against the paper’s own tabulated gas properties, independently of our Z backend – and NOT for describing a real influx: that goes in as a COMPOSITION and welleng computes the density itself. This function is a diagnostic.- Parameters:
rho_ppg – The target, as a density [ppg] OR a gradient [psi/ft]. Exactly one. The gradient is converted with THIS engine’s constant (see
ppg_to_gradient()).gradient_psi_per_ft – The target, as a density [ppg] OR a gradient [psi/ft]. Exactly one. The gradient is converted with THIS engine’s constant (see
ppg_to_gradient()).p_psia – Exactly one. The KNOWN one; the other is solved.
t_rankine – Exactly one. The KNOWN one; the other is solved.
molar_mass_lbm – Gas molar mass [lbm/lbmol]. Default methane (16.043). A heavier natural gas is ~19-21, i.e. gas gravity 0.65-0.73.
z_fn – Optional real-gas
(P, T) -> Zprovider (e.g. a CoolProp table). Default is the clean-room Hall & Yarborough methane backend.ideal –
TrueforcesZ = 1. Use for reproducing a textbook case that assumes it; do not use for engineering.
- Return type:
- Raises:
ValueError – If not exactly one target is given, or not exactly one known variable, or the target density is unreachable — in which case the message states the range that IS achievable, because that is the diagnostic.
Notes
Density is monotone in both variables (falling with T at fixed P, rising with P at fixed T), verified across 60-400 degF and 500-12,000 psia, so the root is unique and the bracketed solve cannot land on a second branch.
- welleng.kick_tolerance.migration.gradient_to_ppg(gradient_psi_per_ft: float | ndarray)[source]
Pressure gradient [psi/ft] -> density [ppg]. Inverse of
ppg_to_gradient().Useful because the well-control literature quotes influx GRADIENTS, not densities – NOGEPA-50 states a gas range of 0.05-0.15 psi/ft – so this is how a quoted gradient is compared against what welleng computed from (P, T, composition).
Read-only comparison, not an input path. Converting a quoted gradient to a density and feeding it to the engine is hand-injected gas properties arriving through the front door. Z_s/Z_td/rho_gas_s exist ONLY to reproduce a published worked example against the paper’s own tabulated gas properties; a real influx goes in as a COMPOSITION and welleng computes the density itself. Use this to CHECK agreement, not to override it.
- welleng.kick_tolerance.migration.linear_temp_profile(shoe_tvd: float, shoe_temp_rankine: float, td_tvd: float, td_temp_rankine: float) Callable[[ndarray], ndarray][source]
Two-point (shoe + TD) linear temperature gradient – the BASIC case.
Returns a callable
tvd -> T_rankinefor a straight-line geothermal gradient anchored at the casing shoe and at TD. Extrapolated linearly outside[shoe_tvd, td_tvd](so the gradient continues to surface, TVD=0, as the bubble rises). For a full / field temperature survey pass a(tvd_array, T_rankine_array)table instead (the ADVANCED case).- Parameters:
shoe_tvd – Anchor depths [ft TVD];
td_tvdmust differ fromshoe_tvd.td_tvd – Anchor depths [ft TVD];
td_tvdmust differ fromshoe_tvd.shoe_temp_rankine – Temperatures at those depths [degR]. A geothermal gradient has
td_temp_rankine > shoe_temp_rankine(hotter with depth).td_temp_rankine – Temperatures at those depths [degR]. A geothermal gradient has
td_temp_rankine > shoe_temp_rankine(hotter with depth).
- welleng.kick_tolerance.migration.maasp(sections: Sequence[WellSection], fp: Callable[[float], float] | tuple[Sequence[float], Sequence[float]], *, rho_mud_ppg: float, check_depths: Sequence[float] | None = None) MaaspResult[source]
Maximum allowable annular surface pressure, over the WHOLE exposed hole.
MAASP is the surface pressure that just brings the weakest exposed formation to its fracture pressure with the well shut in:
MAASP = min over exposed d of [ P_frac(d) - g.rho_mud.d ]
Shut-in, so no annular friction is deducted. Annular pressure loss is a CIRCULATING term; subtracting it here would understate the closed-in limit and is a different quantity. If a back-pressure or choke margin applies, subtract it from the returned value at the point of use, where its sign is unambiguous.
``maasp_psi`` is the CONVENTION and is not redefined: it is evaluated at the casing shoe, which is what a driller computes by hand and what belongs under a field labelled “MAASP”.
limiting_psicarries the generalisation – the same quantity minimised over every exposed depth.With a CONSTANT fracture gradient the two are identical, because
P_frac(d) - g.rho_mud.d = g.d.(FP_emw - rho_mud)grows with depth and the shallowest exposed point – the shoe – governs. They separate only when a weak zone sits BELOW the shoe, and then the conventional number is the higher, less safe one.governed_by_shoesays which case you are in.MAASP assumes a MUD-FILLED annulus. It is a planning number, not the shut-in limit during a kick: once influx is in the annulus the column is lighter than mud and the surface pressure at which the shoe breaks down is a different quantity (that is what the kick-tolerance solve computes). Do NOT compare a live SICP against MAASP and conclude the shoe is safe.
MAASP depends on the CURRENT mud weight, so it changes as the well is weighted up; it is a property of (hole, fracture profile, mud), not of an influx.
- Parameters:
sections (sequence of WellSection) – Only
is_open_holesections are exposed. Cased formations are behind pipe and are not assessed here (casing BURST is a separate limit – seemax_influx_contained_at_surface()).fp (ProfileLike) – Fracture pressure as a
(tvd, ppg)table or a callable [ppg EMW].rho_mud_ppg (float) – Current mud weight [ppg].
check_depths (sequence of float, optional) – Explicit depths to evaluate. Defaults to the open-hole boundaries plus the fracture-profile breakpoints that fall inside the open hole – the depths where the governing constraint can turn. A CALLABLE
fpexposes no breakpoints, so passcheck_depthsfor one or a narrow weak zone can be stepped over entirely.
- Return type:
- Raises:
ValueError – If no section is open hole – nothing is exposed, so MAASP is undefined.
- welleng.kick_tolerance.migration.migrate(sections: Sequence[WellSection], pp: Callable[[float], float] | tuple[Sequence[float], Sequence[float]], fp: Callable[[float], float] | tuple[Sequence[float], Sequence[float]], *, bhp_psi: float, influx_bbl_bh: float, rho_mud_ppg: float, gas_bh_state, gas_density_mode: str = 'conservative', temp_profile: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, geothermal: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, n_steps: int = 100, mode: str = 'thorough', ideal_gas: bool = False) MigrationResult[source]
March a single gas bubble up the annulus under constant BHP.
Holds the bottom-hole pressure constant at the kill value (Driller’s method; API RP 59 Sec. 4.10; SPE-202426) and steps the bubble top from the bottom to surface. At each step the gas expands by the real-gas law (Boyle + Z + T from the bottom-hole state; API RP 59 Sec. 4.8.7.3), its length follows from the annular capacity of the section(s) it occupies, and the imposed pressure is checked against the pore/fracture window
PP(d) <= P(d) <= FP(d)at every EXPOSED open-hole depth (barrier envelope; API RP 59 Sec. 12.5). Cased intervals are protected and not checked.This is an EXTENSION BEYOND the static NOGEPA single-shoe kick-tolerance method (NOGEPA-50) – a defensible assembly of the cited pieces. The single coherent bubble is the safe-side idealisation; OBM gas solubility is out of scope. See the module docstring.
- Parameters:
sections – Annular sections (
WellSection) covering surface (0) to bottom.pp – Pore and fracture pressure profiles [ppg], each a callable tvd->ppg OR a (tvd_array, ppg_array) table (numpy-interpolated).
fp – Pore and fracture pressure profiles [ppg], each a callable tvd->ppg OR a (tvd_array, ppg_array) table (numpy-interpolated).
bhp_psi – Constant bottom-hole pressure held during the kill [psi].
influx_bbl_bh – Influx volume at bottom-hole conditions V_bh [bbl].
rho_mud_ppg – Mud density [ppg].
gas_bh_state – (P_bh, T_bh_rankine, Z_bh, rho_gas_ppg).
P_bhNone ->bhp_psi;Z_bh/rho_gas_ppgNone -> computed by the Hall-Yarborough methane backend (gas_z.py);T_bh_rankinerequired.gas_density_mode ({"conservative", "exact"}) – Gas-column density treatment, passed through to
pressure_at_depth. Default"conservative"(gas-TOP lightest density, safe-side bound);"exact"integrates the true pressure-dependent local density. Seepressure_at_depth.temp_profile (None | callable | (tvd_array, T_rankine_array)) – Temperature profile T(d) [degR], passed through to
pressure_at_depthAND used in the Boyle expansion (the gas expands at the LOCAL temperature at its top, not the bottom-hole temperature).None(DEFAULT) is ISOTHERMAL atT_bh_rankine– reproduces the previous result exactly. Seelinear_temp_profilefor the basic two-point gradient.geothermal (None | callable | (tvd_array, T_rankine_array)) – Field geothermal gradient, the DEFAULT temperature when
temp_profileis not given (explicittemp_profile>geothermal> isothermal).n_steps – Number of migration steps (bottom -> surface).
- Returns:
.stepsis the per-step animation trajectory; scalar fields summarise the binding (worst) constraint over the whole sweep.- Return type:
- welleng.kick_tolerance.migration.ppg_to_gradient(rho_ppg: float | ndarray) float | ndarray[source]
Density [ppg] -> pressure gradient [psi/ft], with THIS engine’s constant.
In SI this needs no constant at all – a gradient is just
rho * g[Pa/m]. The field-unit form carriesgfolded into a ppg->psi/ft factor, and there are three in circulation which differ by ~0.3%:0.05210 G_PSI_PER_PPG_FT -- what THIS engine weights columns with 0.05200 NOGEPA_G -- the mandated NOGEPA-50 constant 0.0519481 units.PSI_PER_PPG_PER_FT -- exact, from standard gravity
This function deliberately uses the ENGINE’s constant. A gradient reported with any other one does not reproduce the column weight the engine actually applied, and a consumer reconciling the two would be chasing a discrepancy that is purely a choice of constant. (welleng-api lost a morning to exactly that class of mismatch on 2026-07-28.) If you want the gradient under a DIFFERENT convention – to compare against a standard that mandates its own – multiply the density by that constant explicitly, so the choice is visible at the call site.
Note the constant does NOT divide out here, unlike a kick tolerance expressed in equivalent mud weights: this is an absolute gradient, so the ~0.3% spread above is fully present in the answer.
- Parameters:
rho_ppg – Density [ppg]. Scalar or array.
- Returns:
Pressure gradient [psi/ft].
- Return type:
float or ndarray
Examples
>>> round(ppg_to_gradient(1.70133), 6) # a real-gas methane influx 0.088639
- welleng.kick_tolerance.migration.ppg_to_psi(rho_ppg: ndarray, depth_ft: ndarray) ndarray[source]
Gradient pressure of a mud-weight-equivalent column: g * ppg * TVD [psi].
- welleng.kick_tolerance.migration.pressure_at_depth(depth_ft: float | ndarray, *, gas_top_tvd: float, gas_bottom_tvd: float, bottom_tvd: float, bhp_psi: float, rho_mud_ppg: float, gas_bh, gas_density_mode: str = 'conservative', temp_profile: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, geothermal: None | Callable[[float], float] | tuple[Sequence[float], Sequence[float]] = None, g: float = 0.0521, n_sub: int = 40, z_fn=None) float | ndarray[source]
Imposed pressure at
depth_ftfor BHP held constant at the bottom.z_fn(optional): a real-gas Z provider(P_psi, T_rankine) -> Z– e.g. a precomputed CoolPropZTablefor a mixture / CO2 / CCUS influx. WhenNone(default) the clean-room Hall-Yarborough methane backend is used with a warm-started Newton (behaviour unchanged, bit-for-bit).Marching UP from the bottom, hydrostatic is removed: a mud gradient outside the gas interval and a gas gradient inside [gas_top, gas_bottom]. The local gas density is NON-ISOTHERMAL:
rho_gas(P, d) = rho_gas_bh * (P * Z_bh * T_bh) / (P_bh * Z(P, T(d)) * T(d))
with
Z(P, T(d))from the clean-room Hall & Yarborough (1973) methane backend at the LOCAL temperatureT(d).gas_bh = (P_bh, T_bh_rankine, Z_bh, rho_gas_bh). Whentemp_profileisNonethe local temperature equalsT_bheverywhere, T cancels, and this reduces EXACTLY to the previous isothermal densityrho_gas_bh * (P * Z_bh) / (P_bh * Z(P)). Vectorised overdepth_ft.- Parameters:
temp_profile (None | callable | (tvd_array, T_rankine_array)) – Temperature profile T(d) [degR].
None(DEFAULT) is ISOTHERMAL at the bottom-hole temperatureT_bh_rankine(fromgas_bh) – reproduces the previous behaviour exactly. A callabletvd -> T_rankineor a(tvd, T_rankine)table sets a depth-varying temperature (seelinear_temp_profilefor the basic two-point gradient). Hotter gas up-hole is lighter (lower density) -> a HIGHER pressure at/above the gas.geothermal (None | callable | (tvd_array, T_rankine_array)) – The field geothermal gradient, used as the DEFAULT temperature when
temp_profileis not given. Resolution is three-tier: an explicittemp_profilewins; elsegeothermal(the go-to default when a gradient is known); else isothermal atT_bh_rankine.gas_density_mode ({"conservative", "exact"}) –
Gas-column density treatment (each evaluated at the local T(d)):
"conservative"(DEFAULT, safe-side) – use the gas density at the gas TOP (the lowest pressure -> lightest gas) as a CONSTANT for the whole gas column. The lightest-possible column gives the smallest pressure drop across the gas and hence the HIGHEST pressure at and above the gas top (and the shoe). This is the safe-side bound for a fracture barrier: it never under-states the shallow loading. Slightly over-conservative vs. the true integrated column."exact"– integrate the TRUE pressure-dependent local density down the column (forward Euler). This is the technically-correct / true- average result; it lies BELOW the conservative bound.
Ordering at any depth at/above the gas:
P(conservative) >= P(exact) >= P(bottom-hole-constant).n_sub – Sub-steps for the exact forward-Euler integration down the gas interval (also used to seed the gas-top pressure for the conservative mode).
welleng.kick_tolerance.monotonicity module
welleng.kick_tolerance.nogepa module
NOGEPA Industry Standard No. 50 static single-shoe kick-tolerance formula.
Faithful implementation of the mandated Netherlands well-control standard’s “current industry practice” closed form (NOGEPA Industry Standard No. 50 – Kick Tolerances for Well Design and Drilling Operations, 2020-12-09, §3.2). This is the STATIC single-shoe check: the gas bubble top at the casing shoe, the shoe “normally assumed” the weakest exposed zone, temperature and compressibility (Z) neglected. It is provided so welleng can REPRODUCE the mandated calculation exactly and so the gas-migration engine’s static reduction can be validated against it.
NOTE (from the standard itself): NOGEPA-50 §3.2 states KT “should be calculated in
terms of kick volume which can be circulated out … [and] should not be
oversimplified,” lists the factors this formula neglects (gas velocity/distribution,
P & T, gas composition/dispersion/solubility, friction – “static conditions”), and
notes the shoe is only normally assumed weakest. The migration engine
(welleng.kick_tolerance.migration) is the not-oversimplified circulate-out
calculation the standard calls for; this module is the static baseline.
Drilling-kick equations (NOGEPA-50 p.12-13, verbatim), gas top at the shoe:
G_mud = mud_ppg * 0.052 [psi/ft]
P_frac = LOT_ppg * Z_CSG * 0.052 [psi] (fracture pressure at shoe)
H = (P_f - (Z_OH - Z_CSG)*G_mud - P_frac) / (G_gas - G_mud) [ft] gas height
V1 = H * CAP_ann [bbl] influx volume at the shoe
V2 = P1 * V1 / P2 (Boyle to bottom hole) [bbl] = kick tolerance
with P1 = P_frac (at shoe), P2 = P_f (formation pressure at TD)
CAP_ann is the annulus capacity between pipe and hole (KT is “hole geometry
minus the drill string”); G_gas is the gas gradient, 0.05-0.15 psi/ft.
- class welleng.kick_tolerance.nogepa.NogepaResult(kick_tolerance_bbl: float, gas_height_ft: float, influx_at_shoe_bbl: float, p_frac_shoe_psi: float)[source]
Bases:
objectNOGEPA-50 static drilling-kick tolerance and its intermediates.
- __init__(kick_tolerance_bbl: float, gas_height_ft: float, influx_at_shoe_bbl: float, p_frac_shoe_psi: float) None
- gas_height_ft: float
- influx_at_shoe_bbl: float
- kick_tolerance_bbl: float
- p_frac_shoe_psi: float
- welleng.kick_tolerance.nogepa.nogepa_drilling_kick_tolerance(*, formation_pressure_psi: float, z_oh_ft: float, z_csg_ft: float, lot_ppg: float, mud_ppg: float, cap_ann_bbl_per_ft: float, g_gas_psi_per_ft: float = 0.1) NogepaResult[source]
NOGEPA-50 §3.2 static drilling-kick tolerance [bbl] (the
V2value).Reproduces the mandated formula exactly (T and Z ignored, per the standard). The migration engine reduces to this when the casing shoe genuinely governs.