Skip to content

Add support for constructing deviation surveys from Cartesian well path coordinates #76

@MrBeee

Description

@MrBeee

Hi,

I’m using wellpathpy in a workflow where some wells are not provided as deviation surveys (md, inc, azi), but instead as a sequence of Cartesian trajectory points such as (x, y, z) or (northing, easting, tvd/depth).

At the moment, wellpathpy appears to assume that the input survey is already available as measured depth, inclination, and azimuth. That works well for standard deviation-survey data, but it leaves a gap for cases where the source data is only available as sampled coordinates along the borehole path.

Use case

In practice, some well data sets are delivered as a position log:

  • northing / easting / depth
  • x / y / z
  • regularly or irregularly sampled points along the borehole

For these wells, I still need to:

  • reconstruct a usable deviation survey
  • preserve curved-borehole behavior as well as possible
  • resample intermediate points along the trajectory using the existing minimum-curvature workflow

Current limitation

There does not seem to be a built-in way to create a wellpathpy.deviation object directly from Cartesian trajectory points.

A straight segment-by-segment Cartesian-to-spherical conversion is often too crude for curved wells, especially if the resulting survey is later resampled. What is really needed is a helper that derives approximate (md, inc, azi) values from the Cartesian path so the existing minimum_curvature() and resample() functionality can be reused.

Proposed feature

It would be very useful to have a helper such as:

wp.deviation_from_xyz(x, y, z)
Or:
wp.deviation_from_position_log(northing, easting, depth)

Expected behavior

Given arrays of Cartesian well path coordinates, the function should:

  • interpret them as successive stations along the borehole
  • estimate inclination and azimuth at each station
  • estimate measured depth along the curved trajectory
  • return a normal deviation object compatible with the rest of the library
    This would allow downstream code to do things like:
dev = wp.deviation_from_xyz(northing, easting, depth)
pos = dev.minimum_curvature().resample(depths=target_mds)

Why this matters

This would make wellpathpy usable in workflows where the original deviation survey is unavailable, but the trajectory itself is known as sampled coordinates. That seems fairly common in interpreted or exported well data.

At the moment, downstream users need to implement their own conversion logic outside the library, even though the result is conceptually still a deviation survey and is intended to feed directly into wellpathpy’s resampling/interpolation tools.

Notes

I already implemented a local workaround downstream that reconstructs an approximate deviation survey from [(northing, easting, depth)] points and then feeds that into a deviation object. It works well enough to bridge the gap, but this feels like functionality that would fit naturally inside wellpathpy itself..

If this sounds useful, I’d be happy to help refine the API proposal or share the current approach as a starting point.

My current implementation is shown below:

    def deviationFromXYZ(self, northing, easting, depth):
        """Deviation survey

        Compute an approximate deviation survey from the position log, i.e. the
        measured that would be convertable to this well path. It is assumed
        that inclination, azimuth, and measured-depth starts at 0.

        Returns
        -------
        dev : deviation

        The implementation is based on this [1] stackexchange answer by tma,
        which is included verbatim for future reference.

            In order to get a better picture you should look at the problem in
            2d. Your arc from (x1,y1,z1) to (x2,y2,z2) lives in a 2d plane,
            also in the same pane the tangents (a1,i1) and (a2, i2). The 2d
            plane is given by the vector (x1,y1,y3) to (x2,y2,z2) and vector
            converted from polar to Cartesian of (a1, i1). In case their
            co-linear is just a straight line and your done. Given the angle
            between the (x1,y1,z2) and (a1, i1) be alpha, then the angle
            between (x2,y2,z2) and (a2, i2) is –alpha. Use the normal vector of
            the 2d plane and rotate normalized vector (x1,y1,z1) to (x2,y2,z2)
            by alpha (maybe –alpha) and converter back to polar coordinates,
            which gives you (a2,i2). If d is the distance from (x1,y1,z1) to
            (x2,y2,z2) then MD = d* alpha /sin(alpha).

        In essence, the well path (in cartesian coordinates) is evaluated in
        segments from top to bottom, and for every segment the inclination and
        azimuth "downwards" are reconstructed. The reconstructed inc and azi is
        used as "entry angle" of the well bore into the next segment. This uses
        some assumptions deriving from knowing that the position log was
        calculated with the min-curve method, since a straight
        cartesian-to-spherical conversion could be very sensitive [2].

        [1] https://math.stackexchange.com/a/1191620
        [2] I observed low error on average, but some segments could be off by
            80 degrees azimuth
        """
        upper = zip(northing[:-1], easting[:-1], depth[:-1])
        lower = zip(northing[1:], easting[1:], depth[1:])

        # Assume the initial depth and angles are all zero, but this can likely be parametrised.
        incs, azis, mds = [0], [0], [0]
        i1, a1 = 0, 0

        for up, lo in zip(upper, lower):
            up = np.array(up)
            lo = np.array(lo)

            # Make two vectors
            # v1 is the vector from the upper survey station to the lower
            # v2 is the vector formed by the initial inc/azi (given by the
            # previous iteration).
            #
            # The v1 and v2 vectors form a plane the well path arc lives in.
            v1 = lo - up
            v2 = np.array(wp.geometry.direction_vector(i1, a1))

            alpha = wp.geometry.angle_between(v1, v2)
            normal = wp.geometry.normal_vector(v1, v2)

            # v3 is the "exit vector", i.e. the direction of the well bore
            # at the lower survey station, which would in turn be "entry
            # direction" in the next segment.
            v3 = wp.geometry.rotate(v1, normal, -alpha)
            i2, a2 = wp.geometry.spherical(*v3)

            # d is the length of the vector (straight line) from the upper
            # station to the lower station.
            d = np.linalg.norm(v1)
            incs.append(i2)
            azis.append(a2)
            if alpha == 0:
                mds.append(d)
            else:
                mds.append(d * alpha / np.sin(alpha))
            # The current lower station is the upper station in the next
            # segment.
            i1 = i2
            a1 = a2

        mds = np.cumsum(mds)
        return wp.deviation(md=np.array(mds), inc=np.array(incs), azi=np.array(azis))

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions