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.
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}°")
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
}'
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 →
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.
The path passes through four points, each with a position and direction:
| Point | Position | Direction | Role |
|---|---|---|---|
| P0 | pos0 | vec0 | Start — given as input |
| P1 | p1 | vec1 | End of first arc, start of straight line |
| P2 | p2 | vec2 | End of straight line, start of second arc |
| P3 | pos3 | vec3 | Target — given as input |
r. The arc departs P0 in direction vec0 and arrives at P1 in direction vec1.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.
Blue: gentle path (133° total arc). Red: wrap-around path (356° total arc). Both are valid CLC solutions connecting the same P0 and P3.
| Field | Description |
|---|---|
pos0 | P0 — start position [x, y, z] |
vec0 | P0 — start direction (unit vector) |
pos3 | P3 — target position [x, y, z] |
vec3 | P3 — target direction (unit vector) — desired arrival heading |
r | Turning radius for C1 (arc at P0) |
r3 | Turning radius for C2 (arc at P3) — defaults to r if omitted |
| Field | Description |
|---|---|
p1, vec1 | P1 — junction between C1 and L |
p2, vec2 | P2 — junction between L and C2 |
arc1_deg | Arc angle of C1 (P0 → P1) in degrees |
arc2_deg | Arc angle of C2 (P2 → P3) in degrees |
total_arc_deg | arc1 + arc2 in degrees |
L_length | Length of straight segment (P1 → P2) |
md | Total path length: L1 + L2 + L3 where L1 = arc1 length, L2 = line length, L3 = arc2 length. (In drilling: measured depth.) |
error_deg | Angular matching error — lower is more accurate |
r_used, r3_used | Actual turning radii used (may differ from input if radius sweep is active) |
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).
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.
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.
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 pair | 25 ms | 5.5 ms | 0.015 ms |
| Speedup vs Baez | — | 4.5× | 1,667× |
| Hardware | MacBook M2 Pro | Ryzen 5950X (single core) | RTX 3080 Ti |
| Language | Mathematica | Python / numpy | Python / Triton |
| All solutions | Yes (mean 4, up to 7) | Yes | Yes |
| 4-solution rate | 84% | ~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.
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) | |
|---|---|---|
| Method | Algebraic elimination + polynomial root-finding | Grid-based ray/torus intersection |
| Accuracy | Machine precision (~1e-12) | Grid-resolution dependent |
| Solutions per pair | Up to 7 | Typically 1–3 |
| Speed (10K pairs, GPU) | ~0.015 ms/pair | ~0.35 ms/pair |
| Inc/azi filter | ✓ | ✗ (bypassed) |
| Radius sweep | ✓ | ✓ |
| Asymmetric radii | ✓ | ✓ |
Response field accuracy | Ignored | Used |
The request accepts a completeness field with two values:
| Mode | Shortest-path recovery | Alternate-path recovery | Relative speed |
|---|---|---|---|
fast (default) | 100% | ~99.75% | ~6× faster per pair at large batches |
complete | 100% | 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.
auto / cpu / gpuThe request accepts an optional compute field that overrides the default backend selection. Analytical solver only — the deprecated toroidal path ignores this field.
| Value | Behaviour |
|---|---|
auto (default) | The bridge picks GPU when the batch is large enough and CUDA is available, otherwise CPU. Same threshold as previous versions. |
cpu | Forces 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. |
gpu | Forces 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.
json vs numpySet 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-end | numpy end-to-end | json wire | numpy wire |
|---|---|---|---|---|
| 100,000 | 9.0 s | 3.4 s | 22.1 MB | 5.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.)
[ 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):
| Index | Column | Meaning |
|---|---|---|
| 0–2 | p1x, p1y, p1z | Junction between first arc and line |
| 3–5 | vec1x, vec1y, vec1z | Tangent direction along the line (= vec2) |
| 6–8 | p2x, p2y, p2z | Junction between line and second arc |
| 9 | arc1_rad | First arc angle (radians) |
| 10 | arc2_rad | Second arc angle (radians) |
| 11 | total_arc_rad | Sum of arc1 + arc2 |
| 12 | rotation1_rad | First-arc rotation angle (drilling: toolface of first curve) |
| 13 | rotation2_rad | Second-arc rotation angle (drilling: toolface of second curve) |
| 14 | L_length | Length of the straight line segment |
| 15 | md | Total path length (drilling: measured depth) |
| 16 | r_used | First-arc turning radius |
| 17 | r3_used | Second-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).
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.
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:
| Index | Column |
|---|---|
| 0–2 | pos0_x, pos0_y, pos0_z |
| 3–5 | vec0_x, vec0_y, vec0_z |
| 6–8 | pos3_x, pos3_y, pos3_z |
| 9–11 | vec3_x, vec3_y, vec3_z |
| 12 | r |
| 13 | r3 (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.)
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})
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.
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_scale | Effective radius | Meaning |
|---|---|---|
| 0.9 | 573 × 0.9 = 516 m | 10% tighter turns (DLS ≈ 3.33°/30 m) |
| 1.0 | 573 × 1.0 = 573 m | Design radius (DLS = 3.00°/30 m) |
| 1.1 | 573 × 1.1 = 630 m | 10% 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.
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.
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.
inc = acos(v_V), in [0°, 180°]. 0° is tangent pointing in +V (straight down).[0°, 360°). Wraps through 0°/360° when min > max (e.g. {min: 350, max: 10} accepts ±10° of north).Defaults:
filter field entirely → no filter runs; response shape is unchanged (back-compat with pre-v0.4.4 clients).inc_range_deg or azi_range_deg → that axis is not checked; the other still runs.min_per_pair / max_per_pair → all pairs use the global min / max.return_pair_status → defaults to false; the pair_status array is not returned (saves payload size on large batches).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:
| Field | Meaning |
|---|---|
applied | true 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_returned | Path-level counts |
pairs_solved | Pairs with at least one surviving CLC branch |
pairs_unreachable | Pairs where the solver returned zero branches — geometry unreachable, independent of filter |
pairs_all_filtered | Pairs 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.
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_reason | When |
|---|---|
"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.
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 term | Drilling term |
|---|---|
| r, r3 | Turning radius (from DLS) |
| arc1, arc2 | Dogleg angles |
| md (total path length) | Measured depth |
| rotation1, rotation2 | Toolface angles (referenced to high side, or north when inclination is near zero) |
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.
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.
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.
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.
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.
This solver is designed for high-throughput batch workloads. Key properties clients can rely on:
fast (default) and complete; see Completeness modes.Details of the underlying method will appear in a forthcoming paper.
3D CLC paths appear in any domain where a vehicle or tool must travel between two oriented points with constrained curvature:
||P3 − P0|| > 4r (their "sufficiently far" condition, §III Note 2). IEEE XploreInteractive solver · API docs · [email protected] · Author · Guide v4.8