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 depth d the worst string position is d + 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.052 differs from welleng’s 0.0521 by ~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: object

Result 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: object

A 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: object

Operational + 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; set fluid to 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) and inc_shoe (hole inclination at the shoe) are interpolated from the survey at shoe_md / td_md via min-curvature (interpolate_md); every other input (rho_mud, PP, P_lot, V_dpa, …) is passed through in params. The survey is duck-typed — any object with interpolate_md(md) -> node exposing pos_nev and inc_deg works.

Node has no direct .tvd; its position is pos_nev = [North, East, Vertical] (welleng NEV convention), so pos_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 equals survey.tvd only at a zero datum (survey.tvd is the LOCAL depth, per the Survey docstring); on a datum-shifted / sidetrack survey they differ by start_nev, and the global pos_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: object

Result 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: object

Migration kick tolerance V* AND where/why it is limited.

max_influx_bbl is the largest influx that can be circulated out. The binding fields describe the breach that limits it. binding_tvd is 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: object

Result 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: object

Outcome of a bubble migration sweep.

steps is 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: object

One 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: object

NOGEPA-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: object

One 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, not capacity * dTVD. Supply these (from a survey) and the section reports capacity_per_tvd_ft accordingly. Leave them None and 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, not capacity * dTVD. Supply these (from a survey) and the section reports capacity_per_tvd_ft accordingly. Leave them None and 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-mean sec(inc). Equals the raw capacity for a vertical section. Every volume in the TVD-domain engines is this * 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 of P(gas position) (§module docstring) and bisects the influx to the first fracture. gas_density_mode selects 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 prebuilt fluid_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):
sectionsWellSection with TVD top/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_profile is 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. None keeps 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 envelope min(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.

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 string pipe_od_in inside it:

capacity = (inner_id^2 - pipe_od^2) / 1029.4

pipe_od_in = 0 gives 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 grade is given the catalogue also yields the minimum internal yield pressure (API TR 5C3 Eq. 10, Barlow, already derated by minimum wall), and the section carries burst_design_factor times 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_td and supply an influx density rho_gas_s to 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 kick extra (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}, ...].

composition is a mole-fraction dict in CoolProp canonical names, consumed as-is by KickInputs.fluid / analytical_kick_tolerance(gas_composition=). The natural-gas / sour entries are representative examples, not standards. For the full fluid list use CoolProp’s get_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 composition at (P, T) via CoolProp.

Parameters:
  • composition (dict) – Component mole fractions, e.g. {"Methane": 0.9, "CO2": 0.1} (names are case-insensitive; common aliases like CO2/N2/H2S are 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 of p_psia or t_rankine and 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_psia to 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_s exist 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) -> Z provider (e.g. a CoolProp table). Default is the clean-room Hall & Yarborough methane backend.

  • idealTrue forces Z = 1. Use for reproducing a textbook case that assumes it; do not use for engineering.

Return type:

GasState

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 explicit t_pc_rankine/p_pc_psia);

  • CoolProp real-EOS (accurate) for a composition mole-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 if t_pc_rankine/p_pc_psia are 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 a composition is 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_apl EXACTLY, for any rho_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 by test_kick_model_revision.py.

DO NOT back-derive H from KickResult.capacity instead. 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 pairs P_lot - P_apl + P_atm with P_td, while A-7 pairs P_lot - P_apl with P_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_influx respects an injected rho_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_rankine for 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_tvd must differ from shoe_tvd.

  • td_tvd – Anchor depths [ft TVD]; td_tvd must differ from shoe_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_psi carries 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_shoe says 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_hole sections are exposed. Cased formations are behind pipe and are not assessed here (casing BURST is a separate limit – see max_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 fp exposes no breakpoints, so pass check_depths for one or a narrow weak zone can be stepped over entirely.

Return type:

MaaspResult

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 – set P_s to the allowable and the equation gives the longest tolerable column, which the per-section capacities then turn into an influx. Surface pressure rises monotonically with L (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.0 if 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_bh None -> bhp_psi; Z_bh / rho_gas_ppg None -> computed by the Hall-Yarborough methane backend (gas_z.py); T_bh_rankine required.

  • 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. See pressure_at_depth.

  • temp_profile (None | callable | (tvd_array, T_rankine_array)) – Temperature profile T(d) [degR], passed through to pressure_at_depth AND used in the Boyle expansion (the gas expands at the LOCAL temperature at its top, not the bottom-hole temperature). None (DEFAULT) is ISOTHERMAL at T_bh_rankine – reproduces the previous result exactly. See linear_temp_profile for the basic two-point gradient.

  • geothermal (None | callable | (tvd_array, T_rankine_array)) – Field geothermal gradient, the DEFAULT temperature when temp_profile is not given (explicit temp_profile > geothermal > isothermal).

  • n_steps – Number of migration steps (bottom -> surface).

Returns:

.steps is the per-step animation trajectory; scalar fields summarise the binding (worst) constraint over the whole sweep.

Return type:

MigrationResult

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 V2 value).

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 carries g folded 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_ft for BHP held constant at the bottom.

z_fn (optional): a real-gas Z provider (P_psi, T_rankine) -> Z – e.g. a precomputed CoolProp ZTable for a mixture / CO2 / CCUS influx. When None (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 temperature T(d). gas_bh = (P_bh, T_bh_rankine, Z_bh, rho_gas_bh). When temp_profile is None the local temperature equals T_bh everywhere, T cancels, and this reduces EXACTLY to the previous isothermal density rho_gas_bh * (P * Z_bh) / (P_bh * Z(P)). Vectorised over depth_ft.

Parameters:
  • temp_profile (None | callable | (tvd_array, T_rankine_array)) – Temperature profile T(d) [degR]. None (DEFAULT) is ISOTHERMAL at the bottom-hole temperature T_bh_rankine (from gas_bh) – reproduces the previous behaviour exactly. A callable tvd -> T_rankine or a (tvd, T_rankine) table sets a depth-varying temperature (see linear_temp_profile for 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_profile is not given. Resolution is three-tier: an explicit temp_profile wins; else geothermal (the go-to default when a gradient is known); else isothermal at T_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_td is the bottom-hole pressure the influx gas is stationed at. Default None uses the DRILL scenario pressure scenario_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.fluid is 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_gravity is 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 (roughly g 0.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_intensity is 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 depth d:

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:

  1. gas TOP at a section boundary or a PP/FP breakpoint (classic shoe case)

  2. gas BOTTOM at a section boundary – the fill alignments (was the missed set)

  3. the DEEPEST bubble: gas bottom at TD (initial position)

  4. 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) with C a 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: object

Result 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 of P(gas position) (§module docstring) and bisects the influx to the first fracture. gas_density_mode selects 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 prebuilt fluid_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):
sectionsWellSection with TVD top/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_profile is 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. None keeps 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 envelope min(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.

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 – set P_s to the allowable and the equation gives the longest tolerable column, which the per-section capacities then turn into an influx. Surface pressure rises monotonically with L (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.0 if 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 depth d:

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_td and (unused) rho_gas_td.

  • Influx station – (P_shoe, T_influx). P_shoe is 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). Gives Z_s and rho_gas_s.

    T_influx is REVISION-DEPENDENT (see MODEL_REVISIONS). As published it is the shoe temperature T_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: object

Operational + 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; set fluid to 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) and inc_shoe (hole inclination at the shoe) are interpolated from the survey at shoe_md / td_md via min-curvature (interpolate_md); every other input (rho_mud, PP, P_lot, V_dpa, …) is passed through in params. The survey is duck-typed — any object with interpolate_md(md) -> node exposing pos_nev and inc_deg works.

Node has no direct .tvd; its position is pos_nev = [North, East, Vertical] (welleng NEV convention), so pos_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 equals survey.tvd only at a zero datum (survey.tvd is the LOCAL depth, per the Survey docstring); on a datum-shifted / sidetrack survey they differ by start_nev, and the global pos_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: object

Result 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 – see resolve_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_td and supply an influx density rho_gas_s to 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_apl EXACTLY, for any rho_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 by test_kick_model_revision.py.

DO NOT back-derive H from KickResult.capacity instead. 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 pairs P_lot - P_apl + P_atm with P_td, while A-7 pairs P_lot - P_apl with P_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_influx respects an injected rho_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_td is the bottom-hole pressure the influx gas is stationed at. Default None uses the DRILL scenario pressure scenario_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.fluid is 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_A P_td and swab_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_intensity is 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 explicit t_pc_rankine/p_pc_psia);

  • CoolProp real-EOS (accurate) for a composition mole-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 if t_pc_rankine/p_pc_psia are 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 a composition is 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 y that produced it.

y is returned so a caller stepping through nearby pressures (a gas-column integration) can pass it as the y0 warm-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); y is confined to (0, 1). y0 lets a caller WARM-START from a nearby prior solve (consecutive sub-steps in a gas-column integration have close pressures, so the previous y converges 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_gravity is 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 (roughly g 0.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: object

Precomputed 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).

__init__(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]
rho_ppg(p_psia, t_rankine)[source]

Interpolated gas density [ppg] at (P, T).

z(p_psia, t_rankine)[source]

Interpolated compressibility factor Z(P, T).

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}, ...].

composition is a mole-fraction dict in CoolProp canonical names, consumed as-is by KickInputs.fluid / analytical_kick_tolerance(gas_composition=). The natural-gas / sour entries are representative examples, not standards. For the full fluid list use CoolProp’s get_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 composition at (P, T) via CoolProp.

Parameters:
  • composition (dict) – Component mole fractions, e.g. {"Methane": 0.9, "CO2": 0.1} (names are case-insensitive; common aliases like CO2/N2/H2S are 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 string pipe_od_in inside it:

capacity = (inner_id^2 - pipe_od^2) / 1029.4

pipe_od_in = 0 gives 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 grade is given the catalogue also yields the minimum internal yield pressure (API TR 5C3 Eq. 10, Barlow, already derated by minimum wall), and the section carries burst_design_factor times 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 WellSection list 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 (wellbore section tops/bottoms),

  • every string-geometry change (string section 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 with split_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=True and 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 section at tvd [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 tvd is not inside the section, if the section carries no MD extent, or if the survey crosses tvd more 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 (default None = 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_profile two-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 / migrate expose gas_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: object

A 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: object

Migration kick tolerance V* AND where/why it is limited.

max_influx_bbl is the largest influx that can be circulated out. The binding fields describe the breach that limits it. binding_tvd is 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: object

Result 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: object

Outcome of a bubble migration sweep.

steps is 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: object

One 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: object

One 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, not capacity * dTVD. Supply these (from a survey) and the section reports capacity_per_tvd_ft accordingly. Leave them None and 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, not capacity * dTVD. Supply these (from a survey) and the section reports capacity_per_tvd_ft accordingly. Leave them None and 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-mean sec(inc). Equals the raw capacity for a vertical section. Every volume in the TVD-domain engines is this * 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 of p_psia or t_rankine and 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_psia to 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_s exist 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) -> Z provider (e.g. a CoolProp table). Default is the clean-room Hall & Yarborough methane backend.

  • idealTrue forces Z = 1. Use for reproducing a textbook case that assumes it; do not use for engineering.

Return type:

GasState

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_rankine for 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_tvd must differ from shoe_tvd.

  • td_tvd – Anchor depths [ft TVD]; td_tvd must differ from shoe_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_psi carries 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_shoe says 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_hole sections are exposed. Cased formations are behind pipe and are not assessed here (casing BURST is a separate limit – see max_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 fp exposes no breakpoints, so pass check_depths for one or a narrow weak zone can be stepped over entirely.

Return type:

MaaspResult

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_bh None -> bhp_psi; Z_bh / rho_gas_ppg None -> computed by the Hall-Yarborough methane backend (gas_z.py); T_bh_rankine required.

  • 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. See pressure_at_depth.

  • temp_profile (None | callable | (tvd_array, T_rankine_array)) – Temperature profile T(d) [degR], passed through to pressure_at_depth AND used in the Boyle expansion (the gas expands at the LOCAL temperature at its top, not the bottom-hole temperature). None (DEFAULT) is ISOTHERMAL at T_bh_rankine – reproduces the previous result exactly. See linear_temp_profile for the basic two-point gradient.

  • geothermal (None | callable | (tvd_array, T_rankine_array)) – Field geothermal gradient, the DEFAULT temperature when temp_profile is not given (explicit temp_profile > geothermal > isothermal).

  • n_steps – Number of migration steps (bottom -> surface).

Returns:

.steps is the per-step animation trajectory; scalar fields summarise the binding (worst) constraint over the whole sweep.

Return type:

MigrationResult

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 carries g folded 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_ft for BHP held constant at the bottom.

z_fn (optional): a real-gas Z provider (P_psi, T_rankine) -> Z – e.g. a precomputed CoolProp ZTable for a mixture / CO2 / CCUS influx. When None (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 temperature T(d). gas_bh = (P_bh, T_bh_rankine, Z_bh, rho_gas_bh). When temp_profile is None the local temperature equals T_bh everywhere, T cancels, and this reduces EXACTLY to the previous isothermal density rho_gas_bh * (P * Z_bh) / (P_bh * Z(P)). Vectorised over depth_ft.

Parameters:
  • temp_profile (None | callable | (tvd_array, T_rankine_array)) – Temperature profile T(d) [degR]. None (DEFAULT) is ISOTHERMAL at the bottom-hole temperature T_bh_rankine (from gas_bh) – reproduces the previous behaviour exactly. A callable tvd -> T_rankine or a (tvd, T_rankine) table sets a depth-varying temperature (see linear_temp_profile for 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_profile is not given. Resolution is three-tier: an explicit temp_profile wins; else geothermal (the go-to default when a gradient is known); else isothermal at T_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: object

NOGEPA-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 V2 value).

Reproduces the mandated formula exactly (T and Z ignored, per the standard). The migration engine reduces to this when the casing shoe genuinely governs.