use crate::algorithms::wells;
use crate::foundation::{GeoError, Point3, Result};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Station {
pub md: f64,
pub inc_deg: f64,
pub azi_deg: f64,
}
impl Station {
pub fn new(md: f64, inc_deg: f64, azi_deg: f64) -> Self {
Self {
md,
inc_deg,
azi_deg,
}
}
}
#[derive(Debug, Clone)]
pub enum TrajectoryInput {
Xyz(Vec<Point3>),
MdIncAzi(Vec<Station>),
Stations(Vec<Station>),
PositionedSurvey(Vec<(Station, Point3)>),
Hold { from: Station, to_md: f64 },
Steer {
from: Station,
build_per_100: f64,
turn_per_100: f64,
to_md: f64,
},
}
#[derive(Debug, Clone, Copy)]
struct Node {
md: f64,
p: Point3,
dir: Option<[f64; 3]>,
}
#[derive(Debug, Clone)]
pub struct Trajectory {
nodes: Vec<Node>,
}
const STEER_STEP: f64 = 30.0;
impl Trajectory {
pub fn from_input(input: TrajectoryInput, head: (f64, f64), kb: f64) -> Result<Self> {
let nodes = match input {
TrajectoryInput::Xyz(pts) => nodes_from_xyz(pts)?,
TrajectoryInput::MdIncAzi(s) | TrajectoryInput::Stations(s) => {
min_curvature(&s, head, kb)?
}
TrajectoryInput::PositionedSurvey(rows) => nodes_from_positioned(rows)?,
TrajectoryInput::Hold { from, to_md } => {
let end = Station::new(to_md, from.inc_deg, from.azi_deg);
min_curvature(&[from, end], head, kb)?
}
TrajectoryInput::Steer {
from,
build_per_100,
turn_per_100,
to_md,
} => {
let stations = steer_stations(from, build_per_100, turn_per_100, to_md);
min_curvature(&stations, head, kb)?
}
};
Ok(Trajectory { nodes })
}
pub fn md_range(&self) -> (f64, f64) {
match (self.nodes.first(), self.nodes.last()) {
(Some(a), Some(b)) => (a.md, b.md),
_ => (f64::NAN, f64::NAN),
}
}
pub fn xyz(&self, md: f64) -> Option<Point3> {
let (lo, hi) = self.md_range();
if md.is_nan() || md < lo || md > hi {
return None;
}
for w in self.nodes.windows(2) {
let (a, b) = (w[0], w[1]);
if md >= a.md && md <= b.md {
let span = b.md - a.md;
if span <= 0.0 {
return Some(a.p);
}
let f = (md - a.md) / span;
return Some(match (a.dir, b.dir) {
(Some(t1), Some(t2)) => wells::arc_point(a.p, t1, t2, f, span),
_ => lerp3(a.p, b.p, f),
});
}
}
self.nodes.first().filter(|n| n.md == md).map(|n| n.p)
}
pub fn tvd(&self, md: f64) -> Option<f64> {
self.xyz(md).map(|p| p.z)
}
pub fn md_at_tvd(&self, tvd: f64) -> Option<f64> {
for w in self.nodes.windows(2) {
let (a, b) = (w[0], w[1]);
let (z0, z1) = (a.p.z, b.p.z);
let within = (tvd >= z0 && tvd <= z1) || (tvd <= z0 && tvd >= z1);
if within {
let dz = z1 - z0;
if dz == 0.0 {
return Some(a.md);
}
return Some(a.md + (tvd - z0) / dz * (b.md - a.md));
}
}
self.nodes.first().filter(|n| n.p.z == tvd).map(|n| n.md)
}
}
fn min_curvature(stations: &[Station], head: (f64, f64), kb: f64) -> Result<Vec<Node>> {
let rows: Vec<(f64, f64, f64)> = stations
.iter()
.map(|s| (s.md, s.inc_deg, s.azi_deg))
.collect();
let positioned = wells::survey_positions(&rows, head, kb)?;
Ok(stations
.iter()
.zip(positioned)
.map(|(s, (p, dir))| Node {
md: s.md,
p,
dir: Some(dir),
})
.collect())
}
fn nodes_from_positioned(rows: Vec<(Station, Point3)>) -> Result<Vec<Node>> {
if rows.is_empty() {
return Err(GeoError::OutOfRange(
"trajectory needs at least one station".into(),
));
}
let mut nodes = Vec::with_capacity(rows.len());
let mut prev_md = f64::NEG_INFINITY;
for (s, p) in rows {
if s.md <= prev_md {
return Err(GeoError::OutOfRange(
"station measured depth must strictly increase".into(),
));
}
prev_md = s.md;
nodes.push(Node {
md: s.md,
p,
dir: Some(wells::tangent(s.inc_deg, s.azi_deg)),
});
}
Ok(nodes)
}
fn nodes_from_xyz(points: Vec<Point3>) -> Result<Vec<Node>> {
let first = *points
.first()
.ok_or_else(|| GeoError::OutOfRange("trajectory needs at least one point".into()))?;
let mut nodes = Vec::with_capacity(points.len());
let mut md = 0.0;
let mut prev = first;
nodes.push(Node {
md,
p: first,
dir: None,
});
for p in points.into_iter().skip(1) {
md += dist3(prev, p);
nodes.push(Node { md, p, dir: None });
prev = p;
}
Ok(nodes)
}
fn steer_stations(
from: Station,
build_per_100: f64,
turn_per_100: f64,
to_md: f64,
) -> Vec<Station> {
let at = |md: f64| {
let d = md - from.md;
Station::new(
md,
from.inc_deg + build_per_100 * d / 100.0,
from.azi_deg + turn_per_100 * d / 100.0,
)
};
let mut out = vec![from];
let mut md = from.md + STEER_STEP;
while md < to_md - 1e-9 {
out.push(at(md));
md += STEER_STEP;
}
out.push(at(to_md));
out
}
fn lerp3(a: Point3, b: Point3, t: f64) -> Point3 {
Point3::new(
a.x + (b.x - a.x) * t,
a.y + (b.y - a.y) * t,
a.z + (b.z - a.z) * t,
)
}
fn dist3(a: Point3, b: Point3) -> f64 {
((b.x - a.x).powi(2) + (b.y - a.y).powi(2) + (b.z - a.z).powi(2)).sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn traj(input: TrajectoryInput, head: (f64, f64), kb: f64) -> Trajectory {
Trajectory::from_input(input, head, kb).unwrap()
}
#[test]
fn golden_min_curvature_survey() {
let t = traj(
TrajectoryInput::MdIncAzi(vec![
Station::new(3500.0, 15.0, 20.0),
Station::new(3600.0, 25.0, 45.0),
]),
(1000.0, 2000.0),
100.0,
);
let p0 = t.xyz(3500.0).unwrap();
assert_relative_eq!(p0.x, 1000.0, epsilon = 1e-9);
assert_relative_eq!(p0.y, 2000.0, epsilon = 1e-9);
assert_relative_eq!(p0.z, 3400.0, epsilon = 1e-9);
let p = t.xyz(3600.0).unwrap();
assert_relative_eq!(p.x, 1000.0 + 19.449, epsilon = 0.01);
assert_relative_eq!(p.y, 2000.0 + 27.216, epsilon = 0.01);
assert_relative_eq!(p.z, 3400.0 + 94.005, epsilon = 0.01);
}
#[test]
fn vertical_well_degenerate() {
let t = traj(
TrajectoryInput::Stations(vec![
Station::new(0.0, 0.0, 0.0),
Station::new(1000.0, 0.0, 0.0),
Station::new(2000.0, 0.0, 0.0),
]),
(500.0, 600.0),
30.0,
);
for md in [0.0, 750.0, 1000.0, 1500.0, 2000.0] {
let p = t.xyz(md).unwrap();
assert_relative_eq!(p.x, 500.0, epsilon = 1e-9);
assert_relative_eq!(p.y, 600.0, epsilon = 1e-9);
assert_relative_eq!(p.z, md - 30.0, epsilon = 1e-9); assert_relative_eq!(t.tvd(md).unwrap(), md - 30.0, epsilon = 1e-9);
}
}
#[test]
fn outside_md_range_is_none() {
let t = traj(
TrajectoryInput::Stations(vec![
Station::new(100.0, 0.0, 0.0),
Station::new(200.0, 0.0, 0.0),
]),
(0.0, 0.0),
0.0,
);
assert_eq!(t.md_range(), (100.0, 200.0));
assert!(t.xyz(99.0).is_none());
assert!(t.xyz(201.0).is_none());
assert!(t.xyz(150.0).is_some());
}
#[test]
fn xyz_follows_min_curvature_arc_between_stations() {
let t = traj(
TrajectoryInput::MdIncAzi(vec![
Station::new(3500.0, 15.0, 20.0),
Station::new(3600.0, 25.0, 45.0),
]),
(0.0, 0.0),
0.0,
);
let a = t.xyz(3500.0).unwrap();
let b = t.xyz(3600.0).unwrap();
let mid = t.xyz(3550.0).unwrap();
let (cx, cy, cz) = ((a.x + b.x) / 2.0, (a.y + b.y) / 2.0, (a.z + b.z) / 2.0);
let dev = ((mid.x - cx).powi(2) + (mid.y - cy).powi(2) + (mid.z - cz).powi(2)).sqrt();
assert!(
dev > 1e-3,
"arc midpoint should depart from the chord (dev={dev})"
);
assert_relative_eq!(t.xyz(3500.0).unwrap().z, a.z, epsilon = 1e-12);
assert_relative_eq!(t.xyz(3600.0).unwrap().z, b.z, epsilon = 1e-12);
assert!(a.z < mid.z && mid.z < b.z);
}
#[test]
fn md_at_tvd_on_build_up_path() {
let t = traj(
TrajectoryInput::Stations(vec![
Station::new(0.0, 0.0, 0.0),
Station::new(1000.0, 0.0, 0.0),
Station::new(2000.0, 30.0, 0.0),
]),
(0.0, 0.0),
0.0,
);
let (lo, hi) = t.md_range();
let target = (t.tvd(lo).unwrap() + t.tvd(hi).unwrap()) / 2.0;
let md = t.md_at_tvd(target).unwrap();
assert!(md >= lo && md <= hi);
assert_relative_eq!(t.tvd(md).unwrap(), target, epsilon = 1e-9);
}
#[test]
fn hold_is_a_straight_constant_segment() {
let t = traj(
TrajectoryInput::Hold {
from: Station::new(0.0, 30.0, 90.0),
to_md: 1000.0,
},
(0.0, 0.0),
0.0,
);
let p = t.xyz(1000.0).unwrap();
assert_relative_eq!(p.x, 500.0, epsilon = 1e-6); assert_relative_eq!(p.y, 0.0, epsilon = 1e-9); assert_relative_eq!(p.z, 30.0_f64.to_radians().cos() * 1000.0, epsilon = 1e-6);
}
#[test]
fn xyz_input_uses_positions_directly() {
let t = traj(
TrajectoryInput::Xyz(vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 0.0, 100.0),
]),
(999.0, 999.0),
999.0,
);
assert_eq!(t.md_range(), (0.0, 100.0));
let p = t.xyz(50.0).unwrap();
assert_relative_eq!(p.x, 0.0, epsilon = 1e-9);
assert_relative_eq!(p.z, 50.0, epsilon = 1e-9);
}
#[test]
fn steer_builds_inclination() {
let t = traj(
TrajectoryInput::Steer {
from: Station::new(0.0, 0.0, 0.0),
build_per_100: 3.0,
turn_per_100: 0.0,
to_md: 1000.0,
},
(0.0, 0.0),
0.0,
);
let (_, hi) = t.md_range();
assert_relative_eq!(hi, 1000.0, epsilon = 1e-9);
let p = t.xyz(hi).unwrap();
assert!(p.x.hypot(p.y) > 0.0);
assert!(p.z < 1000.0);
}
#[test]
fn non_increasing_md_errors() {
let r = Trajectory::from_input(
TrajectoryInput::Stations(vec![
Station::new(100.0, 0.0, 0.0),
Station::new(100.0, 0.0, 0.0),
]),
(0.0, 0.0),
0.0,
);
assert!(r.is_err());
}
}