3D Dubins Path Solver

Find the shortest curvature-constrained path between two oriented points in 3D space. Also known as CLC (Curve-Line-Curve) or CSC (Curve-Straight-Curve) paths. Applications include directional drilling, UAV/drone flight planning, autonomous vehicles, underwater vehicle routing, pipeline routing, and robotics.

Try it now — no API key needed

Interactive 3D solver — adjust inputs, click Run, see solutions plotted in 3D. Or copy and paste any of the examples below.

Python

Requires the requests library (pip install requests). Save as a .py file and run, or paste into a Python/Jupyter session.

import requests

response = requests.post("https://api.welleng.org/api/v1/solve", json={
    "pairs": [{
        "pos0": [0, 0, 0],    "vec0": [0, 0, 1],
        "pos3": [100, 50, 600], "vec3": [0.2, 0.1, 0.97],
        "r": 573
    }],
    "solver": "analytical",
    "shortest_only": True
})

data = response.json()
for sol in data["results"][0]["solutions"]:
    print(f"Path length: {sol['md']:.1f}m, arc: {sol['total_arc_deg']:.1f}°")
curl (Terminal / Command Prompt)

Open a terminal (macOS/Linux) or Command Prompt (Windows) and paste the following. No installation needed — curl is built into all modern operating systems.

curl -s --compressed -X POST https://api.welleng.org/api/v1/solve \
  -H "Content-Type: application/json" \
  -d '{
    "pairs": [{
      "pos0": [0, 0, 0],    "vec0": [0, 0, 1],
      "pos3": [100, 50, 600], "vec3": [0.2, 0.1, 0.97],
      "r": 573
    }],
    "solver": "analytical",
    "shortest_only": true
  }'
JavaScript (Node.js or browser)

Works in Node.js 18+ (which has built-in fetch) or in a browser console. No dependencies required.

const response = await fetch("https://api.welleng.org/api/v1/solve", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    pairs: [{
      pos0: [0, 0, 0],    vec0: [0, 0, 1],
      pos3: [100, 50, 600], vec3: [0.2, 0.1, 0.97],
      r: 573
    }],
    solver: "analytical",
    shortest_only: true,
    // compute: "auto"   // optional: "auto" (default) | "cpu" | "gpu" — see Compute override
  })
});

const data = await response.json();
data.results[0].solutions.forEach(sol =>
  console.log(`Path length: ${sol.md.toFixed(1)}m, arc: ${sol.total_arc_deg.toFixed(1)}°`)
);

Up to 100 pairs and 20 requests/day without a key. Request a free API key for higher limits. Full API docs →

What is a CLC path?

A CLC (Curve-Line-Curve) path is the 3D equivalent of a Dubins path (detailed overview) — the shortest route between two oriented points when constrained to a minimum turning radius. In 2D, Dubins[3] proved that optimal paths consist of circular arcs and straight lines. The CLC type connects two configurations using an arc, a straight tangent line, then a second arc.

Naming conventions: This path type appears under many names in the literature. CLC (Curve-Line-Curve) and CSC (Curve-Straight-Curve) are equivalent. In Dubins path notation, the specific variants are RSR, RSL, LSR, LSL (R = right turn, S = straight, L = left turn). In directional drilling, the same geometry is called a build-hold-drop or turn-hold-turn trajectory. The other Dubins family — CCC (three consecutive arcs with no straight segment) — is not covered by this solver.

Extending this to 3D is significantly harder. In 2D there are only 6 possible path types; in 3D the turning circle at each point becomes a full family of circles — every possible turning direction perpendicular to the heading. Finding which pair of circles (one at the start, one at the target) can be connected by a straight tangent line is a challenging geometric search problem.

Each configuration is a point in space with a direction (position + unit vector). The solver finds multiple valid CLC paths connecting them, ranked by total arc angle or path length.

Points and conventions

The path passes through four points, each with a position and direction:

PointPositionDirectionRole
P0pos0vec0Start — given as input
P1p1vec1End of first arc, start of straight line
P2p2vec2End of straight line, start of second arc
P3pos3vec3Target — given as input

Path segments

CLC path diagram showing the three segments with turning circles
  1. C1 — circular arc from P0 to P1, with turning radius r. The arc departs P0 in direction vec0 and arrives at P1 in direction vec1.
  2. L — straight line from P1 to P2. Direction is vec1 = vec2 (tangent continuity).
  3. C2 — circular arc from P2 to P3, with turning radius r3. The arc departs P2 in direction vec2 and arrives at P3 in direction vec3.

The key constraint: vec1 = vec2 — the direction at P1 must equal the direction at P2, since the straight line segment connects them.

3D example — two solutions for the same start and target

3D visualization showing two distinct CLC paths between the same endpoints

Blue: gentle path (133° total arc). Red: wrap-around path (356° total arc). Both are valid CLC solutions connecting the same P0 and P3.

Input

FieldDescription
pos0P0 — start position [x, y, z]
vec0P0 — start direction (unit vector)
pos3P3 — target position [x, y, z]
vec3P3 — target direction (unit vector) — desired arrival heading
rTurning radius for C1 (arc at P0)
r3Turning radius for C2 (arc at P3) — defaults to r if omitted

Output (per solution)

FieldDescription
p1, vec1P1 — junction between C1 and L
p2, vec2P2 — junction between L and C2
arc1_degArc angle of C1 (P0 → P1) in degrees
arc2_degArc angle of C2 (P2 → P3) in degrees
total_arc_degarc1 + arc2 in degrees
L_lengthLength of straight segment (P1 → P2)
mdTotal path length: L1 + L2 + L3 where L1 = arc1 length, L2 = line length, L3 = arc2 length. (In drilling: measured depth.)
error_degAngular matching error — lower is more accurate
r_used, r3_usedActual turning radii used (may differ from input if radius sweep is active)

Multiple solutions

Many geometries have more than one valid CLC path. For example, a path can wrap the "short way" or the "long way" around a turning circle. The solver finds multiple solutions where they exist, ranked by your chosen criterion (minimize: "arc" for gentlest path, or "md" for shortest total path length).

Design philosophy: completeness over raw speed

This solver is designed to find every CLC solution — including the tight-arc ones that most published methods miss. That is the single most important thing to understand about it.

Numerical Newton-style 3D CSC solvers work well for well-separated geometries, but quietly miss solutions when either arc becomes small. Small arcs are exactly the configurations that matter in directional drilling: target approaches, small mid-well corrections, dead-reckoning cleanup after a survey. A path planner that returns "no solution" or a 5 km long-way-around path on a configuration with a 500 m short-arc solution is a production hazard — silently producing plans that look plausible but aren't optimal, and that may cost days of rig time before anyone notices.

We benchmark against current public methods — notably Xu, Baryshnikov & Sung (2025)[14], the most recent 3D CSC reformulation — with full methodology and results to appear in a forthcoming paper.

At GPU batch scale the completeness-vs-speed tradeoff disappears: the solver returns every CLC solution at roughly ~0.015 ms/pair of engine time on an RTX 3080 Ti in fast mode.

Accuracy

The default analytical solver returns machine-precision solutions — the geometric error at the arc-line junctions is zero up to floating-point rounding (~1e-12). There are no user-tunable accuracy tiers; every solve is exact.

Performance (v0.4.6, RTX 3080 Ti): ~0.18 ms/pair end-to-end at 100,000-pair batches in fast mode over the Cloudflare tunnel (request + Pydantic + solve + response + gzip + HTTPS), ~0.29 ms/pair in complete mode. Engine-only is ~0.015 / ~0.10 ms/pair respectively; the remainder is JSON serialisation and network transport. Small batches (< 5 pairs) run on CPU where GPU launch overhead exceeds compute time. The compute field in the response indicates which was used.

Published benchmark comparison

To our knowledge, this is the fastest published CLC/CSC solver. Comparison against the only other analytical method that enumerates all valid solutions:

Baez et al (2024)[1]This solver (CPU)This solver (GPU)
Time per pair25 ms5.5 ms0.015 ms
Speedup vs Baez4.5×1,667×
HardwareMacBook M2 ProRyzen 5950X (single core)RTX 3080 Ti
LanguageMathematicaPython / numpyPython / Triton
All solutionsYes (mean 4, up to 7)YesYes
4-solution rate84%~83%~83%

Baez et al. report 0.02499 ± 0.00097 s per query in Mathematica on an Apple M2 Pro (arXiv:2405.08710, §V). Solution-count statistics (84% four-solution) agree closely with ours (~83%), providing independent validation of completeness. Iterative numerical methods (e.g. [24] in Baez) report 11.0 ± 11.6 s — roughly 700,000× slower than GPU batch.

Solver backends: analytical vs toroidal (toroidal deprecated)

The API exposes two backends; the solver request field chooses between them.

Deprecation notice. solver="toroidal" is deprecated and will be removed in v0.5.0. It predates the analytical solver and is kept only for backwards compatibility. It does not support the inc/azi filter (requests combining filter + toroidal bypass the filter). No new features or performance work will land on it.

Responses from the toroidal backend carry Deprecation: true and Sunset: v0.5.0 HTTP headers (RFC 8594). Migrate to solver="analytical" — it's strictly faster, more complete (up to 7 solutions vs 1–3), and numerically exact.

analytical (default, recommended)toroidal (deprecated)
MethodAlgebraic elimination + polynomial root-findingGrid-based ray/torus intersection
AccuracyMachine precision (~1e-12)Grid-resolution dependent
Solutions per pairUp to 7Typically 1–3
Speed (10K pairs, GPU)~0.015 ms/pair~0.35 ms/pair
Inc/azi filter✗ (bypassed)
Radius sweep
Asymmetric radii
Response field accuracyIgnoredUsed

Completeness modes

The request accepts a completeness field with two values:

ModeShortest-path recoveryAlternate-path recoveryRelative speed
fast (default)100%~99.75%~6× faster per pair at large batches
complete100%100%Baseline

Use complete if you need every CLC alternate (research, full enumeration). For almost every production path-planning workload, fast is the correct default.

Compute override: auto / cpu / gpu

The request accepts an optional compute field that overrides the default backend selection. Analytical solver only — the deprecated toroidal path ignores this field.

ValueBehaviour
auto (default)The bridge picks GPU when the batch is large enough and CUDA is available, otherwise CPU. Same threshold as previous versions.
cpuForces the CPU path regardless of pair count. Useful for self-isolating a suspected GPU-only bug — if a request fails or returns surprising solutions on GPU, repeat with "compute": "cpu" and compare.
gpuForces the GPU path. Returns 400 Bad Request if no CUDA is available on the server.

The compute field in the response always echoes the backend that actually ran, so you can confirm the override took effect.

Response format: json vs numpy

Set format: "numpy" in the request to receive a packed binary response (Content-Type application/octet-stream) in place of JSON. This is an opt-in; the default is still "json".

At batch scale, JSON encoding and per-solution Pydantic field access dominate server-side time, and the decompressed payload is large (~77 MB raw / ~23 MB gzipped at 100,000 pairs). The numpy format ships a flat float32 array plus a compact JSON header, cutting both server CPU time and wire size substantially:

N pairs (fast)json end-to-endnumpy end-to-endjson wirenumpy wire
100,0009.0 s3.4 s22.1 MB5.9 MB

(Localhost, RTX 3080 Ti, v0.4.3. End-to-end over the Cloudflare tunnel adds ~2 s depending on the client's downlink.)

Wire format (schema version 1)

[ 2 bytes:  uint16 LE — header JSON length ]
[ H bytes:  UTF-8 JSON header                ]
[ S * 4:    int32 LE  — pair_idx             ]
[ S*K * 4:  float32 LE — sol rows (S x K)    ]

The header JSON carries n_sols (S), n_cols (K, currently 18), columns (ordered list of column names), schema_version, plus the usual response metadata (total_pairs, total_solutions, total_ms, compute, solver, solver_version, auth).

Column layout (indices correspond to positions in the (S, K) float32 array):

IndexColumnMeaning
0–2p1x, p1y, p1zJunction between first arc and line
3–5vec1x, vec1y, vec1zTangent direction along the line (= vec2)
6–8p2x, p2y, p2zJunction between line and second arc
9arc1_radFirst arc angle (radians)
10arc2_radSecond arc angle (radians)
11total_arc_radSum of arc1 + arc2
12rotation1_radFirst-arc rotation angle (drilling: toolface of first curve)
13rotation2_radSecond-arc rotation angle (drilling: toolface of second curve)
14L_lengthLength of the straight line segment
15mdTotal path length (drilling: measured depth)
16r_usedFirst-arc turning radius
17r3_usedSecond-arc turning radius

Fields dropped from the JSON format because they are derivable (client converts radians → degrees when needed), always zero for the analytical solver (error_deg, error_rad), or duplicated (vec2 equals vec1 for any CLC path).

Decoder (Python, 5 lines)

import json, struct, numpy as np, requests

r = requests.post("https://api.welleng.org/api/v1/solve", json={
    "pairs": [...], "format": "numpy",
}, headers={"X-API-Key": "wek_..."}).content

hlen = struct.unpack_from("<H", r, 0)[0]
header = json.loads(r[2:2+hlen])
n, k = header["n_sols"], header["n_cols"]
pair_idx = np.frombuffer(r, dtype="<i4", count=n, offset=2+hlen)
arr = np.frombuffer(r, dtype="<f4", count=n*k, offset=2+hlen+n*4).reshape(n, k)
# arr[pair_idx == i] gives the rows for input pair i

Use json (the default) for interactive, small-batch, or browser clients; use numpy for high-throughput batch workloads where download size and decode speed matter.

Request format: JSON vs binary (application/octet-stream)

Symmetric counterpart to the numpy response. Submit a packed binary body instead of JSON. At 500K pairs this shaves ~55 MB of upload and a few seconds of server-side validation.

Available on every tier (demo, free, standard, premium) — same pairs-per-request caps as JSON mode. No extra entitlement needed.

Set Content-Type: application/octet-stream on the POST. The body is:

[ 2 bytes:  uint16 LE — header JSON length      ]
[ H bytes:  UTF-8 JSON header                   ]
[ N * 14 * 4: float32 LE — pair rows (N × 14)   ]

Per-row columns:

IndexColumn
0–2pos0_x, pos0_y, pos0_z
3–5vec0_x, vec0_y, vec0_z
6–8pos3_x, pos3_y, pos3_z
9–11vec3_x, vec3_y, vec3_z
12r
13r3 (or NaN to inherit from r)

The header JSON carries the same top-level fields as a JSON request: solver, minimize, shortest_only, completeness, r_scale_min, r_scale_max, n_r_steps, accuracy, compute (auto / cpu / gpu), format (response format — json or numpy, independent of request format), and filter. Plus two required framing fields: schema_version: 1 and n_pairs: N.

Filter block works in binary mode — put it in the header JSON exactly as in a JSON request. Schema v1 does not support per-pair filter overrides in binary mode; if you need them, submit via JSON. (Schema v2 with per-pair override columns is planned for when demand materialises.)

Encoder (Python, 8 lines)

import json, struct, numpy as np, requests

pairs = np.zeros((N, 14), dtype="<f4")       # fill in columns
pairs[:, 3:6] = [0, 0, 1]                     # vec0
pairs[:, 12] = 573                            # r
pairs[:, 13] = np.nan                         # r3 inherits

header = json.dumps({"schema_version": 1, "n_pairs": N,
                     "solver": "analytical", "format": "numpy"}).encode()
body = struct.pack("<H", len(header)) + header + pairs.tobytes()
requests.post(URL, data=body,
              headers={"Content-Type": "application/octet-stream",
                       "X-API-Key": KEY})

Encoder with filter (Python)

The filter block goes in the header JSON, exactly as in a JSON request — nothing else changes:

header = json.dumps({
    "schema_version": 1, "n_pairs": N,
    "solver": "analytical",
    "format": "numpy",                  # response format — numpy-in / numpy-out
    "filter": {
        "inc_range_deg": {"min": 0, "max": 60},
        "azi_range_deg": {"min": 40, "max": 95},
        "return_pair_status": False,
    },
}).encode()
body = struct.pack("<H", len(header)) + header + pairs.tobytes()
# POST as above. The numpy response carries the filter counts in the
# header JSON just like a JSON response.

The response comes back in whatever format the header requested — so a numpy-in / numpy-out round-trip is the fastest path end-to-end for large batches.

Interactive schema: Swagger UI documents both content types for the /api/v1/solve endpoint — click the application/octet-stream tab under Request body to see the field-level description.

Radius sweep

r_scale_min and r_scale_max are ratio multipliers applied to the turning radius r. For example, with DLS = 3°/30 m the design radius is r ≈ 573 m:

r_scaleEffective radiusMeaning
0.9573 × 0.9 = 516 m10% tighter turns (DLS ≈ 3.33°/30 m)
1.0573 × 1.0 = 573 mDesign radius (DLS = 3.00°/30 m)
1.1573 × 1.1 = 630 m10% gentler turns (DLS ≈ 2.73°/30 m)

Set r_scale_min=0.9 and r_scale_max=1.1 with n_r_steps=5 to solve at exactly 5 radii across that range (0.9, 0.95, 1.0, 1.05, 1.1). If the design radius (1.0) falls within the range, the nearest grid point is snapped to it — so you always get the design radius without using an extra solve. All radii are batched in a single GPU call, so the incremental cost of additional radii is low.

Radius sweep: solving at multiple radii in one GPU call

Useful for finding the optimal turning radius or checking feasibility at different curvatures. A minor increase in DLS (smaller radius) can result in a significantly shorter total path length. In drilling: optimising dogleg severity.

Inclination / azimuth filter

Free on every tier, including the no-key demo. The kernel cost is sub-μs per path, so there's no reason to paywall it — feel free to try it out. Reject CLC paths whose tangent ever strays outside an operational inc/azi envelope. Paths are filtered inside the solve call, before response assembly, so rejected paths never incur the per-solution response-build cost. For tight envelopes this is often a faster request than the unfiltered equivalent, not a slower one.

Coordinate convention: [x, y, z] = [E, N, V], V positive = down.

Defaults:

Tier access: available on every tier (demo, free, standard, premium). The only caveat is toroidal-backend requests — those bypass the filter because the kernel targets the analytical solver.

Basic request:

{
  "pairs": [ ... ],
  "solver": "analytical",
  "shortest_only": true,
  "filter": {
    "inc_range_deg": {"min": 0, "max": 60},
    "azi_range_deg": {"min": 40, "max": 95},
    "return_pair_status": true
  }
}

Every filtered response carries a filter block with:

FieldMeaning
appliedtrue when the filter ran; false when bypassed
bypass_reason"premium_feature_required" on lower tiers; "filter_requires_analytical_solver" if solver=toroidal
paths_pre_filter / paths_filtered_out / paths_returnedPath-level counts
pairs_solvedPairs with at least one surviving CLC branch
pairs_unreachablePairs where the solver returned zero branches — geometry unreachable, independent of filter
pairs_all_filteredPairs where the solver found branches but all were rejected by the envelope — envelope too tight

pairs_unreachable vs pairs_all_filtered is the key diagnostic: the first means "no CLC exists for this geometry — widen r or move the target"; the second means "CLC exists but your envelope rejected it — relax the envelope." Both map to rejected pairs, but the fix is different.

Per-pair overrides. Optionally tighten (or loosen) individual pairs by supplying min_per_pair / max_per_pair arrays of length n_pairs. null at any slot inherits from the global min / max:

"filter": {
  "inc_range_deg": {
    "min": 0, "max": 90,                               // global baseline
    "max_per_pair": [null, 30, null, 30, null]          // pairs 1 & 3 use 30°, others 90°
  }
}

Per-pair status. Add "return_pair_status": true to get a per-pair pair_status array aligned to your input order, with values "solved", "filtered_out", or "unreachable". Omit it on large batches to reduce payload size.

Bypass cases (response has filter.applied=false)

The filter runs on every tier, but two situations cause it to be skipped — in both cases the request still succeeds (200 OK, no error) and the filter block is echoed back with applied=false and a bypass_reason:

bypass_reasonWhen
"filter_requires_analytical_solver"You set solver="toroidal". The filter kernel targets the analytical solver's path representation; toroidal paths don't have the same structure.
(rare) "cpu_path_not_supported_yet"Internal fallback path for batches below the analytical GPU threshold (~5 pairs). Fix on the roadmap.

In a bypass the response carries your filter spec echoed back, unfiltered results, and no pair_status. Always check filter.applied before acting on results.

Drilling context

Coordinate system: The solver uses x = East, y = North, z = Down (TVD). Survey data in (North, East, TVD) must be transformed to (x, y, z) = (East, North, TVD) before submitting to the API.

DLS to radius: The turning radius r is derived from dogleg severity (DLS) and course length:

r = (180 / π) × (course_length / DLS)

where DLS is in degrees per course_length (e.g. °/30 m or °/100 ft). For example, DLS = 3°/30 m gives r ≈ 573 m.

Terminology mapping:

Solver termDrilling term
r, r3Turning radius (from DLS)
arc1, arc2Dogleg angles
md (total path length)Measured depth
rotation1, rotation2Toolface angles (referenced to high side, or north when inclination is near zero)

Try the solver

The WellEng CLC Solver is available as a REST API — send a JSON request with your start and target configurations, get ranked CLC solutions back in milliseconds. No installation required.

The solver runs on GPU (NVIDIA RTX 3080 Ti) and handles batch workloads of up to 500,000 pairs per request (premium tier). At 100K-pair batches in fast mode, expect ~0.18 ms/pair end-to-end over the Cloudflare tunnel (engine ~0.015 ms/pair; the rest is Pydantic + JSON + gzip + HTTPS). Small batches run on CPU. The compute field in the response tells you which was used.

Quick start — curl example

curl -s --compressed -X POST https://api.welleng.org/api/v1/solve \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_KEY" \
  -d '{
    "pairs": [{
      "pos0": [0, 0, 0], "vec0": [0, 0, 1],
      "pos3": [100, 50, 600], "vec3": [0.2, 0.1, 0.97],
      "r": 573
    }],
    "solver": "analytical",
    "shortest_only": true
  }'

Important: Always use https://api.welleng.org (no port number). Use --compressed to enable gzip — large batch responses compress ~5-10x, significantly reducing transfer time. Responses are gzip-encoded automatically when the client supports it.

Radius sweep

To test multiple turning radii, add r_scale_min, r_scale_max, and optionally n_r_steps:

{
  "pairs": [{"pos0": ..., "vec0": ..., "pos3": ..., "vec3": ..., "r": 573}],
  "solver": "analytical",
  "shortest_only": true,
  "r_scale_min": 0.8,
  "r_scale_max": 1.2,
  "n_r_steps": 5
}

This tests 5 r1-scales × 5 r2-scales = 25 permutations per pair. With the default shortest_only: true, one shortest solution is returned per permutation (so 25 solutions per pair); set shortest_only: false to receive every valid solution from each permutation. If n_r_steps is omitted, it defaults to 3 when a sweep is requested.

Background: the 3D Dubins problem

From 2D to 3D

In 1957, Lester Dubins proved that the shortest path between two oriented points in a plane, subject to a minimum turning radius, is composed of at most three segments — each either a circular arc (C) at the minimum radius or a straight line (S). This gives six possible Dubins path types: CSC (three variants) and CCC (three variants). The problem has elegant closed-form solutions in 2D and is widely used in robot motion planning, UAV path planning, and vehicle routing.

Extending Dubins paths to 3D is an open problem in computational geometry. The key difficulty: in 2D, each point has exactly two turning circles (left and right). In 3D, the set of all possible turning circles from a point forms a continuous family — every direction perpendicular to the heading defines a valid turning circle. Finding a valid CLC path requires identifying one circle at the start and one at the target that can be connected by a mutual tangent line — a search over a much larger space than the 2D case.

Classical approaches and their limitations

Several approaches have been proposed for 3D Dubins-like path planning:

Analytical constructions. The most directly relevant prior work is by Hota and Ghose (IISc Bangalore), who in their 2010 IROS paper[4] gave an explicit geometrical construction for the 3D CSC (Curve-Straight-Curve) path — the same path family as CLC. They proved that when the start and target are "sufficiently far apart," specifically ||P3 − P0|| > 4r, a CSC path is guaranteed to exist and can be computed without iteration. For smaller separations, the optimal path may be CCC or of the helicoidal type, which their construction does not cover. In a 2014 follow-up[5], the same authors extended this to UAV trajectory planning with wind and addressed a singular case at 180° turning angles using a cross-product reformulation. Hota and Ghose's work is the analytical foundation most relevant to the drilling CLC problem, and their 4r sufficient-distance condition is a useful rule of thumb for when a clean CSC solution will exist.

More recently, Baez, Navkar and Becker (2024)[1] reformulated the problem as an RRPRR inverse-kinematics system and used resultant elimination to give a closed-form treatment that removes the 4r sufficient-distance restriction and enumerates all up-to-seven valid CSC solutions for any valid configuration. This is the direct theoretical successor to Hota and Ghose on the 3D CSC side.

Other analytical work extends Dubins' 2D results to restricted 3D cases — Chitsaz and LaValle (2007)[2] characterised time-optimal paths for a Dubins airplane (constrained pitch), and Owen, Beard and McLain (2014)[8] provided practical implementations for fixed-wing UAVs. These are effective for their specific domains but don't generalise to arbitrary 3D orientations.

Iterative numerical methods. In directional drilling, the standard approach is a fixed-point iteration that adjusts toolface and dogleg angles until the endpoint converges — the compendium of such calculations based on the minimum curvature method is set out in Sawaryn & Thorogood (2005)[9]. The authors themselves acknowledge the limitations of the iterative scheme: "from a safety-critical-systems perspective, neither the above scheme nor that proposed by Liu and Shi[6] are completely satisfactory. In neither case is convergence proved, and no definitive statement is made regarding the conditions under which no solutions exist (e.g., when the radii of curvature are too large to hit the target). Further work is required." The follow-up Part 2 (Sawaryn & Tulceanu, 2009)[10] extends the treatment to steering and landing applications. The welleng[17] library's Connector class takes a related numerical-optimisation approach — a damped fixed-point iteration wrapped around scipy.optimize.minimize — and is reliable for realistic well geometries. However, iterative methods are inherently sequential (one pair at a time), limiting throughput for batch workloads.

The scaling problem. All of these approaches process one pair at a time. In applications like multi-target well planning, anti-collision checking, or real-time UAV re-routing, you may need to solve thousands to hundreds of thousands of pairs simultaneously. Sequential methods that take 5-15 ms per pair become impractical at this scale. This solver was designed specifically for batch throughput — at 500,000 pairs per request (the premium-tier cap on a 12 GB GPU), the fast path runs at roughly 0.015 ms/pair engine time.

Our approach

This solver is designed for high-throughput batch workloads. Key properties clients can rely on:

Details of the underlying method will appear in a forthcoming paper.

Applications

3D CLC paths appear in any domain where a vehicle or tool must travel between two oriented points with constrained curvature:

References

  1. Baez, V.M., Navkar, N. and Becker, A.T. (2024). "An Analytic Solution to the 3D CSC Dubins Path Problem". arXiv:2405.08710. Removes the 4r sufficient-distance restriction; shows up to 7 valid CSC solutions via RRPRR inverse-kinematics / resultant-elimination. arXiv · Code
  2. Chitsaz, H. and LaValle, S.M. (2007). "Time-optimal paths for a Dubins airplane". IEEE CDC. PDF
  3. Dubins, L.E. (1957). "On Curves of Minimal Length with a Constraint on Average Curvature, and with Prescribed Initial and Terminal Positions and Tangents". American Journal of Mathematics. Wikipedia · Grokipedia
  4. Hota, S. and Ghose, D. (2010). "Optimal Geometrical Path in 3D with Curvature Constraint". IEEE/RSJ IROS, pp. 113–118. Foundational analytical CSC construction in 3D; proves a CSC path exists whenever ||P3 − P0|| > 4r (their "sufficiently far" condition, §III Note 2). IEEE Xplore
  5. Hota, S. and Ghose, D. (2014). "Optimal Trajectory Planning for Unmanned Aerial Vehicles in Three-Dimensional Space". Journal of Aircraft, 51(2), pp. 681–688. Extends the 2010 construction to wind; resolves the 180° singularity via cross-product reformulation. DOI: 10.2514/1.C032245
  6. Liu, X. and Shi, Z. (2001). "Improved Method Makes a Soft Landing of Well Path". Oil & Gas Journal, October 2001, p. 48.
  7. Manyam, S.G., Kumar, D., Darbha, S. et al. (2024). "On the Reachability of 3-Dimensional Paths with a Prescribed Curvature Bound". arXiv:2403.18707. arXiv
  8. Owen, M., Beard, R. and McLain, T. (2014). "Implementing Dubins Airplane Paths on Fixed-Wing UAVs". Handbook of Unmanned Aerial Vehicles, Springer. Link
  9. Sawaryn, S.J. and Thorogood, J.L. (2005). "A Compendium of Directional Calculations Based on the Minimum Curvature Method". SPE 84246-PA. SPE Drilling & Completion. OnePetro
  10. Sawaryn, S.J. and Tulceanu, M.A. (2009). "A Compendium of Directional Calculations Based on the Minimum Curvature Method — Part 2: Extension to Steering and Landing Applications". SPE 110014-PA. SPE Drilling & Completion. OnePetro
  11. Scholes, H. (1992). "Constant Curvature Method for Planning a 3-D Directional Well". SPE 24381. OnePetro
  12. Sun, Y. et al. (2021). "Subsea field layout optimization (Part I) — directional well trajectory planning based on 3D Dubins Curve". Journal of Petroleum Science and Engineering. ScienceDirect
  13. Váňa, P., Pěnička, R. and Faigl, J. (2020). "Minimal 3D Dubins Path with Bounded Curvature and Pitch Angle". IEEE ICRA. Decoupled pitch/yaw formulation with an NLP solver. PDF
  14. Xu, L., Baryshnikov, Y. and Sung, C. (2025). "Reparametrization of 3D CSC Dubins Paths Enabling 2D Search". arXiv:2503.11560. arXiv
  15. Wikipedia: Motion planning — broader context for path planning under curvature constraints.
  16. Wikipedia: Directional drilling — the primary application domain.
  17. welleng — open-source well engineering library by the same author, which motivated this work.

Interactive solver · API docs · [email protected] · Author · Guide v4.8