welleng package

Subpackages

Submodules

welleng.architecture module

class welleng.architecture.BHA(*args, **kwargs)[source]

Bases: String

__init__(*args, **kwargs)[source]

Inherits from String class, but this makes it more intuitive.

class welleng.architecture.String(name, top, bottom, *args, method='bottom_up', **kwargs)[source]

Bases: object

__init__(name, top, bottom, *args, method='bottom_up', **kwargs)[source]

A generic well bore architecture collection, e.g. a casing string made up a a number of different lengths of weights and grades.

Parameters:
  • name (str) – The name of the collection.

  • top (float) – The shallowest measured depth at the top of the collection of items in meters.

  • bottom (float) – The deepest measured depth at the bottom of the collection of items in meters.

  • method (string (default: 'bottom up')) – The method in which items are added to the collection, either ‘bottom up’ starting from the deepest element and adding items above, else ‘top down’ starting from the shallowest item and adding items below.

add_section(**kwargs)[source]
add_section_bottom_up(**kwargs)[source]

Sections built from the bottom up until the top of the top section is equal to the defined string top.

Default is to extend the section to the top of the String as defined in the String.top property (when length = top = None).

Parameters:

add_section_top_down(**kwargs)[source]

Sections built from the top down until the bottom of the bottom section is equal to the defined string bottom.

at(md)[source]

The section spanning the given measured depth.

Parameters:

md (float) – Measured depth in meters, within (top, bottom].

Returns:

The section dict (as stored in sections), carrying its ‘top’ and ‘bottom’ measured depths along with whatever geometry was added with it (‘id’ for a WellBore, ‘od’ for a BHA).

Return type:

dict

breakpoints()[source]

The measured depths at which this string’s geometry changes – every section top and bottom, sorted and de-duplicated.

Return type:

list of float

depth(md)[source]
class welleng.architecture.WellBore(*args, **kwargs)[source]

Bases: String

__init__(*args, **kwargs)[source]

Inherits from String class, but this makes it more intuitive.

welleng.acceptance module

Separation-factor acceptance criteria — ISCWSA/OWSG anti-collision.

welleng owns the separation-factor MATHS (welleng.clearance). This module adds the small typed POLICY layer the standard also defines, so the four consumers of the SF (welleng-api, -probcol, -pathfinder, and any caller) stop each inventing “acceptable” privately.

It is NOT a policy engine. It carries the standard’s graded thresholds, the action each triggers, the HSE-risk parameter set that accompanies them, and a classify(sf) -> Verdict. What a consumer DOES with a verdict — reject a candidate, record it, maximise a target — is the consumer’s business, not this module’s.

Source

Sodling, Clark & Allen, “The Development and Testing of an Enhanced Anti-Collision Rule”, SPE-187073 (SPE Drill & Compl 34, 2019), DOI 10.2118/187073-PA – the same paper whose separation rule IscwsaClearance implements. The graded criteria and their actions are the paper’s own; the HSE-risk parameter set (k = 3.5 Williamson 1998; the crossing-probability caution Bang 2017) is the recommendation it makes for HSE-risk wells.

The standard’s own escape hatch is respected: “where local regulations are more conservative they take precedence” – an operator override may only TIGHTEN the critical floor (raise it), never loosen it below the mandatory SF = 1.

class welleng.acceptance.AcceptanceCriteria(sf_critical: float = 1.0, sf_review: float = 1.25, sf_exclude: float = 5.0, k: float = 3.5, surface_margin_m: float = 0.3, project_ahead_sigma_m: float = 0.5, source: str = 'SPE-187073 (Sodling, Clark & Allen, 2019); DOI 10.2118/187073-PA', operator_override: bool = False)[source]

Bases: object

The standard’s graded acceptance criteria, with an explicit operator override.

Construct the standard via standard(); tighten it via with_operator_floor(). classify maps an SF to a Verdict.

__init__(sf_critical: float = 1.0, sf_review: float = 1.25, sf_exclude: float = 5.0, k: float = 3.5, surface_margin_m: float = 0.3, project_ahead_sigma_m: float = 0.5, source: str = 'SPE-187073 (Sodling, Clark & Allen, 2019); DOI 10.2118/187073-PA', operator_override: bool = False) None
classify(sf: float) Verdict[source]

Classify a separation factor into a Verdict.

Bands, checked in order so an operator floor above the review threshold still behaves: sf < sf_critical -> CRITICAL; sf > sf_exclude -> EXCLUDE; sf < sf_review -> REVIEW; else ACCEPTABLE.

k: float = 3.5
operator_override: bool = False
project_ahead_sigma_m: float = 0.5
sf_critical: float = 1.0
sf_exclude: float = 5.0
sf_review: float = 1.25
source: str = 'SPE-187073 (Sodling, Clark & Allen, 2019); DOI 10.2118/187073-PA'
classmethod standard() AcceptanceCriteria[source]

The SPE-187073 criteria, unmodified.

surface_margin_m: float = 0.3
to_dict() dict[source]

Canonical JSON-serialisable form, for a provenance stamp.

The BLESSED serialisation (see Verdict.to_dict()) – fields listed explicitly so the stamp is a stable contract, not whatever dataclasses.asdict() happens to reflect.

with_operator_floor(sf_min: float) AcceptanceCriteria[source]

A tightened copy whose critical floor is sf_min.

An override may only TIGHTEN: sf_min below the mandatory SF = 1 is refused, because more-conservative local regulation takes precedence over the standard but nothing may drop below its mandatory minimum.

class welleng.acceptance.Verdict(sf: float, band: str, acceptable: bool, action: str, criterion: AcceptanceCriteria)[source]

Bases: object

The classification of a single separation factor. Read-only.

__init__(sf: float, band: str, acceptable: bool, action: str, criterion: AcceptanceCriteria) None
acceptable: bool
action: str
band: str
criterion: AcceptanceCriteria
sf: float
to_dict() dict[source]

Canonical JSON-serialisable form, for a provenance stamp.

The BLESSED serialisation – every consumer stamps a verdict identically rather than each hand-rolling dataclasses.asdict(), so a stored result’s record of what “acceptable” meant is byte-identical across repos. Fields are listed explicitly (not reflected) so the stamp is a stable contract: a new internal field cannot silently change it. The nested criterion recurses through its own AcceptanceCriteria.to_dict().

welleng.acceptance.classify(sf: float, criteria: AcceptanceCriteria | None = None) Verdict[source]

Classify sf against criteria (the SPE-187073 standard by default).

welleng.clearance module

class welleng.clearance.Clearance(reference: Survey, offset: Survey, k: float = 3.5, sigma_pa: float = 0.5, Sm: float = 0.3, Rr: float = 0.4572, Ro: float = 0.3048, kop_depth: float = -inf)[source]

Bases: object

Initialize a welleng.clearance.Clearance object.

Parameters:
  • reference (welleng.survey.Survey object) – The current well from which other wells are referenced.

  • offset (welleng.survey.Survey object) – The other well.

  • k (float) – The dimensionless scaling factor that determines the probability of well crossing.

  • sigma_pa (float) – Quantifies the 1-SD uncertainty in the projection ahead of the current survey station. Its value is partially correlated with the projection distance, determined as the current survey depth to the bit plus the next survey interval. The magnitude of the actual uncertainty also depends on the planned curvature and on the actual BHA performance at the wellbore attitude in the formation being drilled. The project-ahead uncertainty is only an approximation, and although it is predominantly oriented normal to the reference well, it is mathematically convenient to define sigma_pa as being the radius of a sphere.

  • Sm (float) – The surface margin term increases the effective radius of the offset well. It accommodates small, unidentified errors and helps overcome one of the geometric limitations of the separation rule, described in the Separation-Rule Limitations section. It also defines the minimum acceptable slot separation during facility design and ensures that the separation rule will prohibit the activity before nominal contact between the reference and offset wells, even if the position uncertainty is zero.

  • Rr (float) – The openhole radius of the reference borehole (in meters).

  • Ro (float) – The openhole radius of the offset borehole (in meters).

  • kop_depth (float) – The kick-off point (measured) depth along the well bore - the default value assures that the first survey station is utilized.

References

Sawaryn, S. J., Wilson, H.. , Bang, J.. , Nyrnes, E.. , Sentance, A.. , Poedjono, B.. , Lowdon, R.. , Mitchell, I.. , Codling, J.. , Clark, P. J., and W. T. Allen. “Well-Collision-Avoidance Separation Rule.” SPE Drill & Compl 34 (2019): 01–15. doi: https://doi.org/10.2118/187073-PA

__init__(reference: Survey, offset: Survey, k: float = 3.5, sigma_pa: float = 0.5, Sm: float = 0.3, Rr: float = 0.4572, Ro: float = 0.3048, kop_depth: float = -inf)[source]
sf_vs_md()[source]

Separation-factor-versus-MD profiles for both methods along the reference well — a single consistent computation, inherited by every Clearance subclass.

At each reference station, returns the minimum over the offset stations of (i) the pedal / support-function separation factor (the ISCWSA separation rule) and (ii) the exact combined-ellipsoid Mahalanobis separation factor, evaluated over the same station pairing with this clearance’s k, Sm and sigma_pa. Because only the metric differs, the exact factor is >= the pedal factor at every station (Kantorovich). Comparing two different Clearance subclasses station-by-station is not apples-to-apples — they pair reference/offset and resolve closest approach differently, and their profiles can appear to cross.

Returns:

  • md ((N,) ndarray) – Reference-well measured depth at each station.

  • pedal_sf ((N,) ndarray) – Pedal / support-function (ISCWSA separation-rule) separation factor.

  • mahalanobis_sf ((N,) ndarray) – Exact combined-ellipsoid (Mahalanobis) separation factor.

class welleng.clearance.IscwsaClearance(*clearance_args, minimize_sf=None, **clearance_kwargs)[source]

Bases: Clearance

Parameters:

clearance_args: List

See ‘welleng.clearance.Clearance` for args.

minimize_sf: bool

If True (default), then the closest points on the reference well are determined and added to the ref object as interpolated stations.

clearance_kwargs: dict

See ‘welleng.clearance.Clearance` for kwargs.

Attributes:

Roarray of floats

The radius of the offset well at each station of the off well.

Rrarray

The radius of the reference well at each station on the ref well.

sfarray of floats

The calculated Separation Factor to the closest point on the offset well for each station on the reference well.

Smfloat

The surface margin term increases the effective radius of the offset well. It accommodates small, unidentified errors and helps overcome one of the geometric limitations of the separation rule, described in the Separation-Rule Limitations section. It also defines the minimum acceptable slot separation during facility design and ensures that the separation rule will prohibit the activity before nominal contact between the reference and offset wells, even if the position uncertainty is zero.

calc_hole: array of floats

The calculated combined equivalent radius of the two well bores, i.e. the sum or their radii plus margins.

closest:

The closest point on the off well from each station on the ref well.

distance_cc:

The closest center to center distance for each station on the ref well to the off well.

eou_boundary:

The sum of the ellipse of uncertainty radii of the ref and off wells.

eou_separation:

The distance between the ellipses of uncertainty of the ref and off wells.

hoz_bearing:

The horizontal bearing between the closest points in radians.

hoz_bearing_deg:

The horizontal bearing between the closest points in degrees.

idx: int

The index of the closest point on the off well for each station on the ref well.

masd:

The Minimum Allowable Separation Distance from the ref well.

off: Survey

The offset well Survey.

off_pcr:

The Pedal Curve Radii for each station on the off well.

off_cov_hla:

The covariance matrix in the HLA domain for each station of the off well.

off_cov_nev:

The covariance matrix in the NEV domain for each station of the off well.

off_nevs:

The NEV coordinates of the off well.

offset: Survey

The initial offset well Survey.

offset_nevs:

The initial NEV coordinates of the offset well.

ref: Survey

The ref well Survey.

ref_pcr:

The Pedal Curve Radii for each station on the ref well.

ref_cov_hla:

The covariance matrix in the HLA domain for each station of the ref well.

ref_cov_nev:

The covariance matrix in the NEV domain for each station of the ref well.

ref_nevs:

The NEV coordinates of the ref well.

reference: Survey

The initial reference well Survey.

reference_nevs:

The initial NEV coordinates of the reference well.

sf:

The Separation Factor between the closest point on the off well for each station on the ref well.

toolface_bearing:

The toolface bearing in radians from each station on the ref well to the closest point on the off well.

trav_cyl_azi_deg:

The heading in degrees from each station on teh ref well to the closest point on the off well.

wellbore_separation:

The distance between the edge of the wellbore for each station on the ref well to the closest point on the off well.

get_lines()[source]

Generate line data for visualizing clearance between wells.

get_sf_mins()[source]

Compute minimum separation factor indices and values.

__init__(*clearance_args, minimize_sf=None, **clearance_kwargs)[source]
get_lines()[source]

Extracts the closest points between wells for each survey section.

get_sf_mins()[source]

Method for assessing whether a minima has occurred between survey station SF values on the reference well and if so calculates the minimum SF value between stations (between the previous and next station relative to the identified station).

Modifies the sf attribute to include the interpolated minimum sf values.

class welleng.clearance.MahalanobisClearance(*args, n_candidates=8, tol=0.001, **kwargs)[source]

Bases: Clearance

Anti-collision using the exact Mahalanobis k-sigma boundary of the combined (relative-position) uncertainty ellipsoid, rather than the pedal-curve support-function approximation used by the ISCWSA separation rule (IscwsaClearance).

Subclasses the lightweight Clearance base (NOT IscwsaClearance) so it does not pay for the pedal-curve separation-factor minimisation it does not use; it needs only the reference/offset surveys, their covariances and hole radii.

The separation rule measures the combined ellipsoid’s extent toward the offset with its support function sqrt(uT.Sigma.u) (the tangent distance), which always over-states the ellipsoid’s reach in an off-axis direction and so is conservative. This class instead uses the true ellipsoid-surface distance — the Mahalanobis distance of the radii-adjusted centre-to-centre vector in the combined covariance metric:

SF = min over both curves of

sqrt(d’T (Sigma_ref + Sigma_off + sigma_pa^2 I)^-1 d’) / k

where d' is the centre-to-centre vector shortened by the combined hole radii and surface margin Sm, and sigma_pa^2 I is the isotropic project-ahead floor (mirroring the separation rule’s sigma_pa term), which keeps the metric finite where a survey covariance is degenerate. The minimum is found over both wells by broadphase (all-pairs at the surveys’ own stations) plus a continuous narrowphase refinement, so the worst point between stations is captured without an externally imposed step. SF < 1 means the offset lies within the k-sigma combined ellipsoid (collision). It is a k-sigma geometric boundary method (not a probability of collision), fast and analytic — no mesh or collision library required.

This is the Mahalanobis distance of Brooks (SPE-116155, 2008; after Alfano’s satellite-conjunction work); see papers/anti-collision- conservatism.md for the derivation, validation and references. If you use this method, please cite welleng (doi:10.5281/zenodo.20968887) and that paper. Author: Jonathan Corcutt, Corcutt Beheer B.V. (ORCID 0009-0008-1953-7760).

Parameters:
  • reference (welleng.survey.Survey) – The two wells (with an error model so cov_nev is populated).

  • offset (welleng.survey.Survey) – The two wells (with an error model so cov_nev is populated).

  • k (float, default 3.5) – Confidence multiple (inherited from Clearance).

  • Sm (float, default 0.3) – Surface margin, m (inherited).

  • sigma_pa (float, default 0.5) – Isotropic project-ahead floor, m (inherited). sigma_pa > 0 guarantees a positive-definite combined covariance; set 0 for the pure combined-ellipsoid metric (e.g. when reproducing Brooks).

  • kop_depth (float, default -inf) – Kick-off depth below which to scan (inherited; for sidetracks).

  • n_candidates (int, default 8) – Number of globally-lowest broadphase stations polished by the narrowphase, IN ADDITION to every local minimum of the broadphase profile (so a sharp crossing is refined whatever its rank). For the ISCWSA standard set the result already converges with a single candidate; this is defensive headroom, not a tuned value, and remains a heuristic rather than a guarantee.

  • tol (float, default 1e-3) – Narrowphase convergence tolerance on the curve parameter (measured depth), m.

sf

Separation factor at each reference station; min(sf) < 1 is a collision.

Type:

numpy.ndarray

min_sf

The governing (minimum) separation factor over all stations.

Type:

float

__init__(*args, n_candidates=8, tol=0.001, **kwargs)[source]
property min_sf

The governing (minimum) separation factor; < 1 is a collision.

class welleng.clearance.MeshClearance(*clearance_args, n_verts: int = 12, sigma: float = 2.445, return_data: bool = True, return_meshes: bool = False, polygon_fit: str = 'circumscribed', **clearance_kwargs)[source]

Bases: Clearance

Class to calculate the clearance between two well bores using a novel mesh clearance method. This method is experimental and was developed to provide a fast method for determining if well bores are potentially colliding.

This class requires that trimesh is installed along with python-fcl.

Parameters:
  • n_verts (int) – The number of points (vertices) used to generate the uncertainty ellipses which are used to generate a trimesh representation of the well bores. The default is 12 which is a good balance between accuracy and speed.

  • sigma (float) – The required/desired sigma value representation of the generated mesh. The default value of 2.445 represents about 98.5% confidence of the well bore being located within the volume of the generated mesh.

get_lines()[source]

Generate line data for visualizing clearance between wells.

__init__(*clearance_args, n_verts: int = 12, sigma: float = 2.445, return_data: bool = True, return_meshes: bool = False, polygon_fit: str = 'circumscribed', **clearance_kwargs)[source]
get_lines()[source]

Extracts the closest points between wells for each survey section.

welleng.clearance.combined_cov_mesh(survey, other, k=2.445, n_verts=12, Sm=0.0, polygon_fit='circumscribed')[source]

Build a trimesh of survey’s uncertainty tube carrying the COMBINED relative-position covariance Sigma_survey + Sigma_other.

A collision check between two uncertain wells should use the combined (relative-position) uncertainty Sigma_ref + Sigma_off (the variance of P_off - P_ref), which a single ellipsoid represents exactly. Building a separate k-sigma mesh for each well and testing surface overlap instead sums their extents linearly (k(sigma_ref + sigma_off)) rather than in quadrature (k*sqrt(sigma_ref^2 + sigma_off^2)), which over-states the required standoff by up to sqrt(2) (symmetric case) and raises false collision alarms.

This builds ONE mesh — survey inflated by the combined covariance and the combined hole radii (survey.radius + other.radius + Sm) — intended to be tested against the centreline of other (e.g. via mesh.contains(other.pos_nev) or a trimesh CollisionManager against a zero-uncertainty tube). It is the mesh/visualisation counterpart of MahalanobisClearance and is consistent with it (same RSS combined covariance), but not numerically identical: it omits the project-ahead floor sigma_pa, maps other’s covariance by nearest Euclidean station, and discretises the surface into n_verts facets. For the exact pairwise separation factor use MahalanobisClearance; use this where a triangulated surface is needed (visualisation or a multi-well collision-manager scene).

Parameters:
  • survey (welleng.survey.Survey) – The two wells. other’s covariance and hole radius are mapped onto survey by nearest position and folded into the returned mesh.

  • other (welleng.survey.Survey) – The two wells. other’s covariance and hole radius are mapped onto survey by nearest position and folded into the returned mesh.

  • k (float) – The sigma multiple defining the uncertainty surface (default 2.445).

  • n_verts – Passed through to welleng.mesh.WellMesh (polygon_fit defaults to “circumscribed” so the polygon never under-counts the ellipse).

  • polygon_fit – Passed through to welleng.mesh.WellMesh (polygon_fit defaults to “circumscribed” so the polygon never under-counts the ellipse).

  • Sm – Passed through to welleng.mesh.WellMesh (polygon_fit defaults to “circumscribed” so the polygon never under-counts the ellipse).

Returns:

survey’s combined-covariance uncertainty tube.

Return type:

trimesh.Trimesh

Notes

The combination is pairwise (it depends on other), so a candidate checked against N offsets needs N combined meshes; the other well is taken as a deterministic centreline.

welleng.clearance.get_ref_sigma(sigma1, sigma2, sigma3, kop_index)[source]

welleng.composition module

Compose a single wellbore’s survey sections into one tied survey.

A wellbore is rarely surveyed by a single continuous run of one tool. It is drilled and surveyed in ordered sections (legs), each potentially using a different survey tool / ISCWSA error model, and each tied on to the end of the previous section. Naively concatenating the per-section covariances is wrong in two opposite ways:

  • Restarting the systematic error at every section understates the uncertainty of a section that is really one continuous survey (the systematic sensor biases keep accumulating correlated down the hole — they are one physical realisation, not a fresh draw per section).

  • Treating a genuine tool change as one continuous survey overstates the correlation: a new tool’s sensor biases are an independent realisation, so its systematic error must restart (while the accumulated position uncertainty of course carries forward).

SurveyComposition ties the sections together with per-component-correct covariance carry:

  1. Consecutive sections sharing a tool (same error_model and tool_id) are grouped into one Survey so their systematic error accumulates correlated — exactly as if surveyed in one run.

  2. At a tool change the next group is tied on at the previous group’s end position, carrying the accumulated covariance. The new tool’s systematic is independent (restarts); only the accumulated position covariance and the shared global geomagnetic terms carry across the tie.

  3. Whether the global (declination / B-field, DECG / DBHG) and systematic terms are shared across a given tie is controlled by a ShareMode, defaulting to the ISCWSA Side-track Clearance RP (2022) recommendation: within one wellbore / campaign the global geomag terms stay correlated across the tie (same geomag date and model), while sensor systematic resets at a tool change. A section drilled in a different campaign (e.g. a side-track years later, different geomag secular variation) can be marked all_independent.

The result is one unified Survey whose total covariance and per-component (cov_nev_global / cov_nev_systematic / cov_nev_random) breakdown are correctly composed. Preserving that breakdown is what lets the relative-error / anti-collision framework (welleng.conditioning.combine_covariances()) later difference two composed wellbores with correct cancellation of shared global terms.

class welleng.composition.SurveyComposition(sections: Sequence[SurveySection], header: SurveyHeader | None = None, share_mode: Literal['all_independent', 'globals_shared', 'globals_and_systematic_shared'] = 'globals_shared')[source]

Bases: object

Tie a wellbore’s ordered survey sections into one continuous survey.

Parameters:
  • sections (sequence of SurveySection) – Ordered from shallow to deep. Each section ties on to the previous.

  • header (welleng.survey.SurveyHeader, optional) – Default header applied to any section that does not carry its own.

  • share_mode (ShareMode, optional) – Default share mode for every tie whose section does not set one and for which the context keys do not force a choice. Defaults to "globals_shared" (the ISCWSA side-track RP recommendation).

Notes

Auto share-mode. When a section’s share_mode is None the tie before it is resolved from context: if the two sides name different geomag_model values, or their survey_date values differ by more than two years, the tie is all_independent (the global geomagnetic realisation has drifted / changed model and no longer cancels); otherwise the composition share_mode (default globals_shared) is used.

Mixed error models across a shared tie. The shared (globally- or systematically-correlated) component of a multi-group run is computed by building that run as a single survey using the run’s first section’s error model. This is exact when all groups in the run use the same error model (the common case). If they differ, the shared component uses the first model as the reference and a warning is issued.

__init__(sections: Sequence[SurveySection], header: SurveyHeader | None = None, share_mode: Literal['all_independent', 'globals_shared', 'globals_and_systematic_shared'] = 'globals_shared') None[source]
survey() Survey[source]

Return the unified, tied Survey.

The result carries the composed total covariance (cov_nev / cov_hla) and its per-component breakdown (cov_nev_global / cov_nev_systematic / cov_nev_random). It is cached.

class welleng.composition.SurveySection(md: ArrayLike | None = None, inc: ArrayLike | None = None, azi: ArrayLike | None = None, survey: Survey | None = None, deg: bool = True, error_model: str | None = None, tool_id: str | None = None, survey_date: str | None = None, geomag_model: str | None = None, share_mode: Literal['all_independent', 'globals_shared', 'globals_and_systematic_shared'] | None = None, header: SurveyHeader | None = None)[source]

Bases: object

One surveyed section (leg) of a wellbore, tied on to the previous one.

Provide either raw md / inc / azi arrays (with deg and an optional header) or an existing Survey via survey. The first station of each section must coincide (in measured depth) with the last station of the previous section — that shared station is the tie-on.

Parameters:
  • md (array_like, optional) – Section geometry. Ignored if survey is given.

  • inc (array_like, optional) – Section geometry. Ignored if survey is given.

  • azi (array_like, optional) – Section geometry. Ignored if survey is given.

  • survey (welleng.survey.Survey, optional) – An existing survey to take geometry (and, if not overridden, the error_model / header) from.

  • deg (bool, default True) – Whether inc / azi are in degrees.

  • error_model (str, optional) – ISCWSA error model name for this section’s tool. Defaults to DEFAULT_ERROR_MODEL.

  • tool_id (str, optional) – Identifier of the physical survey tool / run. Consecutive sections with the same error_model and tool_id are treated as one continuous survey (systematic error stays correlated). A change of tool_id (or error_model) marks a tool change / tie. None is treated as “the same unspecified tool continuing” — so if you never say the tool changed, it does not.

  • survey_date (str, optional) – YYYY-MM-DD date the section was surveyed. Used only to auto-pick a default share_mode at the tie before this section.

  • geomag_model (str, optional) – Name of the geomagnetic reference model (e.g. "BGGM2020"). Used only to auto-pick a default share_mode.

  • share_mode ({'all_independent', 'globals_shared', 'globals_and_systematic_shared'}, optional) – Explicit override for how the tie before this section shares error components with the previous group. If None, auto-picked from the context keys (see SurveyComposition).

  • header (welleng.survey.SurveyHeader, optional) – Survey header (geomag field, dip, location) for this section’s error model. Falls back to the composition-level default.

__init__(md: ArrayLike | None = None, inc: ArrayLike | None = None, azi: ArrayLike | None = None, survey: Survey | None = None, deg: bool = True, error_model: str | None = None, tool_id: str | None = None, survey_date: str | None = None, geomag_model: str | None = None, share_mode: Literal['all_independent', 'globals_shared', 'globals_and_systematic_shared'] | None = None, header: SurveyHeader | None = None) None
azi: ArrayLike | None = None
deg: bool = True
error_model: str | None = None
geomag_model: str | None = None
header: SurveyHeader | None = None
inc: ArrayLike | None = None
md: ArrayLike | None = None
share_mode: Literal['all_independent', 'globals_shared', 'globals_and_systematic_shared'] | None = None
survey: Survey | None = None
survey_date: str | None = None
tool_id: str | None = None

welleng.conditioning module

Shared-error conditioning of two wells’ combined covariance.

The standard ISCWSA pair calculation assumes the two wells’ position-error vectors are entirely independent — combined covariance Σ_A + Σ_B. This is correct between geographically and temporally separated wells, where the underlying error sources (magnetic declination, geomagnetic reference field, sensor biases, …) really do realise independently. It is not correct for two wells drilled from the same platform within a short time window, where:

  • Global error sources (magnetic declination model errors, BGGM/IFR field model errors, true-vs-grid azimuth correction) are realised identically in both wells. They share one realisation.

  • Systematic error sources (sensor biases, tool misalignment, …) are independent if the two wells used different tools, but identical if they shared one MWD service per platform run. The honest assumption depends on the operator and the campaign.

  • Random error sources (per-station survey noise) are always independent.

When global and (optionally) systematic terms are shared, they cancel in the position difference:

cov(X_A − X_B)

= cov_random_A + cov_random_B + cov_systematic_A + cov_systematic_B (if independent) + (cov_global_A − cov_global_B) (zero if shared)

i.e. the difference covariance can be substantially smaller than the naive sum, which translates into a substantially lower probability of collision than the naive ISCWSA calculation reports.

This module provides helpers to construct the correct combined covariance under different assumptions about which error components are shared between the two wells.

class welleng.conditioning.CombinedCovariance(cov_combined: ndarray[tuple[Any, ...], dtype[float64]], cov_naive: ndarray[tuple[Any, ...], dtype[float64]], sigma_naive: ndarray[tuple[Any, ...], dtype[float64]], sigma_combined: ndarray[tuple[Any, ...], dtype[float64]], reduction_factor: ndarray[tuple[Any, ...], dtype[float64]])[source]

Bases: object

Result of combining two wells’ covariances under a sharing assumption.

cov_combined

The covariance of X_A X_B per station after accounting for the share-mode.

Type:

(n, 3, 3) ndarray

cov_naive

The naive Σ_A + Σ_B for comparison.

Type:

(n, 3, 3) ndarray

sigma_naive

sqrt(max eigenvalue) of cov_naive — naive 1σ in worst direction, per station.

Type:

(n,) ndarray

sigma_combined

Same for cov_combined.

Type:

(n,) ndarray

reduction_factor

sigma_naive / sigma_combined per station — how much the share-mode tightens the combined uncertainty.

Type:

(n,) ndarray

__init__(cov_combined: ndarray[tuple[Any, ...], dtype[float64]], cov_naive: ndarray[tuple[Any, ...], dtype[float64]], sigma_naive: ndarray[tuple[Any, ...], dtype[float64]], sigma_combined: ndarray[tuple[Any, ...], dtype[float64]], reduction_factor: ndarray[tuple[Any, ...], dtype[float64]]) None
cov_combined: ndarray[tuple[Any, ...], dtype[float64]]
cov_naive: ndarray[tuple[Any, ...], dtype[float64]]
reduction_factor: ndarray[tuple[Any, ...], dtype[float64]]
sigma_combined: ndarray[tuple[Any, ...], dtype[float64]]
sigma_naive: ndarray[tuple[Any, ...], dtype[float64]]
welleng.conditioning.combine_covariances(cov_total_a: ndarray[tuple[Any, ...], dtype[float64]], cov_total_b: ndarray[tuple[Any, ...], dtype[float64]], *, cov_global_a: ndarray[tuple[Any, ...], dtype[float64]] | None = None, cov_global_b: ndarray[tuple[Any, ...], dtype[float64]] | None = None, cov_systematic_a: ndarray[tuple[Any, ...], dtype[float64]] | None = None, cov_systematic_b: ndarray[tuple[Any, ...], dtype[float64]] | None = None, share_mode: Literal['all_independent', 'globals_shared', 'globals_and_systematic_shared'] = 'globals_shared') CombinedCovariance[source]

Combine two wells’ per-station covariances under a share-mode.

Parameters:
  • cov_total_a ((n, 3, 3) ndarray) – Total covariance per station for each well, as produced by the ISCWSA error model.

  • cov_total_b ((n, 3, 3) ndarray) – Total covariance per station for each well, as produced by the ISCWSA error model.

  • cov_global_a ((n, 3, 3) ndarray, optional) – Global-error component per well (magnetic declination, BGGM / IFR field model errors). Required when share_mode includes global cancellation. The ISCWSA model in welleng exposes this as Survey.cov_nev_global.

  • cov_global_b ((n, 3, 3) ndarray, optional) – Global-error component per well (magnetic declination, BGGM / IFR field model errors). Required when share_mode includes global cancellation. The ISCWSA model in welleng exposes this as Survey.cov_nev_global.

  • cov_systematic_a ((n, 3, 3) ndarray, optional) – Systematic-error component per well (sensor biases, tool misalignment). Required when share_mode is 'globals_and_systematic_shared'. ISCWSA exposes this as Survey.cov_nev_systematic.

  • cov_systematic_b ((n, 3, 3) ndarray, optional) – Systematic-error component per well (sensor biases, tool misalignment). Required when share_mode is 'globals_and_systematic_shared'. ISCWSA exposes this as Survey.cov_nev_systematic.

  • share_mode ({'all_independent', 'globals_shared', 'globals_and_systematic_shared'}) –

    Which components are realised identically in the two wells:

    • 'all_independent': classical naive Σ_A + Σ_B (no cancellation). Equivalent to passing neither global nor systematic arrays.

    • 'globals_shared' (default): global error sources realise identically; their contribution cancels in the difference covariance. This is the operationally honest default for two wells from the same platform.

    • 'globals_and_systematic_shared': both global and systematic terms cancel. Stronger assumption — only appropriate if the same MWD service / tool was used on both wells.

Return type:

CombinedCovariance

welleng.connector module

Wellbore trajectory connector.

Resolves a minimum-curvature connection between two stations (each a position and/or a direction), classifying it into the appropriate type — a straight hold, a single curve (min-curvature), a curve-hold, or a curve-hold-curve (the circle-line-circle, CLC, point-to-target case) — and returns the arc/hold sections, doglegs and measured depths. The curve-hold-curve case is solved in closed form via Sawaryn (2021, SPE-204111-PA) — see welleng.sawaryn_analytical.

class welleng.connector.Connector(node1: Node | None = None, node2: Node | None = None, pos1: ArrayLike = [0.0, 0.0, 0.0], vec1: ArrayLike | None = None, inc1: float | None = None, azi1: float | None = None, md1: float = 0, dls_design: float | None = 3.0, dls_design2: float | None = None, md2: float | None = None, pos2: ArrayLike | None = None, vec2: ArrayLike | None = None, inc2: float | None = None, azi2: float | None = None, degrees: bool = True, unit: str = 'meters', min_error: float = 1e-05, delta_dls: float = 0.1, min_tangent: float = 0.0, max_iterations: int = 1000, force_min_curve: bool = False, closest_approach: bool = False, on_infeasible: str = 'raise', direct_only: bool = False)[source]

Bases: object

Solves minimum-MD wellbore trajectories between two survey stations.

Automatically selects the appropriate geometric method (hold, curve-hold, min-curve, or curve-hold-curve) based on the provided start/end constraints and computes control points for the connecting path segment. The solver honours a maximum dog-leg severity (DLS) constraint where geometrically feasible.

method

The geometric method used (‘hold’, ‘min_curve’, ‘curve_hold_curve’, ‘min_dist_to_target’, or ‘min_curve_to_target’).

Type:

str

node_start

Start survey station as a Node.

Type:

Node

node_end

End survey station as a Node.

Type:

Node

pos1

Start position in NEV coordinates.

Type:

ndarray of shape (3,)

vec1

Unit direction vector at the start position.

Type:

ndarray of shape (3,)

inc1

Inclination at the start position (radians).

Type:

float

azi1

Azimuth at the start position (radians).

Type:

float

md1

Measured depth at the start position.

Type:

float

pos2

Position at the end of the first arc section in NEV coordinates. Equal to vec2 direction at this point.

Type:

ndarray of shape (3,) or None

vec2

Unit direction vector at the end of the first arc. Equals vec3 for curve-hold-curve solutions.

Type:

ndarray of shape (3,) or None

inc2

Inclination at the end of the first arc (radians).

Type:

float or None

azi2

Azimuth at the end of the first arc (radians).

Type:

float or None

md2

Measured depth at the end of the first arc.

Type:

float or None

pos3

Position at the start of the second arc (end of the hold section) in NEV coordinates. Only set for curve-hold-curve solutions.

Type:

ndarray of shape (3,) or None

vec3

Unit direction vector at the start of the second arc. Only set for curve-hold-curve solutions.

Type:

ndarray of shape (3,) or None

inc3

Inclination at the start of the second arc (radians).

Type:

float or None

azi3

Azimuth at the start of the second arc (radians).

Type:

float or None

md3

Measured depth at the start of the second arc.

Type:

float or None

md_target

Measured depth at the target position.

Type:

float

pos_target

Target position in NEV coordinates.

Type:

ndarray of shape (3,)

vec_target

Target unit direction vector in NEV coordinates.

Type:

ndarray of shape (3,)

inc_target

Target inclination (radians).

Type:

float

azi_target

Target azimuth (radians).

Type:

float

dogleg

Dogleg angle of the first arc (radians).

Type:

float

dogleg2

Dogleg angle of the second arc (radians). Only set for curve-hold-curve solutions.

Type:

float or None

dist_curve

Arc length of the first curve section.

Type:

float

dist_curve2

Arc length of the second curve section.

Type:

float

tangent_length

Length of the hold (tangent) section between the two arcs.

Type:

float or None

dls

Dogleg severity of the first arc (radians per unit length).

Type:

float

dls2

Dogleg severity of the second arc (radians per unit length).

Type:

float

dls_design

Design DLS constraint for the first arc (radians per unit length).

Type:

float

dls_design2

Design DLS constraint for the second arc (radians per unit length).

Type:

float

radius_design

Design turn radius derived from dls_design.

Type:

float

radius_design2

Design turn radius derived from dls_design2.

Type:

float

radius_critical

Critical (minimum geometric) radius for the first arc.

Type:

float

radius_critical2

Critical radius for the second arc.

Type:

float

interpolate(step=30)[source]

Interpolate the solved trajectory at regular MD intervals.

__init__(node1: Node | None = None, node2: Node | None = None, pos1: ArrayLike = [0.0, 0.0, 0.0], vec1: ArrayLike | None = None, inc1: float | None = None, azi1: float | None = None, md1: float = 0, dls_design: float | None = 3.0, dls_design2: float | None = None, md2: float | None = None, pos2: ArrayLike | None = None, vec2: ArrayLike | None = None, inc2: float | None = None, azi2: float | None = None, degrees: bool = True, unit: str = 'meters', min_error: float = 1e-05, delta_dls: float = 0.1, min_tangent: float = 0.0, max_iterations: int = 1000, force_min_curve: bool = False, closest_approach: bool = False, on_infeasible: str = 'raise', direct_only: bool = False) None[source]

Initializes the Connector and solves the trajectory.

Only specific combinations of input data are permitted. For example, providing both a start vector and start inc/azi raises an error. The solver determines the appropriate method from the provided parameters and computes the connecting path immediately.

Parameters:
  • node1 (Node or None) – Start Node. Overrides pos1, vec1, md1 if provided.

  • node2 (Node or None) – End Node. Overrides pos2, vec2, md2 if provided.

  • pos1 (list or ndarray) – Start position as [n, e, v] in NEV coordinates.

  • vec1 (list or ndarray or None) – Start unit direction vector in NEV coordinates.

  • inc1 (float or None) – Start inclination angle.

  • azi1 (float or None) – Start azimuth angle.

  • md1 (float) – Start measured depth.

  • dls_design (float) – Design DLS for the first curve section in deg/30m (meters) or deg/100ft (feet).

  • dls_design2 (float or None) – Design DLS for the second curve section. Defaults to dls_design if None.

  • md2 (float or None) – Target measured depth. Mutually exclusive with pos2.

  • pos2 (list or ndarray or None) – Target position in NEV coordinates.

  • vec2 (list or ndarray or None) – Target unit direction vector in NEV coordinates. Mutually exclusive with inc2/azi2.

  • inc2 (float or None) – Target inclination angle.

  • azi2 (float or None) – Target azimuth angle.

  • degrees (bool) – If True, angles are in degrees; if False, radians.

  • unit (str) – Distance unit, either ‘meters’ or ‘feet’.

  • min_error (float) – Error tolerance for iterative convergence. Must be less than 1.

  • delta_dls (float) – DLS tolerance (deg/30m) for balancing curve sections in curve-hold-curve solutions. Deprecated: accepted for backwards compatibility but unused by the analytic CHC path.

  • min_tangent (float) – Minimum tangent length to stabilize curve-hold-curve iteration. Deprecated: accepted for backwards compatibility but unused by the analytic CHC path.

  • max_iterations (int) – Maximum iteration count for curve-hold-curve fitting. Deprecated: accepted for backwards compatibility but unused by the analytic CHC path.

  • force_min_curve (bool) – If True, forces minimum-curvature method.

  • closest_approach (bool) – If True, finds the closest-approach trajectory when the target is inside the critical radius.

  • on_infeasible (str) – Behaviour when no curve-hold-curve solution exists at the design radii. 'raise' (default) raises ValueError. 'max_radius' falls back to the gentlest feasible curve — the beta=0 biarc at the largest radius admitting a valid CLC (see welleng.sawaryn_analytical.max_radius()) — and emits a UserWarning that the design DLS is exceeded.

  • direct_only (bool) –

    Reject solutions in which either arc turns through more than pi (180 deg) — i.e. accept only the “direct” way round, never a long-way loop. Default False (any arc angle is rendered).

    Warning

    A successful solve is NOT a dogleg-severity feasibility test unless direct_only=True. A long-way (>pi) arc exists at essentially any radius, so a search of the form “does a CLC exist at this DLS” succeeds at almost ANY DLS — it will happily return a multi-kilometre corkscrew for a short pose-to-pose move. Any code that infers reachability from solver success (a DLS bisection, a feasible flag, an input gate) must set direct_only=True or check dogleg / dogleg2 against pi itself.

Raises:

AssertionError – If input parameter combinations are invalid.

azi1: float
azi2: float | None
azi3: float | None
azi_target: float
dist_curve: Any
dist_curve2: Any
distances: tuple
dls: float
dls2: float
dls_design: float
dls_design2: float
dogleg: Any
dogleg2: Any
func_dogleg: Any
func_dogleg2: Any
inc1: float
inc2: float | None
inc3: float | None
inc_target: float
interpolate(step: float = 30) list[source]

Interpolates the connector trajectory at regular MD intervals.

Parameters:

step (float) – Desired delta measured depth between survey points.

Returns:

A list of interpolated survey data dictionaries.

Return type:

list

md1: float
md2: float | None
md3: float | None
md_target: float
method: str
node_end: Node
node_start: Node
pos1: ndarray
pos2: ndarray | None
pos3: ndarray | None
pos_target: ndarray
radii: list
radius_critical: float
radius_critical2: float
radius_design: float
radius_design2: float
tangent_length: float | None
unit: str
vec1: ndarray
vec2: ndarray | None
vec3: ndarray | None
vec_target: ndarray
welleng.connector.check_dogleg(dogleg: ArrayLike) float | ndarray[source]

Ensures the dogleg angle is positive by wrapping negative values.

Accepts scalar or array-like; output shape matches input.

Parameters:

dogleg (float or array_like) – Dogleg angle(s) in radians.

Returns:

The dogleg angle(s) normalized to [0, 2*pi).

Return type:

float or ndarray

welleng.connector.connect_points(cartesians: ArrayLike, vec_start: ArrayLike = [0.0, 0.0, 1.0], dls_design: float | list = 3.0, nev: bool = True, md_start: float = 0.0) list[source]

Connects a sequence of Cartesian points with Connector sections.

Parameters:
  • cartesians (list or ndarray) – Array of shape (n, 3) with positions as [n, e, tvd] (if nev=True) or [x, y, z] (if nev=False).

  • vec_start (list or ndarray) – Unit start direction vector in the corresponding coordinate system.

  • dls_design (float or list) – Design DLS in deg/30m (or deg/100ft). Can be a scalar or array of length n.

  • nev (bool) – If True, cartesians are in NEV coordinates; if False, XYZ.

  • md_start (float) – Measured depth at the first point.

Returns:

A list of Connector objects linking consecutive points.

Return type:

list

welleng.connector.convert_target_input_to_booleans(*inputs: Any) str[source]

Converts target parameters to a binary string for method lookup.

Parameters:

*inputs – Variable number of target parameters (md2, inc2, azi2, pos2, vec2). Each is mapped to ‘1’ if not None, ‘0’ otherwise.

Returns:

A 5-character binary string encoding which parameters were provided.

Return type:

str

welleng.connector.drop_off(target_inc: float, dls: float, delta_md: float | None = None, node: Node | None = None, tol: float = 1e-05) list[source]

Computes trajectory sections to drop off (or build) to a target inclination.

Use extend_to_tvd if a specific TVD target is also required.

Parameters:
  • target_inc (float) – Target inclination in degrees.

  • dls (float) – Design DLS in deg/30m.

  • delta_md (float or None) – Maximum section length in meters. If None, the section is unconstrained.

  • node (Node or None) – Starting Node. Defaults to surface pointing down.

  • tol (float) – Tolerance for tangent section length; sections shorter than this are omitted.

Returns:

A list of Nodes describing the trajectory. Contains one Node (the arc endpoint) or two (arc endpoint plus tangent endpoint) if the target inclination was achieved within the section.

Return type:

list

welleng.connector.extend_to_tvd(target_tvd: float, node: Node | None = None, delta_md: float | None = None, target_inc: float | None = None, dls: float | None = None) list[source]

Computes Connector sections to reach a target TVD with optional inclination change.

Parameters:
  • target_tvd (float) – Target true vertical depth in meters.

  • node (Node or None) – Starting Node. Defaults to surface pointing down.

  • delta_md (float or None) – Maximum section length in meters. If None, unconstrained.

  • target_inc (float or None) – Target inclination in degrees at the target TVD. If provided, the solver attempts to achieve this inclination and holds tangent to the target TVD.

  • dls (float or None) – Design DLS in deg/30m. Defaults to 2.5 if None and target_inc is provided.

Returns:

A list of Connector objects. Contains one Connector (curve only) or two (curve plus tangent hold) if the target inclination was achieved within the section.

Return type:

list

Examples

A well at 30 degrees inclination dropping to vertical:

>>> import welleng as we
>>> node = we.node.Node(pos=[0, 0, 3000], md=4000, inc=30, azi=135)
>>> connectors = we.connector.extend_to_tvd(
...     target_tvd=3200, node=node, target_inc=0, dls=3
... )
welleng.connector.get_curve_hold_data(radius: float | ndarray, dogleg: float | ndarray) tuple[source]

Computes arc length and shape factor for a curve section.

Parameters:
  • radius (float) – Radius of curvature.

  • dogleg (float) – Dogleg angle in radians.

Returns:

A tuple of (dist_curve, func_dogleg) where dist_curve is the arc length and func_dogleg is the minimum-curvature shape factor.

Return type:

tuple

welleng.connector.get_interpolate_hold(section: Connector, step: float = 30, data: list | None = None) list[source]

Interpolates a hold-method Connector section.

Parameters:
  • section (Connector) – A Connector object with method ‘hold’.

  • step (float) – Desired delta measured depth between interpolated points.

  • data (list or None) – Optional list to append results to.

Returns:

A list of interpolated survey data dictionaries.

Return type:

list

welleng.connector.get_interpolate_min_curve_to_target(section: Connector, step: float = 30, data: list | None = None) list[source]

Interpolates a min-curve-to-target Connector section.

Parameters:
  • section (Connector) – A Connector object with method ‘min_curve_to_target’.

  • step (float) – Desired delta measured depth between interpolated points.

  • data (list or None) – Optional list to append results to.

Returns:

A list of interpolated survey data dictionaries.

Return type:

list

welleng.connector.get_interpolate_min_dist_to_target(section: Connector, step: float = 30, data: list | None = None) list[source]

Interpolates a min-dist-to-target Connector section (curve + hold).

Parameters:
  • section (Connector) – A Connector object with method ‘min_dist_to_target’.

  • step (float) – Desired delta measured depth between interpolated points.

  • data (list or None) – Optional list to append results to.

Returns:

A list of interpolated survey data dictionaries.

Return type:

list

welleng.connector.get_interpololate_curve_hold_curve(section: Connector, step: float = 30, data: list | None = None) list[source]

Interpolates a curve-hold-curve Connector section.

Parameters:
  • section (Connector) – A Connector object with method ‘curve_hold_curve’.

  • step (float) – Desired delta measured depth between interpolated points.

  • data (list or None) – Optional list to append results to.

Returns:

A list of interpolated survey data dictionaries.

Return type:

list

welleng.connector.get_min_curve(section: Connector, step: float = 30, data: list | None = None) list[source]

Interpolates a minimum-curve section, dispatching by sub-method.

Parameters:
  • section (Connector) – A Connector object with method ‘min_curve’.

  • step (float) – Desired delta measured depth between interpolated points.

  • data (list or None) – Optional list to append results to.

Returns:

A list of interpolated survey data dictionaries.

Return type:

list

welleng.connector.get_pos(pos1: ndarray, vec1: ndarray, vec2: ndarray, dist_curve: float, func_dogleg: float) ndarray[source]

Computes the end position of a minimum-curvature arc.

Parameters:
  • pos1 (ndarray) – Start position in NEV coordinates.

  • vec1 (ndarray) – Start unit direction vector in NEV coordinates.

  • vec2 (ndarray) – End unit direction vector in NEV coordinates.

  • dist_curve (float) – Arc length of the curve section.

  • func_dogleg (float) – Shape factor (ratio factor) for the curve.

Returns:

End position in NEV coordinates.

Return type:

ndarray

welleng.connector.get_radius_critical(radius: float, distances: tuple, min_error: float) float[source]

Computes the critical radius for a given target geometry.

The critical radius is the minimum curvature radius needed to reach the target with a pure curve (no tangent). Below this radius, a curve-hold path is possible; above it, minimum curvature is needed.

Parameters:
  • radius (float) – Design radius of curvature.

  • distances (tuple) – Tuple of (dist_to_target, dist_perp_to_target, dist_norm_to_target) geometric distances.

  • min_error (float) – Error tolerance factor applied to the result.

Returns:

The critical radius. Returns 0 if the normal distance is zero.

Return type:

float

welleng.connector.get_vec_target(pos1: ArrayLike, vec1: ArrayLike, pos_target: ArrayLike, tangent_length: ArrayLike, dist_curve: ArrayLike, func_dogleg: ArrayLike) ndarray[source]

Derives the target unit vector from curve geometry and target position.

Solves for the direction vector at the end of a curve-hold section given the start state, curve parameters, and target position. Accepts either scalar inputs (legacy shape-(3,) positions/vectors with scalar tangent_length/dist_curve/func_dogleg) or batched inputs (leading batch dims on all arrays, positions/vectors with trailing axis 3).

Parameters:
  • pos1 (ndarray, shape (..., 3)) – Start position in NEV coordinates.

  • vec1 (ndarray, shape (..., 3)) – Start unit direction vector in NEV coordinates.

  • pos_target (ndarray, shape (..., 3)) – Target position in NEV coordinates.

  • tangent_length (float or ndarray, shape (...)) – Length of the tangent (hold) section.

  • dist_curve (float or ndarray, shape (...)) – Arc length of the curve section. Where equal to zero, the input vec1 is returned unchanged (pure-hold fallback).

  • func_dogleg (float or ndarray, shape (...)) – Shape factor (ratio factor) for the curve.

Returns:

Target unit direction vector in NEV coordinates.

Return type:

ndarray, shape (…, 3)

welleng.connector.interpolate_curve(md1: float, pos1: ndarray, vec1: ndarray, vec2: ndarray, dist_curve: float, dogleg: float, func_dogleg: float, step: float | None, endpoint: bool = False) dict[source]

Interpolates survey points along a curve section at regular MD intervals.

Uses Rodrigues’ rotation formula for numerical stability, especially for near-180-degree doglegs where SLERP becomes unstable.

Parameters:
  • md1 (float) – Measured depth at the start of the curve.

  • pos1 (ndarray) – Start position in NEV coordinates.

  • vec1 (ndarray) – Start unit direction vector in NEV coordinates.

  • vec2 (ndarray) – End unit direction vector in NEV coordinates.

  • dist_curve (float) – Arc length of the curve section.

  • dogleg (float) – Total dogleg angle in radians.

  • func_dogleg (float) – Shape factor (ratio factor) for the curve.

  • step (float) – Desired delta measured depth between interpolated points.

  • endpoint (bool) – If True, includes the curve endpoint in the output.

Returns:

Dictionary with keys ‘md’, ‘vec’, ‘inc’, ‘azi’, ‘dogleg’ containing numpy arrays of interpolated survey data.

Return type:

dict

welleng.connector.interpolate_hold(md1: float, pos1: ndarray, vec1: ndarray, md2: float, step: float | None, endpoint: bool = False) dict[source]

Interpolates survey points along a hold (tangent) section.

Parameters:
  • md1 (float) – Measured depth at the start of the hold.

  • pos1 (ndarray) – Start position in NEV coordinates.

  • vec1 (ndarray) – Constant unit direction vector during the hold.

  • md2 (float) – Measured depth at the end of the hold.

  • step (float) – Desired delta measured depth between interpolated points.

  • endpoint (bool) – If True, includes the hold endpoint in the output.

Returns:

Dictionary with keys ‘md’, ‘vec’, ‘inc’, ‘azi’, ‘dogleg’ containing numpy arrays of interpolated survey data.

Return type:

dict

welleng.connector.interpolate_well(sections: Connector | list, step: float = 30) list[source]

Constructs interpolated survey data from a list of Connector sections.

Parameters:
  • sections (Connector or list of Connector) – Connector objects defining the well trajectory.

  • step (float) – Desired delta measured depth between interpolated survey points.

Returns:

A list of interpolated survey data dictionaries.

Return type:

list

welleng.connector.min_curve_to_target(distances: tuple) tuple[source]

Computes minimum-curvature parameters when the design DLS is insufficient.

Used when the target cannot be reached with the design radius, so the curve section uses the minimum radius geometrically required.

Parameters:

distances (tuple) – Tuple of (dist_to_target, dist_perp_to_target, dist_norm_to_target) geometric distances.

Returns:

  • tangent_length (float) – Always 0 (pure curve, no hold).

  • radius_critical (float) – Minimum required radius of curvature.

  • dogleg (float) – Curve angle in radians.

welleng.connector.min_dist_to_target(radius: float | ndarray, distances: tuple) tuple[source]

Computes tangent length and dogleg for a curve-hold section to a target.

Parameters:
  • radius (float) – Radius of curvature for the curve section.

  • distances (tuple) – Tuple of (dist_to_target, dist_perp_to_target, dist_norm_to_target) geometric distances.

Returns:

  • tangent_length (float) – Hold section length.

  • dogleg (float) – Curve angle in radians.

welleng.connector.mod_vec(vec: ndarray, error: float = 1e-05) tuple[source]

Slightly perturbs a direction vector to avoid exact antiparallel degeneracy.

Parameters:
  • vec (ndarray) – Unit direction vector in NEV coordinates.

  • error (float) – Perturbation magnitude applied to the vertical component.

Returns:

A tuple of (perturbed_vec, inclination, azimuth).

Return type:

tuple

welleng.connector.shape_factor(dogleg: ArrayLike) Any[source]

Computes the minimum-curvature shape factor for a dogleg angle.

Parameters:

dogleg (float) – Dogleg angle in radians.

Returns:

The ratio factor (shape factor) for minimum-curvature interpolation.

Return type:

float

welleng.connector.solve_curve_hold_batch(pos1: ArrayLike, vec1: ArrayLike, pos_target: ArrayLike, radius: ArrayLike) dict[source]

Vectorised curve-hold connector: fixed start pose, fixed target pos.

Solves the minimum-MD curve-then-hold geometry from a start pose (pos1, vec1) to a target position pos_target with a given design radius. The target tangent vector is an OUTPUT of the solve — computed analytically from the geometry — not an input. Equivalent to Connector(pos1=..., vec1=..., pos2=pos_target, dls_design=...) in the 'curve_hold' mode (binary code 00110 in _get_initial_methods), but operates element-wise on arrays so a large sweep is one numpy call rather than a Python loop over Connector instances.

Parameters:
  • pos1 (array_like, shape (..., 3)) – Start positions in NEV coordinates. Arbitrary leading batch shape.

  • vec1 (array_like, shape (..., 3)) – Unit direction vectors at the start. Must share pos1’s leading shape.

  • pos_target (array_like, shape (..., 3)) – Target positions in NEV coordinates. Must share pos1’s leading shape.

  • radius (float or array_like, shape (...)) – Design radius of curvature. Broadcasts against the leading shape.

Returns:

All entries are ndarrays whose leading shape matches the inputs.

  • 'pos2' shape (…, 3) — end of the curve / start of the hold.

  • 'vec_target' shape (…, 3) — computed unit tangent at target.

  • 'tangent_length' shape (…) — hold-section length.

  • 'dogleg' shape (…) — curve angle, radians.

  • 'dist_curve' shape (…) — arc length of the curve section.

  • 'md' shape (…) — total measured depth (curve + hold).

Return type:

dict

Notes

When the target is exactly along vec1 (pure-hold degenerate case), the solver returns dogleg = 0, tangent_length = dist_to_target, vec_target = vec1, and pos2 = pos1. This matches the scalar Connector behaviour in that regime.

The underlying helpers (min_dist_to_target, get_curve_hold_data, get_vec_target) have all been array-safe since the vectorisation patch; this function is just a thin wrapper that computes the three intermediate distance scalars and composes the helpers.

welleng.error module

ISCWSA error models for computing wellbore positional uncertainty.

The "ISCWSA MWD Rev5" string remains a selectable error model, but is now a deprecated alias for the Rev 5.11 compliant implementation (“ISCWSA MWD Rev5.11”). As of welleng 0.10.0 the Rev5 YAML and weight functions were corrected against the ISCWSA Rev 5.11 example workbooks, so users who previously selected "ISCWSA MWD Rev5" will get slightly different (and correct) covariance output. "ISCWSA MWD Rev4" is unchanged.

class welleng.error.ErrorModel(survey, error_model='ISCWSA MWD Rev5.11')[source]

Bases: object

A class to initiate the field parameters and error magnitudes for subsequent error calculations.

error_model

Name of the error model used (e.g. 'ISCWSA MWD Rev5.11', the current default; 'ISCWSA MWD Rev4' for legacy Rev 4 behaviour).

Type:

str

survey

The input Survey object.

Type:

welleng.survey.Survey

errors

ToolError object containing per-source error magnitudes and covariance data.

Type:

welleng.errors.tool_errors.ToolError

survey_rad

Array of (md, inc_rad, azi_true_rad) per station, shape (n, 3).

Type:

numpy.ndarray

drdp

Jacobian of position with respect to survey parameters (depth, inclination, azimuth) in NEV coordinates.

Type:

numpy.ndarray

cov_NEVs

Summed covariance matrices in NEV coordinates per station, shape (n, 3, 3). Accessible via errors.cov_NEVs.

Type:

numpy.ndarray

class Error(code, propagation, e_DIA, cov_DIA, e_NEV, e_NEV_star, sigma_e_NEV, cov_NEV)[source]

Bases: object

Standard components of a well bore survey error.

__init__(code, propagation, e_DIA, cov_DIA, e_NEV, e_NEV_star, sigma_e_NEV, cov_NEV)[source]

Initialize an Error with computed error vectors and covariances.

Parameters:
  • code (str) – The error source code identifier.

  • propagation (str) – Propagation type (‘systematic’, ‘random’, ‘global’, or ‘within_pad’).

  • e_DIA (numpy.ndarray) – Error vectors in Depth-Inclination-Azimuth coordinates.

  • cov_DIA (numpy.ndarray) – Covariance matrices in DIA coordinates.

  • e_NEV (numpy.ndarray) – Error vectors in North-East-Vertical coordinates.

  • e_NEV_star (numpy.ndarray) – Single-station NEV error vectors.

  • sigma_e_NEV (numpy.ndarray) – Cumulative NEV error vectors.

  • cov_NEV (numpy.ndarray) – Covariance matrices in NEV coordinates.

__init__(survey, error_model='ISCWSA MWD Rev5.11')[source]

Initialize the error model for a given survey.

Parameters:
  • survey (welleng.survey.Survey) – The survey to compute errors for.

  • error_model (str or dict, optional) – Name of the error model to apply. Defaults to the Rev 5.11 compliant "ISCWSA MWD Rev5.11". The legacy name "ISCWSA MWD Rev5" is accepted as a deprecated alias. Alternatively a prebuilt ISCWSA-JSON-shaped model dict — e.g. a COMPASS IPM imported from an EDM export by welleng.errors.edm_ipm — evaluated by the formula interpreter without any file resolution.

cov_nev_at(md)[source]

Arc-faithful ISCWSA covariance at an interior measured depth md.

Evaluates the (3, 3) NEV covariance directly ON the minimum-curvature arc at md, by propagating each error source’s stored station values through the partial-leg interpolation Jacobian – NOT by interpolating the assembled covariance matrix linearly between stations (which under-reports the separation factor by up to ~25% near doglegs) and NOT by inserting md as a real survey station (which would add a spurious extra measurement and perturb the propagation). The interpolated point is not a new measurement: it inherits the bounding stations’ sources.

For an interior point q at arc-fraction f on leg [i, i+1], station i and station i+1 both drive the partial leg (via the min-curve slerp), so the interior propagates BOTH: the own weight drk(i->q) (far station) AND station i’s out-leg coupling drkplus1(i->q) (near station). This is exact at BOTH ends – the own-only form (drk alone) is exact at f->1 but drops the coupling and biases f->0 by ~1 leg. With qi = drk(i->q).e_DIA[i], qj = drk(i->q).e_DIA[i+1], coup = drkplus1(i->q).e_DIA[i]:

  • systematic/global/well/within_pad (correlated vector sum): sigma(q) = (1-f) qi + f qj + sigma_e_NEV[i] + coup, contributing outer(sigma(q)).

  • random (two INDEPENDENT measurements -> two outer products): cov_NEV[i] - outer(e_NEV_star[i]) + outer(g_i) + outer(g_j) with g_i = e_NEV_star[i] + coup + (1-f) qi and g_j = f qj – the partial q-own term splits (1-f)/f across the two stations (slerp- Jacobian ~ f; exact at both ends, ~slerp tolerance interior – assay’s symbolic Propagator is the exact oracle).

XCLA/XCLH (the course-length recurrence terms, typically dominant on deviated wells) use the partial-course-length convention (cov_NEV[i] + outer(e_NEV(i->q)), _xcl_partial_enev()) – a STATED convention (not MC-validated: course length has no independent MC ground truth at a fractional point), station-exact at f=0,1, one-oracle with the symbolic reference. Any remaining ring-fenced term (_interior_prep() class "linear") uses linear covariance interpolation. Reproduces the stored cov_NEV[i+1] at f -> 1 to machine precision. See derivation (welleng development notes, not shipped).

INTERIOR ACCURACY IS GEOMETRY-DEPENDENT — do not read “exact at both ends” as “accurate throughout”. Both this boundary-anchored form and the continuous-transport form are FIRST-ORDER interior approximations of a nonlinear propagation; the interpolated-position Monte Carlo is the oracle, and the two forms diverge from it most where the 1/sin(inc) azimuth weights are ill-conditioned.

Both columns below are from ONE run against the SAME MC realisation (welleng 0.26.0 and its symbolic reference): 30 m survey 0-3000 m building vertical to 60 deg over 300-1800 m, interior point f = 0.5, interpolated-position MC at N = 300,000 seed 7, dp_basis balanced tangent, smooth measurement terms only (XCLA/XCLH excluded — an NEV-direct e_DIA has no clean measurement-space MC):

inc 18 deg (low-inc build) core 19.03% continuous 2.87% inc 36 deg core 2.73% continuous 0.56% inc 60 deg core 0.38% continuous 0.21%

Read it as: the two forms STRADDLE the MC, the continuous one closer, and the gap is largest at low inclination (~6-7x at 18 deg, seed-stable) and CLOSES toward the MC noise floor (~0.4% at this N) by 40-60 deg. Not a flat ratio — the 36 and 60 deg rows are both at the noise floor, so the ratio there is noise-limited rather than a real 5x.

So this form OVER-states relative to MC, materially below ~30 deg. Station values (f = 0 and f = 1) are unaffected — those are exact. Aligning the boundary form to the continuous transport is tracked for 0.27; until then, treat sub-30 deg inclination interiors as indicative and take a station value, or a continuous-transport form, where the number is load-bearing.

(An earlier version of this table shipped in 0.26.0rc9 with a stale continuous column — 9.1 / 1.6 / 0.16 — measured before assay’s dref fix removed a constant VV double-count. It overstated their error, so the published “~2x closer” UNDER-sold the gap. Withdrawn in rc14, replaced here with the provenance above.)

Parameters:

md (float) – Measured depth of the interior point (survey depth units).

Returns:

The (3, 3) NEV covariance at md.

Return type:

numpy.ndarray

drk_dAz(survey)[source]

Derivative of position with respect to azimuth at each station.

Parameters:

survey (array_like) – Survey stations as (md, inc_rad, azi_rad) rows.

Returns:

Shape (n, 3) array of NEV derivatives.

Return type:

numpy.ndarray

drk_dDepth(survey)[source]

Derivative of position with respect to measured depth at each station.

Equal to 0.5 * (unit_vec[i] + unit_vec[i+1]) in NEV coordinates – the direction-cosine part of minimum curvature without the RF or delta_md. When the survey starts at the zero datum (md[0] == 0) station 0 has no segment above it and takes the full station-0 wellbore tangent instead of the half-segment average: a depth error at the first station shifts the along-hole position by the full tangent (ISCWSA random depth carries its full variance at the surface, DRFR cov_VV(0) = mag^2). A survey that starts below the datum (md[0] != 0) is tied on, so its station 0 stays zero (see _drdp). This row-0 value is consumed only by _e_NEV_star (drkplus1_dDepth slices [1:]).

Parameters:

survey (array_like) – Survey stations as (md, inc_rad, azi_rad) rows.

Returns:

Shape (n, 3) array of NEV derivatives.

Return type:

numpy.ndarray

drk_dInc(survey)[source]

Derivative of position with respect to inclination at each station.

Parameters:

survey (array_like) – Survey stations as (md, inc_rad, azi_rad) rows.

Returns:

Shape (n, 3) array of NEV derivatives.

Return type:

numpy.ndarray

Note

The N/E columns are azimuth-dependent even at inc == 0 (½·Δmd·cos(inc) ·{cos,sin}(azi)). Survey canonicalises azimuth to 0 at vertical stations (see Survey._make_angles); a consumer feeding this model WITHOUT that preprocessing must apply azi = where(inc == 0, 0, azi) first, or the covariance diverges at vertical stations.

drkplus1_dAz(survey)[source]

Derivative of next-station position with respect to azimuth.

Parameters:

survey (array_like) – Survey stations as (md, inc_rad, azi_rad) rows.

Returns:

Shape (n, 3) array of NEV derivatives.

Return type:

numpy.ndarray

drkplus1_dDepth(survey)[source]

Derivative of next-station position with respect to measured depth.

Parameters:

survey (array_like) – Survey stations as (md, inc_rad, azi_rad) rows.

Returns:

Shape (n, 3) array of NEV derivatives.

Return type:

numpy.ndarray

drkplus1_dInc(survey)[source]

Derivative of next-station position with respect to inclination.

Parameters:

survey (array_like) – Survey stations as (md, inc_rad, azi_rad) rows.

Returns:

Shape (n, 3) array of NEV derivatives.

Return type:

numpy.ndarray

welleng.error.get_error_models(tool_index=None)[source]

Return a list of available error model short names.

Parameters:

tool_index (dict, optional) – Pre-loaded tool index dict. If None, loads from disk.

Returns:

Short names of all registered error models.

Return type:

list of str

welleng.error.get_errors(error)[source]

Extract the six unique covariance components from a 3x3 NEV matrix.

Parameters:

error (numpy.ndarray) – A 3x3 covariance matrix in NEV coordinates.

Returns:

[nn, ee, vv, ne, nv, ev] covariance components.

Return type:

list

welleng.error.get_tool_index()[source]

Load the tool error model index from the bundled YAML file.

Returns:

Mapping of tool model names to their configuration parameters.

Return type:

dict

welleng.error.make_diagnostic_data(survey)[source]

Build a per-station diagnostic breakdown of all error model components.

Parameters:

survey (welleng.survey.Survey) – A welleng Survey with an attached ErrorModel (survey.err).

Returns:

Nested dict keyed by MD, then error code, containing the six unique covariance components and a TOTAL row summing all codes.

Return type:

dict

welleng.fluid module

class welleng.fluid.DensityDiesel[source]

Bases: object

__init__()[source]

An interpolation wrapper of the pressure, temperature and density diesel data provided in the SPE 11118 paper.

get_density(pressure, temperature)[source]

Interpolate diesel density for given pressure and temperature using the lookup data provided in SPE 11118 paper.

class welleng.fluid.Fluid(fluid_density, reference_temp=32.0, reference_pressure=0.0, base_fluid_water_ratio=0.2, weighting_material='Barite')[source]

Bases: object

__init__(fluid_density, reference_temp=32.0, reference_pressure=0.0, base_fluid_water_ratio=0.2, weighting_material='Barite')[source]

Density profile calculated from SPE 11118 Mathematical Field Model Predicts Downhold Density Changes in Static Drilling Fluids by Roland R. Sorelle et al.

Warning

Do not use this to obtain a fluid COMPRESSIBILITY. The published coefficients here are a faithful transcription – the water correlation rho_w = 8.63186 - 3.31977e-3*T + 2.3717e-5*P matches Sorelle et al. (1982) as reproduced in Mitchell, Fundamentals of Drilling Engineering, Eq. 3.5 – and they imply c_water ~ 2.9e-6/psi and c_oil ~ 3.9e-6/psi, so ~3.5e-6/psi for a 12 ppg mud at these volume fractions.

But get_density_profile() responds to pressure_applied at only 1.2e-8/psi at 2000 ft and 5.0e-8/psi at 8000 ft – two orders of magnitude low, and depth-dependent, which a compressibility should not be. pressure_applied enters only alpha_1/beta_1, which appear both as a prefactor and inside log((alpha_1 + alpha_2*depth) / alpha_1), and the two largely cancel.

The defect is in the depth-averaging, not the data. Measured 2026-07-27; unfixed because this module is slated for replacement by the welleng-drilling fluid model.

This paper was written in oilfield units, so we’ll convert inputs to ppg, ft, F and psi.

Parameters:
  • fluid_density (float) – The combined fluid density in ppg at reference conditions.

  • reference_temp (float (default 32.0)) – The reference temperature in Fahrenheit

  • reference_pressure (float (default 0.0)) – The reference pressure in psig.

  • weighting_material (str) – The material being used to weight the drilling fluid (see the WEIGHTING_MATERIAL_DENSITY dictionary).

get_density_profile(depth, temperature, pressure_applied=0.0, density_bounds=(6.0, 25.0))[source]

Function that returns a density profile of the fluid, adjusted for temperature and compressibility and assuming that the fluid’s reference parameters are the surface parameters.

Parameters:
  • depth (float or list or (n) array of floats) – The vertical depth of interest relative to surface in feet.

  • temperature (float or list or (n) array of floats) – The temperature corresponding to the vertical depth of interest in Fahrenheit.

  • pressure_applied (float (default=0.)) – Additional pressure applied to the fluid in psi.

  • density_bounds – Density bounds to constrain the optimization algorithm in ppg.

welleng.fluid.main()[source]

welleng.geomag module

Client for the BGS geomagnetic model web service.

Replaces the unmaintained magnetic_field_calculator package (which spoke plain http, had no timeout and is no longer developed) with a small stdlib client for the same service. The geomagnetic models themselves live server side at the British Geological Survey, so this client stays current as BGS revises them.

Service documentation: https://geomag.bgs.ac.uk/web_service/GMModels/help/parameters

The service URL structure is GMModels/<model>/<revision>?<parameters> with altitude in km above the WGS84 spheroid and dates as yyyy-mm-dd. This module speaks welleng conventions at its boundary — altitude in METRES (negative below datum) — and converts at the wire.

exception welleng.geomag.GeomagLookupError[source]

Bases: ValueError

The BGS lookup failed (bad request, service down, no network).

welleng.geomag.KNOWN_MODELS = ('wmm', 'igrf')

‘wmm’ (current-epoch default) and ‘igrf’ (historic coverage — the fallback for pre-window survey dates).

Type:

models useful to welleng

welleng.geomag.lookup_field(latitude, longitude, altitude=0.0, date=None, model='wmm', revision='current', timeout=10.0)[source]

Look up the geomagnetic field from the BGS web service.

Parameters:
  • latitude (float) – Geodetic coordinates in decimal degrees.

  • longitude (float) – Geodetic coordinates in decimal degrees.

  • altitude (float) – Height in METRES above the WGS84 spheroid (negative below — e.g. a subsea wellhead). Converted to the service’s km at the wire.

  • date (str, optional) – yyyy-mm-dd. Omitted -> the service’s default epoch.

  • model (str) – 'wmm' (default) or 'igrf' (historic coverage).

  • revision (str) – Model revision; 'current' tracks the latest server-side.

  • timeout (float) – Socket timeout in seconds.

Returns:

The service’s geomagnetic-field-model-result payload, e.g. result['field-value']['total-intensity']['value'] (nT), ['inclination'] (dip, deg down-positive) and ['declination'] (deg east-positive).

Return type:

dict

Raises:

GeomagLookupError – On any HTTP error (with the server’s message, e.g. a date outside the model’s validity window), connection failure or bad payload.

welleng.hierarchy module

Well hierarchy + wellbore network graph — the container surveys hang on.

This models the master-data hierarchy that a survey (and its error/clearance results) attaches to, and the wellbore network graph used to propagate relative position uncertainty correctly between wellbores that share ancestry.

OSDU schema source (the canonical Well-Known-Schemas this maps to): https://community.opengroup.org/osdu/data/data-definitions — WKS JSON under Generated/master-data/ (e.g. Wellbore.1.x.0.json, Well, Field, Organisation) and entity-relationship docs under E-R/master-data/.

Hierarchy (maps to OSDU master-data — see that doc for the field-level mapping, grounded separately):

Organisation -> Field -> Site(WellSiteStructure) -> Well(slot + Datum)

-> Wellbore* -> Survey

The wellbore graph is a forest: every wellbore section has a parent — a parent wellbore (a sidetrack/lateral kicked off at kickoff_md) or, for a root wellbore, the Well (its surface location). Wells on one Site share the site’s geodetic CRS + convergence (common systematic -> cancels in relative use); each Well carries its own slot position (+ slot_radial_error) and local datum (RKB). EDM cross-check confirms the spine CD_PROJECT(CRS) -> CD_SITE -> CD_WELL(slot,datum) -> CD_WELLBORE(parent_wellbore_id) -> survey.

Why a graph (the load-bearing reason). For relative error between two wellbores (e.g. two laterals off one parent, or two sidetracks) you must NOT sum their full independent uncertainties — the survey they share up to the divergence point carries the SAME systematic errors, which cancel in the relative sense. Walking to the lowest common ancestor (LCA) splits the two paths into a shared trunk (systematic-common) and two divergent branches (independent); only the divergent parts sum. This is the correct multilateral / sidetrack relative-uncertainty treatment and the basis for anti-collision between wells of common ancestry.

class welleng.hierarchy.Datum(name: str, elevation: float = 0.0, reference: str = 'MSL', realisations: list[~welleng.hierarchy.DatumRealisation] = <factory>)[source]

Bases: object

A local spatial / depth datum (e.g. a platform RKB / wellhead reference).

Represents the vertical reference a well’s measured depths are quoted against (rotary kelly bushing, rotary table, wellhead, or mean sea level). A datum is attached per-Well but is typically physically shared by every wellbore drilled from one platform.

Parameters:
  • name (str) – Human-readable datum name / identifier.

  • elevation (float, default 0.0) – Datum elevation, in metres, above mean sea level (or the field reference given by reference).

  • reference (str, default "MSL") – The elevation reference frame — one of "MSL", "RKB", "RT", "wellhead", etc.

  • realisations (list of DatumRealisation, optional) – The datum’s position-survey history, oldest first — an APPEND-ONLY document chain (see DatumRealisation). Manage it through add_realisation(); never overwrite an entry.

Notes

Because the datum is shared by every wellbore on a platform, the datum’s own position error is a common systematic term between those wellbores and therefore cancels in relative (wellbore-to-wellbore) uncertainty use. This is what makes a datum re-survey benign for on-platform work: all wells on the platform move together under a datum shift, so their relative positions and relative covariance are invariant. Only absolute positioning — and relative work against wells NOT on this datum — changes.

A wellbore records which realisation its positions were computed under via Wellbore.datum_realisation; shift_between() composes the chain’s shifts so positions quoted under an old realisation can be re-referenced, and mixed-realisation comparisons become detectable instead of silent.

__init__(name: str, elevation: float = 0.0, reference: str = 'MSL', realisations: list[~welleng.hierarchy.DatumRealisation] = <factory>) None
add_realisation(realisation: DatumRealisation) DatumRealisation[source]

Append a new position realisation to the chain (append-only).

The first realisation may carry supersedes=None; every subsequent one must supersede the current head, so the chain stays a single unbroken document trail.

Raises:

ValueError – If realisation.supersedes does not reference the current head, or its id already exists in the chain.

property current_realisation: DatumRealisation | None

The head (most recent) realisation, or None if no chain.

elevation: float = 0.0
name: str
provenance() list[tuple[str, str | None, str | None]][source]

The document chain as [(id, date, document), ...], oldest first — the audit trail of every position survey of this datum.

realisation(id_: str) DatumRealisation[source]

Return the realisation with id_.

Raises:

KeyError – If the id is not in the chain.

realisations: list[DatumRealisation]
reference: str = 'MSL'
shift_between(from_id: str, to_id: str) tuple[float, float, float][source]

Composed position shift (dN, dE, dV) from one realisation’s frame to another’s, in metres.

Walks the chain: consecutive shifts add; walking backwards negates. Add the result to a position quoted under from_id to express it under to_id.

class welleng.hierarchy.DatumRealisation(id: str, date: str | None = None, document: str | None = None, shift: tuple[float, float, float] = (0.0, 0.0, 0.0), radial_error: float = 0.0, supersedes: str | None = None)[source]

Bases: object

One survey-in of a datum’s position — a link in the datum’s document chain.

A platform / wellhead location is not a fact, it is a measurement, and it gets re-measured: an installed platform is often re-surveyed years later with better technology, moving the reference every well on it hangs off. The industry habit of silently overwriting the wellhead coordinates destroys the audit trail and mixes wells computed under different origin realisations. A DatumRealisation is one such measurement, kept forever: what was measured, when, by which source document, how far it moved the datum, and how well it is now known.

Parameters:
  • id (str) – Unique realisation identifier (e.g. "D1-v2").

  • date (str or None, default None) – Survey date, YYYY-MM-DD.

  • document (str or None, default None) – Reference to the source document (survey report number / URI) — the provenance link of the chain.

  • shift (tuple of float, default (0, 0, 0)) – Position shift (dN, dE, dV) in metres FROM the superseded realisation to this one (the original realisation carries zeros).

  • radial_error (float, default 0.0) – 1-sigma horizontal position uncertainty of THIS realisation, metres. A re-survey usually reduces this — the new realisation’s value replaces (not adds to) the old one for absolute-positioning use.

  • supersedes (str or None, default None) – The id of the realisation this one supersedes; None for the original. Enforced append-only by Datum.add_realisation().

__init__(id: str, date: str | None = None, document: str | None = None, shift: tuple[float, float, float] = (0.0, 0.0, 0.0), radial_error: float = 0.0, supersedes: str | None = None) None
date: str | None = None
document: str | None = None
id: str
radial_error: float = 0.0
shift: tuple[float, float, float] = (0.0, 0.0, 0.0)
supersedes: str | None = None
class welleng.hierarchy.Field(id: str, name: str = '', parent: _Node | None = None)[source]

Bases: _Node

A field / asset grouping the sites of a development.

Maps to OSDU master-data--Field. Inherits the _Node fields; parent points to the owning Organisation.

__init__(id: str, name: str = '', parent: _Node | None = None) None
class welleng.hierarchy.Organisation(id: str, name: str = '', parent: _Node | None = None)[source]

Bases: _Node

Operating organisation — the top of the master-data hierarchy.

Maps to OSDU master-data--Organisation. Inherits the _Node fields (id, name, parent); an Organisation normally has no parent.

__init__(id: str, name: str = '', parent: _Node | None = None) None
class welleng.hierarchy.Site(id: str, name: str = '', parent: _Node | None = None, crs: str | None = None, convergence: float | None = None, is_field_centre: bool = False, location: tuple[float, float] | None = None)[source]

Bases: _Node

A site / surface structure grouping wells — a platform, pad, or subsea template.

Maps to OSDU master-data--WellSiteStructure (EDM CD_SITE). Carries the shared spatial reference for the wells drilled from it, so that the common geodetic terms can be identified and cancelled in relative use.

Parameters:
  • id (str) – Unique node identifier (inherited from _Node).

  • name (str, default "") – Human-readable site name (inherited from _Node).

  • parent (_Node or None, default None) – The owning Field (inherited from _Node).

  • crs (str or None, default None) – Geodetic coordinate reference system (zone / datum, e.g. "UTM-31N" / "ED50") shared by every well on the site (EDM CD_PROJECT).

  • convergence (float or None, default None) – Grid-vs-true-north convergence, in radians, for the site origin.

  • is_field_centre (bool, default False) – True if this site is the field-centre coordinate origin.

  • location (tuple of float or None, default None) – Site map location as (northing, easting), in metres.

Notes

Wells on one site share crs and convergence — these are common systematic terms that cancel in the relative (wellbore-to-wellbore) uncertainty sense. Only the per-well slot offsets differ (see Well).

__init__(id: str, name: str = '', parent: _Node | None = None, crs: str | None = None, convergence: float | None = None, is_field_centre: bool = False, location: tuple[float, float] | None = None) None
convergence: float | None = None
crs: str | None = None
is_field_centre: bool = False
location: tuple[float, float] | None = None
class welleng.hierarchy.Well(id: str, name: str = '', parent: _Node | None = None, slot: tuple[float, float] | None = None, slot_radial_error: float = 0.0, wellhead_depth: float | None = None, datum: Datum | None = None)[source]

Bases: _Node

A well — the surface location / wellhead the root wellbore(s) hang off.

Maps to OSDU master-data--Well (EDM CD_WELL). Holds the slot position and its uncertainty (the surface-location error source in relative uncertainty) together with the well’s local depth datum.

Parameters:
  • id (str) – Unique node identifier (inherited from _Node).

  • name (str, default "") – Human-readable well name (inherited from _Node).

  • parent (_Node or None, default None) – The owning Site (inherited from _Node).

  • slot (tuple of float or None, default None) – Slot offset (ns, ew) from the site origin, in metres.

  • slot_radial_error (float, default 0.0) – Radial (1-sigma) slot-position uncertainty, in metres.

  • wellhead_depth (float or None, default None) – Wellhead depth, in metres.

  • datum (Datum or None, default None) – The per-well RKB / rotary depth datum (EDM CD_DATUM is per-well, not per-site).

Notes

Two wells on one site share the site CRS, so in relative uncertainty only their slot offsets and slot_radial_error differ — the shared CRS / convergence cancels (see Site).

__init__(id: str, name: str = '', parent: _Node | None = None, slot: tuple[float, float] | None = None, slot_radial_error: float = 0.0, wellhead_depth: float | None = None, datum: Datum | None = None) None
datum: Datum | None = None
slot: tuple[float, float] | None = None
slot_radial_error: float = 0.0
wellhead_depth: float | None = None
class welleng.hierarchy.WellNetwork[source]

Bases: object

A forest of wellbore sections rooted at wells / platforms.

A lightweight forest (single parent per node) whose nodes are the master-data entities (Wellbores and their Well / Site / Field / Organisation ancestors) and whose parent -> child links encode “child kicks off / hangs off parent”. Provides the ancestry queries (roots, leaves, lowest common ancestor) and the shared / divergent split that relative-error propagation needs, plus native JSON persistence.

Notes

The graph is a forest: every wellbore has a parent — a parent wellbore (sidetrack / lateral) or, for a root wellbore, its Well. The load-bearing reason for a graph is relative uncertainty: two wellbores of common ancestry share the same survey up to their divergence point, so those systematic errors are common and cancel in the relative sense. Walking to the lowest common ancestor splits the two paths into a shared trunk (systematic-common) and two independent divergent branches; only the divergent parts sum. See relative_covariance().

Examples

Build a small Site -> Well -> Wellbore tree with two laterals off one top hole, then query its topology.

>>> from welleng.hierarchy import Site, Well, Wellbore, WellNetwork
>>> net = WellNetwork()
>>> site = Site(id='S1', name='PadA')
>>> well = Well(id='W1', name='W1', parent=site)
>>> top = Wellbore(id='WB1', name='TopHole', parent=well)
>>> lat1 = Wellbore(id='WB2', name='Lat1', parent=top, kickoff_md=1000.0)
>>> lat2 = Wellbore(id='WB3', name='Lat2', parent=top, kickoff_md=1200.0)
>>> for n in (site, well, top, lat1, lat2):
...     _ = net.add(n)
>>> [w.id for w in net.roots()]
['WB1']
>>> sorted(w.id for w in net.leaves())
['WB2', 'WB3']
>>> net.lowest_common_ancestor('WB2', 'WB3')
'WB1'
>>> net.shared_and_divergent('WB2', 'WB3')
(['WB1', 'W1'], ['WB2'], ['WB3'])
__init__() None[source]
add(node: _Node) _Node[source]

Add a node and wire its parent edge into the graph.

Registers node in the network and, when its parent is a Well (root wellbore) or a Wellbore (sidetrack), adds the directed parent -> node edge. The full container ancestor chain (Well -> Site -> Field -> Organisation) is also registered so the whole hierarchy serialises and is queryable.

Parameters:

node (_Node) – The entity to add — typically a Wellbore or Well (container ancestors are pulled in automatically via parent).

Returns:

The same node that was passed in (for chaining).

Return type:

_Node

Examples

>>> from welleng.hierarchy import Well, Wellbore, WellNetwork
>>> net = WellNetwork()
>>> well = Well(id='W1', name='W1')
>>> _ = net.add(Wellbore(id='WB1', name='TopHole', parent=well))
>>> [w.id for w in net.roots()]
['WB1']
ancestors(id_: str) list[str][source]

Return the ancestry chain of a node, nearest-first.

Walks parent edges from id_ up to (and including) its root, so the returned list starts with id_ itself and ends at the top container entity on that path.

Parameters:

id (str) – The node identifier to start from.

Returns:

Node ids from id_ up to and including the root, nearest-first.

Return type:

list of str

classmethod from_dict(data: dict) WellNetwork[source]

Reconstruct a WellNetwork from to_dict() output.

Rebuilds every entity, wires parents by id (recursively, so a child is built after its parent), and re-creates each wellbore’s Survey from the serialised md / inc / azi + header.

Parameters:

data (dict) – A dict in the shape produced by to_dict().

Returns:

The reconstructed network.

Return type:

WellNetwork

leaves() list[Wellbore][source]

Return the leaf wellbores of the forest.

A leaf wellbore has no children — a TD section or the deepest lateral on its branch.

Returns:

The childless wellbores.

Return type:

list of Wellbore

Examples

>>> from welleng.hierarchy import Well, Wellbore, WellNetwork
>>> net = WellNetwork()
>>> well = Well(id='W1', name='W1')
>>> top = Wellbore(id='WB1', name='TopHole', parent=well)
>>> lat = Wellbore(id='WB2', name='Lat1', parent=top, kickoff_md=1000.0)
>>> for n in (well, top, lat):
...     _ = net.add(n)
>>> [w.id for w in net.leaves()]
['WB2']
classmethod load_json(path: str) WellNetwork[source]

Load a model from a JSON file.

Parameters:

path (str) – Filesystem path to a JSON file written by save_json().

Returns:

The reconstructed network. See save_json() for a round-trip example.

Return type:

WellNetwork

lowest_common_ancestor(a: str, b: str) str | None[source]

Return the lowest common ancestor (LCA) of two nodes.

The LCA is the deepest node that is an ancestor of both a and b — their divergence point in the forest.

Parameters:
  • a (str) – First node identifier.

  • b (str) – Second node identifier.

Returns:

The id of the deepest shared ancestor, or None if the two nodes share no ancestry (e.g. different platforms).

Return type:

str or None

Examples

>>> from welleng.hierarchy import Well, Wellbore, WellNetwork
>>> net = WellNetwork()
>>> well = Well(id='W1', name='W1')
>>> top = Wellbore(id='WB1', name='TopHole', parent=well)
>>> lat1 = Wellbore(id='WB2', name='Lat1', parent=top, kickoff_md=1000.0)
>>> lat2 = Wellbore(id='WB3', name='Lat2', parent=top, kickoff_md=1200.0)
>>> for n in (well, top, lat1, lat2):
...     _ = net.add(n)
>>> net.lowest_common_ancestor('WB2', 'WB3')
'WB1'
node(id_: str) _Node[source]

Return the registered entity for a node id.

Parameters:

id (str) – The node identifier.

Returns:

The stored entity.

Return type:

_Node

Raises:

KeyError – If no node with id_ has been added.

relative_covariance(a: str, b: str, md_a: float | None = None, md_b: float | None = None, error_model: str = 'ISCWSA MWD Rev5.11', share_mode: str | None = None)[source]

Relative position covariance between two wellbores of common ancestry.

Computes the NEV covariance of the difference in position between the compared points of wellbores a and b (their ends, or their point of closest approach — the anti-collision use), correctly cancelling the systematic errors carried by the survey they share up to their divergence point.

Parameters:
  • a (str) – First wellbore node identifier.

  • b (str) – Second wellbore node identifier.

Returns:

The 3x3 relative-position covariance matrix in the NEV frame, in metres squared.

Return type:

numpy.ndarray

Raises:

ValueError – If either wellbore has no survey attached (raised by the underlying _abs_cov() helper).

Notes

The result is NOT the naive independent sum Cov(a) + Cov(b). There are two correlation dimensions:

  1. Trajectory ancestry (the graph). The shared trunk is the same physical survey inherited by both wellbores, so ALL of its error terms (systematic and the once-realised random) are common and CANCEL in the difference — net shared-trunk contribution is ~0. Only the divergent branches have independent trajectories.

  2. Error-source context (the keys). Even the divergent branches are not fully independent: where they share a source (tool_id = same tool run, survey_date + location = same geomagnetic declination / dip / B, geomag_model / IFR, the Well datum / Site convergence) the systematic part of that term correlates between them, so:

    Cov_rel ~ Cov(branch_a) + Cov(branch_b)
              - 2 * Cov_shared_source(branch_a, branch_b)
    

    with the shared trunk dropped. Drop the cross term (treat as independent) only when the branches share no tool / geomag / datum context. This is the multilateral / sidetrack relative-uncertainty rule.

This implements the ISCWSA Side-track Clearance RP (2022) §3.2.2 method (b) — “subtract the covariance at the Side-track point” — which is Williamson (2000, SPE-67616-PA) Eq. A-24 specialised to a fully-correlated shared trunk: with C_A = C_st + C_branchA and C_B = C_st + C_branchB:

C_rel = C_A(a) + C_B(b) - 2 * C_st   ( = C_branchA + C_branchB )

where C_st is the absolute covariance at the deepest common (side-track) point. Each wellbore’s absolute covariance comes from welleng’s ErrorModel (survey.cov_nev); the surveys are assumed full-from-surface (as EDM definitive surveys are), so they agree over the shared trunk.

Implemented is the trunk cancellation (the dominant effect). Deferred: the source-context cross-term - 2 * Cov_shared_source between the divergent branches (same tool run / geomag date / datum — the RP’s same-job / different-job correlation table, McGregor partial correlation). Without it two same-job laterals are treated as independent below the kickoff, which is slightly conservative (over-states relative uncertainty) — the safe direction. Add it with the per-term systematic / random classification from the error engine.

roots() list[Wellbore][source]

Return the root wellbores of the forest.

A root wellbore is one whose parent is a Well rather than another wellbore — i.e. the top hole of a drilled tree, with no parent wellbore.

Returns:

The root wellbores, in insertion order.

Return type:

list of Wellbore

Examples

>>> from welleng.hierarchy import Well, Wellbore, WellNetwork
>>> net = WellNetwork()
>>> well = Well(id='W1', name='W1')
>>> _ = net.add(Wellbore(id='WB1', name='TopHole', parent=well))
>>> [w.id for w in net.roots()]
['WB1']
save_json(path: str) None[source]

Save the model to a JSON file.

Parameters:

path (str) – Filesystem path to write the JSON to (overwritten if it exists).

Return type:

None

Examples

>>> import os, tempfile
>>> from welleng.hierarchy import Well, Wellbore, WellNetwork
>>> net = WellNetwork()
>>> well = Well(id='W1', name='W1')
>>> _ = net.add(Wellbore(id='WB1', name='TopHole', parent=well))
>>> path = os.path.join(tempfile.mkdtemp(), 'net.json')
>>> net.save_json(path)
>>> reloaded = WellNetwork.load_json(path)
>>> [w.id for w in reloaded.roots()]
['WB1']
shared_and_divergent(a: str, b: str) tuple[list[str], list[str], list[str]][source]

Split the two ancestry paths at their lowest common ancestor.

Partitions the ancestry of a and b into the shared trunk (the LCA and everything above it) and the two divergent branches below the LCA — the partition relative-error propagation is built on.

Parameters:
  • a (str) – First node identifier.

  • b (str) – Second node identifier.

Returns:

(shared, branch_a, branch_b) where shared is the common trunk (LCA and above), branch_a is the divergent portion of a’s ancestry below the LCA (nearest-first), and branch_b the same for b. If the two nodes share no ancestry, shared is empty and each branch is the node’s full ancestry.

Return type:

tuple of (list of str, list of str, list of str)

Notes

Systematic errors on the shared trunk are COMMON to both wellbores and cancel in the relative sense; only the independent divergent branches sum. See relative_covariance().

Examples

>>> from welleng.hierarchy import Well, Wellbore, WellNetwork
>>> net = WellNetwork()
>>> well = Well(id='W1', name='W1')
>>> top = Wellbore(id='WB1', name='TopHole', parent=well)
>>> lat1 = Wellbore(id='WB2', name='Lat1', parent=top, kickoff_md=1000.0)
>>> lat2 = Wellbore(id='WB3', name='Lat2', parent=top, kickoff_md=1200.0)
>>> for n in (well, top, lat1, lat2):
...     _ = net.add(n)
>>> net.shared_and_divergent('WB2', 'WB3')
(['WB1', 'W1'], ['WB2'], ['WB3'])
to_dict() dict[source]

Serialise the whole model to a JSON-safe dict.

Emits the entity graph (each node’s type + parent id) together with each wellbore’s survey (raw md / inc / azi in radians + header + context keys). Round-trips via from_dict().

Returns:

A plain, JSON-serialisable dict with keys "welleng_hierarchy_version" and "nodes".

Return type:

dict

Notes

Azimuth is serialised in the header’s OWN north reference (grid / true / magnetic) and that reference is recorded alongside it, so the round-trip is frame-exact and never silently reinterprets one reference as another (grid and true differ by convergence; true and magnetic by declination, which is date + location dependent).

This is the light welleng-native persistence form; welleng.osdu is the OSDU interchange form.

class welleng.hierarchy.Wellbore(id: str, name: str = '', parent: _Node | None = None, kickoff_md: float | None = None, survey: object = None, survey_date: str | None = None, tool_id: str | None = None, geomag_model: str | None = None, datum_realisation: str | None = None)[source]

Bases: _Node

A wellbore section — a drilled hole.

Maps to OSDU master-data--Wellbore (EDM CD_WELLBORE). A wellbore is the unit the graph is built from: it is either a root wellbore (top hole, parented by a Well) or a sidetrack / lateral kicked off another Wellbore.

Parameters:
  • id (str) – Unique node identifier (inherited from _Node).

  • name (str, default "") – Human-readable wellbore name (inherited from _Node).

  • parent (_Node or None, default None) – Either the Well (root wellbore) or the Wellbore this section kicked off from (sidetrack / lateral). The OSDU edge is Wellbore.KickOffWellbore / the EDM parent_wellbore_id.

  • kickoff_md (float or None, default None) – The tie / divergence measured depth on the parent, in metres — the point at which this section departs its parent. See Notes.

  • survey (object, default None) – The section’s survey (a welleng.survey.Survey / future MinCurve result; OSDU work-product-component--WellboreTrajectory), covering [tie_on_md, td_md].

  • survey_date (str or None, default None) – Survey acquisition date. An error-source correlation key: geomagnetic terms (declination / dip / B) are date + location dependent via secular variation.

  • tool_id (str or None, default None) – Survey tool run identifier. An error-source correlation key: sections from the same tool run share that tool’s systematic error.

  • geomag_model (str or None, default None) – Geomagnetic model / IFR / IIFR reference used. An error-source correlation key.

Notes

kickoff_md is derived, not a native OSDU field — OSDU has no KickOffMD. Infer it from the child trajectory’s top MD relative to the parent (EDM supplies it directly via CD_SURVEY_HEADER.tie_on_depth). It is the divergence point the relative-error split needs.

The survey_date / tool_id / geomag_model fields are the error-source context keys. They decide how much two sections’ errors correlate even when their trajectories are independent (divergent branches): sections sharing a key share that systematic error source, so it partly correlates between them — orthogonal to the graph ancestry. The datum and grid convergence come from the parent Well / Site and are shared keys too.

__init__(id: str, name: str = '', parent: _Node | None = None, kickoff_md: float | None = None, survey: object = None, survey_date: str | None = None, tool_id: str | None = None, geomag_model: str | None = None, datum_realisation: str | None = None) None
datum_realisation: str | None = None
geomag_model: str | None = None
kickoff_md: float | None = None
parent: _Node | None = None
survey: object = None
survey_date: str | None = None
tool_id: str | None = None
welleng.hierarchy.network_from_edm(reader, *, surveys: bool = False) WellNetwork[source]

Build a WellNetwork from a parsed EDM/COMPASS export.

Maps the EDM master-data spine indexed by welleng.exchange.edm_stream.EDMReader onto the hierarchy entities and wires the wellbore forest (parent-wellbore sidetrack edges, root wellbores parented by their Well). All lengths are converted from the reader’s source units (feet for the public Volve export) to metres at this boundary; angles arrive in degrees and convergence is stored in radians.

The table -> entity mapping:

EDM table

Hierarchy entity

CD_PROJECT

Field (project_name); also supplies the site CRS (geo datum + zone) when derivable

CD_SITE

Site (name, convergence [deg -> rad], is_field_center, map location [-> metres])

CD_WELL

Well (name, slot (ns, ew) + radial error, wellhead depth [-> metres])

CD_DATUM

Datum on its Well (elevation [-> metres]; the default datum is preferred)

CD_WELLBORE

Wellbore; parent_wellbore_id is the sidetrack edge, ko_md [-> metres] the kickoff; root wellbores parent to their Well

Parameters:
  • reader (welleng.exchange.edm_stream.EDMReader) – An indexed EDM reader (or any object exposing the same projects / sites / wells / datums / wellbores / source_units / survey() surface).

  • surveys (bool, keyword-only, default False) – When True, attach each wellbore’s definitive ACTUAL survey (converted to metres, grid azimuth reference) via reader.survey(...). A wellbore whose survey is missing or fails to assemble is skipped with a UserWarning — the import never aborts on a single wellbore. The default (False) builds the cheap structure-only network.

Returns:

The assembled network: Field -> Site -> Well (+ Datum) -> Wellbore forest, with surveys attached when requested.

Return type:

WellNetwork

Warns:

UserWarning – Per wellbore whose survey cannot be assembled (surveys=True only), and per wellbore whose parent_wellbore_id cannot be resolved (it is rooted to its Well instead).

welleng.io module

welleng.io.acr_setup(sheet, data)[source]
welleng.io.get_clearance_data(well, sheet, data)[source]
welleng.io.get_standard_data(filename)[source]
welleng.io.get_well_data(well, sheet, data)[source]
welleng.io.import_iscwsa_collision_data(filename)[source]
welleng.io.make_survey(data, well)[source]

welleng.mesh module

Wellbore mesh generation from survey data and positional uncertainty.

class welleng.mesh.WellMesh(survey: Survey, n_verts: int = 12, sigma: float = 3.0, sigma_pa: float = 0.5, Sm: float = 0, method: str = 'ellipse', polygon_fit: str = 'circumscribed')[source]

Bases: object

Triangular mesh representing a wellbore’s positional uncertainty envelope.

s

The input Survey object.

Type:

welleng.survey.Survey

vertices

Vertex positions array, shape (n_stations, n_verts, 3).

Type:

numpy.ndarray

faces

Triangle face index array, shape (n_faces, 3).

Type:

numpy.ndarray

mesh

Lightweight mesh container with vertices and faces attributes.

Type:

types.SimpleNamespace

n_verts

Number of vertices per station cross-section.

Type:

int

sigma

Sigma multiplier for the uncertainty envelope.

Type:

float

radius

Wellbore radius at each station.

Type:

numpy.ndarray

nevs

Station positions in NEV coordinates, shape (n_stations, 3).

Type:

numpy.ndarray

figure()[source]

Create a plotly 3D figure of the well mesh.

__init__(survey: Survey, n_verts: int = 12, sigma: float = 3.0, sigma_pa: float = 0.5, Sm: float = 0, method: str = 'ellipse', polygon_fit: str = 'circumscribed')[source]

Create a WellMesh object from a welleng Survey object.

Parameters:
  • survey (welleng.survey.Survey) – The survey from which to build the mesh.

  • n_verts (int, optional) – The number of vertices along the uncertainty ellipse edge from which to construct the mesh. Recommended minimum is 12 and that the number is a multiple of 4.

  • sigma (float, optional) – The desired standard deviation sigma value of the well bore uncertainty.

  • sigma_pa (float, optional) – The desired “project ahead” value. A remnant of the ISCWSA method but may be used in the future to accommodate for well bore curvature that is not captured by the mesh.

  • Sm (float, optional) – From the ISCWSA method, this is an additional factor applied to the well bore radius of the offset well to oversize the hole.

  • method (str, optional) – The method for constructing the uncertainty edge. Either “ellipse”, “pedal_curve” or “circle”.

  • polygon_fit (str, optional) – How the n_verts polygon approximates the uncertainty ellipse: “circumscribed” (default) scales the polygon out by 1 / cos(pi / n_verts) so its edges are tangent to and the polygon fully contains the ellipse — it never under-represents the uncertainty for the given sigma (the safety-conservative choice); “inscribed” places the vertices on the ellipse, which under-counts the uncertainty area between vertices. Only applies to the “ellipse”/”circle” methods.

figure(type='mesh3d', **kwargs)[source]

Create a plotly figure of this mesh.

Parameters:
  • type (str, optional) – Plotly figure type, default ‘mesh3d’.

  • **kwargs – Passed to welleng.visual.figure().

Returns:

A plotly Figure instance.

Return type:

plotly.graph_objects.Figure

welleng.mesh.fix_mesh(mesh)[source]

Fix a non-watertight mesh by removing duplicate and degenerate faces, then repairing windings and normals.

Parameters:

mesh (trimesh.Trimesh) – The mesh to repair.

Returns:

A repaired mesh with correct windings and normals.

Return type:

trimesh.Trimesh

welleng.mesh.get_ends(n_verts, rows)[source]

Build cap faces for the first and last cross-section rings.

End-cap triangles fan from center vertices (the wellpath positions) appended after all ring vertices, rather than from circumference vertex 0.

Parameters:
  • n_verts (int) – Number of vertices per cross-section ring.

  • rows (int) – Number of cross-section rings along the wellbore.

Returns:

(top_faces, bottom_faces), each of shape (n_verts, 3).

Return type:

tuple of numpy.ndarray

welleng.mesh.get_faces(n_verts, rows)[source]

Build triangular face indices for a tubular mesh.

Parameters:
  • n_verts (int) – Number of vertices per cross-section ring.

  • rows (int) – Number of cross-section rings along the wellbore.

Returns:

Face index array of shape (n_faces, 3).

Return type:

numpy.ndarray

welleng.mesh.make_trimesh_scene(data)[source]

Construct a trimesh scene. A collision manager can’t be saved, but a scene can and a scene can be imported into a collision manager.

Parameters:

data (list) – List of welleng.mesh.WellMesh objects.

Returns:

A trimesh scene containing all well meshes.

Return type:

trimesh.scene.scene.Scene

welleng.mesh.sliced_mesh(survey, n_verts=12, sigma=3.0, sigma_pa=0.5, Sm=0, start=0, stop=-1, step=1, method='mesh_ellipse')[source]

Generates a list of mesh objects of a user defined length.

Parameters:
  • survey (welleng.survey.Survey) – The survey from which to build the meshes.

  • n_verts (int, optional) – The number of vertices along the uncertainty ellipse edge from which to construct the mesh. Recommended minimum is 12 and that the number is a multiple of 4.

  • sigma (float, optional) – The desired standard deviation sigma value of the well bore uncertainty.

  • sigma_pa (float, optional) – The desired “project ahead” value. A remnant of the ISCWSA method but may be used in the future to accommodate for well bore curvature that is not captured by the mesh.

  • Sm (float, optional) – From the ISCWSA method, this is an additional factor applied to the well bore radius of the offset well to oversize the hole.

  • method (str, optional) – The method for constructing the uncertainty edge. Either “ellipse” or “pedal_curve”.

Returns:

List of mesh namespace objects.

Return type:

list

welleng.mesh.to_trimesh(well_mesh)[source]

Convert a WellMesh to a trimesh.Trimesh object.

This is the single point where trimesh is constructed from the stored geometry arrays. Call this explicitly wherever a true trimesh object is required (e.g. collision detection in MeshClearance).

Parameters:

well_mesh (WellMesh)

Return type:

trimesh.Trimesh

welleng.mesh.transform_trimesh_scene(scene, origin=None, scale=100, redux=0.25)[source]

Transforms a scene by scaling it, reseting the origin/datum and performing a reduction in the number of triangles to reduce the file size.

Parameters:
  • scene (trimesh.scene.scene.Scene) – A trimesh scene of well meshes.

  • origin (array_like, optional) – 3D array [x, y, z]. The origin of the scene from which the new scene will reset to [0, 0, 0].

  • scale (float, optional) – A scalar reduction will be performed using this float.

  • redux (float, optional) – The desired reduction ratio for the number of triangles in each mesh.

Returns:

A transformed, scaled and reprocessed scene.

Return type:

trimesh.scene.scene.Scene

welleng.node module

Wellbore survey node representing a position and direction in a well trajectory.

class welleng.node.Node(pos: ArrayLike | None = None, vec: ArrayLike | None = None, md: float | None = None, inc: float | None = None, azi: float | None = None, unit: str = 'meters', degrees: bool = True, nev: bool = True, cov_nev: ndarray | None = None, interpolated: bool = False, **kwargs: Any)[source]

Bases: object

A survey station in a wellbore trajectory.

Stores position, direction vector, measured depth, and covariance for a single point along a well path. Coordinates can be specified in either NEV (north-east-vertical) or XYZ convention.

pos_nev

Position as [north, east, vertical].

Type:

list

pos_xyz

Position as [x, y, z].

Type:

list

vec_nev

Unit direction vector in NEV.

Type:

list

vec_xyz

Unit direction vector in XYZ.

Type:

list

inc_rad

Inclination in radians.

Type:

float

inc_deg

Inclination in degrees.

Type:

float

azi_rad

Azimuth in radians.

Type:

float

azi_deg

Azimuth in degrees.

Type:

float

md

Measured depth along the wellbore.

Type:

float

unit

Unit of measurement (default ‘meters’).

Type:

str

cov_nev

3x3 covariance matrix in NEV coordinates.

Type:

ndarray

__init__(pos: ArrayLike | None = None, vec: ArrayLike | None = None, md: float | None = None, inc: float | None = None, azi: float | None = None, unit: str = 'meters', degrees: bool = True, nev: bool = True, cov_nev: ndarray | None = None, interpolated: bool = False, **kwargs: Any) None[source]

Initialize a Node with position and direction.

Parameters:
  • pos (array_like, optional) – Position as a 3-element array. Interpreted as NEV or XYZ depending on the nev flag.

  • vec (array_like, optional) – Unit direction vector (3-element). If provided, inc and azi are ignored.

  • md (float, optional) – Measured depth along the wellbore.

  • inc (float, optional) – Inclination angle.

  • azi (float, optional) – Azimuth angle.

  • unit (str) – Length unit, default 'meters'.

  • degrees (bool) – If True, inc and azi are in degrees.

  • nev (bool) – If True, pos and vec are in NEV coordinates; otherwise XYZ.

  • cov_nev (ndarray, optional) – Covariance matrix (1, 3, 3). Defaults to zeros.

  • interpolated (bool) – True if this node was interpolated between survey stations (default False).

  • **kwargs – Additional attributes set on the instance.

azi_deg: float | None
azi_rad: float | None
check_angle_inputs(inc: float | None, azi: float | None, vec: ArrayLike | None, nev: bool, degrees: bool) None[source]
cov_nev: ndarray
get_pos(pos: ArrayLike | None, nev: bool) None[source]
inc_deg: float | None
inc_rad: float | None
interpolated: bool
md: float | None
pos_nev: list | None
pos_xyz: list | None
properties() dict[source]

Return all instance attributes as a dictionary.

Returns:

Mapping of attribute names to their values.

Return type:

dict

unit: str
vec_nev: list | None
vec_xyz: list | None
welleng.node.get_node_params(node: Node) tuple[list | None, list | None, float | None][source]

Extract position, direction, and measured depth from a Node.

Parameters:

node (Node) – A Node instance.

Returns:

A tuple of (pos_nev, vec_nev, md).

Return type:

tuple

welleng.sawaryn_analytical module

Analytical 3D Curve-Line-Curve (CLC) point-to-target solver.

An open implementation of the closed-form point-to-target solution of Sawaryn (2021):

Sawaryn, S. J. (2021). “A Generalized Solution to the Point-to-Target Problem Using the Minimum Curvature Method.” SPE Drilling & Completion. DOI: 10.2118/204111-PA.

A CLC trajectory connects a kickoff station (position + unit tangent) to a target station with two circular arcs (radii R1, R2) joined by a straight tangent of length beta. The solution is parameterised by the tangent length and the two subtended arc angles alpha1, alpha2.

The corrected Eq. 15, and a note on the printed one

The paper presents the solution two ways: the forward constraint equations (Eqs. 11-13: eta1, eta4, eta14 as functions of alpha1, alpha2, beta) and the eliminated implicit form (Eq. 15: a degree-10 polynomial in beta whose real positive roots are every solution). The printed Eq. 15 is not scale- covariant — it does not reproduce the paper’s own worked roots under length normalisation, i.e. it carries a transcription error in the eliminated polynomial (whose true expansion the paper notes is “~4000 terms, beyond human capability”). The eq15 function below reproduces the printed form and is retained only to document that trap (see test_eq15_is_trapped).

The correct degree-10 coefficients were re-derived by replicating Sawaryn’s own surd-elimination (Appendix B: the half-angle quadratics B-15/B-16 and the bilinear constraint B-19), eliminating the surds symbolically (_eq15_coeffs). These power the vectorised closed-form solver solve_clc, which returns every CLC solution and reproduces Example 2 of SPE-204111-PA exactly. Subtended angles are reported as the true dogleg in [0, 2*pi) so the measured depth ranks solutions correctly. Planar (eta14 ~ 0) and parallel- tangent (|mu| = 1) cases are handled by solve_clc_2d (the paper’s biquadratic 2D form). Forward-verified solvers (solve_clc_analytical scan, solve_clc_resultant per-instance resultant) are provided as cross-checks.

This supersedes the iterative scheme of Sawaryn & Thorogood (2005, “A Compendium of Directional Calculations Based on the Minimum Curvature Method”, SPE-84246-PA) that welleng’s Connector inherits.

Citation

Use of this work requires citation. Cite Sawaryn (2021, SPE-204111-PA) for the underlying mathematics, and — for any use of this (welleng’s) implementation or its corrected coefficients — you must also cite welleng (software concept DOI 10.5281/zenodo.20968887) and the welleng analytical-CLC paper:

Corcutt, J. (2026). An Open, Vectorized Closed-Form Solver for the 3D Curve-Hold-Curve Point-to-Target Problem. Zenodo. DOI 10.5281/zenodo.21130979.

implements his last and most general solution to a problem he advanced for over four decades. —————————————————————————-

welleng.sawaryn_analytical.eq15(beta, psi2, eta1, eta4, eta14, mu, R1, R2)[source]

Sawaryn Eq. 15 — the eliminated degree-10 polynomial. REFERENCE ONLY.

The printed form is NOT scale-covariant (it does not reproduce the paper’s own worked roots under length normalisation) — it carries a transcription/ print error in the eliminated polynomial. solve_clc_analytical does not use it; it forward-verifies via the clean Eqs. 11-13 + 18-25 instead. Kept here only to document the discrepancy.

welleng.sawaryn_analytical.forward(alpha1, alpha2, beta, mu, R1, R2)[source]

Forward model (Eqs. 11-13): (eta1, eta4, eta14) from the path parameters.

Verified exact against SPE-204111-PA Example 2. Returns None where the angular surd is negative (geometrically inconsistent).

welleng.sawaryn_analytical.max_radius(p1, t1, p4, t4, ratio=1.0)[source]

Largest radius admitting a valid CLC — the gentlest feasible curve.

The point-to-target CLC is reachable with both arc doglegs <= pi only up to a maximum radius; beyond it the target is reachable only by a > pi (loop) arc, which the minimum-curvature renderer cannot draw. That maximum is the beta = 0 (curve-curve / biarc) boundary, where the hold vanishes — equivalently the largest root of the constant coefficient c0 of the corrected Eq. 15 whose biarc has both doglegs <= pi. This is the analytic form of the classical “critical radius”: the gentlest curvature that still reaches the target. A caller can fall back to it when no CLC exists at the design radii, instead of iterating the radius down.

Parameters:
  • p1 ((3,) array_like) – Kickoff / target positions and unit tangents (N, E, V).

  • t1 ((3,) array_like) – Kickoff / target positions and unit tangents (N, E, V).

  • p4 ((3,) array_like) – Kickoff / target positions and unit tangents (N, E, V).

  • t4 ((3,) array_like) – Kickoff / target positions and unit tangents (N, E, V).

  • ratio (float, default 1.0) – R2 / R1. 1.0 is symmetric radii; otherwise the second radius scales with the first along this ratio.

Returns:

radius (R1), radius2 (R2), beta (0.0), alpha1, alpha2 (biarc doglegs, radians) and total_md; None if no feasible biarc exists (the target is unreachable under the pi constraint).

Return type:

dict or None

Notes

Closed-form CONDITION of Sawaryn (2021, SPE-204111-PA) – the roots of c0 – located by a bracketing scan plus Brent, so the condition is analytic but its solution is not iteration-free. Parallel tangents (|mu| = 1) — where the general form is singular — are handled by a 2D feasibility bisection (solve_clc_2d()). See solve_clc() for the general (fixed-design-radius) solve.

welleng.sawaryn_analytical.solve_clc(p1, t1, p4, t4, R1, R2=None, return_all=False)[source]

Solve the CLC point-to-target problem for a single station pair.

Main entry point. Runs the general closed-form solver first; degenerate pairs (parallel/antiparallel tangents |mu| = 1, or planar eta14 ~ 0) auto-fall back to solve_clc_2d(), so the caller need not pre-classify.

Parameters:
  • p1 ((3,) array_like) – Kickoff position and unit tangent (N, E, V); t1 is a unit vector.

  • t1 ((3,) array_like) – Kickoff position and unit tangent (N, E, V); t1 is a unit vector.

  • p4 ((3,) array_like) – Target position and unit tangent.

  • t4 ((3,) array_like) – Target position and unit tangent.

  • R1 (float) – First / second arc radii. R2 defaults to R1 (symmetric arcs). Unit-agnostic, so long as positions and radii share one length unit (e.g. all metres, or all feet).

  • R2 (float) – First / second arc radii. R2 defaults to R1 (symmetric arcs). Unit-agnostic, so long as positions and radii share one length unit (e.g. all metres, or all feet).

  • return_all (bool, default False) – If False, return only the shortest (minimum measured-depth) solution. If True, return every valid CLC solution.

Returns:

return_all=False: the shortest solution as a dict with keys beta (tangent / hold length), alpha1, alpha2 (arc doglegs, radians) and total_md (measured depth); None if no CLC exists. return_all=True: list of such dicts, shortest first.

Return type:

dict or list of dict or None

welleng.sawaryn_analytical.solve_clc_2d(p1, t1, p4, t4, R1, R2=None, return_all=False)[source]

Planar / singular CLC solve (eta14 ~ 0) — Sawaryn Eq. 34, biquadratic in beta.

Covers the degenerate 2D case AND the parallel/antiparallel-tangent singularities mu = +-1 (where the general form’s 1/(1-mu^2) blows up): eta14 is identically 0 there, so this biquadratic subsumes Sawaryn’s Eqs 37/38. The +- is the two arc senses (Figs 10/11). Verification uses Eqs 11-12 only (the out-of-plane surd sits at 0 numerically here).

return_all=False (default): the shortest solution dict, or None. return_all=True: list of all solution dicts, shortest first.

welleng.sawaryn_analytical.solve_clc_analytical(p1, t1, p4, t4, R1, R2=None, n_scan=4000, tol=1e-06)[source]

Solve the CLC point-to-target problem (Sawaryn 2021), forward-verified.

Parameters:
  • p1 ((3,) array — kickoff position and unit tangent (N, E, V); t1 is) – a unit vector, as tangents are throughout welleng (cf. Survey.vec_nev).

  • t1 ((3,) array — kickoff position and unit tangent (N, E, V); t1 is) – a unit vector, as tangents are throughout welleng (cf. Survey.vec_nev).

  • p4 ((3,) array — target position and unit tangent.)

  • t4 ((3,) array — target position and unit tangent.)

  • R1 (float — first/second arc radii (R2 defaults to R1, symmetric) – arcs). Unit-agnostic: any length unit, so long as positions and radii share it (e.g. all metres, or all feet). The returned lengths come back in that same unit; angles are radians.

  • R2 (float — first/second arc radii (R2 defaults to R1, symmetric) – arcs). Unit-agnostic: any length unit, so long as positions and radii share it (e.g. all metres, or all feet). The returned lengths come back in that same unit; angles are radians.

Returns:

  • list of dict, sorted by total measured depth, each with keys (driller’s

  • terms in brackets)

  • beta / line — straight tangent length (the hold section);

  • alpha1, alpha2 — subtended arc angles in radians (the dogleg of each

  • build/turn); arc1, arc2 — arc lengths R*alpha (the build

  • sections); total_md — total measured depth; residual. The build-plane

  • toolface is not returned but is recoverable from the reconstructed tangent.

  • Complete (every real CLC solution is returned.)

welleng.sawaryn_analytical.solve_clc_landing(p1, t1, p0, t4, R1, R2=None, return_all=False)[source]

Land onto a LINE target: p4 = p0 + k*t4, solving for the scalar k.

The landing problem (Sawaryn 2021, Appendix C): the target is not a fixed point but any point on the line through p0 in direction t4; the free parameter is the along-line distance k, and the connection is a biarc (beta = 0). On that line the invariants collapse to low-order functions of k (Eqs. C-8/C-13/C-17): eta1 = eps1 + mu*k, eta4 = eps4 + k, eta14 = eps14 (constant), psi^2 = psi0^2 + 2*eps4*k + k^2, with eps* = (p0 - p1).<basis>. Substituting these into the biarc condition c0 = 0 (the constant coefficient of the corrected Eq. 15) gives a polynomial in k (Eq. 44) whose roots are the landing distances; this is solved numerically for k.

Parameters:
  • p1 ((3,) array_like) – Kickoff position and unit tangent (N, E, V).

  • t1 ((3,) array_like) – Kickoff position and unit tangent (N, E, V).

  • p0 ((3,) array_like) – The landing line: p0 is its anchor (the k = 0 base point) and t4 its unit direction. Named p0 (not p4) because the target point p4 = p0 + k*t4 is the solved output, not an input.

  • t4 ((3,) array_like) – The landing line: p0 is its anchor (the k = 0 base point) and t4 its unit direction. Named p0 (not p4) because the target point p4 = p0 + k*t4 is the solved output, not an input.

  • R1 (float) – Arc radii; R2 defaults to R1.

  • R2 (float) – Arc radii; R2 defaults to R1.

  • return_all (bool, default False) – False -> the shortest feasible landing (both biarc doglegs <= pi) as a dict, or None. True -> every landing root, feasible-first then by MD.

Returns:

Each dict: k (along-line distance), p4 (landing point), beta (0.0), alpha1, alpha2 (biarc doglegs, radians), total_md.

Return type:

dict or list of dict or None

welleng.sawaryn_analytical.solve_clc_resultant(p1, t1, p4, t4, R1, R2=None)[source]

Complete CLC solve via per-instance resultant elimination.

Independent cross-check for the vectorised solve_clc (exercised in the test suite); not on welleng’s hot path.

Eliminates the two half-angle tangents from Sawaryn’s clean forward equations (11-13) by exact-rational resultants -> a polynomial in beta whose real positive roots include EVERY CLC solution (complete by construction - unlike the scan-based solve_clc_analytical, which can drop large-angle / min-MD roots). Spurious roots (from clearing the half-angle denominators and squaring Eq. 13) are removed by forward-verification.

Fast (~90ms/solve; python-flint resultant + arbitrary-precision acb roots, scale-normalised so coefficients stay representable) and complete + deterministic. The rational inputs are truncated to ~5 digits (1e5): the forward-verification filter rejects any root that drifts, so the lower precision buys ~2.4x speed with no loss of completeness.

welleng.sawaryn_analytical.subtended_angles(beta, psi2, eta1, eta4, eta14, mu, R1, R2)[source]

Candidate subtended arc angles (alpha1, alpha2), radians, for a beta.

Eqs. 18-25: each half-angle tangent T = tan(alpha/2) solves a quadratic A T^2 + B T + C = 0. Returns the two branches per arc; forward- verification selects the physical one.

welleng.sawaryn_analytical.tangent(inc_deg, azi_deg)[source]

Unit tangent [N, E, V] from inclination + azimuth (degrees).

welleng.osdu module

OSDU import/export for the well hierarchy — version-pinned and units-aware.

Maps welleng.hierarchy entities to/from OSDU Well-Known-Schema records so a hierarchy can be imported from an OSDU data platform and exported back.

Two hard requirements:

  1. Explicit schema-version reference + quick adaptation. OSDU schemas evolve (minor/patch per M-release). Every mapping pins the exact version in OSDU_SCHEMA_VERSIONS (one place to bump), the OSDU kind string carries it (osdu:wks:master-data--Wellbore:1.1.0), and from_osdu recognises the incoming version and warns/adapts on a mismatch rather than silently mis-mapping. To support a new schema version: add its version to the pin and, if fields moved, a version-specific branch in the entity mapper.

  2. Units-aware. OSDU carries a unit-of-measure per numeric (e.g. a FrameOfReference/UnitOfMeasureID / AsIngestedCoordinates UoM). welleng works internally in metres; every length is converted on the boundary (_to_m on import, _from_m on export). Volve/EDM sources are feet — do not assume metres.

The version-pin + kind parsing + units boundary + the round-trip contract are implemented; per-entity field mapping is filled for the load-bearing entities (Well, Wellbore, WellboreTrajectory) and extends by the same pattern. Schemas: https://community.opengroup.org/osdu/data/data-definitions.

welleng.osdu.OSDU_SCHEMA_VERSIONS: dict[str, str] = {'Field': '1.0.0', 'Organisation': '1.0.0', 'Well': '1.1.0', 'WellSiteStructure': '1.0.0', 'Wellbore': '1.1.0', 'WellboreTrajectory': '1.0.0'}

The OSDU WKS versions this module maps against. Pin to your deployment’s schema registry. Bumping a value here (+ a version branch in the mapper if fields moved) is the whole “adapt quickly” story.

welleng.osdu.build_kind(entity: str, version: str | None = None) str[source]

Build an OSDU kind string for a mapped entity.

Assembles the fully-qualified OSDU Well-Known-Schema kind osdu:wks:<group>--<Entity>:<version> from the entity name, its group-type, and the pinned (or overridden) schema version.

Parameters:
  • entity (str) – The OSDU entity name — a key of OSDU_SCHEMA_VERSIONS, e.g. "Wellbore", "Well", "WellSiteStructure".

  • version (str or None, default None) – Schema version to embed. When None the pinned version from OSDU_SCHEMA_VERSIONS is used.

Returns:

The OSDU kind string, e.g. "osdu:wks:master-data--Wellbore:1.1.0".

Return type:

str

Raises:

KeyError – If entity is not a known mapped entity.

Examples

>>> from welleng.osdu import build_kind
>>> build_kind('Wellbore')
'osdu:wks:master-data--Wellbore:1.1.0'
>>> build_kind('Wellbore', version='1.2.0')
'osdu:wks:master-data--Wellbore:1.2.0'
welleng.osdu.from_osdu(record: dict[str, Any]) Any[source]

Map one OSDU record to the matching welleng.hierarchy entity.

Dispatches on the record’s kind to build the corresponding welleng.hierarchy entity, converting all lengths to internal metres and checking (warning on) the schema version.

Parameters:

record (dict) – An OSDU record shaped {"kind": ..., "id": ..., "data": {...}}. The data payload carries the entity fields; if absent the record itself is used as the data.

Returns:

The mapped hierarchy entity. WellboreTrajectory records return a plain dict of tie metadata (wellbore_id, top_md, base_md, azimuth_reference) because the station bulk is a separately-loaded referenced dataset.

Return type:

Wellbore or Well or Organisation or Field or Site or dict

Raises:

ValueError – If the kind is malformed (via parse_kind()) or names an entity that has no from_osdu mapper.

Warns:

UserWarning – When the record’s schema version differs from the pinned one.

Notes

Parent links (WellID / KickOffWellbore) are NOT resolved here — the caller wires them when assembling the WellNetwork (see network_from_osdu()). kickoff_md is derived, not native OSDU, so it is left None.

Examples

>>> from welleng.osdu import from_osdu
>>> rec = {'kind': 'osdu:wks:master-data--Wellbore:1.1.0', 'id': 'WB1',
...        'data': {'FacilityName': 'TopHole'}}
>>> wb = from_osdu(rec)
>>> type(wb).__name__, wb.id, wb.name
('Wellbore', 'WB1', 'TopHole')
welleng.osdu.network_from_osdu(records: list[dict[str, Any]]) WellNetwork[source]

Assemble a WellNetwork from OSDU records.

Maps every Wellbore record via from_osdu(), then wires the KickOffWellbore / WellID parent edges into a network. Non-wellbore records are ignored.

Parameters:

records (list of dict) – OSDU records (as passed to from_osdu()). Only Wellbore records contribute nodes.

Returns:

The assembled network. A wellbore whose parent is a Well (a root, not yet added) is left with parent=None.

Return type:

WellNetwork

Notes

Deriving each section’s kickoff_md from the trajectory tie MDs is a follow-up, done once the referenced station datasets are loaded (OSDU has no native KickOffMD).

welleng.osdu.parse_kind(kind: str) tuple[str, str, str][source]

Parse an OSDU kind string into its parts.

Inverse of build_kind(): splits osdu:wks:<group>--<Entity>:<version> into its group-type, entity name, and schema version.

Parameters:

kind (str) – An OSDU kind string, e.g. "osdu:wks:master-data--Wellbore:1.1.0".

Returns:

(group, entity, version) — e.g. ("master-data", "Wellbore", "1.1.0").

Return type:

tuple of (str, str, str)

Raises:

ValueError – If kind does not match the expected OSDU kind structure.

Examples

>>> from welleng.osdu import parse_kind
>>> parse_kind('osdu:wks:master-data--Wellbore:1.1.0')
('master-data', 'Wellbore', '1.1.0')
welleng.osdu.to_osdu(entity: Any, *, version: str | None = None, uom: str = 'm') dict[str, Any][source]

Map a welleng.hierarchy entity to an OSDU record.

Inverse of from_osdu(): emits an OSDU record ({"kind": ..., "id": ..., "data": {...}}) at the pinned (or given) schema version, converting internal metres to the requested uom and encoding the parent edge (Well WellID or parent-wellbore KickOffWellbore).

Parameters:
  • entity (Wellbore or Well or Organisation or Field or Site) – The hierarchy entity to export.

  • version (str or None, keyword-only, default None) – Schema version to embed in the kind; None uses the pin (see build_kind()).

  • uom (str, keyword-only, default "m") – The length unit-of-measure to emit numeric depths in.

Returns:

The OSDU record.

Return type:

dict

Raises:

ValueError – If entity is not a type with a to_osdu mapper.

Examples

>>> from welleng.hierarchy import Well, Wellbore
>>> from welleng.osdu import to_osdu
>>> top = Wellbore(id='WB1', name='TopHole', parent=Well(id='W1', name='W1'))
>>> lat = Wellbore(id='WB2', name='Lat1', parent=top, kickoff_md=1000.0)
>>> rec = to_osdu(lat)
>>> rec['kind']
'osdu:wks:master-data--Wellbore:1.1.0'
>>> rec['id'], rec['data']
('WB2', {'FacilityName': 'Lat1', 'KickOffWellbore': 'WB1'})

welleng.survey module

Well survey management, coordinate transforms, and trajectory analysis.

class welleng.survey.SplitSurvey(survey: Survey)[source]

Bases: object

Split a survey into upper and lower station pairs for interval calculations.

Provides paired arrays of inclinations, azimuths, vectors, and doglegs for consecutive survey stations.

__init__(survey: Survey) None[source]
class welleng.survey.Survey(md: ArrayLike, inc: ArrayLike, azi: ArrayLike, n: ArrayLike | None = None, e: ArrayLike | None = None, tvd: ArrayLike | None = None, x: ArrayLike | None = None, y: ArrayLike | None = None, z: ArrayLike | None = None, vec: ArrayLike | None = None, nev: bool = True, header: SurveyHeader | None = None, radius: ArrayLike | None = None, cov_nev: ndarray | None = None, cov_hla: ndarray | None = None, error_model: str | None = None, start_xyz: ArrayLike = [0.0, 0.0, 0.0], start_nev: ArrayLike = [0.0, 0.0, 0.0], start_cov_nev: ArrayLike | None = None, deg: bool = True, unit: str = 'meters', steering: str | ArrayLike | None = None, **kwargs: Any)[source]

Bases: MinCurve

Directional well survey with positions, vectors, errors, and trajectory properties.

Computes wellbore positions via minimum curvature, converts between azimuth reference systems (true/magnetic/grid), calculates dogleg severity, toolface, build/turn rates, and optionally propagates ISCWSA error model covariances.

header

Survey metadata including location, datum, and reference information.

Type:

SurveyHeader

md

Measured depths along the wellbore.

Type:

ndarray of shape (n,)

inc_deg

Inclination angles in degrees.

Type:

ndarray of shape (n,)

inc_rad

Inclination angles in radians.

Type:

ndarray of shape (n,)

azi_grid_deg

Grid azimuth angles in degrees.

Type:

ndarray of shape (n,)

azi_grid_rad

Grid azimuth angles in radians.

Type:

ndarray of shape (n,)

azi_true_deg

True north azimuth angles in degrees.

Type:

ndarray of shape (n,)

azi_true_rad

True north azimuth angles in radians.

Type:

ndarray of shape (n,)

azi_mag_deg

Magnetic north azimuth angles in degrees.

Type:

ndarray of shape (n,)

azi_mag_rad

Magnetic north azimuth angles in radians.

Type:

ndarray of shape (n,)

pos_nev

Station positions in North-East-Vertical coordinates.

Type:

ndarray of shape (n, 3)

pos_xyz

Station positions in X-Y-Z coordinates.

Type:

ndarray of shape (n, 3)

vec_nev

Unit direction vectors in North-East-Vertical coordinates.

Type:

ndarray of shape (n, 3)

vec_xyz

Unit direction vectors in X-Y-Z coordinates.

Type:

ndarray of shape (n, 3)

n

Northing coordinates of each survey station.

Type:

ndarray of shape (n,)

e

Easting coordinates of each survey station.

Type:

ndarray of shape (n,)

tvd

True vertical depth of each survey station.

Type:

ndarray of shape (n,)

x

X coordinates of each survey station.

Type:

ndarray of shape (n,)

y

Y coordinates of each survey station.

Type:

ndarray of shape (n,)

z

Z coordinates (depth) of each survey station.

Type:

ndarray of shape (n,)

dogleg

Dogleg angles between successive stations in radians.

Type:

ndarray of shape (n,)

dls

Dogleg severity per 30 m (or 100 ft) interval.

Type:

ndarray of shape (n,)

delta_md

Measured depth intervals between successive stations.

Type:

ndarray of shape (n,)

rf

Ratio factors from minimum curvature calculation.

Type:

ndarray of shape (n,)

toolface

Toolface angles in radians at each station.

Type:

ndarray of shape (n,)

build_rate

Build rate (inclination change rate) per unit length.

Type:

ndarray of shape (n,)

turn_rate

Turn rate (azimuth change rate) per unit length.

Type:

ndarray of shape (n,)

curve_radius

Radius of curvature at each station.

Type:

ndarray of shape (n,)

radius

Wellbore radius at each station.

Type:

ndarray of shape (n,)

cov_nev

Covariance matrices in North-East-Vertical coordinates.

Type:

ndarray of shape (n, 3, 3) or None

cov_hla

Covariance matrices in High-Lateral-Along-hole coordinates.

Type:

ndarray of shape (n, 3, 3) or None

err

Error model results when an error model is applied.

Type:

ErrorModel or None

survey_deg

Survey data as [md, inc_deg, azi_grid_deg] columns.

Type:

ndarray of shape (n, 3)

survey_rad

Survey data as [md, inc_rad, azi_grid_rad] columns.

Type:

ndarray of shape (n, 3)

vertical_section

Vertical section lateral displacement if a VS azimuth is defined.

Type:

ndarray of shape (n,) or None

interpolate_survey(step=30)[source]

Interpolate survey at regular MD intervals.

interpolate_md(md)[source]

Interpolate survey data at a specific measured depth.

interpolate_tvd(tvd)[source]

Interpolate survey data at a specific true vertical depth.

interpolate_survey_tvd(step=30)[source]

Interpolate survey at regular TVD intervals.

get_error(error_model)[source]

Apply an ISCWSA/OWSG error model to the survey.

get_nev_arr()[source]

Return station positions as (n, 3) NEV array.

get_vertical_section(azimuth)[source]

Compute vertical section along a given azimuth.

set_vertical_section(azimuth)[source]

Set the vertical section azimuth on the survey.

project_to_bit(delta_md)[source]

Project the survey ahead by a given MD.

project_to_target(target, dls_design)[source]

Plan a trajectory to a target location.

figure()[source]

Create a plotly 3D figure of the survey.

save(filename)[source]

Export survey data to file.

maximum_curvature(dls_noise=1.0)[source]

Compute survey using the maximum curvature method.

tortuosity_index()[source]

Calculate the tortuosity index.

modified_tortuosity_index()[source]

Calculate the modified tortuosity index.

directional_difficulty_index()[source]

Calculate the directional difficulty index.

__init__(md: ArrayLike, inc: ArrayLike, azi: ArrayLike, n: ArrayLike | None = None, e: ArrayLike | None = None, tvd: ArrayLike | None = None, x: ArrayLike | None = None, y: ArrayLike | None = None, z: ArrayLike | None = None, vec: ArrayLike | None = None, nev: bool = True, header: SurveyHeader | None = None, radius: ArrayLike | None = None, cov_nev: ndarray | None = None, cov_hla: ndarray | None = None, error_model: str | None = None, start_xyz: ArrayLike = [0.0, 0.0, 0.0], start_nev: ArrayLike = [0.0, 0.0, 0.0], start_cov_nev: ArrayLike | None = None, deg: bool = True, unit: str = 'meters', steering: str | ArrayLike | None = None, **kwargs: Any) None[source]

Initialize a welleng.Survey object.

Geometry is computed INTERNALLY in the grid-azimuth domain (the canonical engine frame); the header’s azi_reference names the INPUT azimuth frame and is converted to grid on construction. The input-reference default is "true" (see SurveyHeader), not grid – the “grid” here is the internal calculation domain, not the expected input.

Parameters:
  • md ((,n) list or array of floats) – List or array of well bore measured depths.

  • inc ((,n) list or array of floats) – List or array of well bore survey inclinations

  • azi ((,n) list or array of floats) – List or array of well bore survey azimuths

  • n ((,n) list or array of floats (default: None)) – List or array of well bore northings

  • e ((,n) list or array of floats (default: None)) – List or array of well bore eastings

  • tvd ((,n) list or array of floats (default: None)) – List or array of local well bore z coordinates, i.e. depth and usually relative to surface or mean sea level.

  • x ((,n) list or array of floats (default: None)) – List or array of local well bore x coordinates, which is usually aligned to the east direction.

  • y ((,n) list or array of floats (default: None)) – List or array of local well bore y coordinates, which is usually aligned to the north direction.

  • z ((,n) list or array of floats (default: None)) – List or array of well bore true vertical depths relative to the well surface datum (usually the drill floor elevation DFE, so not always identical to tvd).

  • vec ((n,3) list or array of (,3) floats (default: None)) – List or array of well bore unit vectors that describe the inclination and azimuth of the well relative to (x,y,z) coordinates.

  • header (SurveyHeader object (default: None)) – A SurveyHeader object with information about the well location and survey data. If left default then a SurveyHeader will be generated with the default properties assigned, but these may not be relevant and may result in incorrect data.

  • radius (float or (,n) list or array of floats (default: None)) – If a single float is specified, this value will be assigned to the entire well bore. If a list or array of floats is provided, these are the radii of the well bore. If None, a well bore radius of 12” or approximately 0.3 m is applied.

  • cov_nev ((n,3,3) list or array of floats (default: None)) – List or array of covariance matrices in the (n,e,v) coordinate system.

  • cov_hla ((n,3,3) list or array of floats (default: None)) – List or array of covariance matrices in the (h,l,a) well bore coordinate system (high side, lateral, along hole).

  • error_model (str (default: None)) – Name of the survey-tool error model used to compute the position covariance. Leave as None for no uncertainty calculation. The recommended/standard model is "ISCWSA MWD Rev5.11" (the validated ISCWSA standard); "ISCWSA MWD Rev4" is the legacy model. The OWSG toolcode library ("MWD+SRGM", +SAG, +AX, +IFR, gyro stacks "GYRO-NS" / "GYRO-NS-CT" / "GYRO-MWD", …) is also selectable. List every available name with welleng.error.get_error_models(); switch by passing a different name. Raises if the name is unrecognised.

  • start_xyz ((,3) list or array of floats (default: [0,0,0])) – The start position of the well bore in (x,y,z) coordinates.

  • start_nev ((,3) list or array of floats (default: [0,0,0])) – The start position of the well bore in (n,e,v) coordinates.

  • start_cov_nev ((,3,3) list or array of floats (default: None)) – The covariance matrix for the start position of the well bore in (n,e,v) coordinates.

  • deg (boolean (default: True)) – Indicates whether the provided angles are in degrees (True), else radians (False).

  • unit (str (default: 'meters')) – Indicates whether the provided lengths and distances are in ‘meters’ or ‘feet’, which impacts the calculation of the dls (dog leg severity).

Return type:

A welleng.survey.Survey object.

azi_grid_deg: ndarray
azi_grid_rad: ndarray
azi_mag_deg: ndarray
azi_mag_rad: ndarray
azi_true_deg: ndarray
azi_true_rad: ndarray
build_rate: ndarray
cov_hla: ndarray | None
cov_nev: ndarray | None
curvature_rate() ndarray[source]

Rate of change of curvature dκ/ds per station (rad per unit length²).

The along-hole derivative of curvature κ = dogleg / Δmd. With the torsion term it completes the 3D stiff-string contact force (SPE-105068-PA, Eq. 20: the EI·τ·dκ/ds binormal term). Zero on a constant-curvature (constant-DLS) section.

Discrete form (SPE-105068-PA, Eq. 18): (κ_{j+1} κ_j) / (s_{j+1} s_j).

Returns:

dkappa_ds – Per-station dκ/ds aligned to self.md; the final station (undefined forward difference) is 0.

Return type:

ndarray of shape (n,)

curve_radius: ndarray
deg: bool
delta_md: ndarray
directional_difficulty_index(data: bool = False, depth_units: str = 'meters', **kwargs: Any) float | ndarray[source]

Directional Difficulty Index (DDI), IADC/SPE 59196 (Oag & Williams).

DDI = log10(MD * AHD * cumulative-dogleg / TVD), computed in feet. See directional_difficulty_index() for the definition, units and validation notes.

Parameters:
  • data (bool) – False (default) -> the well DDI at TD (float); True -> the per-station DDI (n,) array.

  • depth_units (str) – The length unit of the survey’s md/n/e/tvd (default “meters”).

dls: ndarray
dogleg: ndarray
e: ndarray
err: ErrorModel | None
error_model: str | None
figure(type: str = 'scatter3d', **kwargs: Any) Any[source]

Generate a plotly figure of the survey trajectory.

Parameters:
  • type (str) – Plot type passed to welleng.visual.figure.

  • **kwargs – Additional keyword arguments forwarded to the plotting function.

Returns:

A plotly figure object.

Return type:

object

get_error(error_model: str, return_error: bool = False) ErrorModel | Survey[source]

Apply an error model and compute covariance matrices.

Parameters:
  • error_model (str) – Name of the error model (e.g. "ISCWSA_MWD").

  • return_error (bool) – If True, return the ErrorModel object; otherwise return the Survey with updated covariances.

Returns:

The ErrorModel object if return_error is True, otherwise the Survey instance with updated covariance attributes.

Return type:

ErrorModel or Survey

Raises:

AssertionError – If error_model is not a recognized model name.

get_nev_arr() ndarray[source]

Return survey positions as an (n, 3) array of [N, E, TVD].

Returns:

Array of shape (n, 3) with northing, easting, and TVD columns.

Return type:

ndarray

get_vertical_section(vertical_section_azimuth: float, deg: bool = True) ndarray[source]

Calculate the vertical section.

Parameters:
  • vertical_section_azimuth (float) – The azimuth (relative to the reference azimuth defined in the survey header) along which to calculate the vertical section lateral displacement.

  • deg (boolean (default: True)) – Indicates whether the vertical section azimuth parameter is in degrees or radians (True or False respectively).

Returns:

result

Return type:

(n, 1) ndarray

header: SurveyHeader
highside_vec_nev() ndarray[source]

Unit high-side vector at each station, as an (n, 3) [N, E, V] array.

The high side is perpendicular to the wellbore axis, in the vertical plane containing it, pointing to the high side of the hole (up-dip). It is the high-side (H) basis vector of the NEV->HLA transform, expressed in NEV, using the grid azimuth (consistent with get_nev_arr()). For a horizontal well it points straight up ([0, 0, -1]); for a vertical well the high side is undefined and this returns the azimuth direction.

Returns:

Array of shape (n, 3) of unit high-side vectors in [N, E, V].

Return type:

ndarray

inc_deg: ndarray
inc_rad: ndarray
interpolate_md(md: float) Node | None[source]

Method to interpolate a position based on measured depth and return a node.

Parameters:

md (float) – The measured depth of the point of interest.

Returns:

node – A node with attributes describing the point at the provided measured depth.

Return type:

we.node.Node object

Examples

>>> import welleng as we
>>> survey = we.connector.interpolate_survey(
...    survey=we.survey.Survey(
...       md=[0, 500, 1000, 2000, 3000],
...       inc=[0, 0, 30, 90, 90],
...       azi=[0, 0, 45, 135, 180],
...    ),
...    step=30
... )
>>> node = survey.interpolate_md(1234)
>>> node.properties()
{
    'vec_nev': [0.07584209568113438, 0.5840332282889957, 0.8081789187902809],
    'vec_xyz': [0.5840332282889957, 0.07584209568113438, 0.8081789187902809],
    'inc_rad': 0.6297429542197106,
    'azi_rad': 1.4416597719915565,
    'inc_deg': 36.081613454889634,
    'azi_deg': 82.60102042890875,
    'pos_nev': [141.27728744087796, 201.41424652428694, 1175.5823295305202],
    'pos_xyz': [201.41424652428694, 141.27728744087796, 1175.5823295305202],
    'md': 1234.0,
    'unit': 'meters',
    'interpolated': True
}
interpolate_mds(md: ArrayLike) Survey[source]

Method to interpolate positions at an array of measured depths and return a new welleng.Survey object. This is a vectorized equivalent of looping the scalar interpolate_md, and produces a survey equivalent to interpolate_survey when passed the same station measured depths.

Parameters:

md ((,n) list or array of floats) – The measured depths of the points of interest.

Returns:

  • A welleng.survey.Survey object with an interpolated property

  • indicating whether each station was interpolated (True) or is an

  • original survey station (False).

Examples

>>> import welleng as we
>>> import numpy as np
>>> survey = we.survey.Survey(
...       md=[0, 500, 1000, 2000, 3000],
...       inc=[0, 0, 30, 90, 90],
...       azi=[0, 0, 45, 135, 180],
...    )
>>> survey_interp = survey.interpolate_mds(np.arange(0, 3000, 30))
interpolate_survey(step: float = 30, dls: float = 1e-08) Survey[source]

Convenience method for interpolating a Survey object’s MD.

interpolate_survey_tvd(start: float | None = None, stop: float | None = None, step: float = 10) Survey[source]

Convenience method for interpolating a Survey object’s TVD.

interpolate_tvd(tvd: float) list[source]

Interpolate the survey at a target true vertical depth.

Reversal-robust (Sawaryn & Thorogood 2005, SPE-84246-PA): returns every crossing of tvd, so a target hit twice by a TVD reversal yields two Nodes.

Parameters:

tvd (float) – The true vertical depth at which to interpolate.

Returns:

Every crossing of tvd, sorted by measured depth (normally a single element; empty if tvd is outside the well’s TVD range).

Return type:

list of Node

Notes

Breaking change (welleng 0.15.0): returns a list of Nodes instead of a single Node. Use interpolate_tvd(tvd)[0] on a monotonic well for the previous behaviour.

maximum_curvature(dls_noise: float = 1.0, steering: str | ArrayLike | None = None) Survey[source]

Create a well trajectory using the Maximum Curvature method.

Parameters:
  • survey (welleng.survey.Survey object)

  • dls_noise (float) – The additional Dog Leg Severity (DLS) in deg/30m used to calculate the curvature for the initial section of the survey interval.

  • steering ({'slide', 'rotary'}, or (,n) array-like of those / bool) –

    The slide/rotary mode of each leg - a physical drilling property that is not inferable from the survey (inclination, azimuth, measured depth); it is either a single value applied to every leg, or one entry per survey station (the mode of the leg arriving at that station):

    • 'slide' - the leg was steered by sliding a bent motor at an oriented toolface. The extra dls_noise curvature is applied in the surveyed toolface, giving a directional deflection (the survey-interval error has a consistent sign - the well lands shallower).

    • 'rotary' - the leg was drilled rotating (RSS or rotary hold). The toolface averages out over rotation, so no directional deflection is applied (the leg keeps its minimum-curvature path); the survey-interval error there is random, not directional.

    A boolean array is read as True == slide. Defaults to 'slide' - the conservative choice, since folding a directional bias into a symmetric (rotary) treatment would under-state the error; rotary is therefore opt-in and must be positively declared. Falls back to Survey.steering when the argument is left as None.

Returns:

survey_new – A revised survey object calculated using the Minimum Curvature method with updated survey positions and additional mid-point stations.

Return type:

welleng.Survey.survey object

Raises:

ValueError – If an array is given whose length does not match the number of survey stations.

md: ndarray
modified_tortuosity_index(rtol: float = 1.0, dls_tol: float | None = 0.001, step: float | None = 1.0, dls_noise: float | None = 1.0, data: bool = False, **kwargs: Any) ndarray | dict[source]

Convenience method for the Modified Tortuosity Index (MTI): a native-3D, dimensionless variant of the Tortuosity Index (TI) of Ashok et al. ([IADD presentation](https://www.iadd-intl.org/media/files/files/47d68cb4/iadd-luncheon-february-22-2018-v2.pdf)) and D’Angelo et al. (SPE/IADC-194099-MS).

Compared with tortuosity_index(), the MTI divides each curve turn’s (L_cs / L_xs - 1) term by its arc length L_cs and uses L_c (rather than 1 / L_c) as the normalizing factor, which makes the result independent of the survey’s unit of length (a survey in feet and the same survey in metres give the same MTI). See [the method post](https://jonnymaserati.github.io/2022/05/26/a-modified-tortuosity-index.html).

Warning

“MTI” here means Modified Tortuosity Index. In SPE/IADC-194099-MS “MTI” denotes the unrelated Mapped Tortuosity Index (planned curve turns mapped onto the as-drilled path); do not conflate the two.

By default the survey is pre-processed with the maximum-curvature method (interpolated to step then dls_noise deg/30m added) so that the MTI is robust to survey-station frequency; set dls_noise=None to use the raw minimum-curvature survey instead.

Parameters:
  • rtol (float) – Relative tolerance when testing normal-vector continuity (passed to numpy.isclose as both rtol and atol).

  • dls_tol (float or None) – If not None, additionally require dogleg-severity continuity within this tolerance when sectionizing.

  • step (float or None) – Step length (metres) for interpolating the survey before applying the maximum-curvature method. Ignored if dls_noise is None.

  • dls_noise (float or None) – Incremental Dog Leg Severity (deg/30m) added by the maximum-curvature method. If None, no pre-processing is done and minimum curvature is assumed. When applied, every leg is treated as a slide (steering='slide') - the maximum, worst-case tortuosity the method is defined to give; the slide/rotary distinction of maximum_curvature() is deliberately not exposed here, since the MTI is a conservative geometric quality metric rather than an error-propagation calculation.

  • data (bool) – If True, return a dict of intermediate properties instead of the array.

Returns:

mti – Per-station modified tortuosity index, or a dict of intermediate results (starts, mds, locs, l_cs, l_xs, mti, survey …) if data is True.

Return type:

(n,) ndarray or dict

References

Further details on the maximum-curvature method and survey-frequency robustness are [here](https://jonnymaserati.github.io/2022/06/19/modified-tortuosity-index-survey-frequency.html).

n: ndarray
normals: ndarray
pos_nev: ndarray
pos_xyz: ndarray
project_to_bit(delta_md: float, dls: float | None = None, toolface: float | None = None) Node[source]

Convenience method to project the survey ahead to the bit.

Parameters:
  • delta_md (float) – The along hole distance from the surveying tool to the bit in meters.

  • dls (float) – The desired dog leg severity (deg / 30m) between the surveying tool and the bit. Default is to project the DLS of the last survey section.

  • toolface (float) – The desired toolface to project from at the last survey point. The default is to project the current toolface from the last survey station.

Returns:

node

Return type:

welleng.node.Node object

project_to_target(node_target: Node, dls_design: float = 3.0, delta_md: float | None = None, dls: float | None = None, toolface: float | None = None, step: float = 30) Survey[source]

Project a wellpath from the end of this survey to a target node.

Parameters:
  • node_target (Node) – The target Node to connect to.

  • dls_design (float) – Design dogleg severity (deg/30m) for the connection.

  • delta_md (float or None) – Along-hole distance from survey tool to bit. If None, projection starts at the last survey station.

  • dls (float or None) – DLS for the projection to the bit. Defaults to last survey DLS.

  • toolface (float or None) – Toolface for the projection to the bit. Defaults to last survey toolface.

  • step (float) – Survey interval (m) for the projected wellpath.

Returns:

A Survey object representing the projected path to the target.

Return type:

Survey

radius: ndarray
rf: ndarray
save(filename: str) None[source]

Saves a minimal (control points) survey listing as a .csv file, including the survey header information.

Parameters:

filename (str) – The path and filename for saving the text file.

set_vertical_section(vertical_section_azimuth: float, deg: bool = True) None[source]

Sets the vertical_section_azimuth property in the survey header and the vertical section data with the data calculated for the input azimuth.

Parameters:
  • vertical_section_azimuth (float) – The azimuth (relative to the reference azimuth defined in the survey header) along which to calculate the vertical section lateral displacement.

  • deg (boolean (default: True)) – Indicates whether the vertical section azimuth parameter is in degrees or radians (True or False respectively).

steering: ndarray | None
survey_deg: ndarray
survey_rad: ndarray
toolface: ndarray
torsion() ndarray[source]

Geometric torsion τ per station (radians per unit length).

The helical rate of the wellpath — the rate at which the osculating-plane normal (self.normals, i.e. the Serret-Frenet binormal direction) rotates along the trajectory. Zero on a planar (2D) trajectory; nonzero only where the well turns out of plane. This is a geometric property of the path and is distinct from mechanical drillstring twist.

Discrete form (Mitchell & Samuel 2009, SPE-105068-PA, Eq. 17):

τ_j = arccos(b_{j-1} · b_j) / Δs_j

with b the unit osculating-plane normal and Δs_j the central measured-depth spacing about station j. This is the same normal-vector continuity the (modified) tortuosity index uses — a section of constant normal is planar and has zero torsion.

Returns:

torsion – Per-station geometric torsion (rad/unit length), aligned to self.md. The two end stations (undefined) and any straight-hold sections (undefined osculating plane) are set to 0.

Return type:

ndarray of shape (n,)

Notes

Consumers (e.g. the stiff-string T&D 3D contact terms, SPE-105068-PA Eq. 20) should prefer a smooth (spline) trajectory: a minimum-curvature survey gives a bending-moment discontinuity at stations (App. F), so torsion evaluated on raw min-curve stations is a station-spaced approximation.

tortuosity_index(rtol: float = 0.01, dls_tol: float | None = None, data: bool = False, **kwargs: Any) ndarray | dict[source]

Convenience method for the Tortuosity Index (TI), a native-3D variant of the method presented in the [IADD presentation](https://www.iadd-intl.org/media/files/files/47d68cb4/iadd-luncheon-february-22-2018-v2.pdf) by Pradeep Ashok et al. and in SPE/IADC-194099-MS by D’Angelo et al. (itself adapted from the retinal-vessel tortuosity work of Grisan et al.).

The published method computes a tortuosity index separately in the inclination and azimuth domains and combines them as the root of the sum of squares. However, the arc-length and chord-length terms used in each domain are the full 3D quantities (only the curve-turn detection differs between the domains), so the two components are not independent and the 3D curvature is effectively double-counted. This method avoids that by sectionizing the trajectory directly in 3D: a curve turn is considered continuous while its normal vector (vec_i x vec_j) remains constant, which inherently accounts for torsion. See tortuosity_index() for the implementation and modified_tortuosity_index() for the dimensionless variant.

Note that TI is not dimensionless (the result scales with the unit of length, since the 1 / L_c normalization carries length units); per SPE/IADC-194099-MS a scale factor of 1e7 is applied so values fall in a convenient range. Compute L_c in feet to compare with the published reference ranges.

Parameters:
  • rtol (float) – Relative tolerance when testing normal-vector continuity (passed to numpy.isclose as both rtol and atol).

  • dls_tol (float or None) – If not None, additionally require dogleg-severity continuity within this tolerance when sectionizing.

  • data (bool) – If True, return a dict of intermediate properties instead of the array.

  • **kwargscoeff (length unit conversion, default 0.3048 -> feet) and kappa (scale factor, default 1e7) may be overridden.

Returns:

ti – Per-station tortuosity index, or a dict of results if data.

Return type:

ndarray or dict

tortuosity_views(modified: bool = True, target_md: float | None = None, **kwargs: Any) dict[source]

Total, remaining and local readings of the tortuosity profile.

The tortuosity index is evaluated at every station, so it is a profile; these are the three engineering reads of it (see the MTI paper):

  • total: the value at the end (the whole-well KPI scalar);

  • remaining: the increment still to accumulate from each station to the end (or to target_md) — what is left to drill;

  • local: the along-hole gradient of the index — flags a single tortuous interval that the total would hide.

Parameters:
  • modified (bool) – If True (default) use the dimensionless modified_tortuosity_index(); otherwise use tortuosity_index().

  • target_md (float or None) – Reference depth for remaining; defaults to total depth.

  • **kwargs – Passed through to the underlying index method.

Returns:

{'md', 'total', 'remaining', 'local'} where md is the depth grid the profile is evaluated on (the maximum-curvature pre-processed grid when modified pre-processing is active).

Return type:

dict

turn_rate: ndarray
tvd: ndarray
tvd_turning_points() ndarray[source]

The measured depths at which the well passes through horizontal.

TVD is monotonic in measured depth between consecutive turning points, so these are the cuts a TVD-domain treatment needs to stay single-valued (Sawaryn & Thorogood 2005, SPE-84246-PA, Eq. 31).

Returns:

Turning-point measured depths, ascending; empty if TVD is monotonic throughout.

Return type:

(,n) ndarray of float

unit: str
vec_nev: ndarray
vec_radius_nev: ndarray
vec_xyz: ndarray
x: ndarray
y: ndarray
z: ndarray
class welleng.survey.SurveyData(survey: Survey)[source]

Bases: object

Lightweight container for combining survey data from multiple sections.

Extracts the minimal data needed from Survey objects and provides methods to append additional sections and reconstruct a unified Survey.

__init__(survey: Survey) None[source]

A class for extracting the minimal amount of data from a Survey object, with methods for combining data from a list of surveys that describe an entire well path.

Parameters:

survey (welleng.survey.Survey)

append_survey(survey: Survey) None[source]

Method to extract data from a survey and append it to the existing survey data existing in the instance.

Parameters:

survey (welleng.survey.Survey)

get_survey() Survey[source]

Method to create a welleng.survey.Survey object from the survey data existing in the instance.

Returns:

survey

Return type:

welleng.survey.Survey

class welleng.survey.SurveyHeader(name: str | None = None, longitude: float | None = None, latitude: float | None = None, altitude: float | None = None, survey_date: str | None = None, G: float = 9.80665, b_total: float | None = None, earth_rate: float = 0.26251614, dip: float | None = None, declination: float | None = None, convergence: float = 0, azi_reference: str = 'true', vertical_inc_limit: float = 0.0001, xcl_representation: str = 'nev_direct', dp_basis: str = 'balanced_tangent', deg: bool = True, depth_unit: str = 'meters', surface_unit: str = 'meters', mag_defaults: dict = {'b_total': 50000.0, 'declination': 0.0, 'dip': 70.0}, vertical_section_azimuth: float = 0, grid_scale_factor: float = 1.0)[source]

Bases: object

Metadata for a well survey including location, magnetic field, and reference systems.

Stores the geographic position, magnetic field parameters (total field, dip, declination), convergence, azimuth reference system, and unit conventions needed to interpret and process directional survey data.

mag_source tracks the provenance of each geomagnetic reference value ('user' / 'lookup' / 'default'); magnetic error models refuse 'default' (see ErrorModel). Assigning b_total, dip or declination — at construction or any time after — marks that field 'user'.

__init__(name: str | None = None, longitude: float | None = None, latitude: float | None = None, altitude: float | None = None, survey_date: str | None = None, G: float = 9.80665, b_total: float | None = None, earth_rate: float = 0.26251614, dip: float | None = None, declination: float | None = None, convergence: float = 0, azi_reference: str = 'true', vertical_inc_limit: float = 0.0001, xcl_representation: str = 'nev_direct', dp_basis: str = 'balanced_tangent', deg: bool = True, depth_unit: str = 'meters', surface_unit: str = 'meters', mag_defaults: dict = {'b_total': 50000.0, 'declination': 0.0, 'dip': 70.0}, vertical_section_azimuth: float = 0, grid_scale_factor: float = 1.0) None[source]

A class for storing header information about a well.

Parameters:
  • name (string (default: None)) – The assigned name of the well bore.

  • longitude (float (default: None)) – The longitude of the surface location of the well. If left default (None) then it will be assigned to Grenwich, the undisputed center of the universe.

  • latitude (float (default: None)) – The latitude of the surface location of the well. If left default (None) then it will be assigned to Grenwich, the undisputed center of the universe.

  • altitude (float (default: None)) – The altitude of the surface location in METRES above mean sea level (negative for below MSL, e.g. a subsea wellhead). If left default (None) then it will be assigned to 0. Converted to km at the BGS magnetic-lookup boundary.

  • survey_date (YYYY-mm-dd (default: None)) – The date on which the survey data was recorded. If left default then the current date is assigned.

  • G (float (default: 9.80665)) – The gravitational field strength in m/s^2.

  • b_total (float (default: None)) – The total magnetic field strength in nT. If left default, the value is looked up from the BGS geomagnetic web service (welleng.geomag) using the longitude, latitude, altitude and survey_date properties — a real latitude/longitude must be provided for the result to count as a valid reference (see mag_source).

  • earth_rate (float (default: 0.26249751949994715)) – The rate of rotation of the earth in radians per hour.

  • noise_reduction_factor (float (default: 1.0)) – A fiddle factor for random gyro noise.

  • dip (float (default: None)) – The dip (inclination) of the magnetic field relative to the earth’s horizontal. If left default, the value is looked up from the BGS geomagnetic web service (welleng.geomag). The unit (deg or rad) is determined by the deg property.

  • declination (float (default: None)) – The angle between true north and magnetic north at the well location. If left default, the value is looked up from the BGS geomagnetic web service (welleng.geomag).

  • convergence (float (default: 0)) – The angle of convergence between the projection meridian and the line from true north through the location of the well.

  • azi_reference (string (default: 'true')) – The reference system the INPUT azimuths are given in – “true”, “magnetic” or “grid”. This is the input frame, NOT the engine’s internal geometry frame: positions are always computed in grid (the canonical frame), converting the input via convergence/declination. A magnetic MWD survey MUST set “magnetic” so declination is applied (and its magnetic error terms are framed correctly); the default “true” assumes true-north input. Note that survey calculations are performed in the “grid” reference and converted to and from the other systems.

  • vertical_inc_limit (float (default 0.0001)) – For survey inclination angles less than the vertical_inc_limit (in degrees), calculations are approximated to avoid singularities and errors.

  • deg (bool (default: True)) – Indicates whether the survey angles are measured in degrees (True) or radians (False).

  • depth_unit (string (default: "meters")) – The unit of depth for the survey data, either “meters” or “feet”.

  • surface_unit (string (default: "feet")) – The unit of distance for the survey data, either “meters” or “feet”.

  • vertical_section_azimuth (float (default: 0.0)) – The azimuth along which to determine the vertical section data for the well trajectory.

  • grid_scale_factor (float (default: 1.0)) – Scale factor applied during when determining the grid coordinates from the provided survey data.

class welleng.survey.SurveyParameters(projection: str = 'EPSG:23031')[source]

Bases: Proj

Class for calculating survey parameters for input to a Survey Header.

This is a wrapper of pyproj that tries to simplify the process of getting convergence, declination and dip values for a survey header.

Notes

Requires pyproj; the magnetic-field values need internet access (the BGS geomagnetic web service, via welleng.geomag).

For reference, here’s some EPSG codes: {

‘UTM31_ED50’: ‘EPSG:23031’, ‘UTM31_WGS84’: ‘EPSG:32631’, ‘RD’: ‘EPSG:28992’, ‘ED50-UTM31’: ‘EPSG:23031’, ‘ED50-NEDTM’: ‘EPSG:23095’, # assume same as ED50-UTM31 ‘ETRS89-UTM31’: ‘EPSG:25831’, ‘ED50-UTM32’: ‘EPSG:23032’, ‘ED50-GEOGR’: ‘EPSG:4230’, ‘WGS84-UTM31’: ‘EPSG:32631’

}

References

For more info on transformations between maps, refer to the pyproj project [here](https://pypi.org/project/pyproj/).

__init__(projection: str = 'EPSG:23031') None[source]

Initiates a SurveyParameters object for conversion of map coordinates to WGS84 lat/lon for calculating magnetic field properties.

Parameters:

projection (str (default: "EPSG:23031")) – The EPSG code of the map of interest. The default represents ED50/UTM zone 31N.

References

For codes refer to [EPSG](https://epsg.io).

get_factors_from_x_y(x: float, y: float, altitude: float | None = None, date: str | None = None) dict[source]

Calculates the survey header parameters for a given map coordinate.

Parameters:
  • x (float) – The x or East/West coordinate.

  • y (float) – The y or North/South coordinate.

  • altitude (float (default: None)) – The altitude or z value coordinate in metres above mean sea level (negative for below MSL). If none is provided this will default to zero (sea level). Converted to km at the BGS magnetic-lookup boundary.

  • date (str (default: None)) – The date of the survey, used when calculating the magnetic parameters. Will default to the current date.

Returns:

x: float

The x coordinate.

y: float

The y coordinate.

northing: float

The Northing (negative values are South).

easting: float

The Easting (negative values are West).

latitude: float

The WGS84 latitude.

longitude: float

The WGS84 longitude.

convergence: float

Te grid convergence for the provided coordinates.

scale_factor: float

The scale factor for the provided coordinates.

magnetic_field_intensity: float

The total field intensity for the provided coordinates and time.

declination: float

The declination at the provided coordinates and time.

dip: float

The dip angle at the provided coordinates and time.

date:

The date used for determining the magnetic parameters.

Return type:

dict

Examples

In the following example, the parameters for Den Haag in The Netherlands are looked up with the reference map ED50 UTM Zone 31N.

>>> import pprint
>>> from welleng.survey import SurveyParameters
>>> calculator = SurveyParameters('EPSG:23031')
>>> survey_parameters = calculator.get_factors_from_x_y(
...     x=588319.02, y=5770571.03
... )
>>> pprint(survey_parameters)
{'convergence': 1.01664403471959,
'date': '2023-12-16',
'declination': 2.213,
'dip': -67.199,
'easting': 588319.02,
'latitude': 52.077583926214494,
'longitude': 4.288694821453205,
'magnetic_field_intensity': 49381,
'northing': 5770571.03,
'scale_factor': 0.9996957469340414,
'srs': 'EPSG:23031',
'x': 588319.02,
'y': 5770571.03}
transform_coordinates(coords: ArrayLike, to_projection: str, altitude: float | None = None, **kwargs: Any) ArrayLike[source]

Transforms coordinates from instance’s projection to another projection.

Parameters:
  • coords (arraylike) – A list of decimal coordinates to transform from the instance projection to the specified projection system. Can be 2D or 3D in (x, y, z) format, where x is East/West and y is North/South.

  • to_projection (str) – The EPSG code of the desired coordinates.

Returns:

result – An array of transformed coordinates in the desired projection.

Return type:

ArrayLike

Examples

Convert the coordinates of Den Haag from ED50-UTM31 to WGS84-UTM31:

>>> from welleng.survey import SurveyParameters
>>> calculator = SurveyParameters('EPSG:23031')
>>> result = calculator.transform_coordinates(
...     coords=[(588319.02, 5770571.03)], to_projection='EPSG:32631'
... )
>>> print(result)
[[ 588225.93417027 5770360.56500115]]

To place a well head given as geographic latitude/longitude – e.g. an EDM geo_latitude / geo_longitude – into a map’s projection (say, the ED50 UTM31N frame a set of horizons use), start from the matching geographic CRS. Coordinate order follows the source CRS’s own axis order: a geographic CRS such as ED50 (EPSG:4230) is (latitude, longitude), not (lon, lat):

>>> geo = SurveyParameters('EPSG:4230')  # ED50 geographic
>>> geo.transform_coordinates(
...     coords=[(58.4417, 1.8875)], to_projection='EPSG:23031'
... )  
array([[ 435051.34, 6478573.19]])

Use the coordinates’ own datum as the source CRS (ED50 geographic here, not WGS84) so no spurious datum shift is introduced. Do NOT feed an EDM’s geo_offset_east / geo_offset_north here when they are in a different map system than the target (as on Volve) – go via the geographic lat/long instead.

class welleng.survey.TurnPoint(md: float | None = None, inc: float | None = None, azi: float | None = None, build_rate: float | None = None, turn_rate: float | None = None, dls: float | None = None, toolface: float | None = None, method: str | None = None, target: Any | None = None, tie_on: bool = False, location: list | None = None)[source]

Bases: object

A control point in a well plan, representing a hold or curve section.

Used when discretizing a survey into sections for export to planning software (e.g. Landmark COMPASS .wbp format).

__init__(md: float | None = None, inc: float | None = None, azi: float | None = None, build_rate: float | None = None, turn_rate: float | None = None, dls: float | None = None, toolface: float | None = None, method: str | None = None, target: Any | None = None, tie_on: bool = False, location: list | None = None) None[source]

Initialize a TurnPoint.

Parameters:
  • md (float or None) – Measured depth.

  • inc (float or None) – Inclination in degrees.

  • azi (float or None) – Azimuth in degrees.

  • build_rate (float or None) – Build rate in deg per unit length.

  • turn_rate (float or None) – Turn rate in deg per unit length.

  • dls (float or None) – Dogleg severity.

  • toolface (float or None) – Toolface angle in degrees.

  • method (str or None) – Planning method code (e.g. "920" for minimum curvature).

  • target (object or None) – Associated target, if any.

  • tie_on (bool) – Whether this is the tie-on point.

  • location (list or None) – Position as [x, y, z].

welleng.survey.directional_difficulty_index(survey: Survey, data: bool = False, depth_units: str = 'meters', **kwargs: Any) float | ndarray[source]

Directional Difficulty Index (DDI).

IADC/SPE 59196 “The Directional Difficulty Index - A New Approach to Performance Benchmarking”, Oag & Williams (2000):

DDI = log10( MD * AHD * Tortuosity / TVD )

where AHD is the along-hole displacement (horizontal departure, sqrt(N**2 + E**2) at the station) and Tortuosity is the cumulative absolute dogleg angle (total curvature imposed on the wellbore) in degrees.

Units: DDI is not dimensionless – MD * AHD / TVD carries a length unit – so it is conventionally computed in FEET (the paper’s basis, target range ~5-8). Declare the survey’s length unit via depth_units (welleng’s Survey.unit is not a reliable length label) and DDI converts to feet, so the same well in metres or feet gives the same DDI.

Validation: SPE-59196 gives its results graphically (Figs 7-12: DDI vs MD/inclination/AHD for designer J- and S-wells; Fig 13 field wells with DDI 6.31-6.84; Fig 14 bands <6 / 6.0-6.4 / 6.4-6.8 / >6.8). A build-and-hold J-well family reproduces the published RANGE (~5-7.6) and the increasing trend with departure/inclination, and realistic ERD wells fall in the field band. Exact per-well numbers need the designer wells’ precise geometry (given only as plots) and the paper’s tortuosity is under-specified, so this uses the standard cumulative-dogleg reading – a DIFFERENT tortuosity from tortuosity_index() (a normalized 3D index).

Parameters:
  • survey (welleng.survey.Survey)

  • data (bool) – If False (default) return the well DDI at TD (a float). If True return the DDI at every survey station (an (n,) array).

  • depth_units (str) – The length unit the survey’s md/n/e/tvd are in (default “meters”).

Returns:

The DDI at TD, or the per-station DDI array when data=True.

Return type:

float or (n,) numpy.ndarray

welleng.survey.export_csv(survey: Survey, filename: str | None, tolerance: float = 0.1, dls_cont: bool = False, decimals: int = 3, **kwargs: Any) DataFrame | None[source]

Function to export a minimalist (only the control points - i.e. the begining and end points of hold and/or turn sections) survey to input into third party trajectory planning software.

Parameters:
  • survey (welleng.survey.Survey object)

  • filename (str) – The path and filename for saving the text file.

  • tolerance (float (default: 0.1)) – How close the the final N, E, TVD position of the minimalist survey should be to the original survey point (e.g. within 1 meter)

  • dls_cont (bool) – Whether to explicitly check for dls continuity. May result in a larger number of control points but a trajectory that is a closer fit to the survey.

  • decimals (int (default: 3)) – Number of decimal places provided in the output file listing

welleng.survey.from_connections(section_data: Any, step: float | None = None, survey_header: SurveyHeader | None = None, start_nev: ArrayLike = [0.0, 0.0, 0.0], start_xyz: ArrayLike = [0.0, 0.0, 0.0], start_cov_nev: ArrayLike | None = None, radius: float = 10, deg: bool = False, error_model: str | None = None, depth_unit: str = 'meters', surface_unit: str = 'meters', decimals: int | None = None) Survey[source]

Constructs a well survey from a list of sections of control points.

Parameters:
  • section_data (list of dicts with section data)

  • start_nev – The starting position in NEV coordinates.

  • radius (float (default: 10)) – The radius is passed to the welleng.survey.Survey object and represents the radius of the wellbore. It is also used when visualizing the results, so can be used to make the wellbore thicker in the plot.

  • decimals (int (default=6)) – Round the md decimal when checking for duplicate surveys.

Returns:

survey – A Survey object constructed from the connections.

Return type:

Survey

welleng.survey.func(x0: float, survey: Survey, dls_cont: bool, tolerance: float) float[source]

Objective function for optimizing control-point tolerance in export_csv.

Parameters:
  • x0 (float) – Current tolerance value being optimized.

  • survey (Survey) – The original Survey object.

  • dls_cont (bool) – Whether to check DLS continuity.

  • tolerance (float) – Target positional tolerance for the endpoint.

Returns:

Absolute difference between the target tolerance and the maximum endpoint position error.

Return type:

float

welleng.survey.get_circle_radius(survey: Survey, **targets: Any) tuple[source]

Compute curvature circle centers and endpoints for each survey interval.

Parameters:
  • survey (Survey) – A Survey object.

  • **targets – Reserved for future target data support.

Returns:

Tuple of (starts, ends) arrays representing circle center positions and their corresponding survey station positions.

Return type:

tuple of ndarray

welleng.survey.get_data(tol: float, survey: Survey, dls_cont: bool) ndarray[source]

Extract control-point data from a survey at a given tolerance.

Parameters:
  • tol (float) – Tolerance for section boundary detection (used as rtol and atol).

  • survey (Survey) – A Survey object.

  • dls_cont (bool) – Whether to check DLS continuity between sections.

Returns:

Array of shape (n, 10) with MD, inc, azi, N, E, TVD, DLS, toolface, build rate, and turn rate for each control point.

Return type:

ndarray

welleng.survey.get_node(survey: Survey, idx: int, interpolated: bool = False) Node[source]

Extract a Node from a survey at a given index.

Parameters:
  • survey (Survey) – A Survey object.

  • idx (int) – Index of the survey station.

  • interpolated (bool) – Whether this station was interpolated.

Returns:

A Node with position, vector, and MD from the survey station.

Return type:

Node

welleng.survey.get_node_tvd(survey: Survey, node1: Node, node2: Node, tvd: float, node_origin: Node) Node | None[source]

Connect two nodes and interpolate to a target TVD.

Parameters:
  • survey (Survey) – The parent Survey object.

  • node1 (Node) – Start node.

  • node2 (Node) – End node (position is cleared and recomputed via Connector).

  • tvd (float) – Target true vertical depth.

  • node_origin (Node) – Origin node for the interpolation reference.

Returns:

A Node at the target TVD between the two input nodes.

Return type:

Node

welleng.survey.get_sections(survey: Survey, rtol: float = 0.1, atol: float = 0.1, dls_cont: bool = False, **targets: Any) list[source]

Tries to discretize a survey file into hold or curve sections. These sections can then be used to generate a WellPlan object to generate a .wbp format file for import into Landmark COMPASS, thus converting a survey file to an editable well trajectory.

Note that this is in development and only tested on output from planning software. In its current form it likely won’t be too successful on “as drilled” surveys (but optimizing the tolerances may help).

Parameters:
  • survey (welleng.survey.Survey object)

  • rtol (float (default: 1e-1)) – The relative tolerance when comparing the normals using the numpy.isclose() function.

  • atol (float (default: 1e-2)) – The absolute tolerance when comparing the normals using the numpy.isclose() function.

  • dls_cont (bool) – Whether to explicitly check for dls continuity. May results in a larger number of control points but a trajectory that is a closer fit to the survey.

  • **targets (list of Target objects) – Not supported yet…

Returns:

sections – List of TurnPoint objects representing control points.

Return type:

list of TurnPoint

welleng.survey.get_unit(unit: str) str | None[source]

Normalize a unit string to 'meters' or 'feet'.

Parameters:

unit (str) – Input unit string (e.g. 'm', 'meters', 'ft', 'feet').

Returns:

'meters', 'feet', or None if unrecognized.

Return type:

str or None

welleng.survey.interpolate_md(survey: Survey, md: float) Survey | None[source]

Interpolates a survey at a given measured depth.

welleng.survey.interpolate_mds(survey: Survey, md: ArrayLike) Survey[source]

Interpolates a survey at an array of measured depths, returning a new welleng.survey.Survey object that includes the original survey stations plus the requested (interpolated) measured depths.

This is a vectorized equivalent of looping the scalar interpolate_md. Any requested depth that coincides with an existing survey station is dropped (the station is already present in the output).

Parameters:
  • survey (welleng.survey.Survey) – A survey object with at least two survey stations.

  • md ((,n) list or array of floats) – The measured depths of the points of interest.

Returns:

survey_interpolated

Return type:

welleng.survey.Survey object

welleng.survey.interpolate_survey(survey: Survey, step: float = 30, dls: float = 1e-08) Survey[source]

Interpolate a sparse survey with the desired md step.

Parameters:
  • survey (welleng.survey.Survey object)

  • step (float (default=30)) – The desired delta md between stations.

  • dls (float (default=0.01)) – The design DLS used to calculate the minimum curvature. This will be the minimum DLS used to fit a curve between stations so should be set to a small value to ensure a continuous curve is fit without any tangent sections.

Returns:

survey_interpolated – Note that a interpolated property is added indicating if the survey stations is interpolated (True) or not (False).

Return type:

welleng.survey.Survey object

welleng.survey.interpolate_survey_tvd(survey: Survey, start: float | None = None, stop: float | None = None, step: float = 10) Survey[source]

Interpolate a survey at regular TVD intervals.

Reversal-robust (welleng 0.15.0): builds regular TVD levels spanning the well’s full TVD range and inserts a station at every crossing of each level (so a level revisited by a TVD reversal is represented at each pass), interleaved with the original survey stations. Crossings are found with the closed-form, turning-point-segmented interpolate_tvd() (Sawaryn & Thorogood 2005, SPE-84246-PA).

Parameters:
  • survey (Survey) – A Survey object.

  • start (float or None) – TVD level anchor. Levels are placed at start + k * step. Defaults to the first survey station’s TVD.

  • stop (float or None) – Upper TVD bound for the levels. Defaults to the well’s maximum TVD.

  • step (float) – TVD interval between interpolated levels.

Returns:

A Survey object with stations at regular TVD levels plus the original survey stations, ordered by measured depth.

Return type:

Survey

welleng.survey.interpolate_tvd(survey: Survey, tvd: float, **kwargs: Any) list[source]

Interpolate a survey at a target true vertical depth.

Reversal-robust: does not assume monotonic TVD. The survey is walked segment by segment; each minimum-curvature arc is split at its TVD turning point (where the well goes horizontal) into monotonic spans, and every crossing of the target TVD is solved for in closed form. All crossings are returned, sorted by measured depth.

Method: Sawaryn & Thorogood (2005), “A Compendium of Directional Calculations Based on the Minimum Curvature Method” (SPE-84246-PA), Interpolation at a Plane (Eqs. 25-27 and Eq. 1) with the target plane horizontal, plus the Turning Point construction (Eq. 31) to segment each arc into monotonic-TVD spans. See also _arc_tvd_crossings() and _horizontal_tangent_delta().

Parameters:
  • survey (Survey) – A Survey object.

  • tvd (float) – The target true vertical depth.

  • **kwargs

    node_originNode, optional

    Interpolate on the sub-arc that starts at this node (rather than a survey station), spanning to the next survey station. Used to reference the interpolation to a previously interpolated point.

Returns:

Every crossing of tvd, sorted by measured depth (normally a single element; an empty list if tvd is outside the well’s TVD range).

Return type:

list of Node

Notes

Breaking change (welleng 0.15.0): this returns a list of Nodes instead of a single Node. On a monotonic well, interpolate_tvd(tvd)[0] recovers the previous single-crossing behaviour.

welleng.survey.make_survey_header(data: dict) SurveyHeader[source]

Takes a dictionary of survey header data with the same keys as the SurveyHeader class properties and returns a SurveyHeader object.

welleng.survey.modified_tortuosity_index(survey: Survey, rtol: float = 1.0, dls_tol: float | None = 0.001, data: bool = False, **kwargs: Any) ndarray | dict[source]

Calculate the Modified Tortuosity Index (MTI): a native-3D, dimensionless variant of the Tortuosity Index (TI) of Ashok et al. ([IADD presentation](https://www.iadd-intl.org/media/files/files/47d68cb4/iadd-luncheon-february-22-2018-v2.pdf)) and D’Angelo et al. (SPE/IADC-194099-MS).

The trajectory is split into curve-turn / hold sections in 3D via normal-vector continuity (see _get_ti_data()). Each section’s (L_cs / L_xs - 1) term is divided by its arc length L_cs and the running sum is scaled by n / (n + 1) and the curve length L_c, making the result independent of the unit of length. L_cs is the along-hole (arc) distance from the section start to each station and L_xs the corresponding straight-line (chord) distance.

Note: “MTI” here is the Modified Tortuosity Index; in SPE/IADC-194099-MS “MTI” is the unrelated Mapped Tortuosity Index.

Parameters:
  • survey (welleng.survey.Survey)

  • rtol (float) – Relative tolerance for normal-vector continuity (also used as atol).

  • dls_tol (float or None) – If not None, also require dogleg-severity continuity within this tolerance.

  • data (bool) – If True, return a dict of intermediate properties.

  • **kwargscoeff (unit conversion, default 1.0) and kappa (scale factor, default 1) may be overridden.

Returns:

mti

Return type:

ndarray or dict

welleng.survey.project_ahead(pos: ndarray, vec: ndarray, delta_md: float, dls: float, toolface: float, md: float = 0.0) Node[source]

Apply a simple arc or hold from a current position and vector.

Parameters:
  • pos – Current position in n, e, tvd coordinates.

  • vec – Current vector in n, e, tvd coordinates.

  • delta_md (float) – The desired along hole projection length.

  • dls (float) – The desired dogleg severity of the projection. Entering 0.0 will result in a hold section.

  • toolface (float) – The desired toolface for the projection.

  • md (float (optional)) – The current md if applicable.

Returns:

node

Return type:

welleng.node.Node object

welleng.survey.project_to_target(survey: Survey, node_target: Node, dls_design: float = 3.0, delta_md: float | None = None, dls: float | None = None, toolface: float | None = None, step: float = 30) Survey[source]

Project a wellpath from the end of a current survey to a target, taking account of the location of the bit relative to the surveying tool if the delta_md property is not None.

Parameters:
  • survey (welleng.survey.Survey obj)

  • node_target (welleng.node.Node obj)

  • dls_design (float) – The dls from which to construct the projected wellpath.

  • delta_md (float) – The along hole length from the surveying sensor to the bit.

  • dls (float) – The desired dogleg severity for the projection from the survey tool to the bit. Entering 0.0 will result in a hold section.

  • toolface (float) – The desired toolface for the projection from the survey tool to the bit.

  • step (float) – The desired survey interval for the projected wellpath to the target.

Returns:

node

Return type:

welleng.survey.Survey obj

welleng.survey.slice_survey(survey: Survey, start: int, stop: int | None = None) Survey[source]

Take a slice from a welleng.survey.Survey object.

Parameters:
  • survey (welleng.survey.Survey object)

  • start (int) – The start index of the desired slice.

  • stop (int (default: None)) – The stop index of the desired slice, else the remainder of the well bore TD is the default.

Returns:

s – A survey object of the desired slice is returned.

Return type:

welleng.survey.Survey object

welleng.survey.splice_surveys(surveys: list) Survey[source]

Join together an ordered list of surveys for a well (for example, a list of surveys with a different error model for each survey).

Parameters:

surveys (list of welleng.survey.Survey objects) – The first survey in the list is assumed to be the shallowest and the survey header data is taken from this well. Subsequent surveys are assumed to be ordered by depth, with the first md of the next survey being equal to the last md of the previous survey.

Returns:

spliced_survey – A single survey consisting of the input surveys placed together.

Return type:

welleng.survey.Survey object

Notes

The returned survey will include the covariance data describing the well bore uncertainty, but will not include the error models since these may be different for each well section.

welleng.survey.survey_to_df(survey: Survey) DataFrame[source]

Convert a Survey object to a pandas DataFrame.

Parameters:

survey (Survey) – A Survey object.

Returns:

DataFrame with columns for MD, inclination, azimuths, positions, DLS, toolface, build rate, and turn rate.

Return type:

pd.DataFrame

welleng.survey.tortuosity_index(survey: Survey, rtol: float = 0.01, dls_tol: float | None = None, data: bool = False, **kwargs: Any) ndarray | dict[source]

Calculate the Tortuosity Index (TI), a native-3D variant of the method of Ashok et al. ([IADD presentation](https://www.iadd-intl.org/media/files/files/47d68cb4/iadd-luncheon-february-22-2018-v2.pdf)) and D’Angelo et al. (SPE/IADC-194099-MS).

The trajectory is split into curve-turn / hold sections in 3D via normal-vector continuity (see _get_ti_data()); each section’s (L_cs / L_xs - 1) term is accumulated, scaled by n / (n + 1) and normalized by 1 / L_c, then by kappa (1e7 per SPE/IADC-194099-MS). L_cs is the along-hole (arc) distance from the section start to each station and L_xs the corresponding straight-line (chord) distance.

TI is not dimensionless: the result scales with the unit of length, so coeff defaults to 0.3048 to express L_c in feet and match the published reference ranges. See modified_tortuosity_index() for the dimensionless variant.

Parameters:
  • survey (welleng.survey.Survey)

  • rtol (float) – Relative tolerance for normal-vector continuity (also used as atol).

  • dls_tol (float or None) – If not None, also require dogleg-severity continuity within this tolerance.

  • data (bool) – If True, return a dict of intermediate properties.

  • **kwargscoeff (unit conversion, default 0.3048 -> feet) and kappa (scale factor, default 1e7) may be overridden.

Returns:

ti

Return type:

ndarray or dict

welleng.survey.tortuosity_views(profile: ArrayLike, md: ArrayLike, target_md: float | None = None) dict[source]

Derive total, remaining and local readings from a tortuosity profile.

Parameters:
  • profile (array_like) – A per-station tortuosity index profile (TI or MTI), monotonic non-decreasing.

  • md (array_like) – Measured depth at each station, same length as profile.

  • target_md (float or None) – Reference depth for the remaining calculation; defaults to the last station (total depth).

Returns:

total (float, the profile value at target_md / end), remaining (ndarray, total minus the profile — what is left to accumulate from each station), local (ndarray, the along-hole gradient d(profile)/d(md) — the rate of tortuosity accumulation).

Return type:

dict

welleng.survey.tvd_turning_points(survey: Survey) ndarray[source]

The measured depths at which the well’s TVD turns (passes horizontal).

Dimensionless geometry – delegates to MinCurve.tvd_turning_points() (Sawaryn & Thorogood 2005, SPE-84246-PA, Eq. 31). Kept as a module function for back-compatibility.

welleng.surface module

Gridded surfaces (e.g. seismic horizons) — a light geometry primitive.

A Surface is a 2.5-D structural surface: a single value Z (depth or two-way time) over a regular (inline, crossline) grid, with the map projection (X, Y) of every node. It answers the query a trajectory planner needs — what is the surface depth at an arbitrary map location — by bilinear interpolation, and supports above/below/within tests against one or a pair of surfaces (a formation interval = top + base).

This is the open, scalar reference primitive; consumers (e.g. the trajectory planner) use it for target surfaces and geosteering corridors. Populate one from an exchange reader such as welleng.exchange.openworks.read_ow_horizon().

class welleng.surface.Surface(z: ndarray, il: ndarray, xl: ndarray, affine: ndarray, name: str = '', domain: str = 'DEPTH', crs: str | None = None, _inv: ndarray = None)[source]

Bases: object

A gridded 2.5-D surface on a regular (inline, crossline) grid.

Build via from_nodes() (scattered node records) rather than the constructor directly.

Parameters:
  • z (ndarray, shape (n_il, n_xl)) – Surface value at each grid node (NaN where a node is absent).

  • il (ndarray) – Sorted, regularly-spaced inline / crossline coordinate vectors.

  • xl (ndarray) – Sorted, regularly-spaced inline / crossline coordinate vectors.

  • affine (ndarray, shape (2, 3)) – Maps [il, xl, 1] -> [X, Y] (the 3-D survey geometry).

  • name (str) – Horizon / surface name.

  • domain (str) – 'DEPTH' or 'TWT'.

  • crs (str, optional) – Source cartographic system, if known.

__init__(z: ndarray, il: ndarray, xl: ndarray, affine: ndarray, name: str = '', domain: str = 'DEPTH', crs: str | None = None, _inv: ndarray = None) None
affine: ndarray
crs: str | None = None
domain: str = 'DEPTH'
classmethod from_nodes(il, xl, x, y, z, *, name='', domain='DEPTH', crs=None)[source]

Build a Surface from scattered node records.

Parameters:
  • il (array_like) – Per-node inline, crossline, easting, northing and surface value.

  • xl (array_like) – Per-node inline, crossline, easting, northing and surface value.

  • x (array_like) – Per-node inline, crossline, easting, northing and surface value.

  • y (array_like) – Per-node inline, crossline, easting, northing and surface value.

  • z (array_like) – Per-node inline, crossline, easting, northing and surface value.

il: ndarray
is_below(x, y, tvd)[source]

True where the point (X, Y, tvd) lies below (deeper than) the surface.

NaN surface (off-grid / absent) -> False. Depth increases downward.

name: str = ''
within(base: Surface, x, y, tvd)[source]

True where (X, Y, tvd) is between this surface (top) and base.

A formation interval: top.z_at <= tvd <= base.z_at.

xl: ndarray
z: ndarray
z_at(x, y)[source]

Bilinearly interpolated surface value at map location(s) (X, Y).

Returns NaN outside the grid or where any surrounding node is absent. Accepts scalars or arrays (one surface, many query points).

welleng.target module

Drilling target definitions for wellbore trajectory visualization.

class welleng.target.Target(name, n, e, tvd, shape, locked=0, orientation=0, dip=0, color='green', alpha=0.5, **geometry)[source]

Bases: object

A geometric target zone in 3D space for wellbore trajectory planning.

Represents a target area (circle, ellipse, rectangle, or polygon) at a given subsurface location, with optional orientation and dip. Requires vedo for visualization.

name

Identifier for the target.

Type:

str

n

Northing coordinate.

Type:

float

e

Easting coordinate.

Type:

float

tvd

True vertical depth.

Type:

float

shape

Target geometry type.

Type:

str

locked

Lock state of the target.

Type:

int

orientation

Rotation angle about the vertical axis in degrees.

Type:

float

dip

Dip angle of the target plane in degrees.

Type:

float

color

Display color for rendering.

Type:

str

alpha

Opacity for rendering (0.0 to 1.0).

Type:

float

geometry

Shape-specific dimensional parameters.

Type:

dict

__init__(name, n, e, tvd, shape, locked=0, orientation=0, dip=0, color='green', alpha=0.5, **geometry)[source]

Initialize a Target.

Parameters:
  • name (str) – Identifier for the target.

  • n (float) – Northing coordinate (meters).

  • e (float) – Easting coordinate (meters).

  • tvd (float) – True vertical depth (meters).

  • shape (str) – Target geometry type. One of ‘circle’, ‘ellipse’, ‘rectangle’, or ‘polygon’.

  • locked (int, optional) – Lock state of the target (0 = unlocked).

  • orientation (float, optional) – Rotation angle about the vertical axis in degrees.

  • dip (float, optional) – Dip angle of the target plane in degrees.

  • color (str, optional) – Display color for rendering.

  • alpha (float, optional) – Opacity for rendering (0.0 to 1.0).

  • **geometry (dict) – Shape-specific parameters. For ‘circle’: radius. For ‘ellipse’: radius_1, radius_2, res. For ‘rectangle’: pos1, pos2.

Raises:

AssertionError – If vedo is not installed, shape is invalid, or geometry keys do not match the expected keys for the shape.

plot_data()[source]

Generate a vedo mesh object for rendering the target.

Currently supports the ‘circle’ shape. The target is positioned at (n, e, tvd) and rotated according to dip and orientation.

Returns:

A vedo geometry object representing the target, with the target name assigned to its flag attribute.

Return type:

vedo object

welleng.torque_drag module

Wellbore torque and drag calculations based on Johancsik et al. (SPE 11380-PA).

class welleng.torque_drag.HookLoad(survey, wellbore, string, fluid_density, step=30, name=None, ff_range=(0.1, 0.4, 0.1))[source]

Bases: object

Hookload (broomstick) plot model for running or pulling a string.

get_ff_range(ff_range)[source]

Compute hookloads across a range of friction factors.

get_data()[source]

Retrieve computed hookload data.

figure()[source]

Create a plotly broomstick plot of hookload vs friction factor.

__init__(survey, wellbore, string, fluid_density, step=30, name=None, ff_range=(0.1, 0.4, 0.1))[source]

A class for calculating the hookload or broomstick plot data for running or pulling a string in a wellbore.

Parameters:
  • survey (welleng.survey.Survey instance) – The well trajectory of the scenario being modelled.

  • wellbore (welleng.architecture.WellBore instance) – The well bore architecture of the scenario being modelled.

  • string (welleng.architecture.BHA or welleng.architecture.CasingString)

  • instance – The string being run inside the well bore for the scenario being modelled.

  • fluid_density (float) – The density (in SG) of the fluid in the well bore.

  • step (float) – The measured depth step distance in meters to move the string.

  • name (str) – The name of the scenario being modeled.

  • ff_range – The start, stop and step for the range of friction factors to be used in the hookload calculations.

figure()[source]

Generate a plotly hookload (broomstick) figure.

Returns:

Hookload plot with pickup, slackoff, and rotating traces.

Return type:

plotly.graph_objects.Figure

get_data()[source]

Run torque-drag calculations for each friction factor and depth step.

get_ff_range(ff_range)[source]

Expand the friction factor range into a list of values.

Parameters:

ff_range (tuple of float) – (start, stop, step) for the friction factor range.

class welleng.torque_drag.TorqueDrag(survey, wellbore, string, fluid_density, name=None, wob=None, tob=None, overpull=None)[source]

Bases: object

Torque and drag model for a string in a wellbore.

Computes axial tension and torsion profiles along a drillstring or casing string for pickup, slackoff, rotating, and drilling scenarios using the soft-string (Johancsik) method.

add_survey_points_from_strings(strings)[source]

Add string section boundaries as survey station points.

get_buoyancy_factors()[source]

Calculate buoyancy factors for each survey interval.

get_inc_average()[source]

Calculate average inclination per interval.

get_inc_delta()[source]

Calculate inclination change per interval.

get_azi_delta()[source]

Calculate azimuth change per interval.

get_characteristic_od(strings)[source]

Determine effective OD for each survey interval from string data.

get_weight_buoyed_and_radius(strings)[source]

Calculate buoyed weight and bend radius per interval.

get_coeff_friction_sliding(strings)[source]

Get sliding friction coefficients per interval from string data.

get_forces_and_torsion(mode, friction)[source]

Calculate axial forces and torque along the wellbore.

figure()[source]

Create a plotly figure of string tension and torque.

__init__(survey, wellbore, string, fluid_density, name=None, wob=None, tob=None, overpull=None)[source]

A class for calculating wellbore torque and drag, based on the “Torque and Drag in Directional Wells–Prediction and Measurement (SPE 11380-PA) by C.A. Johancsik et al.

Parameters:
  • survey (welleng.survey.Survey instance) – The well trajectory of the scenario being modelled.

  • wellbore (welleng.architecture.WellBore instance) – The well bore architecture of the scenario being modelled.

  • string (welleng.architecture.BHA or welleng.architecture.CasingString)

  • instance – The string being run inside the well bore for the scenario being modelled.

  • fluid_density (float) – The density (in SG) of the fluid in the well bore.

  • name (str) – The name of the scenario being modeled.

  • wob (float) – The compressive force (weight on bit) applied at the bottom of the string in N.

  • tob (float) – The torque (torque on bit) applied at the bottom of the string in N.m.

  • overpull (float) – The tension applied at the bottom of the string in N.

add_survey_points_from_strings()[source]

Check that there’s survey stations for the top and bottoms of the string sections to ensure that the torque and drag is calculated for these key locations.

figure()[source]

Generate a plotly figure of tension and torque vs depth.

Returns:

Figure with tension (left) and torque (right) subplots.

Return type:

plotly.graph_objects.Figure

get_azi_delta()[source]

Calculate the azimuth change between consecutive survey stations.

get_buoyancy_factors()[source]

Determine the buoyancy factor for each string section and add it to the string sections dict.

get_characteristic_od(section)[source]

Return the effective outer diameter for a string section.

Uses the tooljoint OD if available, otherwise the pipe body OD.

Parameters:

section (int) – Index of the string section.

Returns:

The characteristic outer diameter in meters.

Return type:

float

get_coeff_friction_sliding()[source]

Build an array of sliding friction coefficients mapped to survey stations.

get_forces_and_torsion(wob=False, tob=False, overpull=False)[source]

Compute tension and torque profiles along the string.

Iterates from bit to surface, accumulating normal force, axial tension, and torsion at each survey station. Results are stored in self.tension and self.torque dicts keyed by load case.

Parameters:
  • wob (float, optional) – Weight on bit in Newtons. Must be provided with tob.

  • tob (float, optional) – Torque on bit in N*m. Must be provided with wob.

  • overpull (float, optional) – Additional tension at the bit in Newtons.

get_inc_average()[source]

Calculate the average inclination between consecutive survey stations.

get_inc_delta()[source]

Calculate the inclination change between consecutive survey stations.

get_weight_buoyed_and_radius()[source]

Calculate buoyed weight and contact radius for each survey interval.

welleng.torque_drag.buoyancy_factor(fluid_density, string_density=7.85)[source]
Parameters:
  • fluid_density (float) – The density of the fluid in SG.

  • string_density (float) – The density of the string, typically made from steel.

Returns:

result – The buoyancy factor when when multiplied against the string weight yields the bouyed string weight.

Return type:

float

welleng.torque_drag.figure_hookload(hl, units={'depth': 'ft', 'tension': 'lbf', 'torque': 'ft_lbf'})[source]

Create a plotly hookload (broomstick) figure.

Parameters:
  • hl (HookLoad) – Completed hookload model instance.

  • units (dict, optional) – Unit keys for depth, tension, and torque.

Returns:

Hookload plot with pickup, slackoff, and rotating traces.

Return type:

plotly.graph_objects.Figure

welleng.torque_drag.figure_string_tension_and_torque(td, units={'depth': 'ft', 'tension': 'lbf', 'torque': 'ft_lbf'})[source]

Create a plotly figure showing string tension and torque vs depth.

Parameters:
  • td (TorqueDrag) – Completed torque-drag model instance.

  • units (dict, optional) – Unit keys for depth, tension, and torque.

Returns:

Figure with tension (left) and torque (right) subplots.

Return type:

plotly.graph_objects.Figure

welleng.torque_drag.force_normal(force_tension, inc_average, inc_delta, azi_delta, weight_buoyed)[source]

Calculate the normal contact force between string and wellbore.

Parameters:
  • force_tension (numpy.ndarray) – Axial tension array (pickup, slackoff, rotating) in N.

  • inc_average (float) – Average inclination of the interval in radians.

  • inc_delta (float) – Inclination change over the interval in radians.

  • azi_delta (float) – Azimuth change over the interval in radians.

  • weight_buoyed (float) – Buoyed weight of the string element in N.

Returns:

Normal force array for each load case in N.

Return type:

numpy.ndarray

welleng.torque_drag.force_tension_delta(weight_buoyed, inc_average, coeff_friction_sliding, force_normal)[source]

Calculate the incremental tension change over one survey interval.

Parameters:
  • weight_buoyed (float) – Buoyed weight of the string element in N.

  • inc_average (float) – Average inclination of the interval in radians.

  • coeff_friction_sliding (float) – Sliding friction coefficient for the interval.

  • force_normal (numpy.ndarray) – Normal contact force for each load case in N.

Returns:

Tension increments for (pickup, slackoff, rotating) in N.

Return type:

tuple of float

welleng.torque_drag.torsion_delta(coeff_friction_sliding, force_normal, radius)[source]

Calculate the incremental torsion change over one survey interval.

Parameters:
  • coeff_friction_sliding (float) – Sliding friction coefficient for the interval.

  • force_normal (float) – Normal contact force for the rotating load case in N.

  • radius (float) – Contact radius of the string element in meters.

Returns:

Torsion increment in N*m.

Return type:

float

welleng.units module

Optional, opt-in unit helpers for welleng (pint-backed).

welleng’s core API works in plain floats — SI inside the engine, field units at the API boundary. This module is a convenience layer for users who want unit-tagged quantities and safe conversions: build a quantity, convert it, or strip it back to a float to feed the core. Importing welleng does not require using any of this, and the core signatures stay plain float.

Everything shares the single welleng ureg registry. Mixing pint registries breaks quantity interoperation, so always build quantities through these helpers (or ureg itself) rather than instantiating a fresh UnitRegistry.

Examples

>>> from welleng import units
>>> round(units.to(units.length(1000, 'ft'), 'm'), 6)
304.8
>>> round(units.to(units.hydrostatic_gradient(units.mud_weight(12.5, 'ppg')), 'psi/ft'), 4)
0.6494
>>> round(units.to_rad(units.deg(180)), 6)
3.141593
welleng.units.PSI_PER_PPG_PER_FT: float = 0.05194805194805195

Hydrostatic gradient of a 1 ppg fluid at STANDARD gravity [psi/ft]. 1 lb / 231 in^3 x 12 in/ft = 0.05194805…, computed here rather than typed.

Do not quote this to more figures than gravity justifies. g varies with latitude by about +-0.27% about standard (9.7803 m/s^2 at the equator to 9.8322 at the poles), so the physically achievable range is roughly 0.051809 to 0.052083 – a 0.53% spread. Any digit beyond the fourth is a statement about where the well is, not about arithmetic.

But it mostly CANCELS, which is why the literature does not fuss about it. Where pressures are expressed as equivalent mud weights – the industry convention – the constant divides out of a hydrostatic balance. For the kick-tolerance gas height, BHP = PP.g.TD and FRAC = LOT.g.shoe are both gradient-derived, so

h = [rho_mud.(TD - shoe) - (PP.TD - LOT.shoe)] / (rho_mud - rho_gas)

contains no g at all; and the Boyle ratio FRAC/BHP = (LOT.shoe)/(PP.TD) cancels it as well. With ideal gas the kick tolerance is COMPLETELY independent of this constant, and welleng’s 0.0521 versus the exact value makes no difference to it.

g survives only where an ABSOLUTE pressure enters that is not gradient-derived with the same constant – an annular-pressure-loss term in psi, or a real-gas Z(P, T) evaluation, which needs a true absolute pressure. Those are second-order.

(Recorded because it was got wrong once: comparing two models by matching their PSI values, rather than their equivalent mud weights, makes the constant appear to matter by several percent. It is an artefact of the comparison.)

class welleng.units.Units(**system: str)[source]

Bases: object

Fast, generic unit-conversion boundary — pint at setup, numpy at runtime.

welleng engines compute in canonical SI (see CANONICAL). This converts user inputs to canonical on ingest and canonical to user units on output, and ONLY there — performance-critical callers use the canonical core directly and bypass this entirely.

Speed: the affine (factor, offset) for each unit pair is computed ONCE via pint and cached; conversion is then pure numpy arithmetic (value * factor (+ offset)), scalar or array, with no pint on the hot path. Affine units (e.g. temperature) carry a non-zero offset; multiplicative units do not.

Generic + reusable across welleng modules (survey, drilling, kick, api).

Examples

>>> u = Units(length="ft", angle="degree")
>>> round(u.to_canonical(1000.0, "length"), 4)        # ft -> m
304.8
>>> round(u.from_canonical(3.14159265, "angle"), 3)   # rad -> deg
180.0
>>> round(u.convert(100.0, "psi", "bar"), 6)          # generic pair
6.894757
__init__(**system: str) None[source]
convert(value: float | ndarray, src: str | Unit, dst: str | Unit) float | ndarray[source]

Convert value (scalar or ndarray) from src to dst units.

from_canonical(value: float | ndarray, quantity: str) float | ndarray[source]

Canonical-SI value -> the user’s units for the named quantity.

to_canonical(value: float | ndarray, quantity: str) float | ndarray[source]

User-units value -> canonical SI for the named quantity.

welleng.units.deg(value: int | float) Quantity[source]

A quantity of value degrees.

>>> deg(90).to('radian').magnitude
1.5707963267948966
welleng.units.force(value: int | float, unit: str | Unit = 'newton') Quantity[source]

A force quantity (default newtons).

welleng.units.gravity_at_latitude(latitude_deg: int | float, altitude_m: int | float = 0.0) float[source]

Normal gravity [m/s^2] at a latitude, WGS84 (Somigliana), with a free-air correction for altitude.

>>> round(gravity_at_latitude(0.0), 5)
9.78033
>>> round(gravity_at_latitude(90.0), 5)
9.83218
welleng.units.heat_transfer_coefficient(value: int | float, unit: str | Unit = 'watt / (meter ** 2 * kelvin)') Quantity[source]

A heat-transfer coefficient (default W/(m²·K)). Uses delta temperature.

welleng.units.hydrostatic_gradient(mud_weight_q: Quantity, unit: str | Unit = 'psi/ft') Quantity[source]

Vertical hydrostatic pressure gradient of a static mud column, dP/dz = ρg.

>>> round(to(hydrostatic_gradient(mud_weight(10, 'ppg')), 'psi/ft'), 4)
0.5195
welleng.units.hydrostatic_gradient_at_latitude(mud_weight_q: Quantity, latitude_deg: int | float, unit: str | Unit = 'psi/ft', altitude_m: int | float = 0.0) Quantity[source]

Hydrostatic gradient using LOCAL gravity rather than standard gravity.

A North Sea well (58 deg N) and a Gulf of Mexico well (28 deg N) differ by 0.26% in gravity. Note this rarely changes an answer – see the cancellation note on PSI_PER_PPG_PER_FT. Use it where an absolute pressure is genuinely needed, not to “improve” a hydrostatic balance expressed in equivalent mud weights, where the constant divides out.

>>> round(to(hydrostatic_gradient_at_latitude(mud_weight(1, 'ppg'), 58.0),
...          'psi/ft'), 7)
0.0520059
welleng.units.length(value: int | float, unit: str | Unit = 'meter') Quantity[source]

A length quantity (default metres).

>>> round(to(length(1, 'ft'), 'm'), 6)
0.3048
welleng.units.magnitude(quantity: Quantity) float[source]

The bare magnitude of quantity (float), unit unchanged.

welleng.units.mass_rate(value: int | float, unit: str | Unit = 'kilogram / second') Quantity[source]

A mass flow rate (default kg/s).

welleng.units.mud_weight(value: int | float, unit: str | Unit = 'ppg') Quantity[source]

A mud-weight (density) quantity (default ppg).

>>> round(to(mud_weight(1.2, 'sg'), 'ppg'), 3)
10.014
welleng.units.mud_weight_from_gradient(gradient_q: Quantity, unit: str | Unit = 'ppg') Quantity[source]

Equivalent mud weight (density) of a hydrostatic gradient, ρ = (dP/dz)/g.

welleng.units.pressure(value: int | float, unit: str | Unit = 'psi') Quantity[source]

A pressure quantity (default psi).

welleng.units.rad(value: int | float) Quantity[source]

A quantity of value radians.

welleng.units.specific_heat(value: int | float, unit: str | Unit = 'joule / (kilogram * kelvin)') Quantity[source]

A specific heat capacity (default J/(kg·K)). Uses delta temperature.

welleng.units.temperature(value: int | float, unit: str | Unit = 'degF') Quantity[source]

An absolute temperature (default degF) — an OFFSET unit.

Convert with to()/to_si() (kelvin): the constant offset is applied. Do NOT use this for gradients or specific heats — use temperature_delta().

>>> round(to(temperature(60, 'degF'), 'kelvin'), 2)
288.71
welleng.units.temperature_delta(value: int | float, unit: str | Unit = 'delta_degF') Quantity[source]

A temperature DIFFERENCE (default delta_degF) — a pure scale.

For the per-degree part of gradients, specific heats and conductivities.

>>> round(to(temperature_delta(1, 'delta_degF'), 'kelvin'), 4)
0.5556
welleng.units.temperature_gradient(value: int | float, unit: str | Unit = 'kelvin / meter') Quantity[source]

A temperature gradient (default K/m) — delta-based (K/m, degF/ft).

welleng.units.thermal_conductivity(value: int | float, unit: str | Unit = 'watt / (meter * kelvin)') Quantity[source]

A thermal conductivity (default W/(m·K)). Uses delta temperature.

welleng.units.thermal_diffusivity(value: int | float, unit: str | Unit = 'meter ** 2 / second') Quantity[source]

A thermal diffusivity (default m²/s).

welleng.units.to(quantity: Quantity, unit: str | Unit) float[source]

Magnitude of quantity expressed in unit (float).

>>> to(pressure(1, 'bar'), 'Pa')
100000.0
welleng.units.to_deg(angle: Quantity) float[source]

Magnitude of angle in degrees (float).

>>> round(to_deg(rad(3.141592653589793)), 6)
180.0
welleng.units.to_rad(angle: Quantity) float[source]

Magnitude of angle in radians (float).

welleng.units.to_si(quantity: Quantity) float[source]

Magnitude of quantity in SI base units (float).

welleng.units.torque(value: int | float, unit: str | Unit = 'newton * meter') Quantity[source]

A torque quantity (default N·m).

Note: torque and energy share a dimensionality in pint, so this does not reject energy units.

welleng.utils module

class welleng.utils.Arc(dogleg, radius)[source]

Bases: object

__init__(dogleg, radius)[source]

Generates a generic arc that can be transformed with a specific pos and vec via a transform method. The arc is initialized at a local origin and kicks off down and to the north (assuming an NEV coordinate system).

Parameters:
  • dogleg (float) – The sweep angle of the arc in radians.

  • radius (float) – The radius of the arc in meters.

Returns:

arc

Return type:

Arc object

transform(toolface, pos=None, vec=None, target=False)[source]

Transforms an Arc to a position and orientation.

Parameters:
  • pos ((,3) array)

  • arc. (The desired position to transform the)

  • vec ((,3) array) – The orientation unit vector to transform the arc.

  • target (bool) – If true, returned arc vector is reversed.

Returns:

  • tuple (pos_new, vec_new)

  • pos_new ((,3) array) – The position at the end of the arc post transform.

  • vec_new ((,3) array) – The unit vector at the end of the arc post transform.

welleng.utils.HLA_to_NEV(survey, HLA, cov=True, trans=None)[source]
class welleng.utils.MinCurve(md, inc, azi)[source]

Bases: object

__init__(md, inc, azi)[source]

Generate LOCAL geometric data from a well bore survey.

Positions (poss) are in local coordinates relative to the origin; MinCurve is azimuth-reference agnostic (the caller knows which reference azi is in) and holds no surface/start position or datum state – that belongs to the owning Survey, which applies the start offset, grid scale factor and NEV interpretation to interpret this local geometry.

Parameters:
  • md (list or 1d array of floats) – Measured depth along well path from a datum.

  • inc (list or 1d array of floats) – Well path inclination (relative to z/tvd axis where 0 indicates down), in radians.

  • azi (list or 1d array of floats) – Well path azimuth (relative to y/North axis), in radians.

Notes

MinCurve is units-agnostic: md may be in any length unit and the geometry is all ratios/angles. Dogleg severity (which needs a per-unit coefficient) is the dls() method, into which the caller injects the coefficient for its units.

interpolate(md, angles=False)[source]

Minimum-curvature position at arbitrary measured depth(s).

Interpolates along the min-curve ARC between the bracketing stations (closed-form half-angle position + its analytic tangent, welleng #308) – never a straight chord. This is the light MD->TVD (and n/e) path for shoes / formation tops etc.: no Survey, no covariance, unit-agnostic (the result’s length unit follows md).

Parameters:

md (float or array_like) – Measured depth(s) to interpolate at.

Returns:

LOCAL (East, North, TVD) position relative to station 0 (same column order as poss) – shape (3,) for scalar md else (n, 3). The caller adds any datum/start offset (MinCurve holds no datum). md outside the survey range yields nan.

Return type:

numpy.ndarray

Notes

NEVER linear-interpolate a trajectory. Agrees with welleng.survey.Survey.interpolate_md() to sub-ulp for doglegs up to ~2 rad; near pi this half-angle form is the better-conditioned of the two (~0.1 ulp vs tens for the balanced-tangential node path).

interpolate_tvd(tvd)[source]

Measured depth(s) where the path reaches a target (local) TVD.

The INVERSE of position-at-md. Reversal-robust: does NOT assume monotonic TVD – each min-curve arc is split at its turning point into monotonic spans and every crossing is solved in closed form (Sawaryn & Thorogood 2005, SPE-84246-PA, Interpolation at a Plane, Eqs. 25-27 + Eq. 1; see _arc_tvd_crossings()). Returns all crossing MDs, sorted.

tvd is in the LOCAL frame (relative to station 0, like the TVD column of poss); Survey layers its datum on top. Empty if the target is never reached.

tvd_turning_points()[source]

Measured depths where the path’s TVD turns (passes horizontal).

Between consecutive turning points TVD is monotonic in MD, so these are where a TVD-domain treatment must be cut to stay single-valued. Closed form – Sawaryn & Thorogood (2005, SPE-84246-PA) Turning Point (Eq. 31) per minimum-curvature leg (see _horizontal_tangent_delta()). MDs are in self.md’s units; empty if TVD is monotonic throughout.

welleng.utils.NEV_to_HLA(survey: Annotated[ndarray[tuple[Any, ...], dtype[_ScalarT]], Literal['N', 3]], NEV: Annotated[ndarray[tuple[Any, ...], dtype[_ScalarT]], Literal['N', 3]], cov: bool = True) Annotated[ndarray[tuple[Any, ...], dtype[_ScalarT]], Literal['N, 3']] | Annotated[ndarray[tuple[Any, ...], dtype[_ScalarT]], Literal['N, 3, 3']][source]

Transform from NEV to HLA coordinate system.

Parameters:
  • survey ((n,3) array of floats) – The [md, inc, azi] survey listing array.

  • NEV ((n,3) or (n,3,3) array of floats) – The NEV coordinates or covariance matrices.

  • cov (boolean) – If cov is True then a (n,3,3) array of covariance matrices is expected, else a (n,3) array of coordinates.

Returns:

HLAs – Either a transformed (n,3) array of HLA coordinates or an (n,3,3) array of HLA covariance matrices.

Return type:

NDArray

welleng.utils.annular_volume(od: float, id: float = None, length: float = None)[source]

Calculate an annular volume.

If no id is provided then circular volume is calculated. If no length is provided, then the unit volume is calculated (i.e. the area).

Units are assumed consistent across input parameters, i.e. the calculation is dimensionless.

Parameters:
  • od (float) – The outer diameter.

  • id (float | None, optional) – The inner diameter, default is 0.

  • length (float | None, optional) – The length of the annulus.

Returns:

annular_volume – The (unit) volume of the annulus or cylinder.

Return type:

float

Examples

In the following example we calculate annular volume along a 1,000 meter section length of 9 5/8” casing inside 12 1/4” hole.

>>> from welleng.utils import annular_volume
>>> from welleng.units import ureg
>>> av = annular_volume(
...     od=ureg('12.25 inch').to('meters),
...     id=ureg(f'{9+5/8} inch').to('meter'),
...     length=ureg('1000 meter')
... )
>>> print(av)
29.096093526301622 meter ** 3
welleng.utils.arc_inc_azi_extrema(vec_a, vec_b, dogleg, vertical_eps=0.0001)[source]

Exact inclination + azimuth extrema over minimum-curvature arcs.

A minimum-curvature segment is a planar circular arc whose unit tangent sweeps t(theta) = vec_a * cos(theta) + u * sin(theta) for theta in [0, dogleg], where u is the in-plane unit vector perpendicular to vec_a (so t(0) = vec_a, t(dogleg) = vec_b). All inputs/outputs are in the NEV (north, east, tvd-down) frame.

Two closed-form results (verified against dense sampling):

  • Inclination inc = acos(t_V) with t_V = A cos + B sin (A = vec_a_V, B = u_V); its extrema are at the arc ends plus the interior critical points theta = phi (+/- pi, + 2pi) that fall in [0, dogleg], phi = atan2(B, A). Exact, <=6 evaluations/arc.

  • Azimuth azi = atan2(t_E, t_N) is strictly monotonic along any circular arc: d(azi)/dtheta numerator = vec_a_N u_E - vec_a_E u_N is constant (the cos^2 + sin^2 cross-terms cancel identically). So its extrema are the two ENDPOINTS, swept in direction sign(K); the total signed swing can exceed 2*pi (arc covers all azimuths).

Parameters:
  • vec_a ((n, 3) array — unit start/end tangents (NEV).)

  • vec_b ((n, 3) array — unit start/end tangents (NEV).)

  • dogleg ((n,) array — subtended (dogleg) angle of each arc, radians.)

  • vertical_eps (float — arcs whose minimum inclination is below this (radians)) – pass through vertical, where azimuth is singular; passes_vertical is flagged and the azimuth span should be treated as full-wrap by callers.

Notes

At dogleg exactly pi (antiparallel tangents, vec_b = -vec_a) the arc plane – hence u – is not recoverable from vec_a/vec_b alone; such arcs are treated as degenerate (constant inc/azi, zero swing). This is a measure-zero case for real min-curvature/CLC arcs; near-pi is exact.

Returns:

  • dict with (n,)-arrays (inc_min, inc_max (radians); azi_start,)

  • azi_end (radians, in (-pi, pi]); azi_swing (signed total azimuth

  • change, radians; abs >= 2*pi => all azimuths covered);

  • passes_vertical (bool).

welleng.utils.arc_step(v1, v2, theta, dmd, x)[source]

Minimum-curvature arc kernel – THE single home (welleng #308).

Every min-curve arc evaluation in the library routes through this one function so the derivation lives ONCE: MinCurve.interpolate (interior MD query), min_curve_step() (station construction, x = dmd) and welleng.connector.interpolate_curve() (dense arc render) are all callers, not re-derivations of it.

Parameters:
  • v1 ((n, 3) array) – Start / end UNIT tangents of a min-curve leg, in ANY consistent orthonormal basis – the result comes back in that SAME basis (the kernel is coordinate-agnostic; callers pass [E, N, V] or [N, E, V] and read it back in kind).

  • v2 ((n, 3) array) – Start / end UNIT tangents of a min-curve leg, in ANY consistent orthonormal basis – the result comes back in that SAME basis (the kernel is coordinate-agnostic; callers pass [E, N, V] or [N, E, V] and read it back in kind).

  • theta ((n,) array) – Leg dogleg (radians).

  • dmd ((n,) array) – Leg length.

  • x ((n,) array) – Arc length from the v1 station to the query point (0 <= x <= dmd); x = dmd gives the far station.

Returns:

disp, tangent – Local displacement from the v1 station to the query point, and the unit tangent there.

Return type:

(n, 3) arrays

Notes

Position is the half-angle form – the symbolic-identity rewrite of the canonical R[sin(phi) v1 + (1-cos(phi)) u] that pairs each vector sum with ITS OWN denominator (|v1+v2| = 2cos(theta/2), |v1-v2| = 2sin(theta/2)) so neither ratio amplifies as theta -> pi (~0.1 ulp near pi vs tens of ulp for the /sin and rf = (2/phi) tan(phi/2) forms). It is applied for theta in [0, pi] (the get_dogleg range). The tangent is the Rodrigues u-form cos(phi) v1 + sin(phi) u with u = (v2 - cos(theta) v1)/sin(theta): a one-time 1/sin set-up with no per-query amplifier, and – unlike the normalise-the-derivative shortcut – it keeps the correct sign for theta > pi (the long-way arcs interpolate_curve renders). The exact antiparallel turn (theta = pi, arc plane undetermined) is left as the start tangent.

welleng.utils.cov_from_vec(arr)[source]

Returns a (n, 3, 3) covariance matrix from an (n, 3) array via outer product.

Parameters:

arr ((n, 3) array) – Array of vector components.

Return type:

(n, 3, 3) array

welleng.utils.decimal2dms(decimal: tuple | ndarray[tuple[Any, ...], dtype[_ScalarT]], ndigits: int = None) tuple | ndarray[tuple[Any, ...], dtype[_ScalarT]][source]

Converts a decimal lat, lon to degrees, minutes and seconds.

Parameters:
  • decimal (tuple | arraylike) – A tuple of (lat, direction) or (lon, direction) or arraylike of ((lat, direction), (lon, direction)) coordinates.

  • ndigits (int (default is None)) – If specified, rounds the seconds decimal to the desired number of digits.

Returns:

dms – An array of (degrees, minutes, seconds, direction).

Return type:

arraylike

Examples

If you want to convert the lat/lon coordinates for Den Haag from decimals to degrees, minutes and seconds:

>>> LAT, LON = [(52.078663, 'N'), (4.288788, 'E')]
>>> dms = decimal2dms((LAT, LON), ndigits=6)
>>> print(dms)
[[52 4 43.1868 'N']
 [4 17 19.6368 'E']]
welleng.utils.dls_from_radius(radius)[source]

Returns the dls in degrees from a radius.

welleng.utils.dms2decimal(dms: tuple | ndarray[tuple[Any, ...], dtype[_ScalarT]], ndigits: int = None) ndarray[tuple[Any, ...], dtype[_ScalarT]][source]

Converts a degrees, minutes and seconds lat, lon to decimals.

Parameters:
  • dms (tuple | arraylike) – A tuple or arraylike of (degrees, minutes, seconds, direction) lat and/or lon or arraylike of lat, lon coordinates.

  • ndigits (int (default is None)) – If specified, rounds the decimal to the desired number of digits.

Returns:

degrees – A tuple or array of lats and/or longs in decimals.

Return type:

arraylike

Examples

If you want to convert the lat/lon coordinates for Den Haag from degrees, minutes and seconds to decimals:

>>> LAT, LON = (52, 4, 43.1868, 'N'), (4, 17, 19.6368, 'E')
>>> decimal = dms2decimal((LAT, LON), ndigits=6)
>>> print(decimal)
[[52.078663 'N']
 [4.288788 'E']]
welleng.utils.dms_from_string(text)[source]

Extracts the values from a string dms x or y or northing or easting.

welleng.utils.errors_from_cov(cov, data=False)[source]
Parameters:
  • cov ((n, 3, 3) array) – The error covariance matrices.

  • data (bool (default: False)) – If True returns a dictionary, else returns a list.

welleng.utils.get_angles(vec: Annotated[ndarray[tuple[Any, ...], dtype[_ScalarT]], Literal['N', 3]], nev: bool = False)[source]

Determines the inclination and azimuth from a vector.

Parameters:
  • vec ((n,3) array of floats)

  • nev (boolean (default: False)) – Indicates if the vector is in (x,y,z) or (n,e,v) coordinates.

Returns:

[inc, azi] – A numpy array of incs and axis in radians

Return type:

(n,2) array of floats

welleng.utils.get_arc(dogleg, radius, toolface, pos=None, vec=None, target=False) tuple[source]

Creates an Arc instance and transforms it to the desired position and orientation.

Parameters:
  • dogleg (float) – The swept angle of the arc (arc angle) in radians.

  • radius (float) – The radius of the arc (in meters).

  • toolface (float) – The toolface angle in radians (relative to the high side) to rotate the arc at the desired position and orientation.

  • pos ((,3) array) – The desired position to transform the arc.

  • vec ((,3) array) – The orientation unit vector to transform the arc.

  • target (bool) – If true, returned arc vector is reversed.

Returns:

  • tuple of (pos_new, vec_new, arc.delta_md)

  • pos_new ((,3) array) – The position at the end of the arc post transform.

  • vec_new ((,3) array) – The unit vector at the end of the arc post transform.

  • arc.delta_md (int) – The arc length of the arc.

welleng.utils.get_dogleg(inc1, azi1, inc2, azi2)[source]

Compute the dogleg angle between two survey stations (vectorised).

Uses the numerically stable Haversine form to avoid arccos precision loss at small angles.

Parameters:
  • inc1 (float or array — inclination / azimuth at station 1 (radians))

  • azi1 (float or array — inclination / azimuth at station 1 (radians))

  • inc2 (float or array — inclination / azimuth at station 2 (radians))

  • azi2 (float or array — inclination / azimuth at station 2 (radians))

Returns:

dogleg

Return type:

float or array — dogleg angle in radians

welleng.utils.get_nev(pos, start_xyz=array([0., 0., 0.]), start_nev=array([0., 0., 0.]))[source]

Convert [x, y, z] coordinates to [n, e, tvd] coordinates.

Parameters:
  • pos ((n,3) array of floats) – Array of [x, y, z] coordinates

  • start_xyz ((,3) array of floats) – The datum of the [x, y, z] cooardinates

  • start_nev ((,3) array of floats) – The datum of the [n, e, tvd] coordinates

Return type:

An (n,3) array of [n, e, tvd] coordinates.

welleng.utils.get_rf(dogleg)[source]

Compute the ratio factor (RF) for minimum curvature (vectorised).

Returns 1.0 where dogleg is 0 (limit of the function as dogleg → 0).

Parameters:

dogleg (float or array — dogleg angle(s) in radians)

Returns:

rf

Return type:

float or array — ratio factor(s)

welleng.utils.get_sigmas(cov, long=False)[source]

Extracts the sigma values of a covariance matrix along the principle axii.

Parameters:

cov ((n,3,3) array of floats)

Returns:

arr

Return type:

(n,3) array of floats

welleng.utils.get_toolface(pos1: ndarray[tuple[Any, ...], dtype[_ScalarT]], vec1: ndarray[tuple[Any, ...], dtype[_ScalarT]], pos2: ndarray[tuple[Any, ...], dtype[_ScalarT]]) ndarray[tuple[Any, ...], dtype[_ScalarT]][source]

Returns the toolface(s) of offset position(s) relative to reference positions and vectors. Accepts either single (3,) arrays or batches of (n, 3) arrays; all three arguments must have the same leading dimension.

Parameters:
  • pos1 (ndarray, shape (3,) or (n, 3)) – The reference NEV coordinate(s), e.g. current location.

  • vec1 (ndarray, shape (3,) or (n, 3)) – The reference NEV unit vector(s), e.g. current direction.

  • pos2 (ndarray, shape (3,) or (n, 3)) – The offset NEV coordinate(s), e.g. a target position.

Returns:

toolface – The toolface(s) in radians [0, 2π) to pos2 from pos1 along vec1. Returns a scalar float when single (3,) inputs are given.

Return type:

float or ndarray

welleng.utils.get_toolface_fast(pos1: ndarray[tuple[Any, ...], dtype[_ScalarT]], vec1: ndarray[tuple[Any, ...], dtype[_ScalarT]], pos2: ndarray[tuple[Any, ...], dtype[_ScalarT]]) float[source]

Returns the toolface of a single offset position using a direct closed-form expression — approximately 12× faster than get_toolface for scalar inputs.

Suitable when pos1, vec1 and pos2 are all individual (3,) arrays. For batch use, prefer the vectorised get_toolface.

Parameters:
  • pos1 (array-like, shape (3,)) – The reference NEV coordinate, e.g. current location.

  • vec1 (array-like, shape (3,)) – The reference NEV unit vector, e.g. current direction.

  • pos2 (array-like, shape (3,)) – The offset NEV coordinate, e.g. a target position.

Returns:

toolface – The toolface in radians [0, 2π) to pos2 from pos1 along vec1.

Return type:

float

welleng.utils.get_transform(survey)[source]

Determine the transform for transforming between NEV and HLA coordinate systems.

Parameters:

survey ((n,3) array of floats) – The [md, inc, azi] survey listing array.

Returns:

transform

Return type:

(n,3,3) array of floats

welleng.utils.get_unit_vec(vec)[source]
welleng.utils.get_vec(inc, azi, nev=False, r=1, deg=True)[source]

Convert inc and azi into a vector.

Parameters:
  • inc (array of n floats) – Inclination relative to the z-axis (up)

  • azi (array of n floats) – Azimuth relative to the y-axis

  • r (float or array of n floats) – Scalar to return a scaled vector

Returns:

vec – An (n,3) array of vectors

Return type:

arraylike

welleng.utils.get_xyz(pos, start_xyz=[0.0, 0.0, 0.0], start_nev=[0.0, 0.0, 0.0])[source]
welleng.utils.linear_convert(data, factor)[source]
welleng.utils.make_clc_path(toolface1, dogleg1, distance, toolface2, dogleg2, pos0=None, vec0=None, radius=1.0)[source]

Generate a curve-hold-curve (CLC) path from arc parameters.

Builds the path in three steps: first arc, straight hold, second arc. Useful for constructing known-geometry test cases and for quickly prototyping CLC trajectories.

Parameters:
  • toolface1 (float) – Toolface angle for the first curve in radians.

  • dogleg1 (float) – Sweep angle (dogleg) for the first curve in radians.

  • distance (float) – Length of the straight hold section (same units as radius).

  • toolface2 (float) – Toolface angle for the second curve in radians.

  • dogleg2 (float) – Sweep angle (dogleg) for the second curve in radians.

  • pos0 ((3,) array-like, optional) – Start position [N, E, V]. Defaults to [0, 0, 0].

  • vec0 ((3,) array-like, optional) – Start direction unit vector. Defaults to [0, 0, 1] (pointing down).

  • radius (float, optional) – Arc radius for both curves. Defaults to 1.0.

Returns:

pos1, vec1 – end of first arc dist_curve1 – arc length of first curve pos2, vec2 – end of hold section / start of second arc pos3, vec3 – end of second arc dist_curve2 – arc length of second curve

Return type:

dict with keys

welleng.utils.make_cov(a, b, c, long=False)[source]
welleng.utils.make_long_cov(arr)[source]

Build a (n, 3, 3) covariance matrix from the 6 unique upper-triangle elements per station.

Parameters:

arr ((n, 6) array — columns [aa, ab, ac, bb, bc, cc])

Returns:

cov

Return type:

(n, 3, 3) array

welleng.utils.min_curve_step(delta_md, inc1, azi1, inc2, azi2, rf=None)[source]

Compute position increments using minimum curvature (vectorised).

Delegates the geometry to the single arc kernel arc_step() (welleng #308) evaluated at the far station (x = dmd), so a station position can no longer diverge from MinCurve.interpolate / interpolate_curve.

Parameters:
  • delta_md ((n,) array — measured-depth increments)

  • inc1 ((n,) arrays — start inclination / azimuth (radians))

  • azi1 ((n,) arrays — start inclination / azimuth (radians))

  • inc2 ((n,) arrays — end inclination / azimuth (radians))

  • azi2 ((n,) arrays — end inclination / azimuth (radians))

  • rf (ignored — retained for backward compatibility (the arc kernel no longer) – needs a precomputed ratio factor).

Returns:

deltas

Return type:

(n, 3) array — position increments in [N, E, V] order

welleng.utils.pprint_dms(dms, symbols: bool = True, return_data: bool = False)[source]

Pretty prints a (decimal, minutes, seconds) tuple or list.

Parameters:
  • dms (tuple | list) – An x or y or northing or easting (degree, minute, second).

  • symbols (bool (default: True)) – Whether to print symbols for (deg, min, sec).

  • return_data (bool (default: False)) – If True then will return the string rather than print it.

welleng.utils.radius_from_dls(dls)[source]

Returns the radius in meters from a DLS in deg/30m.

welleng.version module

welleng.visual module

Visualization utilities for wellbore trajectories using vedo/VTK and plotly.

welleng.visual.figure(obj, type='scatter3d', **kwargs)[source]

Create a plotly figure from a survey or mesh object.

Parameters:
  • obj (Survey or WellMesh) – A welleng Survey (for scatter3d/panel) or WellMesh (for mesh3d).

  • type (str, optional) – One of ‘scatter3d’, ‘mesh3d’, or ‘panel’.

  • **kwargs – Passed to the underlying plotly figure builder.

Returns:

A plotly Figure instance.

Return type:

plotly.graph_objects.Figure

welleng.visual.get_lines(clearance)[source]

Add lines per reference well interval between the closest points on the reference well and the offset well and color them according to the calculated Separation Factor (SF) between the two wells at these points.

Parameters:

clearance (welleng.clearance.Clearance) – A welleng clearance object.

Returns:

A vedo.Lines object colored by the object’s SF values.

Return type:

vedo.Lines

welleng.visual.plot(data, colors=None, names=None, lines=None, arrows=None, interactive=True, **kwargs)[source]

Convenience function for quick visualization of well meshes.

Parameters:
  • data (WellMesh or list of WellMesh) – The well mesh(es) to plot.

  • colors (list of str, optional) – Per-item colors when data is a list.

  • names (list of str, optional) – Per-item names (currently unused, reserved for legends).

  • lines (vedo object, optional) – Lines to add to the scene.

  • arrows (vedo object, optional) – Arrows to add to the scene.

  • interactive (bool) – Whether to show an interactive window.

Module contents