welleng package
Subpackages
- welleng.catalog package
- welleng.catalog
CatalogCatalogErrorConnectionSpecConnectionSpec.__init__()ConnectionSpec.as_dict()ConnectionSpec.compression_eff_pctConnectionSpec.compression_strength_klbConnectionSpec.connection_id_inConnectionSpec.connection_od_inConnectionSpec.connection_typeConnectionSpec.coupling_length_inConnectionSpec.delta_turn_maxConnectionSpec.delta_turn_minConnectionSpec.external_pressure_eff_pctConnectionSpec.external_pressure_resistance_psiConnectionSpec.from_api()ConnectionSpec.from_tubular()ConnectionSpec.gradeConnectionSpec.internal_pressure_eff_pctConnectionSpec.internal_pressure_resistance_psiConnectionSpec.makeup_loss_inConnectionSpec.makeup_torque_max_ftlbConnectionSpec.makeup_torque_min_ftlbConnectionSpec.makeup_torque_opt_ftlbConnectionSpec.max_bending_deg_per_100ftConnectionSpec.max_load_coupling_face_klbConnectionSpec.shouldering_torque_max_ftlbConnectionSpec.shouldering_torque_min_ftlbConnectionSpec.tension_eff_pctConnectionSpec.tension_strength_klb
CouplingCatalogCouplingSpecPremiumConnectionCatalogPremiumConnectionSpecPremiumConnectionSpec.__init__()PremiumConnectionSpec.as_dict()PremiumConnectionSpec.categoryPremiumConnectionSpec.designationPremiumConnectionSpec.drift_inPremiumConnectionSpec.id_inPremiumConnectionSpec.joint_efficiencyPremiumConnectionSpec.makeup_torque_ftlbPremiumConnectionSpec.pressure_rating_psiPremiumConnectionSpec.spec_sourcePremiumConnectionSpec.vendorPremiumConnectionSpec.vendor_note
TubularSpecTubularSpec.__init__()TubularSpec.as_dict()TubularSpec.collapse_pressure_psiTubularSpec.drift_inTubularSpec.drift_mTubularSpec.gradeTubularSpec.id_inTubularSpec.id_mTubularSpec.internal_yield_pressure_psiTubularSpec.kindTubularSpec.max_yield_psiTubularSpec.min_uts_psiTubularSpec.min_wall_pctTubularSpec.nominal_weight_ppfTubularSpec.od_inTubularSpec.od_mTubularSpec.pipe_body_yield_klbTubularSpec.plain_end_weight_ppfTubularSpec.wall_inTubularSpec.wall_mTubularSpec.yield_paTubularSpec.yield_psi
buttress_coupling_internal_yield_psi()buttress_coupling_thread_strength_klb()buttress_joint_strength_klb()buttress_pipe_thread_strength_klb()collapse_pressure_psi()coupling_connections()grades()internal_yield_pressure_psi()list_sizes()pipe_body_yield_klb()plain_end_weight_ppf()premium_connections()resolve()resolve_coupling()resolve_premium_connection()round_thread_joint_strength_klb()round_thread_pipe_fracture_strength_klb()round_thread_pullout_strength_klb()- Submodules
- welleng.catalog.catalog module
CatalogCatalogErrorConnectionSpecConnectionSpec.__init__()ConnectionSpec.as_dict()ConnectionSpec.compression_eff_pctConnectionSpec.compression_strength_klbConnectionSpec.connection_id_inConnectionSpec.connection_od_inConnectionSpec.connection_typeConnectionSpec.coupling_length_inConnectionSpec.delta_turn_maxConnectionSpec.delta_turn_minConnectionSpec.external_pressure_eff_pctConnectionSpec.external_pressure_resistance_psiConnectionSpec.from_api()ConnectionSpec.from_tubular()ConnectionSpec.gradeConnectionSpec.internal_pressure_eff_pctConnectionSpec.internal_pressure_resistance_psiConnectionSpec.makeup_loss_inConnectionSpec.makeup_torque_max_ftlbConnectionSpec.makeup_torque_min_ftlbConnectionSpec.makeup_torque_opt_ftlbConnectionSpec.max_bending_deg_per_100ftConnectionSpec.max_load_coupling_face_klbConnectionSpec.shouldering_torque_max_ftlbConnectionSpec.shouldering_torque_min_ftlbConnectionSpec.tension_eff_pctConnectionSpec.tension_strength_klb
CouplingCatalogCouplingSpecPremiumConnectionCatalogPremiumConnectionSpecPremiumConnectionSpec.__init__()PremiumConnectionSpec.as_dict()PremiumConnectionSpec.categoryPremiumConnectionSpec.designationPremiumConnectionSpec.drift_inPremiumConnectionSpec.id_inPremiumConnectionSpec.joint_efficiencyPremiumConnectionSpec.makeup_torque_ftlbPremiumConnectionSpec.pressure_rating_psiPremiumConnectionSpec.spec_sourcePremiumConnectionSpec.vendorPremiumConnectionSpec.vendor_note
TubularSpecTubularSpec.__init__()TubularSpec.as_dict()TubularSpec.collapse_pressure_psiTubularSpec.drift_inTubularSpec.drift_mTubularSpec.gradeTubularSpec.id_inTubularSpec.id_mTubularSpec.internal_yield_pressure_psiTubularSpec.kindTubularSpec.max_yield_psiTubularSpec.min_uts_psiTubularSpec.min_wall_pctTubularSpec.nominal_weight_ppfTubularSpec.od_inTubularSpec.od_mTubularSpec.pipe_body_yield_klbTubularSpec.plain_end_weight_ppfTubularSpec.wall_inTubularSpec.wall_mTubularSpec.yield_paTubularSpec.yield_psi
buttress_coupling_internal_yield_psi()buttress_coupling_thread_strength_klb()buttress_joint_strength_klb()buttress_pipe_thread_strength_klb()collapse_pressure_psi()coupling_connections()grades()internal_yield_pressure_psi()list_sizes()pipe_body_yield_klb()plain_end_weight_ppf()premium_connections()resolve()resolve_coupling()resolve_premium_connection()round_thread_joint_strength_klb()round_thread_pipe_fracture_strength_klb()round_thread_pullout_strength_klb()
- welleng.errors package
- Submodules
- welleng.errors.extract_iscwsa_codes module
- welleng.errors.extract_owsg_codes_generic module
- welleng.errors.tool_errors module
ABIXY_TI1()ABIXY_TI2()ABIZ()ABXY_TI1()ABXY_TI2()ABZ()AMID()AMIL()ASIXY_TI1()ASIXY_TI2()ASIXY_TI3()ASIZ()ASXY_TI1()ASXY_TI2()ASXY_TI3()ASZ()AZ()CNA()CNI()DBH()DBHR()DREF()DSF()DST()DroppedTermMBIXY_TI1()MBIXY_TI2()MBXY_TI1()MBXY_TI2()MBZ()MDI()MFI()MSIXY_TI1()MSIXY_TI2()MSIXY_TI3()MSXY_TI1()MSXY_TI2()MSXY_TI3()MSZ()SAG()SAGE()STRICT_LOCATIONSTRICT_TERMSToolErrorXCL()XCLA()XCLH()XYM1()XYM2()XYM3()XYM3E()XYM3L()XYM4()XYM4E()XYM4L()clear_error_model_cache()
- Module contents
- welleng.exchange package
- Submodules
- welleng.exchange.csv module
- welleng.exchange.edm module
- welleng.exchange.ipm module
- welleng.exchange.wbp module
SurveyPointWellPlanadd_comments()add_header()add_location()add_step()add_survey_point()add_targets()add_turn_point()export()get_key()get_parent_survey()get_unit_key()load()save_to_file()string_strip()strip_duplicates()wbp_to_survey()DirSurveyDirSurvey.__init__()DirSurvey.aziDirSurvey.azimuth_provenance()DirSurvey.borehole_nameDirSurvey.convergenceDirSurvey.coord_systemDirSurvey.declinationDirSurvey.dxDirSurvey.dyDirSurvey.incDirSurvey.lateral_displacement()DirSurvey.max_inclinationDirSurvey.mdDirSurvey.n_stationsDirSurvey.north_refDirSurvey.proc_date_msDirSurvey.proc_methodDirSurvey.remarkDirSurvey.surface_coordinates()DirSurvey.surface_units()DirSurvey.to_welleng()DirSurvey.tvd
NLOGClientNLOGClient.__init__()NLOGClient.boreholes()NLOGClient.boreholes_typed()NLOGClient.details()NLOGClient.dir_surveys()NLOGClient.documents()NLOGClient.documents_typed()NLOGClient.fetch_document()NLOGClient.find_documents()NLOGClient.find_log_curves()NLOGClient.id_for_name()NLOGClient.log_documents()NLOGClient.log_documents_typed()NLOGClient.production()NLOGClient.save_document()NLOGClient.stratigraphy()NLOGClient.suggest()NLOGClient.suggest_typed()NLOGClient.survey_documents()
NLOGErrorStratColumnStratIntervalSurfaceCoordinatesaudit_borehole()DumpAuditWellboreAuditWellboreAudit.__init__()WellboreAudit.annulus_radius_mWellboreAudit.duplicate_mdWellboreAudit.grid_actualWellboreAudit.grid_declaredWellboreAudit.grid_rotation_degWellboreAudit.grid_statusWellboreAudit.max_dls_deg30mWellboreAudit.max_incWellboreAudit.max_torsion_degWellboreAudit.n_dls_spikesWellboreAudit.n_plane_inversionsWellboreAudit.n_stationsWellboreAudit.nonmonotonic_mdWellboreAudit.provenanceWellboreAudit.tvd_worst_step_mWellboreAudit.wellbore
audit_dump()load_dump()
- Module contents
- welleng.interpretation package
- welleng.interpretation.forward module
- welleng.interpretation.qc module
- welleng.interpretation.msa module
- Closed form
- Estimability / the geometry gate
MSAResultMSAResult.biasMSAResult.scaleMSAResult.covarianceMSAResult.stdMSAResult.correlationMSAResult.estimableMSAResult.n_stationsMSAResult.condition_numberMSAResult.__init__()MSAResult.axial_estimableMSAResult.biasMSAResult.condition_numberMSAResult.correlationMSAResult.covarianceMSAResult.estimableMSAResult.n_stationsMSAResult.scaleMSAResult.std
apply_sensor_errors()estimate_sensor_errors()
- welleng.interpretation.correction_uncertainty module
CorrectionUncertaintyCorrectionUncertainty.meanCorrectionUncertainty.covarianceCorrectionUncertainty.stdCorrectionUncertainty.n_drawsCorrectionUncertainty.se_stdCorrectionUncertainty.__init__()CorrectionUncertainty.covarianceCorrectionUncertainty.meanCorrectionUncertainty.n_drawsCorrectionUncertainty.se_stdCorrectionUncertainty.std
correction_covariance_mc()
- Module contents
CorrectionUncertaintyCorrectionUncertainty.meanCorrectionUncertainty.covarianceCorrectionUncertainty.stdCorrectionUncertainty.n_drawsCorrectionUncertainty.se_stdCorrectionUncertainty.__init__()CorrectionUncertainty.covarianceCorrectionUncertainty.meanCorrectionUncertainty.n_drawsCorrectionUncertainty.se_stdCorrectionUncertainty.std
DualDepthResultGeomagReferenceMSAResultMSAResult.biasMSAResult.scaleMSAResult.covarianceMSAResult.stdMSAResult.correlationMSAResult.estimableMSAResult.n_stationsMSAResult.condition_numberMSAResult.__init__()MSAResult.axial_estimableMSAResult.biasMSAResult.condition_numberMSAResult.correlationMSAResult.covarianceMSAResult.estimableMSAResult.n_stationsMSAResult.scaleMSAResult.std
QCResultapply_sensor_errors()correction_covariance_mc()dual_depth_difference()earth_rate_components()estimate_sensor_errors()georef_checks()gyro_to_survey()sensor_to_survey()
- welleng.kick_tolerance package
- Model class & scope (what this is, and is NOT)
- WORST-CASE PORE PRESSURE – the stated convention (READ THIS)
- STRING POSITION – the stated convention (READ THIS)
- MAASP – the stated convention (READ THIS)
- INFLUX DENSITY <-> GRADIENT (READ THIS if you compare against a standard)
- UNITS – the field-units contract (READ THIS)
- Layout
AnalyticalKickToleranceAnalyticalKickTolerance.__init__()AnalyticalKickTolerance.already_fracturedAnalyticalKickTolerance.binding_depth_tvdAnalyticalKickTolerance.binding_gas_bottom_tvdAnalyticalKickTolerance.binding_gas_top_tvdAnalyticalKickTolerance.breakpointsAnalyticalKickTolerance.casing_bindsAnalyticalKickTolerance.maasp_governed_by_shoeAnalyticalKickTolerance.maasp_governing_tvdAnalyticalKickTolerance.maasp_limiting_psiAnalyticalKickTolerance.maasp_psiAnalyticalKickTolerance.max_influx_bblAnalyticalKickTolerance.open_hole_capacity_bblAnalyticalKickTolerance.open_hole_unconstrainedAnalyticalKickTolerance.rho_influx_bh_gradient_psi_per_ftAnalyticalKickTolerance.rho_influx_bh_ppgAnalyticalKickTolerance.surface_containment_bbl
GasStateKickInputsKickInputs.D_lotKickInputs.D_tdKickInputs.PPKickInputs.P_aplKickInputs.P_lotKickInputs.T_sKickInputs.T_tdKickInputs.V_dpaKickInputs.Z_sKickInputs.Z_tdKickInputs.__init__()KickInputs.fluidKickInputs.from_survey()KickInputs.ideal_gasKickInputs.inc_shoeKickInputs.kick_intensityKickInputs.kt_thresholdKickInputs.model_revisionKickInputs.rho_gas_sKickInputs.rho_mud
KickResultKickResult.AKickResult.BKickResult.H_gasKickResult.P_tdKickResult.T_influxKickResult.__init__()KickResult.bubble_length_limitedKickResult.capacityKickResult.capacity_negativeKickResult.caseKickResult.maasp_psiKickResult.marginKickResult.model_revisionKickResult.passedKickResult.pp_at_thresholdKickResult.rho_influxKickResult.rho_influx_gradient_psi_per_ftKickResult.threshold
KickToleranceResultKillSheetInputsKillSheetInputs.__init__()KillSheetInputs.annulus_volume_bblKillSheetInputs.front_tvdKillSheetInputs.geothermal_gradient_degf_per_ftKillSheetInputs.kill_mud_weight_ppgKillSheetInputs.maasp_psiKillSheetInputs.mudKillSheetInputs.mud_weight_ppgKillSheetInputs.pump_output_bbl_per_strokeKillSheetInputs.schedule_stepsKillSheetInputs.scr_pressure_psiKillSheetInputs.scr_rate_spmKillSheetInputs.sicp_psiKillSheetInputs.sidp_psiKillSheetInputs.strictKillSheetInputs.string_volume_bblKillSheetInputs.surface_temp_degfKillSheetInputs.tvd_ft
KillSheetResultKillSheetResult.__init__()KillSheetResult.fcp_psiKillSheetResult.icp_psiKillSheetResult.kill_mud_weight_ppgKillSheetResult.minutes_to_bitKillSheetResult.minutes_totalKillSheetResult.notesKillSheetResult.pressure_at()KillSheetResult.scheduleKillSheetResult.strokes_bit_to_surfaceKillSheetResult.strokes_to_bitKillSheetResult.strokes_total
MaaspResultMigrationResultMigrationStepMudModelNogepaResultPumpScheduleWellSectionanalytical_kick_tolerance()annular_capacity()cased_section()column_pressure()drill_kick()evaluate_envelope()fluid_aliases()fluid_presets()fluid_z_density()gas_density_ppg()gas_state_from_density()gas_z()gradient_to_ppg()hall_yarborough_z()influx_column()kill_sheet()linear_temp_profile()maasp()max_influx_contained_at_surface()methane_properties()migrate()nogepa_drilling_kick_tolerance()open_hole_section()ppg_to_gradient()pressure_at_depth()resolve_gas_properties()standing_pseudo_criticals()string_capacity()sutton_pseudo_criticals()swab_kick()swab_worst_bit()- Submodules
- welleng.kick_tolerance.analytical module
AnalyticalKickToleranceAnalyticalKickTolerance.__init__()AnalyticalKickTolerance.already_fracturedAnalyticalKickTolerance.binding_depth_tvdAnalyticalKickTolerance.binding_gas_bottom_tvdAnalyticalKickTolerance.binding_gas_top_tvdAnalyticalKickTolerance.breakpointsAnalyticalKickTolerance.casing_bindsAnalyticalKickTolerance.maasp_governed_by_shoeAnalyticalKickTolerance.maasp_governing_tvdAnalyticalKickTolerance.maasp_limiting_psiAnalyticalKickTolerance.maasp_psiAnalyticalKickTolerance.max_influx_bblAnalyticalKickTolerance.open_hole_capacity_bblAnalyticalKickTolerance.open_hole_unconstrainedAnalyticalKickTolerance.rho_influx_bh_gradient_psi_per_ftAnalyticalKickTolerance.rho_influx_bh_ppgAnalyticalKickTolerance.surface_containment_bbl
analytical_kick_tolerance()max_influx_contained_at_surface()swab_worst_bit()
- welleng.kick_tolerance.core module
- Clean-room scope
- Where the gas properties are evaluated
- Two first-class cases (kept SEPARATE by design)
- Units
KickInputsKickInputs.D_lotKickInputs.D_tdKickInputs.PPKickInputs.P_aplKickInputs.P_lotKickInputs.T_sKickInputs.T_tdKickInputs.V_dpaKickInputs.Z_sKickInputs.Z_tdKickInputs.__init__()KickInputs.fluidKickInputs.from_survey()KickInputs.ideal_gasKickInputs.inc_shoeKickInputs.kick_intensityKickInputs.kt_thresholdKickInputs.model_revisionKickInputs.rho_gas_sKickInputs.rho_mud
KickResultKickResult.AKickResult.BKickResult.H_gasKickResult.P_tdKickResult.T_influxKickResult.__init__()KickResult.bubble_length_limitedKickResult.capacityKickResult.capacity_negativeKickResult.caseKickResult.maasp_psiKickResult.marginKickResult.model_revisionKickResult.passedKickResult.pp_at_thresholdKickResult.rho_influxKickResult.rho_influx_gradient_psi_per_ftKickResult.threshold
P_td_from_A1()annular_capacity_dpa()constant_A()constant_B()drill_kick()fahrenheit_to_rankine()influx_column()influx_volume_A7()pp_at_threshold_A9()ppg_to_psi()resolve_gas_properties()scenario_P_td()swab_kick()
- welleng.kick_tolerance.envelope module
- welleng.kick_tolerance.gas_z module
- welleng.kick_tolerance.gas_z_coolprop module
- welleng.kick_tolerance.geometry module
- welleng.kick_tolerance.migration module
- welleng.kick_tolerance.monotonicity module
- welleng.kick_tolerance.nogepa module
Submodules
welleng.architecture module
- 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_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
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 consumers
of the SF 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:
objectThe standard’s graded acceptance criteria, with an explicit operator override.
Construct the standard via
standard(); tighten it viawith_operator_floor().classifymaps an SF to aVerdict.- __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 whateverdataclasses.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_minbelow the mandatorySF = 1is 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:
objectThe 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 ownAcceptanceCriteria.to_dict().
- welleng.acceptance.classify(sf: float, criteria: AcceptanceCriteria | None = None) Verdict[source]
Classify
sfagainstcriteria(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 | ndarray | None = None, Ro: float | ndarray | None = None, kop_depth: float = -inf)[source]
Bases:
objectInitialize 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, (n,) array or None) – The openhole radius of the reference borehole (in meters). The clearance reads the radius from
reference.radius; set it there (Survey(radius=...)). If given,Rrmust equal that radius at every station, otherwise aValueErroris raised. Default None.Ro (float, (n,) array or None) – The openhole radius of the offset borehole (in meters), read from
offset.radius, with the same rule asRr. Default None.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 | ndarray | None = None, Ro: float | ndarray | None = None, 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
Clearancesubclass.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,Smandsigma_pa. Because only the metric differs, the exact factor is>=the pedal factor at every station (Kantorovich). Comparing two differentClearancesubclasses 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:
ClearanceThe ISCWSA separation factor between a reference and an offset well (Sawaryn et al., 2017, SPE-187073-MS): the centre-to-centre distance less the two radii and the surface margin, over k times the pedal-curve radius of the two wells’ covariances summed, with the project-ahead uncertainty. The two covariances are summed as independent, as the rule and the ISCWSA common practice prescribe (Current Common Practice in Collision Avoidance Calculations, 2017, §2.9).
Reference: the ISCWSA standard set of well paths for evaluating clearance scenarios (r4, 17 May 2017) lists Rev 4 covariances generated with the first station’s attitude error over the whole first leg and the surface position exact. With
SurveyHeader(first_station_allowance=True, tie_on=True)on its surface wells, and each well’s grid convergence at its own slot as the set states it, the separation factors reproduce to within 0.002 wherever they are below 10; with the default header, which follows ISCWSA’s Rev 4 example calculator, to within 0.01. Its sidetrack (offset 10) takesfirst_station_allowance="uncorrelated"and is compared withkop_depthat the kick-off, which re-zeros the reference there: both wells’ pedal-curve radii then agree with the set’s to its two-decimal rounding. The set computes its factors from those rounded radii, which bounds the agreement where the radii are small.Parameters:
- clearance_args: List
See ‘welleng.clearance.Clearance` for args.
- minimize_sf: bool
If True (default), the separation-factor minimum between each reference station that is a local minimum and its neighbours is found (bounded search on each reference leg) and, where it is below the station’s value, inserted into ref as an interpolated station with its position, attitude and covariance interpolated on the arc (Survey.interpolate_mds). The value reported there is the minimum the search found.
- 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_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:
ClearanceAnti-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
Clearancebase (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 marginSm, andsigma_pa^2 Iis the isotropic project-ahead floor (mirroring the separation rule’ssigma_paterm), 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 < 1means 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_nevis populated).offset (welleng.survey.Survey) – The two wells (with an error model so
cov_nevis 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 > 0guarantees a positive-definite combined covariance; set0for 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. Each candidate is refined across its neighbouring stations and, separately, along each station edge (one well held at a station): the separation factor has a kink at a station, and the minimum can lie on it.
- sf
Separation factor at each reference station;
min(sf) < 1is a collision.- Type:
numpy.ndarray
- min_sf
The governing (minimum) separation factor over all stations.
- Type:
float
- property min_sf
The governing (minimum) separation factor;
< 1is 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:
ClearanceClass 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.
- 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 covarianceSigma_survey + Sigma_other.A collision check between two uncertain wells should use the combined (relative-position) uncertainty
Sigma_ref + Sigma_off(the variance ofP_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 —
surveyinflated by the combined covariance and the combined hole radii (survey.radius + other.radius + Sm) — intended to be tested against the centreline ofother(e.g. viamesh.contains(other.pos_nev)or atrimeshCollisionManageragainst a zero-uncertainty tube). It is the mesh/visualisation counterpart ofMahalanobisClearanceand is consistent with it (same RSS combined covariance), but not numerically identical: it omits the project-ahead floorsigma_pa, mapsother’s covariance by nearest Euclidean station, and discretises the surface inton_vertsfacets. For the exact pairwise separation factor useMahalanobisClearance; 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 ontosurveyby nearest position and folded into the returned mesh.other (welleng.survey.Survey) – The two wells.
other’s covariance and hole radius are mapped ontosurveyby 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_fitdefaults to “circumscribed” so the polygon never under-counts the ellipse).polygon_fit – Passed through to
welleng.mesh.WellMesh(polygon_fitdefaults to “circumscribed” so the polygon never under-counts the ellipse).Sm – Passed through to
welleng.mesh.WellMesh(polygon_fitdefaults 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.combination module
Combining two overlapping surveys of the SAME well into one best estimate.
This is the fusion (uncertainty-reducing) counterpart to
welleng.conditioning, which computes the two-well difference
covariance for anti-collision. Here two surveys (e.g. MWD + a gyro run)
cover an overlapping measured-depth interval of ONE well; their independent
error sources let a weighted combination beat either survey.
The estimator is the Best Linear Unbiased Estimator (BLUE) for two correlated estimates of the same quantity – equivalently the weighted least-squares / inverse-covariance weighting described by Chia et al. (2003), Ledroz et al. (2016) and Bang (SPE-195621). It is NOT a Kalman filter: there is no time/sequence, just two estimates of one position. (Kalman is the right tool for the forward-carry extension – a persistent systematic state propagated into a later section – which is deliberately out of scope here.)
Given two per-station covariances Sigma_A, Sigma_B and the
cross-covariance C = cov(x_A, x_B) from their SHARED error sources:
Sigma_c = Sigma_A - (Sigma_A - C)(Sigma_A + Sigma_B - C - C^T)^-1 (Sigma_A - C)^T x_c = x_A + (Sigma_A - C)(Sigma_A + Sigma_B - C - C^T)^-1 (x_B - x_A)
Key property (the correctness anchor): error components that are fully
shared (common-mode) between the two surveys – captured in C – do NOT
reduce. Only the independent part is averaged down. With C = 0 (fully
independent – e.g. every Volve survey tool is flagged correlate="N") this
collapses to the classic information-add (Sigma_A^-1 + Sigma_B^-1)^-1.
References
Chia, C.R. et al. (2003) – rigorous weighted averaging of overlapping surveys.
Ledroz, A. et al. (2016) – combined-IPM construction (constant weights).
Bang, J. (2019) SPE-195621 – inclination/azimuth-dependent weights.
Torkildsen et al. (2004) SPE-90408 – simultaneous propagation captures the correlation between two tools (otherwise the covariance is underestimated).
- class welleng.combination.ForwardCarry(cov_nominal: ndarray[tuple[Any, ...], dtype[float64]], cov_carried: ndarray[tuple[Any, ...], dtype[float64]], sigma_nominal: ndarray[tuple[Any, ...], dtype[float64]], sigma_carried: ndarray[tuple[Any, ...], dtype[float64]], reduction_factor: ndarray[tuple[Any, ...], dtype[float64]])[source]
Bases:
objectResult of carrying a reference-calibrated systematic into deep stations.
- cov_nominal, cov_carried
Per-deep-station covariance before / after carrying the overlap calibration.
cov_carried<=cov_nominal(systematic reduced; random unchanged).- Type:
(m, 3, 3) ndarray
- sigma_nominal, sigma_carried
Worst-direction 1sigma per deep station.
- Type:
(m,) ndarray
- reduction_factor
sigma_nominal / sigma_carried(>= 1).- Type:
(m,) ndarray
- __init__(cov_nominal: ndarray[tuple[Any, ...], dtype[float64]], cov_carried: ndarray[tuple[Any, ...], dtype[float64]], sigma_nominal: ndarray[tuple[Any, ...], dtype[float64]], sigma_carried: ndarray[tuple[Any, ...], dtype[float64]], reduction_factor: ndarray[tuple[Any, ...], dtype[float64]]) None
- cov_carried: ndarray[tuple[Any, ...], dtype[float64]]
- cov_nominal: ndarray[tuple[Any, ...], dtype[float64]]
- reduction_factor: ndarray[tuple[Any, ...], dtype[float64]]
- sigma_carried: ndarray[tuple[Any, ...], dtype[float64]]
- sigma_nominal: ndarray[tuple[Any, ...], dtype[float64]]
- class welleng.combination.FusedSurvey(cov_fused: ndarray[tuple[Any, ...], dtype[float64]], cov_a: ndarray[tuple[Any, ...], dtype[float64]], cov_b: ndarray[tuple[Any, ...], dtype[float64]], sigma_a: ndarray[tuple[Any, ...], dtype[float64]], sigma_b: ndarray[tuple[Any, ...], dtype[float64]], sigma_fused: ndarray[tuple[Any, ...], dtype[float64]], reduction_factor: ndarray[tuple[Any, ...], dtype[float64]], pos_fused: ndarray[tuple[Any, ...], dtype[float64]] | None = None, md: ndarray[tuple[Any, ...], dtype[float64]] | None = None, inc: ndarray[tuple[Any, ...], dtype[float64]] | None = None, azi: ndarray[tuple[Any, ...], dtype[float64]] | None = None, cov_dia: ndarray[tuple[Any, ...], dtype[float64]] | None = None, innovation_mahalanobis: ndarray[tuple[Any, ...], dtype[float64]] | None = None, innovation_flag: ndarray[tuple[Any, ...], dtype[bool]] | None = None)[source]
Bases:
objectResult of BLUE-combining two overlapping surveys, per station.
- cov_fused
The fused covariance – never larger than either input in any direction (equal only where the two surveys are fully correlated).
- Type:
(n, 3, 3) ndarray
- cov_a, cov_b
The input covariances, echoed for comparison.
- Type:
(n, 3, 3) ndarray
- sigma_a, sigma_b, sigma_fused
sqrt(max eigenvalue)– worst-direction 1sigma per station.- Type:
(n,) ndarray
- reduction_factor
min(sigma_a, sigma_b) / sigma_fused– how much the fusion tightens the better of the two inputs (>= 1; 1 = no gain).- Type:
(n,) ndarray
- innovation_mahalanobis, innovation_flag
Set when positions are supplied.
innovation_mahalanobisis the Mahalanobis distance of the overlap innovationd = x_B - x_Aunder the innovation covarianceS = A + B - C - C^T– i.e.sqrt(d^T S^-1 d)per station. Under the null (the two surveys agree within their stated uncertainty)dis zero-mean Gaussian with covarianceS, sod^T S^-1 dis chi-square with 3 dof.innovation_flagisTruewhere it exceedsqc_chi2– a station whose two surveys disagree by more than their combined uncertainty allows, i.e. a bad station or an under-stated input covariance. Because the covariance-EOU conditioning is joint, one such station propagates through the off-diagonals and biases the whole run, so it must be caught BEFORE conditioning, not after.- Type:
(n,) ndarray or None
- md, inc, azi
The fused best-estimate trajectory as a minimum-curvature listing – set only when a trajectory was requested (
combine_surveys(..., return_trajectory=True)).(md, inc, azi)is drawable by conventional trajectory software: the query MDs with the single-arc (reflection) reconstruction of the fused NEV positions. MD is held exact (it is measured); the reconstruction carries a small position residual against the raw BLUE points (typically cm, well below the metre-scale position uncertainty), accumulating to a closure residual at the segment end. Every station is CALCULATED, not measured – the fused path is a derived best estimate, so the whole listing is a calculated product (no measured/calculated split per station). DLS-faithful: the reconstructed dogleg severity tracks the input surveys.Note
This trajectory reconstruction is a concept pending field validation – verified on synthetic and Volve data, not yet across a range of real wells. The single-arc form is exact when a leg is one min-curve arc; its residual grows with the curvature CHANGE across a leg (build/turn transitions), so it is trajectory-shape dependent. For a typical build (2-3 deg/30 m) it is ~1% of the EOU at standard ~30 m spacing, within a couple of per cent to ~120 m, and becomes a large fraction only beyond ~300 m spacing (an under-sampled survey anyway); the remedy there is to sample the continuous fusion on a finer MD grid (calculated inner stations). Where the fused overlap is interior (its last station is not the well’s total depth), stitching to a single-source continuation should absorb the closure residual in the final section (truncate/extend the last leg’s chord so the segment ends at the correct MD).
- Type:
(n,) ndarray or None
- __init__(cov_fused: ndarray[tuple[Any, ...], dtype[float64]], cov_a: ndarray[tuple[Any, ...], dtype[float64]], cov_b: ndarray[tuple[Any, ...], dtype[float64]], sigma_a: ndarray[tuple[Any, ...], dtype[float64]], sigma_b: ndarray[tuple[Any, ...], dtype[float64]], sigma_fused: ndarray[tuple[Any, ...], dtype[float64]], reduction_factor: ndarray[tuple[Any, ...], dtype[float64]], pos_fused: ndarray[tuple[Any, ...], dtype[float64]] | None = None, md: ndarray[tuple[Any, ...], dtype[float64]] | None = None, inc: ndarray[tuple[Any, ...], dtype[float64]] | None = None, azi: ndarray[tuple[Any, ...], dtype[float64]] | None = None, cov_dia: ndarray[tuple[Any, ...], dtype[float64]] | None = None, innovation_mahalanobis: ndarray[tuple[Any, ...], dtype[float64]] | None = None, innovation_flag: ndarray[tuple[Any, ...], dtype[bool]] | None = None) None
- azi: ndarray[tuple[Any, ...], dtype[float64]] | None = None
- cov_a: ndarray[tuple[Any, ...], dtype[float64]]
- cov_b: ndarray[tuple[Any, ...], dtype[float64]]
- cov_dia: ndarray[tuple[Any, ...], dtype[float64]] | None = None
- cov_fused: ndarray[tuple[Any, ...], dtype[float64]]
- inc: ndarray[tuple[Any, ...], dtype[float64]] | None = None
- innovation_flag: ndarray[tuple[Any, ...], dtype[bool]] | None = None
- innovation_mahalanobis: ndarray[tuple[Any, ...], dtype[float64]] | None = None
- md: ndarray[tuple[Any, ...], dtype[float64]] | None = None
- pos_fused: ndarray[tuple[Any, ...], dtype[float64]] | None = None
- reduction_factor: ndarray[tuple[Any, ...], dtype[float64]]
- sigma_a: ndarray[tuple[Any, ...], dtype[float64]]
- sigma_b: ndarray[tuple[Any, ...], dtype[float64]]
- sigma_fused: ndarray[tuple[Any, ...], dtype[float64]]
- welleng.combination.carry_systematic_forward(target, reference, overlap_idx, deep_idx, *, persist=None, obs_subsample=12)[source]
Carry a reference-calibrated systematic into the target’s deep stations.
An independent
referencesurvey (e.g. a gyro) over an overlap constrains thetargetsurvey’s (e.g. MWD) SYSTEMATIC error realisation; because that realisation persists down-leg, the constraint reduces the target covariance at DEEP stations the reference never reached. This is aided-inertial- navigation / Kalman conditioning (adopt, don’t reinvent; cf. ISCWSA-49 2019 gyro+MWD statistical estimation), in the joint-covariance (Schur) form:overlap obs z = target - reference; the cross-covariance from the target’s SHARED systematic sources is cov(X_deep, z) = A_deep Hm^T; conditioned deep cov = cov(X_deep) - cov(X_deep, z) cov(z)^-1 cov(z, X_deep), with cov(z) = Hm Hm^T + reference cov + target random.
(Woodbury-equivalent to the source-space information form P=(I+H^T R^-1 H)^-1, but inverts the well-conditioned cov(z) rather than the ill-conditioned R.) Observability is geometry-dependent: the overlap must excite the systematic terms that matter at depth; validated by MC (tests).
- Parameters:
target (Survey) – Same station set, each with an error model.
referenceis the independent, more-accurate survey over the overlap.reference (Survey) – Same station set, each with an error model.
referenceis the independent, more-accurate survey over the overlap.overlap_idx (array-like of int) – Station indices the reference covers (the calibration interval).
deep_idx (array-like of int) – Target-only station indices to carry the calibration to.
persist (None | str | iterable of str, optional) – Which propagation classes persist between the overlap and the deep section (and therefore carry).
None(default) carries every correlated source. Physically:'global'(geomagnetic reference / declination) persists across BHA AND tool changes – the assumption-free choice;'systematic'(sensor bias/scale/misalignment) persists only if the SAME MWD tool is reused; a source that re-realises (new tool for that class) is independent between deep and overlap and does not carry. e.g.persist='global'for the unconditional declination carry;persist=('global', 'systematic')when the tool is reused.obs_subsample (int) – Cap on the number of overlap observation stations used (keeps the conditioning block modest); evenly spaced. Default 12.
- Return type:
- welleng.combination.combine_surveys(survey_a, survey_b, mds, *, cross=None, return_trajectory=False, space='nev', exclude_dia=None) FusedSurvey[source]
BLUE-combine two overlapping surveys of the same well at arbitrary MDs.
Evaluates each survey’s covariance at every
mdvia the analytical arc-faithful interior covariancewelleng.error.ErrorModel.cov_nev_at()– the correct way to bring both surveys onto a common measured depth (analytical, not the linear covariance interpolation that under-reports near doglegs). Fusion of the resulting covariances is the same BLUE asfuse_covariances(), so the combination is continuous: query it at any MD in the overlap, on or off a survey station.- Parameters:
survey_a (welleng.survey.Survey) – Two surveys of the SAME well over an overlapping MD interval, each with an error model applied (so
survey.erris set).survey_b (welleng.survey.Survey) – Two surveys of the SAME well over an overlapping MD interval, each with an error model applied (so
survey.erris set).mds (float or array-like) – Measured depth(s) at which to evaluate and combine. Must lie within both surveys’ MD range.
cross ((n, 3, 3) ndarray, optional) – Cross-covariance from shared error sources at each MD. Default None == 0 (fully independent – the
correlate="N"case). Fully-shared components do not reduce.return_trajectory (bool, default False) – Also reconstruct the fused best-estimate trajectory – the BLUE position at each MD, recovered to (md, inc, azi) and returned in
FusedSurvey.md,.inc,.azi(with.pos_fused), a min-curve listing of the combined path. The tie-in tangent is taken fromsurvey_aat the first MD;mdsmust be sorted ascending. This is the single-arc (reflection) reconstruction at the query MDs: MD is held exact (measured), DLS-faithful, carrying a small position residual against the raw BLUE points (typically cm, below the metre-scale position uncertainty). See the note onFusedSurvey.md– this reconstruction is a concept pending field validation.space ({"nev", "dia"}, default "nev") –
The space the fusion is performed in.
"nev"– BLUE on the NEV position covariance (as above). Optimises the position estimate; a trajectory (return_trajectory) is then reconstructed from the fused positions, carrying a small residual."dia"– BLUE on the DIA (measured-depth, inclination, azimuth) covariance: fuse the angles per station (MD is the shared index), giving the fused (md, inc, azi) listing directly. The path is minimum-curvature-valid by construction (no reconstruction, no chord>MD residual), and agrees with the NEV fusion within the EOU..md/.inc/.aziand.pos_fusedare the DIA-fused survey;.cov_fusedis the NEV position EOU (the reported ellipsoid);.cov_diais the fused angle covariance. This is the principled space for producing a fused survey – seeprojects/specs/2026-08-26_dia_space_survey_fusion.md(concept pending field validation). The rigorousC_nev = V C_dia V^Tpropagation and the correlated (single-station-lifts-all) form are follow-on work.
exclude_dia (iterable of str, optional (
space="dia"only)) – Error-source codes omitted from the DIA fusion weighting. DefaultNone= auto: the XCL course-length terms are INCLUDED when both surveys useSurveyHeader.xcl_representation == "dia"(then they are a clean angular error – Codling SPE-187249, folded into the right inc/azi component), and EXCLUDED with a warning otherwise (under the default “nev_direct” representation the XCLe_DIAis not a clean measurement error). Pass an explicit iterable to override – e.g.()to force the full budget, or a wider set to also drop MSA / sag-correction / non-physical-misalignment terms once identified (an expert judgment).
- Return type:
- welleng.combination.covariance_block(survey, exclude=None)[source]
The full-well “solid” NEV covariance block
C_well(3n, 3n), SI m^2.The covariated ellipse of uncertainty: instead of
nindependent per-station ellipsoids, the single block matrix whose diagonal (3, 3) blocks are the per-station covariance (equal tosurvey.err.cov_NEVs) and whose off-diagonal blocks are the cross-station correlation from the shared (systematic / global / well / within_pad) error sources. This is the object that lets a constraint at one station propagate to the whole well.The covariated method of Bulychenkov (2026), Covariance EOU: A Joint-Covariance Framework for Wellbore Positioning and Multisensor Data Fusion (v1, doi:10.5281/zenodo.22148756, his Eq. 4) – a matrix expansion of the conventional ISCWSA / Williamson (SPE 67616) per-station calculation (
C_well = sum_e J_e C_e J_e^T). This is welleng’s open implementation of that method. Each correlated source contributesouter(sigma_e_NEV)(one realisation, fully correlated across the stations it spans); each random source contributes its per-stationcov_NEVon the diagonal only.- Parameters:
survey (welleng.survey.Survey) – A survey with an error model applied (
survey.errset).exclude (str or iterable of str, optional) – Error-source name(s) to omit from the block – used by the pillar-2 term swap (
swap_covariated_term()) to drop a toolcode term (e.g. the blanket sag term"vsag"/SAGE) before an external correction-uncertainty covariance is injected in its place.
- Returns:
(3n, 3n) symmetric PSD block covariance in NEV, SI m^2. The (i, j) block
[3i:3i+3, 3j:3j+3]iscov(pos_i, pos_j).- Return type:
numpy.ndarray
Notes
Materialises the full (3n, 3n) matrix, so cost grows with the square of the number of stations.
- welleng.combination.covariance_block_at(survey, mds)[source]
The continuous covariated covariance block at arbitrary measured depths.
Bulychenkov’s integration operator (2026, Eqs. 25-28, v1, doi:10.5281/zenodo.22148756) already propagates the covariance to points of interest along the well; this couples that off-station covariated structure to welleng’s arc-faithful interior covariance for accuracy:
the diagonal (i, i) blocks are the analytical arc-faithful interior covariance
welleng.error.ErrorModel.cov_nev_at()– exact at any MD, on or off a survey station (avoids the ~25% dogleg under-report a full-leg / linear interior incurs mid-leg); andthe off-diagonal (i, j) blocks are the cross-station correlation of the covariated EOU (
covariance_block()), evaluated at the query MDs.
The arc-faithful interior diagonal is welleng’s accuracy refinement; pairing it with the covariated block’s cross-station structure gives a covariated covariance queryable at any MD – which makes the continuous combination of two surveys on different station grids consistent (it is evaluated at the same query MD for both, so the combination is order-independent) and accurate off-station.
- Parameters:
survey (welleng.survey.Survey) – Survey with an error model applied.
mds (array-like) – Query measured depths (within the survey range).
- Returns:
(3k, 3k) symmetric block covariance at the k query MDs.
- Return type:
numpy.ndarray
Notes
Diagonal blocks are exact (
cov_nev_at). Off-diagonal blocks use the correlated sources’ cumulativesigma_e_NEVinterpolated to the query MDs (first-order interior for the cross terms; exact at survey stations).
- welleng.combination.fuse_covariances(cov_a: ndarray[tuple[Any, ...], dtype[float64]], cov_b: ndarray[tuple[Any, ...], dtype[float64]], *, cross: ndarray[tuple[Any, ...], dtype[float64]] | None = None, pos_a: ndarray[tuple[Any, ...], dtype[float64]] | None = None, pos_b: ndarray[tuple[Any, ...], dtype[float64]] | None = None, qc_chi2: float = 16.266) FusedSurvey[source]
BLUE-combine two overlapping surveys’ per-station covariances.
- Parameters:
cov_a ((n, 3, 3) ndarray) – Per-station NEV covariance of each survey, aligned to the SAME stations (interpolate both to a common measured depth first).
cov_b ((n, 3, 3) ndarray) – Per-station NEV covariance of each survey, aligned to the SAME stations (interpolate both to a common measured depth first).
cross ((n, 3, 3) ndarray, optional) – Cross-covariance
C = cov(x_A, x_B)from the error sources shared by both surveys (global terms realised identically; shared systematic terms if the same tool/reference). DefaultNone== 0 == fully independent (thecorrelate="N"case). Fully-shared components inCare preserved (do not reduce).pos_a ((n, 3) ndarray, optional) – NEV positions of each survey; if both given, the fused position is returned in
FusedSurvey.pos_fused, and the overlap innovation is QC-gated (seeqc_chi2andFusedSurvey.innovation_flag).pos_b ((n, 3) ndarray, optional) – NEV positions of each survey; if both given, the fused position is returned in
FusedSurvey.pos_fused, and the overlap innovation is QC-gated (seeqc_chi2andFusedSurvey.innovation_flag).qc_chi2 (float, default 16.266) – Chi-square(3) threshold for the overlap-innovation QC gate (default is the 99.9th percentile; use 11.345 for 99%, 7.815 for 95%). A station whose innovation
d^T S^-1 dexceeds this is flagged inFusedSurvey.innovation_flagand a warning is raised. Only active whenpos_aandpos_bare both supplied.
- Return type:
- welleng.combination.fuse_covariated(survey_target, survey_obs, obs_mds)[source]
Condition a survey’s covariated EOU on another survey’s positions – the covariated MWD-Gyro fusion (single-station-lifts-the-whole-well).
The MWD-gyro fusion of Bulychenkov (2026, Eqs. 29-30, v1, doi:10.5281/zenodo.22148756). Builds the target’s full-well block covariance (
covariance_block()) and conditions it, in the maximum-likelihood / GLS sense (the standard update, SPE 85111 in block form), on the observing survey’s position estimates atobs_mds. Because the target’s shared systematic (declination, reused-tool bias) is one realisation across every station it spans, the reduction propagates through all correlated stations – not only the observed ones – so a gyro over a short interval tightens the whole MWD run (strongest where the systematic has accumulated, i.e. deep). This is the general form that subsumescarry_systematic_forward()(the forward-only slice).- Parameters:
survey_target (welleng.survey.Survey) – The survey being improved (e.g. the MWD run), full well.
survey_obs (welleng.survey.Survey) – The observing survey (e.g. a gyro), providing position constraints.
obs_mds (array-like) – Measured depths (within both surveys) at which
survey_obsobserves.
- Returns:
{"cov_block": (3n,3n) posterior block, "cov_prior": (3n,3n) prior, "sigma_prior": (n,), "sigma_post": (n,), "reduction_factor": (n,)}–sigma_*are worst-direction 1sigma per target station.- Return type:
dict
Notes
The observation is treated as independent of the target (the
correlate="N"case – true for MWD vs gyro, which share no declination error). Where the two genuinely share a systematic, a cross-covariance term is needed (not modelled here). Gyro-model caveat: the gain scales with the gyro’s real accuracy advantage; the standard OWSG gyro model is deliberately conservative, so the lift is small unless a representative (vendor / IFR) gyro model is used.
- welleng.combination.swap_covariated_term(survey, exclude, injected_dia_cov, *, axis='inc')[source]
Covariated NEV block with a toolcode term REPLACED by a correction’s residual-uncertainty covariance – the pillar-2 term swap.
When a survey correction is applied (BHA sag, a depth-stretch model, an MSA solve), the blanket toolcode term that assumed a generic correction is no longer right: it must be replaced by the residual uncertainty of the ACTUAL correction for this BHA / hole. This returns:
covariance_block(survey, exclude=exclude) + propagate(injected_dia_cov)
i.e.
C_wellwith the named toolcode term(s) dropped and the external correction-uncertainty covariance (fromwelleng.interpretation.correction_covariance_mc(), in the given DIA axis) injected in their place, propagated to NEV through the model’s own systematic operator so the swap is consistent to machine precision.Never keep the toolcode term AND add the correction covariance (double count) nor drop both (under-count): correct the survey,
excludethe term, inject its residual. C_msa reuses this exact hook (axis="inc"/"azi").- Parameters:
survey (welleng.survey.Survey) – Survey with an error model applied.
exclude (str or iterable of str) – Toolcode source name(s) the correction supersedes (e.g.
"vsag").injected_dia_cov ((n, n) array_like or None) – The correction’s residual covariance in
axisspace, one entry per survey station.Nonereturns the excluded block unchanged (the deterministic limit – a perfect, zero-uncertainty correction).axis ({"inc", "depth", "azi"}, default "inc") – Which DIA axis the correction acts on (sag ->
"inc").
- Returns:
(3n, 3n) symmetric NEV block. The pillar-2 realistic-error models this consumes are Bulychenkov (2026), §3, v1, doi:10.5281/zenodo.22148756.
- Return type:
numpy.ndarray
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:
Consecutive sections sharing a tool (same
error_modelandtool_id) are grouped into oneSurveyso their systematic error accumulates correlated — exactly as if surveyed in one run.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.
Whether the global (declination / B-field,
DECG/DBHG) and systematic terms are shared across a given tie is controlled by aShareMode, 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 markedall_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. It carries no per-source
signed vectors, so welleng.conditioning.source_vectors() refuses it and two
composed wellbores combine only as independent.
- 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:
objectTie 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_modeisNonethe tie before it is resolved from context: if the two sides name differentgeomag_modelvalues, or theirsurvey_datevalues differ by more than two years, the tie isall_independent(the global geomagnetic realisation has drifted / changed model and no longer cancels); otherwise the compositionshare_mode(defaultglobals_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]
- 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:
objectOne surveyed section (leg) of a wellbore, tied on to the previous one.
Provide either raw
md/inc/aziarrays (withdegand an optionalheader) or an existingSurveyviasurvey. 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
surveyis given.inc (array_like, optional) – Section geometry. Ignored if
surveyis given.azi (array_like, optional) – Section geometry. Ignored if
surveyis 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/aziare 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_modelandtool_idare treated as one continuous survey (systematic error stays correlated). A change oftool_id(orerror_model) marks a tool change / tie.Noneis treated as “the same unspecified tool continuing” — so if you never say the tool changed, it does not.survey_date (str, optional) –
YYYY-MM-DDdate the section was surveyed. Used only to auto-pick a defaultshare_modeat the tie before this section.geomag_model (str, optional) – Name of the geomagnetic reference model (e.g.
"BGGM2020"). Used only to auto-pick a defaultshare_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 (seeSurveyComposition).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
- 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.
A shared source is one realisation g_k acting on both wells through each
well’s own weights: its position error is v_Ak g_k in A and v_Bk g_k in
B, where v is the source’s signed position-error vector at one standard
deviation (Williamson, 2000, SPE-67616-PA, Eq. A-24, with the cross-well
correlation one for the shared sources and zero for the rest). So
cov(X_A − X_B) = Σ_A + Σ_B − Σ_k (v_Ak v_Bkᵀ + v_Bk v_Akᵀ)
summed over the shared sources. The shared contribution is
(v_Ak − v_Bk)(v_Ak − v_Bk)ᵀ: it cancels only where the two wells weight the
source alike (same attitude and departure). Where they differ it can exceed the
independent sum: two wells departing in opposite azimuths see one declination
error rotate them apart, which doubles their relative error rather than
cancelling it. Covariances alone cannot give the cross term, since a covariance
loses each source’s sign, so sharing takes the signed per-source vectors
(source_vectors()).
This module provides helpers to construct the combined covariance under different assumptions about which error sources 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:
objectResult of combining two wells’ covariances under a sharing assumption.
- cov_combined
The covariance of
X_A − X_Bper station after accounting for the share-mode.- Type:
(n, 3, 3) ndarray
- sigma_naive
sqrt(max eigenvalue)of cov_naive — naive 1σ in worst direction, per station.- Type:
(n,) ndarray
- reduction_factor
sigma_naive / sigma_combinedper station: above 1 where sharing tightens the combined uncertainty, below 1 where it widens it.- 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]], *, vec_global_a: ndarray[tuple[Any, ...], dtype[float64]] | None = None, vec_global_b: ndarray[tuple[Any, ...], dtype[float64]] | None = None, vec_systematic_a: ndarray[tuple[Any, ...], dtype[float64]] | None = None, vec_systematic_b: ndarray[tuple[Any, ...], dtype[float64]] | None = None, share_mode: Literal['all_independent', 'globals_shared', 'globals_and_systematic_shared'] = 'globals_shared', 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) 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, in metres squared, the stations paired row by row.
cov_total_b ((n, 3, 3) ndarray) – Total covariance per station for each well, in metres squared, the stations paired row by row.
vec_global_a ((n, k, 3) ndarray, optional) – Signed position-error vectors, in metres, of the
kglobal sources the two wells share, at the paired stations, in one source order (source_vectors()). Required whenshare_modeshares globals.vec_global_b ((n, k, 3) ndarray, optional) – Signed position-error vectors, in metres, of the
kglobal sources the two wells share, at the paired stations, in one source order (source_vectors()). Required whenshare_modeshares globals.vec_systematic_a ((n, k, 3) ndarray, optional) – The same for the shared systematic sources. Required when
share_modeis'globals_and_systematic_shared'.vec_systematic_b ((n, k, 3) ndarray, optional) – The same for the shared systematic sources. Required when
share_modeis'globals_and_systematic_shared'.share_mode ({'all_independent', 'globals_shared', 'globals_and_systematic_shared'}) –
Which sources are realised identically in the two wells:
'all_independent':Σ_A + Σ_B.'globals_shared'(default): global sources (geomagnetic reference, depth stretch) are one realisation for both wells.'globals_and_systematic_shared': systematic sources are one realisation too; appropriate only where both wells were surveyed by the same tool run.
A shared source contributes
(v_A − v_B)(v_A − v_B)ᵀ, which is smaller than the independentv_A v_Aᵀ + v_B v_Bᵀwhere the wells weight it alike and larger where they weight it oppositely, so sharing can widen the combined covariance as well as tighten it.cov_global_a (optional) – Refused: a covariance has lost each source’s sign, so it cannot give the cross term; pass the
vec_*arguments instead.cov_global_b (optional) – Refused: a covariance has lost each source’s sign, so it cannot give the cross term; pass the
vec_*arguments instead.cov_systematic_a (optional) – Refused: a covariance has lost each source’s sign, so it cannot give the cross term; pass the
vec_*arguments instead.cov_systematic_b (optional) – Refused: a covariance has lost each source’s sign, so it cannot give the cross term; pass the
vec_*arguments instead.
- Return type:
- Raises:
ValueError – On unequal covariance shapes, a
cov_*component, a shared mode without its vectors, vectors of the wrong shape, or an unknown mode.
- welleng.conditioning.source_vectors(survey_a, survey_b, propagation: str)[source]
Signed per-source position-error vectors of the sources two wells share.
- Parameters:
survey_a (welleng.survey.Survey) – Error-modelled surveys (
error_modelset).survey_b (welleng.survey.Survey) – Error-modelled surveys (
error_modelset).propagation ({'global', 'systematic'}) – Which sources to return.
- Returns:
codes (tuple of str) – The error-source codes of that propagation present in both wells, in one order.
vec_a, vec_b ((n_a, k, 3) and (n_b, k, 3) ndarray) – Each source’s position-error vector at one standard deviation, in NEV, in metres, at every station of each well;
k=len(codes). A source’s covariance in a well isv vᵀ.
- Raises:
ValueError – If either survey carries no error model.
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:
objectSolves 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
- 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
- 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
- __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) raisesValueError.'max_radius'falls back to the gentlest feasible curve — the beta=0 biarc at the largest radius admitting a valid CLC (seewelleng.sawaryn_analytical.max_radius()) — and emits aUserWarningthat 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, afeasibleflag, an input gate) must setdirect_only=Trueor checkdogleg/dogleg2against 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
- 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_tvdif 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
vec1is 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.
- 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 positionpos_targetwith a given design radius. The target tangent vector is an OUTPUT of the solve — computed analytically from the geometry — not an input. Equivalent toConnector(pos1=..., vec1=..., pos2=pos_target, dls_design=...)in the'curve_hold'mode (binary code00110in_get_initial_methods), but operates element-wise on arrays so a large sweep is one numpy call rather than a Python loop overConnectorinstances.- 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 returnsdogleg = 0,tangent_length = dist_to_target,vec_target = vec1, andpos2 = pos1. This matches the scalarConnectorbehaviour 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:
objectA 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:
- errors
ToolError object containing per-source error magnitudes and covariance data.
- 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:
objectStandard 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 bywelleng.errors.edm_ipm— evaluated by the formula interpreter without any file resolution.
- cov_dia_at(md, exclude=None)[source]
DIA-space (survey-parameter) covariance at
md, SI (m, rad, rad).The (3, 3) covariance of the (measured-depth, inclination, azimuth) error – the pre-propagation intermediate of
C_nev = V C_dia V^Tand the natural space for combining two surveys of the same well. Unlikecov_nev_at(), this is a linear interpolation of the per-stationcov_DIAbetween the bounding stations: measurement-space uncertainty is smooth in measured depth, so the arc-faithful partial-leg propagation that NEV needs (to avoid the ~25% dogleg under-report) does not apply here.- Parameters:
md (float) – Measured depth, within the survey range.
exclude (iterable of str, optional) – Error-source codes to omit from the sum. Use it to drop terms that are non-physical for angle fusion – notably the local-curvature terms
XCLA/XCLH, which represent between-station curvature representation uncertainty (they inflate raw sigma_inc to several degrees), not the tool’s angular measurement at the station. Seeprojects/specs/2026-08-26_dia_space_survey_fusion.md.
- Returns:
(3, 3) DIA covariance, SI (m, rad, rad).
- Return type:
numpy.ndarray
- 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 insertingmdas 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
qat arc-fractionfon leg[i, i+1], stationiand stationi+1both drive the partial leg (via the minimum-curvature arc), so the interior propagates BOTH: the own weightdrk(i->q)(far station) AND station i’s out-leg couplingdrkplus1(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. Withqi = 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, contributingouter(sigma(q)).random (two INDEPENDENT measurements -> two outer products):
cov_NEV[i] - outer(e_NEV_star[i]) + outer(g_i) + outer(g_j)withg_i = e_NEV_star[i] + coup + (1-f) qiandg_j = f qj– the partial q-own term splits (1-f)/f across the two stations (the arc tangent’s Jacobian ~ f; exact at both ends, to that approximation in the interior).
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. Withxcl_representation == 'dia'they are instead interval angle errors acting on their own leg only (_interior_prep()class"own_only"), also station-exact at f=0,1 and equal to the course-length form on a hold leg. Any remaining ring-fenced term (_interior_prep()class"linear") uses linear covariance interpolation. Reproduces the storedcov_NEV[i+1]atf -> 1to machine precision.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): 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_basisbalanced tangent, smooth measurement terms only (XCLA/XCLH excluded — an NEV-directe_DIAhas 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 = 0andf = 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 a constant VV double-count was removed from it. It overstated that column’s 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), within the survey:
survey.md[0] <= md <= survey.md[-1].- Returns:
The (3, 3) NEV covariance at
md.- Return type:
numpy.ndarray
- Raises:
ValueError – If
mdlies outside the survey. There is no covariance above the first station or beyond the last one to report; extrapolating the partial-leg weights would return a plausible matrix for a depth that was never surveyed.
- 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_dDepthslices[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)).Surveycanonicalises azimuth to 0 at vertical stations (seeSurvey._make_angles); a consumer feeding this model WITHOUT that preprocessing must applyazi = 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
- iscwsa_definition
The ISCWSA Error Model Definition revision whose formulas were applied (
SurveyHeader.iscwsa_definition), and whether a later revision supersedes it (the current one isISCWSA_DEFINITION_CURRENT).
- 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
- 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*Pmatches 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 topressure_appliedat 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_appliedenters onlyalpha_1/beta_1, which appear both as a prefactor and insidelog((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; not yet fixed.
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.geomag module
Geomagnetic field: an offline WMM model + the BGS web-service client.
Two ways to get declination / dip / total field for a survey location:
local_field()– offline. The bundled World Magnetic Model (WMM2025, US-Gov public domain) evaluated locally by spherical-harmonic synthesis: no network, deterministic, fast, valid for its five-year window (2025-2030). This is the default path oflookup_field()when the request is in window.lookup_field()withsource="bgs"– the British Geological SurveyGMModelsweb service, for IGRF / historic / out-of-window dates and other models the local WMM cannot serve. Requires a network.
lookup_field (source="auto", the default) uses the offline model when it
applies and falls back to BGS otherwise, so the common case (current-epoch WMM)
needs no network while the long tail still works online.
The local model reproduces NOAA’s official WMM2025 test values to < 0.001 nT in
every field component (the model’s own numerical-agreement spec is 0.1 nT), far
below the model’s ~physical uncertainty. Coefficients: welleng/data/wmm2025.json
(converted from WMM2025.COF); see its provenance field.
Service documentation: https://geomag.bgs.ac.uk/web_service/GMModels/help/parameters
- exception welleng.geomag.GeomagLookupError[source]
Bases:
ValueErrorThe geomagnetic lookup failed (bad request, out of window, 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
- class welleng.geomag.SurveyField(md: ndarray, b_total: ndarray, dip: ndarray, declination: ndarray, d_b_total: ndarray, d_dip: ndarray, d_declination: ndarray, reference: dict)[source]
Bases:
objectPer-station geomagnetic reference field along a survey (offline WMM).
- d_b_total, d_dip, d_declination
Difference of each quantity from the wellhead reference (station 0) – the SIZE of the per-station refinement.
d_declinationis the one that matters: it rotates azimuth, so its span over the well is the potential lateral-position gain of using a per-station reference.- Type:
(n,) ndarray
- reference
The single wellhead-reference field
local_fieldreturns, for comparison / the conventional single-value path.- Type:
dict
- __init__(md: ndarray, b_total: ndarray, dip: ndarray, declination: ndarray, d_b_total: ndarray, d_dip: ndarray, d_declination: ndarray, reference: dict) None
- b_total: ndarray
- d_b_total: ndarray
- d_declination: ndarray
- d_dip: ndarray
- declination: ndarray
- dip: ndarray
- md: ndarray
- reference: dict
- welleng.geomag.field_along_survey(survey, date=None, descend=False)[source]
Per-station geomagnetic reference field along a survey, computed OFFLINE.
An opt-in refinement: instead of one wellhead declination/dip/B for the whole well, evaluate the bundled WMM at each station’s own geographic position. For an extended-reach well crossing a field gradient the declination varies across the trajectory; using a single surface value puts a small systematic azimuth error on the deep, stepped-out sections. The effect is negligible for ordinary wells (< 0.01 deg declination) and reaches ~0.1-0.2 deg (~10-20 m of lateral position over the reach) only for long extended-reach wells at high latitude or near a magnetic anomaly.
By default this varies the horizontal position (latitude/longitude from the wellhead plus the survey’s north/east) and holds altitude at the wellhead reference – the LATERAL gradient, which is the real, physically-clean effect. The vertical (downward-continuation to true vertical depth) is deliberately NOT applied by default: it is both tiny (< 0.03 deg over 3 km) and outside the WMM’s certified altitude range (its floor is -1 km, and the crustal field a well actually descends into is not in the model). Pass
descend=Trueto evaluate at-tvdanyway, understanding it is a minor extrapolation.Horizontal position uses a local flat-earth step from the wellhead (
dlat = north / 111320,dlon = east / (111320 cos lat)); over a well’s extent that is well within the smoothness of the geomagnetic field.- Parameters:
survey (welleng.survey.Survey) – A survey whose header carries
latitude/longitude(the wellhead) and which exposesn/e/tvdstation positions.date (str or float, optional) –
yyyy-mm-ddor a decimal year. Defaults to the header’ssurvey_dateif set, else the model epoch.descend (bool, default False) – If True, evaluate at each station’s
-tvd(downward continuation); default holds altitude at the wellhead reference (lateral gradient only).
- Return type:
Notes
This is the SCALAR reference applied per station (one
local_fieldcall each) – the open, auditable form.It changes the reference field’s MEAN, not the ISCWSA declination UNCERTAINTY term (which still covers the spread). And it is a modelling refinement of the TRUE field – do not use it to silently re-correct a survey whose azimuths already had a declination applied at acquisition; its value is on the raw magnetic-to-true path.
- welleng.geomag.local_field(latitude, longitude, altitude=0.0, date=None)[source]
Geomagnetic field from the bundled WMM, computed OFFLINE.
- 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 datum).
date (float or str, optional) – Decimal year (e.g.
2027.5) or"yyyy-mm-dd".None-> the model epoch.
- Returns:
The same shape as
lookup_field()– ageomagnetic-field-model-resultpayload withfield-value(total-intensity/inclination/declination/horizontal-intensity/north-intensity/east-intensity/vertical-intensity, each{"value", "units"}), plusmodel_base_dateandmodel_name– so it is a drop-in for the BGS payload.- Return type:
dict
- Raises:
GeomagLookupError – If
dateis outside the bundled model’s validity window.
- welleng.geomag.lookup_field(latitude, longitude, altitude=0.0, date=None, model='wmm', revision='current', timeout=10.0, source='auto')[source]
Look up the geomagnetic field, offline where possible.
- 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 or float, optional) –
yyyy-mm-ddor a decimal year. Omitted -> the model epoch.model (str) –
'wmm'(default) or'igrf'(historic; BGS only).revision (str) – BGS model revision;
'current'tracks the latest server-side.timeout (float) – Socket timeout in seconds (BGS path only).
source ({"auto", "local", "bgs"}) –
"auto"(default): the bundled offline WMM when it applies (model="wmm"anddatein its window), else BGS."local"forces the offline WMM (raises out of window)."bgs"forces the web service.
- Returns:
A
geomagnetic-field-model-resultpayload:result['field-value']['total-intensity']['value'](nT),['inclination'](dip, deg down-positive),['declination'](deg east-positive). The offline and BGS payloads share this shape.- Return type:
dict
- Raises:
GeomagLookupError – Out-of-window local request, HTTP error, 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 | None = None, reference: str | None = None, realisations: list[DatumRealisation] = <factory>)[source]
Bases:
objectA 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, optional) – Datum elevation, in metres, above mean sea level (or the field reference given by
reference).None– the default – means NOT RECORDED, and is not the same as zero. Zero is a claim that the datum sits at the reference: offshore that says the rotary table is at sea level, understating every TVDSS by the air gap plus the RT height (commonly 20-45 m). An unrecorded elevation therefore staysNoneso a consumer has to handle it, rather than reading a fabricated sea-level datum as a measured one.reference (str, optional) – The elevation reference frame —
"MSL","RKB","RT","seabed", and so on. Free-form by design (field data uses whatever the operator wrote), butosdu_reference()maps it onto the OSDUVerticalMeasurementTypevocabulary so the frame is expressible as a code rather than a habit.None– the default – means the frame was not recorded. It is NOT defaulted to"MSL": that would make an unrecorded frame indistinguishable from a stated one, and would letosdu_reference()emit a confidentMeanSeaLevelcode derived from nothing.realisations (list of DatumRealisation, optional) – The datum’s position-survey history, oldest first — an APPEND-ONLY document chain (see
DatumRealisation). Manage it throughadd_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 | None = None, reference: str | None = None, realisations: list[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.supersedesdoes not reference the current head, or itsidalready exists in the chain.
- property current_realisation: DatumRealisation | None
The head (most recent) realisation, or
Noneif no chain.
- elevation: float | None = None
- name: str
- osdu_reference() str | None[source]
referenceas an OSDUVerticalMeasurementTypecode, or None.A depth datum is the one field that moves EVERY depth in the well if it is wrong, and it was carried here as a free string whose vocabulary lived in a trailing comment. This maps it onto the published list (
RotaryTable,KellyBushing,MeanSeaLevel,DrillFloor,Seafloor, …).Nonemeans the string did not resolve confidently – which is the honest answer for a bare"wellhead", since OSDU distinguishes the casing-head, tubing-head and top/bottom flanges and the input does not. It is also the answer when no frame was recorded at all. The storedreferenceis never rewritten.
- 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 | None = None
- 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_idto express it underto_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 | None = None, supersedes: str | None = None)[source]
Bases:
objectOne 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
DatumRealisationis 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 or None, default None) –
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.
Nonemeans NOT ESTABLISHED, and0.0means established as negligible. They are different claims and only one of them is usually true: a datum whose uncertainty nobody recorded is not a perfectly known datum. Nothing in core consumes this yet, which is exactly why it is worth fixing now — the value becomes load-bearing the moment a relative-covariance path starts adding slot uncertainty, and by then every stored realisation would read as exact.supersedes (str or None, default None) – The
idof the realisation this one supersedes;Nonefor the original. Enforced append-only byDatum.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 | None = None, supersedes: str | None = None) None
- date: str | None = None
- document: str | None = None
- id: str
- radial_error: float | None = None
- 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:
_NodeA field / asset grouping the sites of a development.
Maps to OSDU
master-data--Field. Inherits the_Nodefields;parentpoints to the owningOrganisation.- __init__(id: str, name: str = '', parent: _Node | None = None) None
- class welleng.hierarchy.Organisation(id: str, name: str = '', parent: _Node | None = None)[source]
Bases:
_NodeOperating organisation — the top of the master-data hierarchy.
Maps to OSDU
master-data--Organisation. Inherits the_Nodefields (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:
_NodeA site / surface structure grouping wells — a platform, pad, or subsea template.
Maps to OSDU
master-data--WellSiteStructure(EDMCD_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 (EDMCD_PROJECT).convergence (float or None, default None) – Grid-vs-true-north convergence, in radians, for the site origin.
is_field_centre (bool, default False) –
Trueif 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
crsandconvergence— these are common systematic terms that cancel in the relative (wellbore-to-wellbore) uncertainty sense. Only the per-well slot offsets differ (seeWell).- __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 | None = None, wellhead_depth: float | None = None, datum: Datum | None = None)[source]
Bases:
_NodeA well — the surface location / wellhead the root wellbore(s) hang off.
Maps to OSDU
master-data--Well(EDMCD_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 or None, default None) – 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_DATUMis 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_errordiffer — the shared CRS / convergence cancels (seeSite).- __init__(id: str, name: str = '', parent: _Node | None = None, slot: tuple[float, float] | None = None, slot_radial_error: float | None = None, wellhead_depth: float | None = None, datum: Datum | None = None) None
- slot: tuple[float, float] | None = None
- slot_radial_error: float | None = None
1-sigma slot-position uncertainty, metres.
None= NOT ESTABLISHED;0.0= established as negligible. See DatumRealisation.radial_error.
- wellhead_depth: float | None = None
- class welleng.hierarchy.WellNetwork[source]
Bases:
objectA 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'])
- add(node: _Node) _Node[source]
Add a node and wire its parent edge into the graph.
Registers
nodein the network and, when itsparentis aWell(root wellbore) or aWellbore(sidetrack), adds the directedparent -> nodeedge. 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
WellboreorWell(container ancestors are pulled in automatically viaparent).- Returns:
The same
nodethat 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 withid_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
WellNetworkfromto_dict()output.Rebuilds every entity, wires parents by id (recursively, so a child is built after its parent), and re-creates each wellbore’s
Surveyfrom the serialised md / inc / azi + header.- Parameters:
data (dict) – A dict in the shape produced by
to_dict().- Returns:
The reconstructed network.
- Return type:
- 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:
- 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
aandb— 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
Noneif 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
aandb(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.
md_a (float, optional) – Measured depth in metres of the compared point on each wellbore; the nearest station is used, TD when
None.md_b (float, optional) – Measured depth in metres of the compared point on each wellbore; the nearest station is used, TD when
None.error_model (str) – Error model used for a survey that carries none.
share_mode ({None, 'all_independent', 'globals_shared', 'globals_and_systematic_shared'}, optional) – Which error sources the divergent branches share below the side-track point (see Notes);
Nonetreats them as independent.
- 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: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.
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 Welldatum/ 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_branchAandC_B = C_st + C_branchB:C_rel = C_A(a) + C_B(b) - 2 * C_st ( = C_branchA + C_branchB )
where
C_stis the absolute covariance at the deepest common (side-track) point. Each wellbore’s absolute covariance comes from welleng’sErrorModel(survey.cov_nev); the surveys are assumed full-from-surface (as EDM definitive surveys are), so they agree over the shared trunk.The trunk cancellation is always applied. The source-context cross term between the divergent branches is applied when
share_modenames shared sources: each shared source contributes(v_a − v_b)(v_a − v_b)ᵀ, withvits signed position-error vector below the side-track point in each branch (welleng.conditioning.combine_covariances()). That is smaller than the independent sum where the branches weight the source alike and larger where they weight it oppositely, so neithershare_modeis conservative in general.Noneor'all_independent'treats the divergent branches as independent.
- roots() list[Wellbore][source]
Return the root wellbores of the forest.
A root wellbore is one whose parent is a
Wellrather 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']
Split the two ancestry paths at their lowest common ancestor.
Partitions the ancestry of
aandbinto 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)wheresharedis the common trunk (LCA and above),branch_ais the divergent portion ofa’s ancestry below the LCA (nearest-first), andbranch_bthe same forb. If the two nodes share no ancestry,sharedis 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.osduis 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:
_NodeA wellbore section — a drilled hole.
Maps to OSDU
master-data--Wellbore(EDMCD_WELLBORE). A wellbore is the unit the graph is built from: it is either a root wellbore (top hole, parented by aWell) or a sidetrack / lateral kicked off anotherWellbore.- 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 theWellborethis section kicked off from (sidetrack / lateral). The OSDU edge isWellbore.KickOffWellbore/ the EDMparent_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; OSDUwork-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_mdis derived, not a native OSDU field — OSDU has noKickOffMD. Infer it from the child trajectory’s top MD relative to the parent (EDM supplies it directly viaCD_SURVEY_HEADER.tie_on_depth). It is the divergence point the relative-error split needs.The
survey_date/tool_id/geomag_modelfields 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
WellNetworkfrom a parsed EDM/COMPASS export.Maps the EDM master-data spine indexed by
welleng.exchange.edm_stream.EDMReaderonto 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_PROJECTField(project_name); also supplies the site CRS (geo datum + zone) when derivableCD_SITESite(name, convergence [deg -> rad],is_field_center, map location [-> metres])CD_WELLWell(name, slot(ns, ew)+ radial error, wellhead depth [-> metres])CD_DATUMDatumon its Well (elevation [-> metres]; the default datum is preferred)CD_WELLBOREWellbore;parent_wellbore_idis 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) viareader.survey(...). A wellbore whose survey is missing or fails to assemble is skipped with aUserWarning— 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:
- Warns:
UserWarning – Per wellbore whose survey cannot be assembled (
surveys=Trueonly), and per wellbore whoseparent_wellbore_idcannot be resolved (it is rooted to its Well instead).
welleng.io module
welleng.lithology module
Lithology / stratigraphy column rendering.
Draws a lithostratigraphic column against depth – a flat colour per interval,
optionally overlaid with the FGDC lithologic pattern for that rock type – and
composes it beside a log plot (welleng.exchange.las.plot_curves()) or a
well schematic.
Reference data
nl_groups() returns the Dutch (NL) lithostratigraphic groups shipped in
welleng/data/nl_lithology_patterns.yaml: group name, colour, and an FGDC
section-37 pattern code. Colours are a presentation choice; the pattern codes
are from FGDC-STD-013-2006 (US public domain).
Pattern images
The FGDC pattern PNGs are NOT vendored – they are a separate CC0 repackaging
(github.com/davenquinn/geologic-patterns). Point FgdcPatterns at
its assets/png directory, or set WELLENG_FGDC_PATTERNS. Without them the
column still renders, in flat colour: patterns are an enhancement, never a
requirement (a caller with no assets must still get a usable figure).
matplotlib and PIL are lazy imports behind the welleng[las] extra.
Example
>>> from welleng.lithology import nl_groups, plot_lithology
>>> plot_lithology(nl_groups(), out="litho.png")
- class welleng.lithology.FgdcPatterns(directory: str | PathLike | None = None)[source]
Bases:
objectResolves FGDC pattern codes to RGBA tiles from a CC0 PNG asset directory.
The PNGs carry NATIVE transparency (background alpha 0, opaque line work), so the RGBA is used as-is. Do not derive alpha from luminance – that path was tried and is wrong; it grays the fill and loses the line work.
- property available: bool
True when a pattern directory is set and exists.
- tile(code: int, thickness: float, tile_depth: float | None = None)[source]
RGBA tile stack for an interval, or None if the asset is absent.
tile_depthis the depth interval ONE tile should occupy; repeats arethickness / tile_depth. Callers derive it from the visible depth window (seeTILES_PER_AXIS) so the pattern keeps a constant size on paper regardless of zoom.
- class welleng.lithology.Interval(name: str, top: float, base: float, colour: str = '#ffffff', pattern: int | None = None, note: str = '')[source]
Bases:
objectOne lithostratigraphic interval.
patternis an FGDC section-37 code.- __init__(name: str, top: float, base: float, colour: str = '#ffffff', pattern: int | None = None, note: str = '') None
- base: float
- colour: str = '#ffffff'
- name: str
- note: str = ''
- pattern: int | None = None
- property thickness: float
base - top, in the depth units of the interval.
- top: float
- exception welleng.lithology.LithologyError[source]
Bases:
ExceptionLithology reference data or pattern assets could not be used.
- class welleng.lithology.PatternMatch(code: int | None, reason: str, words: tuple[str, ...] = ())[source]
Bases:
objectWhat a unit name said, and – when it said nothing usable – WHY.
⭐ The reason is the point, because a refusal is not self-describing.
pattern_from_namereturningNonewas read by its caller as “no information, keep what you had”, and what the caller had was the RGD GROUP pattern. In the Zechstein that is HALITE, so declining to read the word “clay” in “Grey Salt Clay Member” did not leave the unit unlabelled – it labelled it salt, on a barrier drawing, inside the seal.Only
no_lithologymeans the group is still the best available statement. Every other reason means we know the unit is NOT simply its group’s rock, and falling back to the group is then a claim the name has actively contradicted.- __init__(code: int | None, reason: str, words: tuple[str, ...] = ()) None
- code: int | None
FGDC pattern number, or
None.
- property may_fall_back_to_group: bool
Whether a caller may substitute the unit’s GROUP pattern.
⚠️ NECESSARY, NOT SUFFICIENT – do not branch on this alone. It answers only “did the NAME say something that contradicts the group?”. The group pattern also describes a RANK, and giving it to a formation or member inside that group is a separate error this flag knows nothing about.
A consumer branched on this flag and applied the group pattern itself. When the rank rule landed, its drawing did not change – the corrected code was never on its path. Call
pattern_for_unit(), which makes the whole decision.
- reason: str
matched·ambiguous(two lithologies, or a class word too broad) ·unmapped(a rock the FGDC chart has no pattern for) ·no_lithology(the name does not mention rock at all) ·no_name.
- words: tuple[str, ...] = ()
The lithology words seen, so a caller can say what it could not resolve.
- welleng.lithology.colour(unit_id: str) str | None[source]
Colour (hex) for ANY RGD unit code, rolled up to its group. Or
None.A member or formation inherits its group’s colour rather than falling to grey, so a consumer plotting a Dutch column does not have to invent one.
Nonefor a code the nomenclature does not know, or a group the shipped table does not carry – deliberately not a fallback palette: an invented colour that looks official is worse than an obviously absent one, exactly as with the OCR names.⚠️ These colours are welleng’s presentation choice, NOT a published standard. TNO’s DINOloket stratigraphic nomenclature is the authority and is not yet ingested here. Say so on any figure that uses them.
- welleng.lithology.group_of(unit_id: str) str | None[source]
Roll an RGD unit code up to its GROUP-rank code.
A formation or member code resolves through its ancestor chain (the last entry is the group); a code already at group rank returns itself. Returns None for a code the nomenclature does not know.
- welleng.lithology.intervals_from_nlog(column, label: str = 'formation') list[Interval][source]
Colour an NLOG
StratColumn.label:"formation"(default)The unit’s own name at formation/member rank, and its own lithology where the name says it – so the Z4 Fringe Sandstone Member is drawn as sand rather than inheriting the Zechstein evaporite pattern. Colour stays the GROUP’s, which is what makes a column read as stratigraphy rather than as a rock-type mosaic.
"group"Group name and group pattern throughout – the coarse view.
"code"The raw RGD code as logged, for debugging. (
"unit"is accepted as the old spelling of this.)
⭐ One pattern per GROUP is a simplification and it bites at the worst place. The Rijnland Group is marl AND clay AND sandstone, and a well’s cap rock is a claystone member inside it; the Zechstein is an evaporite group containing a sandstone reservoir. A consumer hit both, and had to hand-write an override table to stop a flow zone being shaded as salt on a barrier drawing. That override now happens here, from the name, and only where the name is unambiguous – see
pattern_from_name().An unrecognised or unmatched unit renders flat white with no pattern – an unknown unit should look unknown rather than be given a colour it has not earned. Intervals with no
bottom_mdare dropped, since a band needs a base; that is a data gap, not something to interpolate over.
- welleng.lithology.intervals_from_tops(tops: Sequence[tuple[str, float]], base: float, lookup: Iterable[Interval] | None = None) list[Interval][source]
Build intervals from
(name, top_depth)pairs plus a finalbase.Colour and pattern are taken from
lookup(default: the NL groups) by case-insensitive name match; an unmatched name renders in flat white, which is deliberate – an unknown unit should look unknown, not be given a plausible colour it has not earned.
- welleng.lithology.nl_groups() list[Interval][source]
The Dutch lithostratigraphic groups, shallow to deep.
Tops/bases are NOT included – they are per-well. Each interval comes back with
top/baseof 0.0; assign them, or useintervals_from_tops().
- welleng.lithology.nl_groups_by_code() dict[str, Interval][source]
RGD group-rank code -> template
Interval(colour + pattern).
- welleng.lithology.pattern(unit_id: str) int | None[source]
FGDC pattern code for ANY RGD unit code, rolled up to its group.
Same roll-up and same
Nonepolicy ascolour().⚠️ One pattern per GROUP is a simplification – a group spans more than one rock type – and an ornament carries meaning a colour does not: the box/cross-hatch patterns read as EVAPORITE, so ornamenting a carbonate with one says salt. Override per interval at formation or member rank where the distinction matters.
- welleng.lithology.pattern_for_unit(name: str | None, rank: str | None = None, group_pattern: int | None = None) int | None[source]
THE pattern decision for one stratigraphic unit. Call this; don’t rebuild it.
Combines BOTH gates, which is the whole point. The NAME gate (
pattern_match_from_name()) and the RANK gate each give a confident wrong answer alone, and they used to live in different places – the name half public, the rank half buried inintervals_from_nlog(). A consumer holding only the public half reassembled the decision fromPatternMatch.may_fall_back_to_groupand applied the group pattern itself, so when the rank rule shipped its sheet did not change: three units went on wearing a hatch they had not earned. The fix was a check, in the right place, and it still did not travel, because the caller had duplicated the decision instead of calling it.- Parameters:
name (str or None) – The unit’s own name, at its own rank.
rank (str or None) –
"group"/"subgroup"/"formation"/"member"/None. ⚠️Nonemeans NOT RECORDED and REFUSES the group fallback: not knowing what a unit IS is not a reason to assert what it is MADE OF.group_pattern (int or None) – The parent group’s pattern. Used only where both gates allow it.
- Returns:
FGDC pattern number, or
Nonefor “we cannot say” – which a renderer draws UNPATTERNED, never as the group’s rock.- Return type:
int or None
- welleng.lithology.pattern_from_name(name: str | None) int | None[source]
FGDC pattern implied by a unit NAME, or
Nonewhen it does not say.Word-boundary matched, and deliberately unwilling:
a name with no lithology word returns
None– “Ommelanden Formation” says nothing about the rock, so the caller keeps whatever it had.a name with two different lithologies returns
None. Picking the first would be a coin toss on a barrier drawing.a name whose rock the FGDC chart has no pattern for returns
Nonerather than the nearest one: anhydrite is not gypsum, and 667 is gypsum only.⚠️ Substring matching is wrong here. Buntsandstein contains “sandstein” and the Dutch unit logs 127 gAPI – claystone-dominated. Word boundaries are what stop a proper noun being read as a description.
This is a NAME heuristic, not a lithology record. It exists because the shipped table carries one pattern per GROUP, and a group spans more than one rock: it is how the Z4 Fringe SANDSTONE Member stops inheriting the Zechstein evaporite pattern and being drawn as salt on a barrier drawing.
- welleng.lithology.pattern_match_from_name(name: str | None) PatternMatch[source]
pattern_from_name(), plus why it refused. SeePatternMatch.
- welleng.lithology.pattern_names() dict[int, str][source]
FGDC code -> the standard’s own wording, for the codes we reference.
- welleng.lithology.plot_lithology(intervals: Sequence[Interval], ax=None, patterns: FgdcPatterns | str | None = None, depth_range: tuple[float, float] | None = None, label: bool = True, label_width: int = 12, label_gutter: float = 0.0, out: str | None = None, title: str | None = 'lithology')[source]
Draw a lithology column (depth down the y axis).
patternsmay be anFgdcPatterns, a directory, or None (flat colour). Returns the Axes.Labels are MEASURED, not estimated. A name is drawn inside its band only when the rendered text actually fits the band’s THICKNESS – for a thin member that is the binding constraint, and widening the track does nothing about it. Anything that does not fit goes to a leader out of the column, never to suppression: an unlabelled band reads as an unnamed one, which is the same silence as an interval that is not drawn at all.
⭐ Measured rather than estimated because of how estimating fails. A
width / (0.6 * fontsize)rule overflows on a wide name and wastes the column on a narrow one, and a glyph-metrics table asserts a font the renderer may not have used – it is right on one machine and silently wrong on another, including in the direction of dropping a label that WOULD have fitted. A claim about the renderer has to be made by the renderer.label_gutteris the width (in axis units, the column being 1.0) kept clear to the right for those leaders.0.0keeps every label inside the band and lets a long one overrun, which is the previous behaviour.
- welleng.lithology.plot_log_with_lithology(las, intervals: Sequence[Interval], tracks: Sequence[Sequence[str]] | None = None, depth_range: tuple[float, float] | None = None, patterns: FgdcPatterns | str | None = None, out: str | None = None, title: str | None = None)[source]
Log tracks with a lithology column alongside, on a shared depth axis.
lasis awelleng.exchange.las.LasFile. Returns the Figure.
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:
objectTriangular mesh representing a wellbore’s positional uncertainty envelope.
- s
The input Survey object.
- Type:
- 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
verticesandfacesattributes.- 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
- __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.
- 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
WellMeshto atrimesh.Trimeshobject.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:
objectA 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
nevflag.vec (array_like, optional) – Unit direction vector (3-element). If provided,
incandaziare 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,
incandaziare in degrees.nev (bool) – If True,
posandvecare 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
- 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.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_analyticaldoes 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
Nonewhere 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
<= pionly 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 thebeta = 0(curve-curve / biarc) boundary, where the hold vanishes — equivalently the largest root of the constant coefficientc0of 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.0is 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) andtotal_md;Noneif no feasible biarc exists (the target is unreachable under thepiconstraint).- 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()). Seesolve_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 planareta14 ~ 0) auto-fall back tosolve_clc_2d(), so the caller need not pre-classify.- Parameters:
p1 ((3,) array_like) – Kickoff position and unit tangent (N, E, V);
t1is a unit vector.t1 ((3,) array_like) – Kickoff position and unit tangent (N, E, V);
t1is 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.
R2defaults toR1(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.
R2defaults toR1(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 keysbeta(tangent / hold length),alpha1,alpha2(arc doglegs, radians) andtotal_md(measured depth);Noneif 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’s1/(1-mu^2)blows up):eta14is 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);
t1is) – a unit vector, as tangents are throughout welleng (cf.Survey.vec_nev).t1 ((3,) array — kickoff position and unit tangent (N, E, V);
t1is) – 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 (
R2defaults toR1, 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 (
R2defaults toR1, 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 eachbuild/turn);
arc1,arc2— arc lengthsR*alpha(the buildsections);
total_md— total measured depth;residual. The build-planetoolface 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
p0in directiont4; the free parameter is the along-line distancek, and the connection is a biarc (beta = 0). On that line the invariants collapse to low-order functions ofk(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, witheps* = (p0 - p1).<basis>. Substituting these into the biarc conditionc0 = 0(the constant coefficient of the corrected Eq. 15) gives a polynomial ink(Eq. 44) whose roots are the landing distances; this is solved numerically fork.- 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:
p0is its anchor (thek = 0base point) andt4its unit direction. Namedp0(notp4) because the target pointp4 = p0 + k*t4is the solved output, not an input.t4 ((3,) array_like) – The landing line:
p0is its anchor (thek = 0base point) andt4its unit direction. Namedp0(notp4) because the target pointp4 = p0 + k*t4is the solved output, not an input.R1 (float) – Arc radii;
R2defaults toR1.R2 (float) – Arc radii;
R2defaults toR1.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_landing_region(p1, t1, t4, target, R1, R2=None, k=1.0)[source]
Minimum-MD CLC landing into a target REGION, final tangent
t4fixed.Finds the shortest measured-depth curve-hold-curve path from
(p1, t1)that reaches any point oftargetwhile arriving with tangentt4. The landing position is free within the region; the final tangent is given (a region has no single direction to couple it to).- 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).
t4 ((3,) array_like) – Final unit tangent at the landing (fixed).
target (welleng.target.Target) –
The landing region. Supported shapes:
planar (in the target plane, from position +
orientation/dip):point,rectangle(pos1/pos2),circle(radius),ellipse(radius_1/radius_2),polygon(vertices);volumetric (in NEV):
cube(half_extents),sphere(radius),gaussian(the Mahalanobis ellipsoid ofcovat radiusk).
R1 (float) – Arc radii;
R2defaults toR1.R2 (float) – Arc radii;
R2defaults toR1.k (float) – Mahalanobis radius for a
gaussiantarget (default 1 = the 1-sigma ellipsoid); ignored for other shapes.
- Returns:
The shortest landing:
beta,alpha1,alpha2,total_md(assolve_clc()) plusp4(the solved landing point, NEV); for a planar target alsoab(its in-plane coordinates).Noneif no point of the region is reachable with a feasible CLC.- Return type:
dict or None
Notes
Feasible here means both doglegs are <= pi (error-modellable). This is STRICTER than
solve_clc(), which also returns paths that loop the long way (a dogleg > pi), so a caller moving fromsolve_clcto the region API may see some solutions disappear. Landing on a LINE with the exit tangent along that line is a different problem — usesolve_clc_landing(), not alineshape here (the region API takest4as an independent fixed tangent).
- 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 quadraticA T^2 + B T + C = 0. Returns the two branches per arc; forward- verification selects the physical one.
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:
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 OSDUkindstring carries it (osdu:wks:master-data--Wellbore:1.1.0), andfrom_osdurecognises 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.Units-aware. OSDU carries a unit-of-measure per numeric (e.g. a
FrameOfReference/UnitOfMeasureID/AsIngestedCoordinatesUoM). welleng works internally in metres; every length is converted on the boundary (_to_mon import,_from_mon 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
kindstring 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
Nonethe pinned version fromOSDU_SCHEMA_VERSIONSis used.
- Returns:
The OSDU kind string, e.g.
"osdu:wks:master-data--Wellbore:1.1.0".- Return type:
str
- Raises:
KeyError – If
entityis 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.hierarchyentity.Dispatches on the record’s
kindto build the correspondingwelleng.hierarchyentity, converting all lengths to internal metres and checking (warning on) the schema version.- Parameters:
record (dict) – An OSDU record shaped
{"kind": ..., "id": ..., "data": {...}}. Thedatapayload carries the entity fields; if absent the record itself is used as the data.- Returns:
The mapped hierarchy entity.
WellboreTrajectoryrecords 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
kindis malformed (viaparse_kind()) or names an entity that has nofrom_osdumapper.- 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 theWellNetwork(seenetwork_from_osdu()).kickoff_mdis derived, not native OSDU, so it is leftNone.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
WellNetworkfrom OSDU records.Maps every
Wellborerecord viafrom_osdu(), then wires theKickOffWellbore/WellIDparent edges into a network. Non-wellbore records are ignored.- Parameters:
records (list of dict) – OSDU records (as passed to
from_osdu()). OnlyWellborerecords 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:
Notes
Deriving each section’s
kickoff_mdfrom the trajectory tie MDs is a follow-up, done once the referenced station datasets are loaded (OSDU has no nativeKickOffMD).
- welleng.osdu.parse_kind(kind: str) tuple[str, str, str][source]
Parse an OSDU
kindstring into its parts.Inverse of
build_kind(): splitsosdu: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
kinddoes 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.survey_to_osdu(survey: Any, wellbore_id: str, *, version: str | None = None, uom: str = 'm', namespace: str = '') dict[str, Any][source]
Map a
welleng.survey.Surveyto an OSDUWellboreTrajectory.Emits the trajectory’s HEADER – the record that says how the survey was referenced, computed and acquired. The station array itself is a separate dataset in OSDU and is not produced here.
Every reference-data field is populated only when it RESOLVES; a field we cannot state is omitted rather than guessed at, because a wrong azimuth reference or calculation method is silently wrong in the direction that still looks plausible.
namespaceis the data partition. Without it the reference ids are partition-relative and will not match the schema pattern – fine for composing, not for ingesting.- Parameters:
survey (welleng.survey.Survey)
wellbore_id (str) – The parent wellbore’s OSDU id.
- welleng.osdu.to_osdu(entity: Any, *, version: str | None = None, uom: str = 'm') dict[str, Any][source]
Map a
welleng.hierarchyentity 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 requesteduomand encoding the parent edge (WellWellIDor parent-wellboreKickOffWellbore).- 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;Noneuses the pin (seebuild_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
entityis not a type with ato_osdumapper.
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.osdu_ref module
OSDU reference-data code lists — resolve, validate, annotate.
welleng.osdu maps the shape of an OSDU record (which entity, which
schema version, which units). This module supplies the other half: the
controlled vocabularies that a record’s ...TypeID / ...ID fields
must carry. A record with the right shape and no reference-data ids is
structurally valid and semantically empty.
Design
Annotation, never renaming. welleng’s own vocabulary does not change –
azi_reference="true" stays "true". resolve() maps a value ONTO an
OSDU code and hands it back for the caller to record alongside; it never
rewrites the caller’s string. Adoption that is not additive is a breaking
change for every consumer.
Governance decides strictness. OSDU publishes each list under one of three governance models, and that is exactly the right validation policy, so it is used directly rather than re-invented per list:
Data
One slimmed JSON per list in welleng/data/osdu/, refreshed by
scripts/fetch_osdu_reference_data.py. Only the lists welleng actually uses
are carried; 451 are published. Each file records its source URL, retrieval
date, governance tier and AttributionAuthority – several of these lists
are curated by PPDM rather than by OSDU, and that travels with the data.
Source: https://community.opengroup.org/osdu/data/data-definitions (Apache-2.0).
Example
>>> from welleng import osdu_ref
>>> osdu_ref.resolve("VerticalMeasurementType", "RKB")
'RotaryTable'
>>> osdu_ref.resolve("AzimuthReferenceType", "grid")
'GridNorth'
>>> osdu_ref.osdu_id("CementPlugType", "Abandonment")
'reference-data--CementPlugType:Abandonment'
- welleng.osdu_ref.FIXED = 'FIXED'
Governance tiers, strictest first.
- welleng.osdu_ref.LOCAL = 'LOCAL'
Governance tiers, strictest first.
- class welleng.osdu_ref.Match(code: str | None, how: str)[source]
Bases:
NamedTupleA resolution result, and HOW it was reached.
An export layer has to be able to refuse a weak match, and a bare
code | Nonecannot express “matched, but only on similarity”.howis one of:"exact"the value IS the published code.
"name"it matched the code’s display name (case and separators folded).
"alias"it matched a hand-kept field synonym – a judgement that two vocabularies mean the same thing, defensible but ours, not OSDU’s.
"none"no match;
codeisNone.
Truthy exactly when a code was found, so
if resolve_match(...)reads naturally.- code: str | None
Alias for field number 0
- how: str
Alias for field number 1
- welleng.osdu_ref.OPEN = 'OPEN'
Governance tiers, strictest first.
- exception welleng.osdu_ref.OsduRefError[source]
Bases:
KeyErrorA reference-data list or code could not be resolved.
- welleng.osdu_ref.codes(list_name: str) Dict[str, str][source]
{code: display name}for a list. Deprecated codes are not present.
- welleng.osdu_ref.curve_property(mnemonic: str, vendor: str | None = None) str | None[source]
The measured property behind a log-curve
mnemonic, orNone.Mnemonics are contractor- and vintage-specific: bulk density ships as
RHOB(Schlumberger, Halliburton),RHOZ(Schlumberger) andBDCX(Baker Hughes). This resolves them through the published vendor map rather than through a hand-written name list, which is what reported “no density curve” on 12 of 14 wells that had one.vendornarrows the lookup when the log says who ran it. Without it, a mnemonic that means different things to different vendors returnsNonerather than picking one – an ambiguous answer is worse than none for a measurement that feeds a calculation.
- welleng.osdu_ref.curve_quantity(mnemonic: str, vendor: str | None = None) str | None[source]
The UNIT QUANTITY behind a mnemonic (
"mass per volume", …), or None.More robust than
curve_property()and usually the one to reach for. Vendors name the same measurement differently –RHOBis density to one and bulk density to another – so the property can disagree while the quantity does not, and the quantity is what makes two curves comparable.Still
Nonewhen the quantity itself is ambiguous, which is the case that matters:DTis time per length to two vendors and plain time to a third, and those are not the same curve.
- welleng.osdu_ref.curve_vendors(mnemonic: str) List[tuple][source]
Every
(vendor, property, unit quantity)published for a mnemonic.Use this when
curve_property()returnsNoneto see whether the mnemonic is unknown or merely ambiguous – the two need different handling.
- welleng.osdu_ref.governance(list_name: str) str[source]
FIXED/OPEN/LOCAL– who may extend the list.
- welleng.osdu_ref.osdu_id(list_name: str, code: str, namespace: str = '') str[source]
The reference-data id a record’s
...IDfield carries.The schemas constrain these to
<namespace>:reference-data--<List>:<code>:<version>, with the version segment optionally empty – so the trailing colon is part of the form, not a typo, and a namespace is required for the id to validate.namespaceleft empty returns the partition-relative stem, which is what a caller composing a record against its own partition wants; it will not match the schema pattern until the partition is prefixed.
- welleng.osdu_ref.pinned_commit(list_name: str) str | None[source]
The upstream commit this list’s values were taken at.
The pin is what makes drift DETECTABLE: a retrieval date says when we looked, only the commit says what we looked at.
scripts/ fetch_osdu_reference_data.py --checkcompares every pin againstmasterand exits non-zero when one has moved – which matters because a code already STORED in someone’s data may have been deprecated or renamed upstream, and nothing in the stored record would show it.
- welleng.osdu_ref.provenance(list_name: str) dict[source]
Source URL, retrieval date, licence and attribution authorities.
- welleng.osdu_ref.resolve(list_name: str, value: str | None) str | None[source]
Best OSDU code for a local
value, orNoneif there is no match.Matching is deliberately forgiving – exact code, display name, then a small hand-kept alias table – because the input is a field string, not an identifier. It is a LOOKUP, not a rewrite: the caller keeps its own value and records this alongside it.
Nonemeans “no confident match”, never “invalid”. Useresolve_match()when the caller needs to act on HOW it matched.
- welleng.osdu_ref.resolve_match(list_name: str, value: str | None) Match[source]
resolve(), but reporting how the match was reached.
- welleng.osdu_ref.survey_tool_type(short_name: str, model_id: str) str | None[source]
OSDU
SurveyToolTypecode for an error model, orNone.The published codes are
"<short name>_<OWSG model id>"– e.g.MWD+SRGM_A001Mc,GYRO-NS_A020Gb– which is exactly the pair every welleng error model already carries in its metadata. 100 of welleng’s 101 models resolve. So this is less an adoption than a label: the models ARE these tools, they simply never said so.The one exception is the ISCWSA 6-axis rotating model (
D007Ma), which is not in the published list at all – not a spelling difference; no code in the 101 carries that id. It returnsNone, and that gap is a contribution candidate rather than something to paper over.
- welleng.osdu_ref.survey_tool_type_for(error_model: str) str | None[source]
OSDU
SurveyToolTypecode for a welleng error-model NAME.welleng names a model by its OWSG short name (
"MWD+SRGM","GYRO-NS_Fl"); the OSDU code appends the model id (MWD+SRGM_A001Mc). Matches on the short-name prefix and returnsNoneunless exactly one code matches – an ambiguous tool is not a tool.
- welleng.osdu_ref.validate(list_name: str, code: str | None, *, field: str = 'value') str | None[source]
Check
codeagainst the list, with strictness set by its governance.FIXEDraises on an unknown code,OPENwarns and passes it through (operators may extend an OPEN list),LOCALpasses silently (the shipped values are a starter set).Noneis always allowed – absence is not a validation question.
welleng.survey module
Well survey management, coordinate transforms, and trajectory analysis.
- class welleng.survey.SplitSurvey(survey: Survey)[source]
Bases:
objectSplit a survey into upper and lower station pairs for interval calculations.
Provides paired arrays of inclinations, azimuths, vectors, and doglegs for consecutive survey stations.
- 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:
MinCurveDirectional 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.
⚠️ IF ALL YOU WANT IS THE WELLPATH, USE
welleng.MinCurveINSTEADSurveySUBCLASSESMinCurve: the geometry is the base class, and whatSurveyadds is a 26-field header. Positions, dogleg, TVD, attitude at a depth and arc interpolation are all available fromMinCurvealone, with 16 attributes instead of 77.⛔ The header is not free, and the cost is not speed (construction is only ~1.4x). Every header field left unset takes a DEFAULT, and several of those defaults are ASSERTIONS about the well, not neutral values:
field
default
what it asserts
latitude51.4934
the well is at Greenwich
longitude0.0098
“
altitude0.0
at sea level
b_total/dip50000 / 70
a geomagnetic field
declination0.0
true north == magnetic north
convergence0.0
grid north == true north
survey_datetoday
surveyed today
🔴 These are load-bearing, not cosmetic:
header.latitudefeeds the gyro earth-rate terms (welleng.errors.tool_errors) and the ISCWSA conformance inputs. ASurveybuilt for geometry and later handed an error model computes its gyro terms at Greenwich, silently, for any well that is not in south-east London.mag_sourcerecords provenance for the magnetic values;latitude,longitude,altitudeandsurvey_datecarry no such marker.⇒ Reach for
Surveywhen the header data EXISTS and something needs it: error models and uncertainty, azimuth referencing, units, survey composition, export. Reach forMinCurvefor a wellpath. Promote withSurvey.from_min_curve()when the extra data turns up – it handles the radians/degrees difference between the two, which silently flattens a well if done by hand.- header
Survey metadata including location, datum, and reference information.
- Type:
- cov_nev
Covariance matrices in North-East-Vertical coordinates.
- cov_hla
Covariance matrices in High-Lateral-Along-hole coordinates.
- err
Error model results when an error model is applied.
- Type:
ErrorModel or None
- vertical_section
Vertical section lateral displacement if a VS azimuth is defined.
- __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_referencenames the INPUT azimuth frame and is converted to grid on construction. The input-reference default is"true"(seeSurveyHeader), 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. When
n,eandtvdare given they ARE the station positions (pos_nevand every position field), kept as given – a survey may be a set of points rather than a path, so they are not recomputed. Their first station is the anchor.supplied_residualreports the largest difference from the minimum-curvature path through the same stations.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).
x,y,zare the same positions ase,n,tvd; given both, they must agree orValueErroris raised.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. The survey keeps a copy: the header passed in is not modified.
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 withwelleng.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_xyzandstart_nevare ONE point, the survey’s anchor, written in two frames: give either. Given both (non-zero), they must be the same point orValueErroris raised; they are never added. When station positions are supplied, the first of them is the anchor and these are not used.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κ/dsper 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: theEI·τ·dκ/dsbinormal term). Zero on a constant-curvature (constant-DLS) section.Discrete form (SPE-105068-PA, Eq. 18):
(κ_{j+1} − κ_j) / (s_{j+1} − s_j).
- 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. Seedirectional_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
- classmethod from_min_curve(min_curve, **kwargs) Survey[source]
Promote a
MinCurveto a fullSurvey.SurveyIS aMinCurve– the geometry is the base class and the header is what aSurveyadds. Reach forMinCurvewhen all that is wanted is a wellpath; promote here when the extra data actually exists and something needs it (error models, azimuth referencing, units, export).⚠️
MinCurveholds inc and azi in RADIANS;Surveytakes DEGREES unless told otherwise. This passesdeg=Falsefor you. Doing the promotion by hand and forgetting it reads each radian value as a degree, which FLATTENS the well rather than raising an error: a 60 degree hold becomes 1.05 degrees, i.e. near-vertical. Measured on a 900 m build-hold. Every value stays in range, the survey is still monotonic and well formed, and nothing downstream objects.⚠️ Every header field left unset takes a DEFAULT, and several of those are assertions rather than neutral values –
latitudedefaults to 51.4934 (Greenwich) and feeds the gyro earth-rate terms. Supply what is known; do not promote to get a Survey-shaped object you do not need.- Parameters:
min_curve (welleng.utils.MinCurve) – The geometry to promote.
**kwargs – Passed to
Survey:header,error_model,start_nevand the rest.degis set toFalseunless given.
- Return type:
Examples
>>> import numpy as np >>> from welleng.utils import MinCurve >>> from welleng.survey import Survey >>> mc = MinCurve( ... np.array([0., 30.]), np.radians([0., 3.]), np.radians([0., 45.]) ... ) >>> survey = Survey.from_min_curve(mc) >>> bool(np.allclose(survey.inc_rad, mc.inc)) True
- 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_erroris True, otherwise the Survey instance with updated covariance attributes.- Return type:
ErrorModel or Survey
- Raises:
AssertionError – If
error_modelis 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, origin: ArrayLike | None = None) ndarray[source]
Vertical section: each station’s horizontal displacement from
origin, projected ontovertical_section_azimuth.Computed from the station positions (
n,e), so it is exact at the stations, needs no azimuth averaging and is correct for a path through north. The survey is not modified.- 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).
origin ((2,) array of floats or None (default: None)) –
(n, e)of the vertical-section origin, in the survey’s position frame (e.g. the slot).Noneuses the first station.
- Returns:
result – Vertical section at each station, in the survey’s length unit.
- Return type:
(n,) 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, or
Noneifmdis above the first station or beyond the last – the survey is never extrapolated.- Return type:
we.node.Node object or None
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]
Insert stations at the measured depths
mdand return the merged survey: EVERY original station plus the new ones, sorted by MD, as a new welleng.Survey (this survey is not modified). It does not return only the requested points – row 0 is still the first survey station.A requested depth that is already a station, or repeated, adds no row. To take just the requested points, select them by depth, e.g.
r.md[np.isin(r.md, md)]; for one point,interpolate_md()returns it directly.- Parameters:
md ((,n) list or array of floats) – The measured depths to insert.
- 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 iftvdis outside the well’s TVD range). Atvdequal to a turning point’s TVD is touched once, not crossed, and returns ONE Node at the turning point.- Return type:
list of Node
Notes
tvdis in this survey’s own depth frame (the frame oftvd), so the survey’s start depth is accounted for here.MinCurve.interpolate_tvd()takes the LOCAL frame instead.Breaking change (welleng 0.15.0): returns a
listof Nodes instead of a single Node. Useinterpolate_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 extradls_noisecurvature 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 toSurvey.steeringwhen the argument is left asNone.
- 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 lengthL_csand usesL_c(rather than1 / 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
stepthendls_noisedeg/30m added) so that the MTI is robust to survey-station frequency; setdls_noise=Noneto use the raw minimum-curvature survey instead.- Parameters:
rtol (float) – Relative tolerance when testing normal-vector continuity (passed to
numpy.iscloseas bothrtolandatol).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_noiseis 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 ofmaximum_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…) ifdatais 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, in RADIANS, relative to the high side – not the same unit as
dlsabove, which is in degrees per 30 meters. 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:
- 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
- property supplied_residual: float | None
Largest |supplied - minimum-curvature| station position, or
Noneif no positions were supplied. Meaningful only when the stations form a path; for a set of points it measures nothing useful.
- 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
bthe unit osculating-plane normal andΔs_jthe central measured-depth spacing about stationj. 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:
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. Seetortuosity_index()for the implementation andmodified_tortuosity_index()for the dimensionless variant.Note that TI is not dimensionless (the result scales with the unit of length, since the
1 / L_cnormalization carries length units); per SPE/IADC-194099-MS a scale factor of1e7is applied so values fall in a convenient range. ComputeL_cin feet to compare with the published reference ranges.- Parameters:
rtol (float) – Relative tolerance when testing normal-vector continuity (passed to
numpy.iscloseas bothrtolandatol).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.
**kwargs –
coeff(length unit conversion, default 0.3048 -> feet) andkappa(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 usetortuosity_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'}wheremdis the depth grid the profile is evaluated on (the maximum-curvature pre-processed grid whenmodifiedpre-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:
objectLightweight 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)
- 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 | None = None, azi_reference: str = 'true', vertical_inc_limit: float = 0.0001, xcl_representation: str = 'nev_direct', dp_basis: str = 'balanced_tangent', tie_on: bool | None = None, first_station_allowance: bool | str | None = None, iscwsa_definition: str = '5.13', 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:
objectMetadata 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_sourcetracks the provenance of each geomagnetic reference value ('user'/'lookup'/'default'); magnetic error models refuse'default'(seeErrorModel). Assigningb_total,dipordeclination— at construction or any time after — marks that field'user'.mag_modelrecords WHICH source a'lookup'came from (e.g.'WMM2025 (local-wmm)'for the bundled offline model, or the BGS model when the web service was used);Nonewhen no lookup happened.Note
A looked-up value is MODEL-derived, not a field measurement. The WMM (whether evaluated offline from the bundled coefficients or via BGS — the two agree to < 0.001 nT, so
'local-wmm'is not less accurate than the service) carries the model’s physical uncertainty — of order ~0.5° in declination at typical latitudes. For survey work needing better than the standard geomagnetic-reference error, supply an in-field-referenced (IFR / BGGM / high-resolution) value directly (which marks the field'user'); do not treat a WMM lookup as a measured reference.mag_modelis there so a consumer can see the value’s origin.- __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 | None = None, azi_reference: str = 'true', vertical_inc_limit: float = 0.0001, xcl_representation: str = 'nev_direct', dp_basis: str = 'balanced_tangent', tie_on: bool | None = None, first_station_allowance: bool | str | None = None, iscwsa_definition: str = '5.13', 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 (seemag_source).earth_rate (float (default: 0.26251614)) – The rate of rotation of the earth in radians per hour (7.292115e-5 rad/s, ISCWSA Error Model Definition Rev 5.13 s4.3).
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.
tie_on (bool or None (default: None)) –
Whether the first survey station is tied on to a prior surveyed section (its attitude/depth uncertainty carried in externally) as opposed to being the well’s own root/slot (fresh uncertainty seeded there). Governs the ISCWSA surface tie-on (§4.7.1.1 slot-attitude allowance) and the station-0 depth-reference seed in the error model.
None(default) infers the role from the measured depth –md[0] == 0is a surface root,md[0] != 0a tie-on. This default reproduces the conventional/tool-vendor covariances (a definitive survey that starts below the datum carries ~zero uncertainty at its first station), and is the recommended setting for reproducing a reference covariance.Set
Falseto force a root at a non-zero first depth. The intent is a SUBSEA well, whose slot is the wellhead on the seabed atmd != 0(the datum is the RKB/MSL at surface): the depth-inferred default would mis-classify that root as a tie-on and seed no first-station uncertainty. Settingtie_on=Falsere-engages the slot-attitude allowance and station-0 depth seed at the wellhead.NOTE – PLACEHOLDER FOR FUTURE WORK. The
Falsepath is a first-order stand-in, not a validated subsea root-uncertainty model: it re-uses the surface-root seed (slot-attitude allowance + station-0 depth reference) at the first station. A proper subsea model (the wellhead position/attitude uncertainty and how it should propagate, validated against field/tool-vendor covariances) is not yet implemented – treattie_on=Falseas opt-in, deliberately more conservative than the reference default, pending that work. Leave itNoneto match a reference covariance exactly.first_station_allowance (bool, "uncorrelated" or None (default: None)) –
How the first station’s own measured attitude error is carried over the first leg.
True: the slot allowance (ISCWSA Error Model Definition §4.7.1.1) – the first leg takes the whole of that error rather than half, coherently with the later stations of the same survey run. In vertical hole the singular weights of XYM3 and XYM4 give station 1 the whole first leg; elsewhere the first station’s inclination and azimuth weights are doubled (§4.7.1.1 eq. 32).Noneapplies each model’s own rule: Rev 5 and later at a surface root, not Rev 4. This reproduces ISCWSA’s own example calculators; for Rev 4 it matches the ISCWSA MWD Rev 4 example spreadsheet at every station of the wells of the ISCWSA standard set of well paths for evaluating clearance scenarios (r4, May 2017), to 1e-13 m on eight of its twelve wells.Trueapplies the allowance at the first station of any survey, including a tie-on;Falsenever.The covariances listed in that clearance set were generated with the allowance applied to Rev 4 and the surface position exact:
first_station_allowance=Truewithtie_on=True, and each well’s grid convergence at its own slot (ED50 / UTM zone 31N, as the set states it), reproduces them within 0.15 mm on eleven of its twelve wells, and its separation factors within 0.002 wherever they are below 10."uncorrelated": a tie-on station surveyed by another run, such as a sidetrack’s kick-off on its parent. The first station’s own attitude error over half a first leg, evaluated through each error source’s weights at its attitude and without the depth terms, is added uncorrelated with the survey’s errors at every later station (ErrorModel.errors.cov_NEV_tie; over the first half-leg the interior covariance takes it in proportion). This is ISCWSA common practice: the tie-on uncertainty “simply added to downhole survey uncertainty”, its correlation “invariably uncorrelated” (Current Common Practice in Collision Avoidance Calculations, 2017, §2.8). Withtie_on=Trueit reproduces the clearance set’s sidetrack (offset 10) within 0.3 mm laterally and 1.5 mm along hole; high side differs by up to 3.9 mm in the build below the kick-off, under 0.2 mm on the hold; that difference is not yet attributed.iscwsa_definition (str (default: "5.13")) – The revision of the ISCWSA Error Model Definition whose formulas are applied: “5.13” (the current revision) or “5.03”. Between them the only change in a formula is the misalignment damping term M of XYM3E/XYM4E (Definition s5.1.2 eq. 39): from Rev 5.12 it applies only where the station interval lies between 0.1 m and the tool model’s minimum course length, and is 1 otherwise; in Rev 5.03 it applies at every interval. Corrections of misprints between the revisions are not reproduced. The error model records the revision it applied and whether it is superseded (
ErrorModel.iscwsa_definition,ErrorModel.iscwsa_definition_superseded).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.
- mag_model: str | None
- osdu_azi_reference() str | None[source]
azi_referenceas an OSDUAzimuthReferenceTypecode."true"/"grid"/"magnetic"map toTrueNorth/GridNorth/MagneticNorth.Note the published list carries a provenance axis welleng does not model:
AssumedGridNorthandInferredMagneticNorthsay the reference was not stated in the source, andCompassNorthis a raw needle reading rather than a corrected azimuth. A header that was assumed rather than read cannot be expressed here, and should carry the assumed/inferred code explicitly instead.
- osdu_magnetic_model() str | None[source]
mag_modelas an OSDUGeoMagneticModelcode, orNone.The stored label is a provenance string (
"WMM2025 (local-wmm)"), not a code, so this strips the source tag and looks up what is left.Nonefor WMM2025 today: the published list stops atWMM2020/IGRF2020/HDGM2024and has no entry for the current model.
- class welleng.survey.SurveyParameters(projection: str = 'EPSG:23031')[source]
Bases:
ProjClass 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, viawelleng.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, area_of_interest: Any | 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.
area_of_interest (pyproj AreaOfInterest, or (west, south, east, north)) – in degrees, optional Where the coordinates are, for choosing the operation.
Noneuses the extent ofcoordsthemselves.
- Returns:
result – An array of transformed coordinates in the desired projection.
- Return type:
ArrayLike
- Raises:
ValueError – If no operation of stated accuracy is valid where the coordinates are, directly or through WGS 84. PROJ’s remaining fallback is a “ballpark” datum shift, which can leave the coordinates unchanged (ED50 to ETRS89, about 130 m in the Netherlands) while appearing to succeed.
Notes
Of the operations valid where the coordinates are, the most accurate with a stated accuracy is used; if none is valid directly, the route through WGS 84 is used, each leg chosen the same way. The operation – its name (both legs for a route), stated accuracy (m, summed over the legs), and whether every direct candidate was usable (
best_availableis False when one needs a transformation grid that is not installed) – is recorded onlast_operation.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) [[ 588226.55871189 5770360.93971979]] >>> calculator.last_operation["name"] 'Inverse of UTM zone 31N + ED50 to WGS 84 (18) + UTM zone 31N'
The operation is the most accurate one (stated accuracy) whose area of use contains the coordinates.
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_northhere 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:
objectA 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.derived_header(header: SurveyHeader, **fields: Any) SurveyHeader[source]
A copy of
headerwithfieldsset; the original is not modified.For building a survey from another one with some header values changed. Setting them on the original header instead would change the survey it belongs to.
- 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
AHDis the along-hole displacement (horizontal departure,sqrt(N**2 + E**2)at the station) andTortuosityis the cumulative absolute dogleg angle (total curvature imposed on the wellbore) in degrees.Units: DDI is not dimensionless –
MD * AHD / TVDcarries a length unit – so it is conventionally computed in FEET (the paper’s basis, target range ~5-8). Declare the survey’s length unit viadepth_units(welleng’sSurvey.unitis 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) pd.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:
- 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.
- 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:
- Returns:
A Node at the target TVD between the two input nodes.
- Return type:
- 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.grid_header(header: SurveyHeader) SurveyHeader[source]
A copy of
headerwhose azimuths are grid-referenced.For a survey DERIVED from another whose angles are already on grid (
azi_grid_rad): building it on the original header would apply the grid convergence a second time. The original header is not modified.
- welleng.survey.interpolate_md(survey: Survey, md: float) Survey | None[source]
Interpolates a survey at a given measured depth.
Returns
Nonewhenmdlies outside the survey – above the first station or beyond the last. There is no extrapolation at either end.
- 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:
- 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 iftvdis outside the well’s TVD range).- Return type:
list of Node
Notes
Breaking change (welleng 0.15.0): this returns a
listof 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 lengthL_csand the running sum is scaled byn / (n + 1)and the curve lengthL_c, making the result independent of the unit of length.L_csis the along-hole (arc) distance from the section start to each station andL_xsthe 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.
**kwargs –
coeff(unit conversion, default 1.0) andkappa(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, in meters.
dls (float) – The desired dogleg severity of the projection, in DEGREES per 30 meters. Entering 0.0 will result in a hold section.
toolface (float) – The desired toolface for the projection, in RADIANS, relative to the high side. Note that this is NOT the same unit as
dlsabove – the two angles in this signature are in different units. Degrees passed here do not fail loudly, they wrap modulo 2*pi and return a plausible different well: from inc 30 deg / azi 45 deg at dls 3 deg/30m,toolface=pi/2turns right as intended (inc 30.1, azi 51.0), whiletoolface=90– the same angle in degrees – drops the well instead (inc 28.8, azi 50.6).md (float (optional)) – The current md if applicable, in meters.
- 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, in meters.
dls (float) – The desired dogleg severity for the projection from the survey tool to the bit, in DEGREES per 30 meters. Entering 0.0 will result in a hold section.
toolface (float) – The desired toolface for the projection from the survey tool to the bit, in RADIANS, relative to the high side – not the same unit as
dlsabove.step (float) – The desired survey interval for the projected wellpath to the target, in meters.
- 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) pd.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 byn / (n + 1)and normalized by1 / L_c, then bykappa(1e7 per SPE/IADC-194099-MS).L_csis the along-hole (arc) distance from the section start to each station andL_xsthe corresponding straight-line (chord) distance.TI is not dimensionless: the result scales with the unit of length, so
coeffdefaults to 0.3048 to expressL_cin feet and match the published reference ranges. Seemodified_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.
**kwargs –
coeff(unit conversion, default 0.3048 -> feet) andkappa(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
remainingcalculation; defaults to the last station (total depth).
- Returns:
total(float, the profile value attarget_md/ end),remaining(ndarray,totalminus the profile — what is left to accumulate from each station),local(ndarray, the along-hole gradientd(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:
objectA 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
Surfacefrom 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
welleng.target module
Drilling target definitions for wellbore trajectory planning and visualization.
A Target is a region in 3D space that a trajectory aims to reach. It
carries a position, a shape, shape geometry, and an orientation (the plane the
shape lies in). Shapes range from a bare point through 1D (line), 2D (circle,
ellipse, rectangle, polygon) to a 3D volume, and a probabilistic
gaussian target defined by a covariance about its mean rather than a hard
boundary.
Construction is geometry-only and requires no optional dependencies; only
Target.plot_data() (rendering) needs vedo.
- class welleng.target.Target(name, n=None, e=None, tvd=None, shape=None, *, location=None, geometry=None, locked=0, orientation=0.0, dip=0.0, azimuth=0.0, cov=None, color='green', alpha=0.5, **geometry_kwargs)[source]
Bases:
objectA geometric or probabilistic target zone in 3D space.
Represents a target region at a given subsurface location, on a plane set by
orientation/dip/azimuth, with a shape-specific geometry. Agaussiantarget instead carries a covariancecovabout its mean (the position), for probabilistic (uncertainty-aware) targeting.The object is a pure data/geometry primitive — it constructs without any optional dependency.
plot_data()(vedo rendering) is the only method that requiresvedo.- name
Identifier for the target.
- Type:
str
- n, e, tvd
Position (northing, easting, true vertical depth), metres. Also exposed as
positionandlocation(a 3-vector).- Type:
float
- shape
One of
SHAPES, orNoneif unspecified.- Type:
str or None
- locked
Lock state of the target.
- Type:
int
- orientation
Rotation about the vertical axis, degrees.
- Type:
float
- dip
Dip of the target plane, degrees.
- Type:
float
- azimuth
Azimuth of the target plane, degrees.
- Type:
float
- cov
NEV covariance for a
gaussiantarget (mean = position);Nonefor hard-boundary shapes.- Type:
ndarray of shape (3, 3) or None
- color
Display colour for rendering.
- Type:
str
- alpha
Opacity for rendering (0.0 to 1.0).
- Type:
float
- geometry
Shape-specific dimensional parameters (e.g.
radiusfor a circle;radius_1/radius_2for an ellipse;pos1/pos2for a rectangle;verticesfor a polygon;thickness_up/thickness_downto extrude a 2D shape into avolume).- Type:
dict
- __init__(name, n=None, e=None, tvd=None, shape=None, *, location=None, geometry=None, locked=0, orientation=0.0, dip=0.0, azimuth=0.0, cov=None, color='green', alpha=0.5, **geometry_kwargs)[source]
Initialize a Target.
- Parameters:
name (str) – Identifier for the target.
n (float, optional) – Position (northing, easting, TVD) in metres. Alternatively pass
location=[n, e, tvd].e (float, optional) – Position (northing, easting, TVD) in metres. Alternatively pass
location=[n, e, tvd].tvd (float, optional) – Position (northing, easting, TVD) in metres. Alternatively pass
location=[n, e, tvd].shape (str, optional) – One of
SHAPES. May be leftNoneand set later.location (sequence of float, optional) – Position as
[n, e, tvd]; an alternative ton/e/tvd.geometry (dict, optional) – Shape-specific parameters as a dict. Alternatively pass them as keyword arguments (e.g.
radius=30), which are collected intogeometry.locked (int, optional) – Lock state (0 = unlocked).
orientation (float, optional) – Target-plane orientation angles, degrees.
dip (float, optional) – Target-plane orientation angles, degrees.
azimuth (float, optional) – Target-plane orientation angles, degrees.
cov (array_like of shape (3, 3), optional) – NEV covariance for a
gaussiantarget (mean = position).color (str, optional) – Display colour.
alpha (float, optional) – Opacity (0.0 to 1.0).
**geometry_kwargs – Shape-specific parameters collected into
geometrywhengeometryis not given explicitly. Forcircle:radius. Forellipse:radius_1,radius_2, optionalres. Forrectangle:pos1,pos2.
- Raises:
ValueError – If
shapeis given but not one ofSHAPES.
- property dim
Positional DOF a landing point has within the shape (see
SHAPES).
- property mean
Mean of a
gaussiantarget (its position);Noneotherwise.
- plot_data()[source]
Generate a vedo mesh object for rendering the target.
Currently supports the
circleshape. 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
flagattribute.- Return type:
vedo object
- Raises:
AssertionError – If
vedois not installed.
- property position
Position as an
ndarray([n, e, tvd]), orNoneif unset.
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:
objectHookload (broomstick) plot model for running or pulling a string.
- __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.
- class welleng.torque_drag.TorqueDrag(survey, wellbore, string, fluid_density, name=None, wob=None, tob=None, overpull=None)[source]
Bases:
objectTorque 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_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.
- __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.
The difference is wrapped into
(-pi, pi]so a station where the well crosses north (e.g. 359.9 -> 0.3 deg, a 0.4 deg turn) is treated as the small turn it is, not an almost-full revolution. Without the wrap the Johancsik bending termT * dazi * sin(inc)explodes at north crossings (welleng#312) – and, becauseforce_normalis called while integrating tension, the error also propagates into the tension profile.
- 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.tensionandself.torquedicts 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.
- 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, andtorque.
- 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, andtorque.
- 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.lint module
Source-level checks for ways of misusing a survey that no test will catch.
Why this is a LINT and not a test
The defect this module exists for is np.interp(md, survey.md, survey.tvd):
a straight chord through (MD, TVD) where the trajectory is a circular arc. It
cannot be caught numerically by the repo that commits it, because the error
scales with the CALLER’s station spacing, not with anything the committing repo
controls. On a densely-sampled survey (~30 m stations) it is right to a few
centimetres; on a 13-station design survey with 480 m gaps the same line is out
by 14.4 m of TVD. So every fixture passes, nothing errors, and the answer
is plausible, in range, and monotonic. The only place to catch it is the source.
Why it lives in CORE
Because the alternative is nine copies. The first consumer to hit this wrote an
excellent regex guard for its own repo – and that guard missed one live
instance in its own source (np.interp(md, p_md, parent.inc), because the
pattern looked for inc_deg/inc_rad and this was .inc). A guard
forked into every repo is the same duplication problem one level up, with each
fork carrying its own holes. This one is AST-based, so it sees the attribute
regardless of what the object is called, and a hole fixed here is fixed for
every consumer at once.
The rule
Flag a call when the interpolated quantity – fp, the third argument –
is a trajectory axis (md, tvd, inc, azi, n/e/northing/easting). That is the
precise statement of the defect, and it is what separates it from the ordinary,
correct thing right next to it:
np.interp(tvd, self.tvd, self.sigma_V) # OK – a PROPERTY against depth np.interp(md, survey.md, survey.tvd) # BAD – the TRAJECTORY itself np.interp(md, p_md, parent.inc) # BAD – and an ANGLE at that np.interp(mid[2], target.tvd, target.md) # BAD – the inverse, same error
Interpolating a property (pore pressure, temperature, a log curve) against a depth axis is not flagged: a lint that cries wolf gets switched off, and then it protects nothing.
Why that exemption is safe – and what it does NOT say
It is NOT that a property is exempt from the geometry. A log sample is a point in
space like any other, so the chord is wrong there too. The deviation of a chord
from the arc is the sagitta, R(1 - cos(kappa*h/2)), which goes as
(kappa*h)^2 – so the STEP decides the magnitude, not the kind of data:
at 4.16 deg/30 m (R = 413 m), chord-vs-arc at the midpoint of a step
h = 1 m 3.0e-04 m h = 20 m 1.2e-01 m h = 5 m 7.6e-03 m h = 30 m 2.7e-01 m
The exemption is safe because of WHO OWNS THE ERROR, not because of the data type. Interpolating the TRAJECTORY hands the caller a position this library is responsible for, wrong by an amount set by their station spacing – which this repo does not control and no test here can fail on. Interpolating a property is a modelling choice the caller makes about data the caller sampled, at a spacing the caller can see.
⚠️ So the exemption transfers badly, and knowingly: a COARSELY sampled property – a 20-30 m formation-tops list, a sparse pressure survey, an MDT point set – passes this lint while carrying 10-30 cm of position error. That cannot be detected statically, because the step is a property of the caller’s data and not of the source text. If you are placing something in space from a sparse depth axis, this lint is silent and it is not evidence that you are right.
(Magnitudes verified against the closed-form sagitta.)
Usage:
python -m welleng.lint src/ # exits 1 on findings
python -m welleng.lint --quiet pkg/ # exit code only
In a consumer’s suite:
from welleng.lint import find_linear_survey_interpolation
def test_no_linear_survey_interpolation():
assert find_linear_survey_interpolation("src") == []
⛔ Do NOT write that test as ``skipif`` on ImportError. A repo that cannot import this check and skips has a check that cannot run reading as a pass. Declare the dependency, or let the test error loudly. A green suite must mean “clean”, never “not checked”.
⚠️ What a clean run does and does not mean. This walks PACKAGE SOURCE. It does not see notebooks, loose scripts, or spec files, so a pass says the PACKAGE is clean, not the REPO. Worth knowing before quoting a zero.
⭐ Why a check rather than a convention: this defect was fixed nine times as CHANGES, and reminders and conventions were both tried. The check found twelve more instances in core the day it existed, and later three in a consumer that had never called a guarded function – it reimplemented the decision, which is invisible to any guard on the function. A corrected line is invisible to the next author; a red suite is not.
- welleng.lint.ANGLE_AXES = frozenset({'azi', 'azi_deg', 'azi_rad', 'azimuth', 'azimuth_deg', 'azis', 'inc', 'inc_deg', 'inc_rad', 'inclination', 'inclination_deg', 'incs'})
angles at the stations, whose difference along MD is not the curvature of the path between them.
- Type:
The attitude axes
- class welleng.lint.Finding(path: str, line: int, axis: str, source: str)[source]
Bases:
objectOne linear interpolation of a trajectory axis.
- __init__(path: str, line: int, axis: str, source: str) None
- axis: str
The trajectory axis being interpolated (the
fpargument).
- line: int
- path: str
- property replacement: str
What to use instead.
- source: str
The offending source line, stripped.
- welleng.lint.TRAJECTORY_AXES = frozenset({'azi', 'azi_deg', 'azi_rad', 'azimuth', 'azimuth_deg', 'azis', 'e', 'easting', 'inc', 'inc_deg', 'inc_rad', 'inclination', 'inclination_deg', 'incs', 'md', 'mds', 'measured_depth', 'n', 'northing', 'pos_nev', 'pos_xyz', 'poss', 'true_vertical_depth', 'tvd', 'tvd_m', 'tvds'})
Attribute/variable names that ARE the trajectory. Interpolating one of these linearly against MD is the defect; interpolating something else against one of them as an axis is ordinary and correct.
- welleng.lint.find_attitude_difference(paths: Iterable[str] | str, exclude: Sequence[str] = ('.venv', 'site-packages', 'build', '.git')) List[Finding][source]
Every
np.gradientornp.diffof station inclination or azimuth.Curvature, build or turn taken as a finite difference of the station angles is not the curvature of the minimum-curvature path. Along each leg the path is one arc of constant curvature,
dogleg / delta_md, stepping at the stations;np.gradient’s central difference averages two legs, smearing that step; a difference of inclination alone drops the turn, which the dogleg carries; and azimuth wraps at 0/360, so a difference across north reads as a near-full turn. Use the survey’s owndogleganddelta_md(survey.dogleg / survey.delta_mdper leg), orsurvey.dls.Same contract as
find_linear_survey_interpolation(): empty when clean; an unparseable file is reported, not skipped. Names are matched as that rule matches them, throughnp.asarray(...), subscripts and a local bound from an attitude axis.
- welleng.lint.find_linear_survey_interpolation(paths: Iterable[str] | str, exclude: Sequence[str] = ('.venv', 'site-packages', 'build', '.git')) List[Finding][source]
Every linear interpolation of a trajectory axis under
paths.pathsmay be a single path or an iterable of them; directories are walked for*.py. Returns an empty list when clean, so a consumer’s suite can assert on it directly.A file that does not parse is REPORTED, not skipped – a lint that silently passes over what it could not read is indistinguishable from one that found nothing.
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.TDandFRAC = LOT.g.shoeare both gradient-derived, soh = [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:
objectFast, 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
- convert(value: float | ndarray, src: str | Unit, dst: str | Unit) float | ndarray[source]
Convert
value(scalar or ndarray) fromsrctodstunits.
- from_canonical(value: float | ndarray, quantity: str) float | ndarray[source]
Canonical-SI
value-> the user’s units for the namedquantity.
- system: dict
- welleng.units.deg(value: int | float) Quantity[source]
A quantity of
valuedegrees.>>> 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.is_valid_unit(unit_str: str | Unit) bool[source]
True if
unit_stris a unit the registry understands.A public predicate for validating a user-supplied unit string at an API/GUI boundary BEFORE conversion, without a
try/exceptaroundQ_.>>> is_valid_unit('lbf/(100*ft**2)') True >>> is_valid_unit('not_a_unit') False
- 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.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 — usetemperature_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, unit: str | Unit, to_unit: str | Unit | None = None) float[source]
Magnitude expressed in a target unit (float). Two call forms:
to(quantity, unit)– express an existing Quantity inunit; orto(value, from_unit, to_unit)– a value + its unit-string, converted toto_unit(drops theQ_(value, from_unit)boilerplate).>>> to(pressure(1, 'bar'), 'Pa') 100000.0 >>> to(1.0, 'bar', 'Pa') 100000.0
- welleng.units.to_deg(angle: Quantity) float[source]
Magnitude of
anglein degrees (float).>>> round(to_deg(rad(3.141592653589793)), 6) 180.0
welleng.utils module
- class welleng.utils.Arc(dogleg, radius)[source]
Bases:
objectA circular arc of given dogleg and radius, built at a local origin.
- __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]
Transform from HLA to NEV coordinate system; the inverse of
NEV_to_HLA().- Parameters:
survey ((n,3) array of floats) – The [md, inc, azi] survey listing array, inc and azi in radians. Unused when
transis given.HLA ((n,3) or (n,3,3) array of floats) – The HLA coordinates or covariance matrices.
cov (boolean) – If True,
HLAis (n,3,3) covariance matrices, else (n,3) coordinates.trans ((n,3,3) array of floats or None) – A precomputed
get_transform()result;Nonecomputes it fromsurvey.
- Returns:
NEV
- Return type:
- class welleng.utils.MinCurve(md, inc, azi, frame: str = 'env')[source]
Bases:
objectMinimum-curvature geometry of a survey in local coordinates.
- __init__(md, inc, azi, frame: str = 'env')[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 referenceaziis in) and holds no surface/start position or datum state – that belongs to the owningSurvey, 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
⛔ ``np.interp`` on ``poss``, ``tvd`` or an angle between stations is the ANTI-PATTERN this class exists to replace. A survey between stations is a circular ARC, not a chord; the chord returns a monotonic, in-range, WRONG answer whose size is set by the CALLER’s station spacing, so no test of the caller’s will fail. Measured on real surveys: 0.33 m of TVD and 3.63 m laterally on a 55-station sidetrack at 3.4 deg/30 m, and 7.22 m of TVD at 150 m spacing. Use
interpolate(),interpolate_tvd()orinc_azi_at().python -m welleng.lint <path>detects it, andwelleng.lint.find_linear_survey_interpolation()is importable so a CONSUMER can assert its own repo clean – the defect has been fixed nine times as changes and twice more in consumers who never called a guarded function, so the check is the only thing that travels.MinCurve is units-agnostic:
mdmay be in any length unit and the geometry is all ratios/angles. Dogleg severity (which needs a per-unit coefficient) is thedls()method, into which the caller injects the coefficient for its units.⚠️ ``poss`` column order follows ``frame``, and the DEFAULT is ``[easting, northing, tvd]`` – x/y, NOT N/E. A consumer adopting this kernel transposed N and E against its own
[northing, easting, tvd]convention, and a transpose is SILENT on any well roughly symmetric in the two axes: it surfaces as a mislocated surface datum, not as an error. Establish the order with a due-north and a due-east probe rather than by readingdelta_x/delta_y, which invite the wrong guess – or just passframe="nev"and stop converting.frame"env"(default) –[easting, northing, tvd], the historical order, whichSurveyconsumes."nev"–[northing, easting, tvd], for a caller whose own convention is N/E and which would otherwise swap every result back.
🔴 ``delta_x``, ``delta_y`` and ``delta_z`` do NOT follow ``frame``. They are per-station increments in AXIS terms – x EAST, y NORTH, z TVD – in BOTH frames. Only
possandinterpolate()followframe. Underframe="nev"poss[:, 0]is NORTHING whiledelta_xis still EASTING, so the two surfaces disagree by construction and mixing them transposes the well. Defensible – x/y are axis names, not compass names – but stated here because the warning above only says not to INFER the order from them, which leaves a reader who takes that warning correctly with no way to learn what they positively are. (Reported by a consumer, 2026-09-20.)This is a BASIS, not a second algorithm. The arc kernel (
arc_step()) is coordinate-agnostic and returns whatever basis it is given, so both frames run the same code over differently-ordered tangents – there is no second path to drift, and no golden anchoring a dead one."nev"is in fact marginally the cheaper of the two:min_curve_stepalready computes in[N, E, V]and the default spends a column swap converting it.
- inc_azi_at(md)[source]
Inclination / azimuth (radians) at measured depth(s) – ATTITUDE ONLY.
interpolate(md, angles=True)also builds the position, and a caller that wants only an attitude pays for a displacement it discards: the half-angle position is roughly 70% of that call. A consumer resolving inc/azi at every mesh boundary of a torque-and-drag solve was doing exactly that, so this skips it.Same arc, same Rodrigues tangent, same answers as
interpolate(md, angles=True)[1:]– asserted bitwise in the tests, because the moment these two could differ there would be two implementations of an attitude.- Returns:
(inc, azi) – Azimuth is in
[0, 2*pi).mdoutside the survey range yieldsnanrather than the nearest station’s attitude.- Return type:
arrays of radians, or floats for scalar
md.
- 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 followsmd).- 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 asposs) – shape(3,)for scalarmdelse(n, 3). The caller adds any datum/start offset (MinCurve holds no datum).mdoutside the survey range yieldsnan.- 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. Nearpithe half-angle position divides bycos(theta/2) -> 0: against a 50-digit reference on a 100 m leg the error grows as ~``1/cos(theta/2)**2`` – 1e-13 m at 170 deg, 4e-12 m at 177 deg, 1e-9 m at 179.9 deg. The tangent (Rodriguesu-form) divides bysin(theta)once, in set-up.
- 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.tvdis in the LOCAL frame (relative to station 0, like the TVD column ofposs);Surveylayers its datum on top. Empty if the target is never reached.⚠️ A TVD in a datum frame (e.g. an EDM or LAS TVD column) passed here is offset by the first station’s depth. Where that offset value still lies inside the local TVD range it returns a WRONG measured depth; where it does not, it returns
[], indistinguishable from “never reached”. Both are silent:MinCurveholds no datum, so it cannot detect either. UseSurvey.interpolate_tvd()(build withSurvey.from_min_curve()and astart_nev) when the TVD carries a datum. A TVD equal to a turning point’s TVD returns one MD.
- leg_frames()[source]
Each leg’s frame,
(v1, u, curved), arrays over the legs.v1(n-1, 3) is the leg’s start tangent andu(n-1, 3) the unit vector in the arc’s plane, perpendicular tov1, towards the end tangent: the tangent at partial doglegphiiscos(phi) v1 + sin(phi) u.curved(n-1,) is False for a straight leg, whoseuis not defined (readv1only). Components are (east, north, vertical down). Computed once per survey.
- 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 inself.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:
- 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
idis provided then circular volume is calculated. If nolengthis 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)fortheta in [0, dogleg], whereuis the in-plane unit vector perpendicular tovec_a(sot(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)witht_V = A cos + B sin(A = vec_a_V,B = u_V); its extrema are at the arc ends plus the interior critical pointstheta = 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)/dthetanumerator= vec_a_N u_E - vec_a_E u_Nis constant (thecos^2 + sin^2cross-terms cancel identically). So its extrema are the two ENDPOINTS, swept in directionsign(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_verticalis flagged and the azimuth span should be treated as full-wrap by callers.
Notes
At
doglegexactlypi(antiparallel tangents,vec_b = -vec_a) the arc plane – henceu– is not recoverable fromvec_a/vec_balone; such arcs are treated as degenerate (constant inc/azi, zero swing). This is a measure-zero case for real min-curvature/CLC arcs; near-piis exact.- Returns:
dict with (n,)-arrays (
inc_min,inc_max(radians);azi_start,)azi_end(radians, in (-pi, pi]);azi_swing(signed total azimuthchange, 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) andwelleng.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
v1station to the query point (0 <= x <= dmd);x = dmdgives the far station.
- Returns:
disp, tangent – Local displacement from the
v1station 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 astheta -> pi(~0.1 ulp near pi vs tens of ulp for the/sinandrf = (2/phi) tan(phi/2)forms). It is applied forthetain[0, pi](theget_doglegrange). The tangent is the Rodriguesu-formcos(phi) v1 + sin(phi) uwithu = (v2 - cos(theta) v1)/sin(theta): a one-time1/sinset-up with no per-query amplifier, and – unlike the normalise-the-derivative shortcut – it keeps the correct sign fortheta > pi(the long-way arcsinterpolate_curverenders). The exact antiparallel turn (theta = pi, arc plane undetermined) is left as the start tangent.
- welleng.utils.best_fit_rotation_2d(a, b, weights=None)[source]
Closed-form 2D rotation angle that best aligns vectors
aontob.Solves the 2D orthogonal-Procrustes (Wahba) problem: the rotation angle
thetaminimisingsum_i w_i |R(theta) a_i - b_i|^2has the exact closed formtheta = atan2( sum_i w_i (a_i x b_i), sum_i w_i (a_i . b_i) )
where
a_i x b_i = a_ix b_iy - a_iy b_ix(the 2D cross product) anda_i . b_iis the dot product. No iteration; a singleatan2.With
weights=Nonethe vectors enter at their own magnitude, so longer vectors dominate the fit – useful whena/bare displacement steps and the longer steps are the more reliable direction estimates. Pass unit vectors (or explicitweights) to weight every pair equally.In directional surveying this recovers the single rotation between two sets of directions – e.g. the grid-convergence / reference offset between a survey’s stated azimuths and the direction of its position steps, or between two surveys of the same well.
- Parameters:
a ((n, 2) array_like) – Two sets of 2D vectors, paired by row.
b_iisa_irotated (plus noise). Any consistent axis convention works (e.g. columns(north, east)); the result is the rotation fromatob.b ((n, 2) array_like) – Two sets of 2D vectors, paired by row.
b_iisa_irotated (plus noise). Any consistent axis convention works (e.g. columns(north, east)); the result is the rotation fromatob.weights ((n,) array_like, optional) – Per-pair weights. Default
Noneweights by vector magnitude.
- Returns:
The best-fit rotation angle in radians, in
(-pi, pi].- Return type:
float
Notes
Derivation: minimising
sum w_i |R a_i - b_i|^2over rotations is maximisingsum w_i b_i^T R a_i. WritingR(theta)and differentiating givestan(theta) = sum w_i (a_i x b_i) / sum w_i (a_i . b_i), resolved to the correct quadrant byatan2. This is the 2D case of Kabsch/Wahba.
- welleng.utils.cov_from_vec(arr)[source]
Returns a (n, 3, 3) covariance matrix from an (n, 3) array via outer product.
- 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.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.
- 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.
- 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).
- welleng.utils.get_sigmas(cov, long=False)[source]
Extracts the sigma values of a covariance matrix along the principle axii.
- 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:
- 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_toolfacefor scalar inputs.Suitable when pos1, vec1 and pos2 are all individual (3,) arrays. For batch use, prefer the vectorised
get_toolface.- Parameters:
- 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.
- 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]
Convert
[n, e, v]positions to(n, 3)[x, y, z].xis east andynorth:start_nevis subtracted frompos, the first two columns are swapped, andstart_xyzis added.
- welleng.utils.linear_convert(data, factor)[source]
Multiply a value, or each item of a list, by
factor.Noneitems pass through asNone. A list in returns a list; any other input returns a single value.
- 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]
Build a covariance matrix from three standard deviations.
long=Falsegives the diagonal matrixdiag(a**2, b**2, c**2);long=Truegives the full outer product (fully correlated). With (n,) array inputs the result is (n, 3, 3); with scalars, (3, 3).
- welleng.utils.make_long_cov(arr)[source]
Build a (n, 3, 3) covariance matrix from the 6 unique upper-triangle elements per station.
- 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 fromMinCurve.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.survey_from_positions(nev, tie_vec, deg=True, mds=None)[source]
Reconstruct a minimum-curvature survey (md, inc, azi) from NEV positions.
Analytical inverse of the minimum-curvature method with a fixed tie-in tangent. On each leg the minimum-curvature chord bisects the two station tangents, so the end tangent is the start tangent reflected about the chord direction:
t_{i+1} = 2 (c . t_i) c - t_i
with the tie-in tangent seeding the march; measured depth accumulates as the arc length
L * alpha / sin(alpha)(alpha= angle between the leg’s start tangent and its chordc). Closed form, O(n), no iteration.Exact (to machine precision) when the position path is representable as a minimum-curvature arc chain – the case for a fused best-estimate trajectory (its stations are a small perturbation of two arc chains). A path that bends more than a single arc per leg can support needs the curve-hold-curve / added-node refinement (
welleng.connector); this is the closed-form first solution, not the only one.- Parameters:
nev ((n, 3) array_like) – Station positions in (north, east, vertical), metres.
tie_vec ((3,) array_like) – Tie-in unit tangent at the first station, in NEV. Fixed – it pins the otherwise free march.
deg (bool, default True) – Return inc/azi in degrees, else radians.
mds ((n,) array_like, optional) – Return these measured depths instead of the reconstructed arc-length MD. The reconstructed MD is self-consistent with the returned inc/azi; supplying the input surveys’ MDs instead carries a small residual (typically sub-mm per 1000 m) where the fused path length differs from the originals.
- Returns:
md, inc, azi
- Return type:
(n,) ndarrays
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:
- 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
welleng – well engineering: survey, error models, clearance, trajectory.
Submodules load ON DEMAND (PEP 562), and that is a correctness decision, not a micro-optimisation.
Every submodule used to be imported eagerly here, so from welleng.survey
import MinCurve – a pure-numpy arithmetic kernel – cost 1.7 s and
dragged in VTK, trimesh, plotly, pandas and scipy, because importing any
submodule runs this file first. A consumer wanting minimum curvature had to
take a renderer with it.
That is not merely slow: it is a standing invitation to FORK the kernel rather
than import it. A family member did exactly that, hand-rolling minimum
curvature and a bisection inverse of interpolate_tvd, and the stated reason
was a dependency boundary this cost is what makes attractive. A principle that
is expensive to obey gets disobeyed, and the disobedience then gets a
rationale. Making the cheap thing correct is core’s job, not the consumer’s
discipline.
welleng.visual / welleng.mesh still work exactly as before – attribute
access triggers the import – so we.visual.figure(...) after import
welleng is unchanged.
- class welleng.EDMReader(path: str, source_units: str = 'feet', with_geopressure: bool = False, with_assemblies: bool = False, with_load_cases: bool = False, with_tool_terms: bool = False)[source]
Bases:
objectStreaming reader for a Landmark EDM/COMPASS XML export.
Build with
open()(or the constructor). Oneiterparsesweep indexes the metadata + survey-station tables; surveys are assembled lazily viasurvey().- Parameters:
path (str) – Path to the EDM XML file.
source_units (str, {"feet", "meters"}) – Units of the stored depth/offset values (default
"feet"– the Volve export). Angles are always degrees.with_geopressure (bool, default False) – Also materialise the geopressure / temperature prognoses, as-run geometry, formation tops, and grade/material catalogue (the large non-survey tables). Off by default keeps the light survey-only sweep. When
True, usepore_pressure(),frac_gradient(),temperature(),geometry(),formations(),grades(),materials().
- schema
Human-readable name + description for each EDM table/field this reader surfaces (see
EDM_SCHEMA) – so consumers, docs, and any viewer do not have to decode rawCD_*codes.- Type:
dict
- __init__(path: str, source_units: str = 'feet', with_geopressure: bool = False, with_assemblies: bool = False, with_load_cases: bool = False, with_tool_terms: bool = False)[source]
- assemblies(wellbore=None) List[Assembly][source]
String assemblies (casing/tubing/liner/BHA), each with its components.
With
wellboregiven, only that wellbore’s strings; otherwise all. Casing components carry Landmark’s stored axial/burst/collapse ratings (the oracle) – seeAssemblyComponent. Requireswith_assemblies=True.
- case_parameters(case_id: str, application: str | None = None) Dict[str, float | None][source]
TU_CASE_ASSEMBLY_PARAMETERfor onecase_idas{parameter_name: parameter_value_num}(e.g.'InitAppSurfPressure'– the load-case input values).applicationfilters on the row’sapplication_name(values'StressCheck'/'WELLCAT'), case-insensitive.
- case_temp_gradient(case_id: str, application: str | None = None) Dict[str, List[Tuple[float, float]]][source]
CD_CASE_TEMP_GRADIENTfor onecase_idas{profile_name: sorted [(md, casing_temp)]}– the per-case temperature profile(s) (the WellCat “case temperature” that seeds the deration + APB baseline).mdis in the file’s source units (feet);casing_tempis raw (degF).applicationfilters StressCheck vs WellCat.
- cases_with_load_config(application: str = 'StressCheck', wellbore=None) List[Dict[str, str]][source]
CD_CASErows for the cases that CARRY a load configuration forapplication– i.e. that haveTU_CASE_ASSEMBLY_PARAMETERrows with thatapplication_name. These are the designable cases.This differs from
design_cases()on purpose:design_casesfilters byCD_CASE.create_app_id(the tool that AUTHORED the case), whereas this enumerates by where the load params actually LIVE. A case authored in COMPASS (the trajectory/link tool) can carry a StressCheck load config — it is returned here but NOT bydesign_cases(application='StressCheck'). (Volve: 422 cases carry StressCheck params vs 169 StressCheck-authored; 279 of the difference are COMPASS-authored.) Use this to enumerate the designable cases, thencase_parameters()per case_id.
- custom_load_profiles(wellbore=None) List[CustomLoadProfile][source]
Per-depth custom load profiles (the load a string sees), points by MD. Requires
with_load_cases=True.
- datum_set(well) List[Dict[str, str]][source]
The full per-rig, dated datum realisations for a well (do NOT collapse to one RT – resolve by a measurement’s date/rig).
- design_cases(wellbore=None, application: str | None = None) List[Dict[str, str]][source]
CD_CASErows – the casing/tubing design load cases – as raw dicts. Filter bywellbore(id or name) and/orapplication.NOTE:
CD_CASEcarries noapplication_name; the authoring app is increate_app_id(versioned strings, e.g.'StressCheck 5000.1','WELLCAT','COMPASS','WELLPLAN ...').applicationmatches it case-insensitively as a substring, so'StressCheck'/'WellCat'both work. Keys include case_id, case_name, phase, scenario_id, assembly_id, fluid_id, hole_sect_group_id, create_app_id.NOTE:
applicationhere means “AUTHORED by that tool” (create_app_id). To enumerate cases that carry a load CONFIG for a tool – including COMPASS-authored cases that StressCheck wrote params onto – usecases_with_load_config().
- dls_overrides(wellbore) List[DlsOverride][source]
Dogleg-severity override intervals for a wellbore (empty if none – which means the survey DLS governs). Requires
with_load_cases=True.
- epp_parameters(wellbore=None) Dict[str, Dict[str, object]][source]
External-pressure-profile parameters, keyed by
load_id->{name: value}. These are PER-LOAD (each load carries its own set; values genuinely differ across loads of a case). Requireswith_load_cases=True.
- find_wellbores(substring: str) List[Wellbore][source]
Return wellbores whose name contains
substring(case-insensitive).
- fluid_rheology(fluid_id: str | None = None) List[FluidRheologyPoint][source]
Fluid rheology points (with joined Fann data), by temperature.
Rows carrying no rheology numbers are skipped (an incomplete catalogue row is not a fluid definition).
fluid_idfilters to one fluid. Requireswith_geopressure=True.
- frac_gradient(wellbore, phase: str | None = None, latest: bool = True, units: str | None = None) List[GeopressureProfile][source]
Fracture-gradient prognoses (pressure in psi is canonical).
tvdinunits(default: the reader’ssource_units); seepore_pressure()for the phase / richest-group / units behaviour.
- geometry(wellbore, group_id: str | None = None) List[HoleSection][source]
As-run hole/casing sections (OD/ID/drift/hole size/shoe MD), by shoe MD.
group_idselects one design scenario (ahole_sect_group_idfromhole_section_groups()) – returned as-is, no dedup. With nogroup_idthe sections are pooled across scenarios and exact-duplicate rows collapsed (a convenience view; use a group for a single coherent string).
- hole_section_groups(wellbore) List[HoleSectionGroup][source]
The geometry design scenarios for a wellbore.
Each is a
hole_sect_group_idwith the case name(s)/phase(s) that reference it (viaCD_CASE) and its section count – so a caller can pick a scenario forgeometry()by name/phase, not an opaque id.
- load_headers(wellbore=None) List[LoadHeader][source]
Named loads per case, sequence-ordered. Requires
with_load_cases=True.
- load_profiles(wellbore=None, application: str | None = 'StressCheck') List[LoadProfile][source]
Enabled design load profiles (design factors + load params), grouped per (case, profile).
applicationfilters by app (default StressCheck;Nonefor all). Requireswith_load_cases=True.
- magnetics(wellbore) Magnetics | None[source]
The wellbore’s geomagnetic reference (operator’s own model).
Returns the operative
DP_MAGNETICrow (highestsequence_no) as aMagnetics(b_total/dip/declination/date/model), orNoneif the EDM has no magnetics for the wellbore. Use it to build a per-wellSurveyHeaderor an interpretationGeomagReferencefrom the operator’s geomag rather than a generic model lookup. Always available (no opt-in flag). Access every row viamagnetics_all.
- magnetics_all(wellbore) List[Magnetics][source]
All
DP_MAGNETICrows for a wellbore, bysequence_no.
- classmethod open(path: str, source_units: str = 'feet', with_geopressure: bool = False, with_assemblies: bool = False, with_load_cases: bool = False, with_tool_terms: bool = False) EDMReader[source]
Open and index an EDM file. Alias for the constructor.
- pore_pressure(wellbore, phase: str | None = None, latest: bool = True, units: str | None = None) List[GeopressureProfile][source]
Pore-pressure prognoses (pressure in psi is canonical).
phasefilters PLAN | ACTUAL | PROTOTYPE (defaultNone= all; note Volve’s ACTUAL is a single-point shallow anchor, not a curve).latestkeeps only the RICHEST group (the full-depth prognosis; ties -> PLAN) rather than a date-newest one.tvdis returned inunits, which defaults to the reader’ssource_unitsso it matches the survey and formation TVD from the same reader; passunits="meters"for SI. Pressure stays psi.
- schema = {'CD_ASSEMBLY': {'description': 'A string: casing / tubing / liner / BHA.', 'fields': {'assembly_name': 'string name', 'assembly_size': 'nominal size', 'hole_size': 'hole size', 'md_assembly_base': 'base MD', 'string_type': 'Casing | Tubing | Liner | BHA | ...', 'tvd_assembly_base': 'base TVD'}, 'name': 'Assembly'}, 'CD_ASSEMBLY_COMP': {'description': "String component: geometry + material, plus Landmark's stored axial/burst/collapse ratings on casing components (a validation oracle -- welleng recomputes).", 'fields': {'approximate_weight': 'stated nominal unit weight lb/ft -- ⚠ UNVERIFIED; a tool-joint uplift convention can overstate it, cross-check against OD/ID geometry', 'axial_rating': 'stored pipe-body yield (klbf)', 'catalog_key_desc': 'description -> AssemblyComponent.description', 'connection_name': 'connection / thread name', 'critical_percent_collapse': 'collapse derating INPUT (not a result)', 'grade': 'pipe grade', 'id_body': 'body ID (in) -- ⚠ can be a 3.000in collar-bore PLACEHOLDER (detectable only by the same ID across DISTINCT ODs); wrong on small pipe, overstates steel/weight', 'id_connection': 'connection ID (in)', 'length': 'component length (ft)', 'length_tool_joint': 'tool-joint length (ft)', 'makeup_torque': 'connection make-up torque', 'material / material_id / density / youngs_modulus / poissons_ratio': 'material properties (for T&D / stress / sag consumers)', 'min_yield_stress': 'min yield stress', 'od_body': 'body OD (in)', 'od_connection': 'connection OD (in)', 'pipe_pressure_burst': 'stored burst rating (psi)', 'pressure_collapse': 'stored collapse rating (psi)', 'sect_type_code': 'DP | CAS | ... (section type)'}, 'name': 'Assembly component'}, 'CD_CASE': {'description': 'A design case/scenario; links a hole-section group to a named case + phase.', 'fields': {'case_name': 'case name', 'hole_sect_group_id': 'hole-section group (geometry scenario)', 'phase': 'PLAN | ACTUAL | PROTOTYPE', 'scenario_id': 'scenario'}, 'name': 'Case'}, 'CD_DATUM': {'description': 'Per-rig, dated depth reference (RT/RKB elevation).', 'fields': {'date_last_modified': 'established', 'datum_elevation': 'elevation above MSL', 'datum_name': 'rig / datum name', 'is_default': 'default flag'}, 'name': 'Depth datum'}, 'CD_FRAC_GRADIENT': {'description': 'Fracture-pressure prognosis point.', 'fields': {'frac_gradient_emw': 'equiv. mud weight (ppg, RKB-datum view)', 'frac_gradient_pressure': 'pressure (psi)', 'tvd': 'TVD'}, 'name': 'Fracture gradient'}, 'CD_GRADE': {'description': 'Casing/tubing grade catalogue (yield, UTS).', 'fields': {'grade': 'grade name', 'is_api': 'API grade flag', 'min_yield_stress': 'min yield (field units)', 'ultimate_tensile_strength': 'UTS'}, 'name': 'Pipe grade'}, 'CD_HOLE_SECT': {'description': 'As-run hole & casing section geometry.', 'fields': {'catalog_key_desc': 'catalogue description', 'hole_size': 'hole size', 'id_casing': 'casing ID', 'id_drift': 'drift ID', 'length': 'section length', 'md_shoe': 'shoe MD', 'od_casing': 'casing OD', 'sect_type_code': 'CAS (cased) | OPEN'}, 'name': 'Hole section'}, 'CD_MATERIAL': {'description': 'Pipe material catalogue (density, elastic props).', 'fields': {'density': 'density (lb/ft3)', 'expansion_coefficient': 'thermal expansion coeff', 'material': 'material name', 'poissons_ratio': "Poisson's ratio", 'youngs_modulus': "Young's modulus (field units)"}, 'name': 'Material'}, 'CD_PORE_PRESSURE': {'description': 'Pore-pressure prognosis point (pressure is canonical).', 'fields': {'is_permeable_zone': 'permeable flag', 'pore_pressure': 'pressure (psi)', 'pore_pressure_emw': 'equiv. mud weight (ppg, RKB-datum view)', 'tvd': 'TVD'}, 'name': 'Pore pressure'}, 'CD_TEMP_GRADIENT': {'description': 'Geothermal temperature prognosis point (Volve degF).', 'fields': {'temperature': 'temperature (degF in Volve)', 'tvd': 'TVD'}, 'name': 'Temperature gradient'}, 'CD_WELL': {'description': 'Well header (surface location, water depth).', 'fields': {'geo_offset_east': 'surface easting (map)', 'geo_offset_north': 'surface northing (map)', 'slot_ew': 'slot east-west offset', 'slot_ns': 'slot north-south offset', 'water_depth': 'water depth', 'well_common_name': 'well name'}, 'name': 'Well'}, 'CD_WELLBORE': {'description': 'Wellbore / sidetrack (parent chain, kick-off).', 'fields': {'ko_md': 'kick-off MD', 'parent_wellbore_id': 'parent wellbore', 'wellbore_name': 'wellbore name'}, 'name': 'Wellbore'}, 'CD_WELLBORE_FORMATION': {'description': 'Prognosed formation top.', 'fields': {'formation_name': 'formation', 'prognosed_md': 'top MD', 'prognosed_tvd': 'top TVD'}, 'name': 'Formation top'}, 'DP_MAGNETIC': {'description': "Per-wellbore reference field (the operator's own geomag model) — feeds a SurveyHeader / interpretation reference.", 'fields': {'declination': 'declination (deg)', 'declination_date': 'model date', 'dip_angle': 'magnetic dip (deg)', 'field_strength': 'total field strength (nT)', 'model_name': 'geomag model (e.g. BGGM / IGRF)'}, 'name': 'Geomagnetic reference'}, 'TU_CUSTOM_LOAD_PROFILE': {'description': 'Per-depth load a string sees (the SF denominator).', 'fields': {'external_pressure': 'external pressure', 'internal_pressure': 'internal pressure', 'measured_depth': 'MD', 'profile_name': 'load-profile name'}, 'name': 'Custom load profile'}, 'TU_DLS_OVERRIDE': {'description': 'An interval overriding the survey dogleg severity (md_top may be below md_base -- use span).', 'fields': {'dogleg_severity': 'override DLS', 'md_base': 'interval base MD', 'md_top': 'interval top MD (not guaranteed shallower)', 'scenario_id': 'scenario'}, 'name': 'Dogleg override'}, 'TU_EPP_PARAMETERS': {'description': 'Per-LOAD external-pressure profile parameters.', 'fields': {'load_id': 'the load these belong to (per-load, not per-case)', 'name': 'parameter name', 'value_num': 'numeric value'}, 'name': 'External-pressure params'}, 'TU_LOAD_HEADER': {'description': 'A named load within a case, with type/category enums.', 'fields': {'load_category': '0 burst | 1 collapse | 2 axial | 3 service', 'load_type': 'load type code', 'name': 'load name (e.g. L.2.Pressure test the tubing)', 'sequence_no': 'order within the case'}, 'name': 'Load header'}, 'TU_LOAD_PROFILE': {'description': 'An enabled design load profile + its parameters (design factors, load inputs). Absent value = default.', 'fields': {'application_name': 'originating app (e.g. StressCheck)', 'parameter_name': 'parameter name (e.g. DesignPipeBurstFactor)', 'parameter_value_num': 'numeric value (absent -> app default)', 'profile_name': 'load-profile name (e.g. BrstGasKickProfile)'}, 'name': 'Load profile'}, 'WP_FLUID_TEMP': {'description': 'Temperature/pressure-resolved drilling-fluid rheology.', 'fields': {'density': 'density', 'k_prime_parameter': "power-law K'", 'n_prime_parameter': "power-law n'", 'plastic_viscosity': 'plastic viscosity', 'pressure': 'pressure', 'temperature': 'temperature', 'yield_point': 'yield point'}, 'name': 'Fluid rheology'}}
Human-readable table/field glossary (cryptic EDM code -> name + desc).
- sidetrack_chain(wellbore) List[Wellbore][source]
Return the parent-wellbore chain, root-first, ending at
wellbore.Follows
parent_wellbore_id. Uses a local accumulator (no mutable default argument) and guards against cycles.
- survey(wellbore, kind: str = 'definitive', phase: str | None = 'ACTUAL', header_id: str | None = None) WellboreSurvey[source]
Assemble a typed
WellboreSurvey.- Parameters:
wellbore (str or Wellbore) – Wellbore id, wellbore name, or a
Wellbore.kind (str, {"definitive", "raw"}) –
"definitive"returns the accumulated survey with covariance and per-interval tools resolved fromCD_SURVEY_PROGRAM;"raw"returns the control-point survey.phase (str, optional) –
PLAN/ACTUAL/PROTOTYPE. Default"ACTUAL".header_id (str, optional) – Select a specific header. If omitted, the header with the most stations (matching
kind/phase) is used.
- Return type:
- survey_headers(wellbore=None, kind: str | None = 'definitive', phase: str | None = None) List[SurveyHeader][source]
Survey headers, filtered by wellbore / kind / phase.
For a single
wellbore(id or common name) the result is sorted by station count (descending), so the first entry is the sensible default selection. Passwellbore=Nonefor EVERY wellbore’s headers (grouped by wellbore);kind=Nonefor both rawCD_SURVEY_HEADER(incl. tool-less pilot/planned) AND definitive headers. This is the complete raw header set – a superset of whatsurvey_runs()projects to (the tool-run view drops headers with no tool interval).
- survey_runs(wellbore=None) List[SurveyRun][source]
Per-interval survey-tool assignments — which tool ran over which measured-depth range — from
CD_SURVEY_PROGRAMjoined to its rawCD_SURVEY_HEADER(wellbore + survey name).One
SurveyRunper program interval, ordered by wellbore thenmd_min. Optionally filtered to onewellbore(id or common name). Always available (CD_SURVEY_PROGRAM is in the default sweep); the raw header supplieswellbore_id/survey_name, the program interval suppliessurvey_tool_idand themd_min/md_maxrun range.
- temp_deration(assembly_comp_id: str) List[Tuple[float, float]][source]
TU_COMP_TEMP_DERATION_POINTfor one component as sorted(deration_temperature, correction_factor)points – the yield deration curve. Derated yield = base_yield * interp(correction_factor, T).
- temperature(wellbore, phase: str | None = None, latest: bool = True, units: str | None = None) List[GeopressureProfile][source]
Temperature (geothermal) prognoses. Volve stores degF;
valueis the raw stored temperature (no unit conversion).tvdinunits(default: the reader’ssource_units).
- tie_chain(header) List[SurveyHeader][source]
Return the tie-on header chain, root-first, ending at
header.Follows
tie_survey_header_id. No mutable default argument; cycle guarded.
- tool_terms(survey_tool_id: str) List[ToolTerm][source]
Raw
DP_TOOL_TERMIPM error terms for a survey tool, bysequence_no(opt-in:with_tool_terms=True).The un-normalised per-tool term rows (
term_name/c_value/c_units/c_formula/vector_type/tie_type/ range). For the assembled error model usewelleng.errors.edm_ipm.parse_edm_ipm(); this is the raw dump a survey-program / IPM layer builds from. Empty list if the tool has none.
- welleng.EDMSurveyHeader
alias of
SurveyHeader
- class welleng.MinCurve(md, inc, azi, frame: str = 'env')[source]
Bases:
objectMinimum-curvature geometry of a survey in local coordinates.
- __init__(md, inc, azi, frame: str = 'env')[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 referenceaziis in) and holds no surface/start position or datum state – that belongs to the owningSurvey, 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
⛔ ``np.interp`` on ``poss``, ``tvd`` or an angle between stations is the ANTI-PATTERN this class exists to replace. A survey between stations is a circular ARC, not a chord; the chord returns a monotonic, in-range, WRONG answer whose size is set by the CALLER’s station spacing, so no test of the caller’s will fail. Measured on real surveys: 0.33 m of TVD and 3.63 m laterally on a 55-station sidetrack at 3.4 deg/30 m, and 7.22 m of TVD at 150 m spacing. Use
interpolate(),interpolate_tvd()orinc_azi_at().python -m welleng.lint <path>detects it, andwelleng.lint.find_linear_survey_interpolation()is importable so a CONSUMER can assert its own repo clean – the defect has been fixed nine times as changes and twice more in consumers who never called a guarded function, so the check is the only thing that travels.MinCurve is units-agnostic:
mdmay be in any length unit and the geometry is all ratios/angles. Dogleg severity (which needs a per-unit coefficient) is thedls()method, into which the caller injects the coefficient for its units.⚠️ ``poss`` column order follows ``frame``, and the DEFAULT is ``[easting, northing, tvd]`` – x/y, NOT N/E. A consumer adopting this kernel transposed N and E against its own
[northing, easting, tvd]convention, and a transpose is SILENT on any well roughly symmetric in the two axes: it surfaces as a mislocated surface datum, not as an error. Establish the order with a due-north and a due-east probe rather than by readingdelta_x/delta_y, which invite the wrong guess – or just passframe="nev"and stop converting.frame"env"(default) –[easting, northing, tvd], the historical order, whichSurveyconsumes."nev"–[northing, easting, tvd], for a caller whose own convention is N/E and which would otherwise swap every result back.
🔴 ``delta_x``, ``delta_y`` and ``delta_z`` do NOT follow ``frame``. They are per-station increments in AXIS terms – x EAST, y NORTH, z TVD – in BOTH frames. Only
possandinterpolate()followframe. Underframe="nev"poss[:, 0]is NORTHING whiledelta_xis still EASTING, so the two surfaces disagree by construction and mixing them transposes the well. Defensible – x/y are axis names, not compass names – but stated here because the warning above only says not to INFER the order from them, which leaves a reader who takes that warning correctly with no way to learn what they positively are. (Reported by a consumer, 2026-09-20.)This is a BASIS, not a second algorithm. The arc kernel (
arc_step()) is coordinate-agnostic and returns whatever basis it is given, so both frames run the same code over differently-ordered tangents – there is no second path to drift, and no golden anchoring a dead one."nev"is in fact marginally the cheaper of the two:min_curve_stepalready computes in[N, E, V]and the default spends a column swap converting it.
- inc_azi_at(md)[source]
Inclination / azimuth (radians) at measured depth(s) – ATTITUDE ONLY.
interpolate(md, angles=True)also builds the position, and a caller that wants only an attitude pays for a displacement it discards: the half-angle position is roughly 70% of that call. A consumer resolving inc/azi at every mesh boundary of a torque-and-drag solve was doing exactly that, so this skips it.Same arc, same Rodrigues tangent, same answers as
interpolate(md, angles=True)[1:]– asserted bitwise in the tests, because the moment these two could differ there would be two implementations of an attitude.- Returns:
(inc, azi) – Azimuth is in
[0, 2*pi).mdoutside the survey range yieldsnanrather than the nearest station’s attitude.- Return type:
arrays of radians, or floats for scalar
md.
- 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 followsmd).- 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 asposs) – shape(3,)for scalarmdelse(n, 3). The caller adds any datum/start offset (MinCurve holds no datum).mdoutside the survey range yieldsnan.- 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. Nearpithe half-angle position divides bycos(theta/2) -> 0: against a 50-digit reference on a 100 m leg the error grows as ~``1/cos(theta/2)**2`` – 1e-13 m at 170 deg, 4e-12 m at 177 deg, 1e-9 m at 179.9 deg. The tangent (Rodriguesu-form) divides bysin(theta)once, in set-up.
- 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.tvdis in the LOCAL frame (relative to station 0, like the TVD column ofposs);Surveylayers its datum on top. Empty if the target is never reached.⚠️ A TVD in a datum frame (e.g. an EDM or LAS TVD column) passed here is offset by the first station’s depth. Where that offset value still lies inside the local TVD range it returns a WRONG measured depth; where it does not, it returns
[], indistinguishable from “never reached”. Both are silent:MinCurveholds no datum, so it cannot detect either. UseSurvey.interpolate_tvd()(build withSurvey.from_min_curve()and astart_nev) when the TVD carries a datum. A TVD equal to a turning point’s TVD returns one MD.
- leg_frames()[source]
Each leg’s frame,
(v1, u, curved), arrays over the legs.v1(n-1, 3) is the leg’s start tangent andu(n-1, 3) the unit vector in the arc’s plane, perpendicular tov1, towards the end tangent: the tangent at partial doglegphiiscos(phi) v1 + sin(phi) u.curved(n-1,) is False for a straight leg, whoseuis not defined (readv1only). Components are (east, north, vertical down). Computed once per survey.
- 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 inself.md’s units; empty if TVD is monotonic throughout.
- class welleng.ProgramInterval(def_survey_header_id: str | None, survey_header_id: str | None, survey_tool_id: str | None, md_top: float | None, md_base: float | None, sequence_no: int | None, tool: SurveyTool | None = None)[source]
Bases:
objectA
CD_SURVEY_PROGRAMrow: a per-interval tool assignment.Joins a definitive survey header to a (raw) survey header + survey tool over the measured-depth range
[md_top, md_base].- __init__(def_survey_header_id: str | None, survey_header_id: str | None, survey_tool_id: str | None, md_top: float | None, md_base: float | None, sequence_no: int | None, tool: SurveyTool | None = None) None
- def_survey_header_id: str | None
- md_base: float | None
- md_top: float | None
- sequence_no: int | None
- survey_header_id: str | None
- survey_tool_id: str | None
- tool: SurveyTool | None = None
- class welleng.SurveyComposition(sections: Sequence[SurveySection], header: SurveyHeader | None = None, share_mode: Literal['all_independent', 'globals_shared', 'globals_and_systematic_shared'] = 'globals_shared')[source]
Bases:
objectTie 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_modeisNonethe tie before it is resolved from context: if the two sides name differentgeomag_modelvalues, or theirsurvey_datevalues differ by more than two years, the tie isall_independent(the global geomagnetic realisation has drifted / changed model and no longer cancels); otherwise the compositionshare_mode(defaultglobals_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]
- class welleng.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:
objectOne surveyed section (leg) of a wellbore, tied on to the previous one.
Provide either raw
md/inc/aziarrays (withdegand an optionalheader) or an existingSurveyviasurvey. 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
surveyis given.inc (array_like, optional) – Section geometry. Ignored if
surveyis given.azi (array_like, optional) – Section geometry. Ignored if
surveyis 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/aziare 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_modelandtool_idare treated as one continuous survey (systematic error stays correlated). A change oftool_id(orerror_model) marks a tool change / tie.Noneis treated as “the same unspecified tool continuing” — so if you never say the tool changed, it does not.survey_date (str, optional) –
YYYY-MM-DDdate the section was surveyed. Used only to auto-pick a defaultshare_modeat the tie before this section.geomag_model (str, optional) – Name of the geomagnetic reference model (e.g.
"BGGM2020"). Used only to auto-pick a defaultshare_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 (seeSurveyComposition).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
- survey_date: str | None = None
- tool_id: str | None = None
- class welleng.SurveyStation(md: float | None, inc: float | None, azi: float | None, tvd: float | None = None, north: float | None = None, east: float | None = None, dls: float | None = None, sequence_no: int | None = None, covariance: ndarray | None = None, ellipse: Tuple[float, float, float] | None = None, tool: SurveyTool | None = None)[source]
Bases:
objectA single assembled survey station.
covariance(when present) is a symmetric3x3array in the file’s native(x, y, z)axes, i.e.x=East, y=North, z=Vertical(see the module docstring), insource_units**2.- __init__(md: float | None, inc: float | None, azi: float | None, tvd: float | None = None, north: float | None = None, east: float | None = None, dls: float | None = None, sequence_no: int | None = None, covariance: ndarray | None = None, ellipse: Tuple[float, float, float] | None = None, tool: SurveyTool | None = None) None
- azi: float | None
- covariance: ndarray | None = None
- dls: float | None = None
- east: float | None = None
- ellipse: Tuple[float, float, float] | None = None
- inc: float | None
- md: float | None
- north: float | None = None
- sequence_no: int | None = None
- tool: SurveyTool | None = None
- tvd: float | None = None
- class welleng.SurveyTool(tool_id: str, name: str, description: str = '', remarks: str = '', correlate: bool = False, is_range_limited: bool = False, inclination_range_min: float | None = None, inclination_range_max: float | None = None, kind: ToolKind = ToolKind.OTHER, raw: Dict[str, str]=<factory>)[source]
Bases:
objectA
CD_SURVEY_TOOLrow (a survey tool definition).- __init__(tool_id: str, name: str, description: str = '', remarks: str = '', correlate: bool = False, is_range_limited: bool = False, inclination_range_min: float | None = None, inclination_range_max: float | None = None, kind: ToolKind = ToolKind.OTHER, raw: Dict[str, str]=<factory>) None
- correlate: bool = False
- description: str = ''
- inclination_range_max: float | None = None
- inclination_range_min: float | None = None
- is_range_limited: bool = False
- name: str
- raw: Dict[str, str]
- remarks: str = ''
- tool_id: str
- class welleng.ToolKind(*values)[source]
Bases:
str,EnumBest-effort classification of a COMPASS survey tool.
- DEFINITIVE = 'definitive'
- GYRO = 'gyro'
- INCLINATION_ONLY = 'inclination_only'
- MWD = 'mwd'
- OTHER = 'other'
- class welleng.Wellbore(wellbore_id: str, name: str, well_id: str, parent_wellbore_id: str | None = None, well_legal_name: str | None = None, raw: Dict[str, str]=<factory>)[source]
Bases:
objectA
CD_WELLBORErow.- __init__(wellbore_id: str, name: str, well_id: str, parent_wellbore_id: str | None = None, well_legal_name: str | None = None, raw: Dict[str, str]=<factory>) None
- name: str
- parent_wellbore_id: str | None = None
- raw: Dict[str, str]
- well_id: str
- well_legal_name: str | None = None
- wellbore_id: str
- class welleng.WellboreSurvey(wellbore: Wellbore, header: SurveyHeader, stations: List[SurveyStation], source_units: str = 'feet')[source]
Bases:
objectAn assembled survey for a wellbore.
- header
- Type:
- stations
- Type:
list of SurveyStation
- source_units
Units of the stored depths/offsets (e.g.
"feet").- Type:
str
- __init__(wellbore: Wellbore, header: SurveyHeader, stations: List[SurveyStation], source_units: str = 'feet') None
- as_arrays() dict[source]
Return the survey as a dict of numpy arrays (native source units).
Keys:
md, inc, azi, tvd, north, east, dls, covariance, tool_name, tool_kind.covarianceis an(n, 3, 3)array (withnanblocks where a station lacks covariance), orNoneif no station has covariance.
- property has_covariance: bool
- header: SurveyHeader
- source_units: str = 'feet'
- stations: List[SurveyStation]
- to_welleng(units: str = 'meters', azi_reference: str = 'grid')[source]
Build a
welleng.survey.Surveyfrom this survey.- Parameters:
units (str, {"meters", "feet"}) – Target units. Depths/offsets are converted from
source_units.azi_reference (str) – Passed to the survey header (default
"grid"– COMPASS offsets are grid-referenced).
- Return type:
- welleng.classify_tool(name: str | None, description: str | None = None, remarks: str | None = None) ToolKind[source]
Classify a survey tool from its name and (optional) description.
Pure function – no I/O, no side effects. Matching is case-insensitive and considers the concatenation of
nameanddescription.- Parameters:
name (str or None) – The
tool_nameattribute of aCD_SURVEY_TOOLrow.description (str, optional) – The
descriptionattribute of the same row.
- Return type:
Notes
Gyro-while-drilling tools (e.g.
"MWD gyro, Gyrodata, GWD70") are classified asToolKind.GYRO– the gyro sensor governs the error behaviour even though the tool is conveyed on an MWD string.
- welleng.open_edm(path: str, source_units: str = 'feet', with_geopressure: bool = False, with_assemblies: bool = False, with_load_cases: bool = False, with_tool_terms: bool = False) EDMReader[source]
Convenience factory mirroring
EDMReader.open().