use crate::foundation::{AlgoError, Result};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Station {
pub md: f64,
pub x: f64,
pub y: f64,
pub z: f64,
pub tvd: f64,
pub incl: f64,
pub azim: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Trajectory {
pub stations: Vec<Station>,
}
pub fn synth_trajectory(
wellhead_xy: [f64; 2],
kb_elevation: f64,
td: f64,
md_step: f64,
seed: u64,
) -> Result<Trajectory> {
let _ = seed; if !(wellhead_xy[0].is_finite() && wellhead_xy[1].is_finite() && kb_elevation.is_finite()) {
return Err(AlgoError::InvalidArgument(
"synth_trajectory: wellhead and kb_elevation must be finite".to_string(),
));
}
if !(td.is_finite() && td > 0.0) {
return Err(AlgoError::InvalidArgument(
"synth_trajectory: td must be finite and > 0".to_string(),
));
}
if !(md_step.is_finite() && md_step > 0.0) {
return Err(AlgoError::InvalidArgument(
"synth_trajectory: md_step must be finite and > 0".to_string(),
));
}
let n_full = (td / md_step).floor() as usize;
let mut tvds: Vec<f64> = (0..=n_full).map(|k| k as f64 * md_step).collect();
if (tvds.last().copied().unwrap_or(0.0) - td).abs() > 1e-9 {
tvds.push(td);
}
let stations = tvds
.into_iter()
.map(|tvd| Station {
md: tvd, x: wellhead_xy[0],
y: wellhead_xy[1],
z: kb_elevation - tvd,
tvd,
incl: 0.0,
azim: 0.0,
})
.collect();
Ok(Trajectory { stations })
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct BuildHold {
pub kickoff_md: f64,
pub build_rate_deg_per_30m: f64,
pub hold_incl_deg: f64,
pub azimuth_deg: f64,
}
pub const MAX_BUILD_RATE_DEG_PER_30M: f64 = 6.0;
impl BuildHold {
pub fn new(
kickoff_md: f64,
build_rate_deg_per_30m: f64,
hold_incl_deg: f64,
azimuth_deg: f64,
) -> Result<BuildHold> {
validate_build(
kickoff_md,
build_rate_deg_per_30m,
hold_incl_deg,
azimuth_deg,
)?;
Ok(BuildHold {
kickoff_md,
build_rate_deg_per_30m,
hold_incl_deg,
azimuth_deg: normalize_azimuth(azimuth_deg),
})
}
fn build_len(&self) -> f64 {
self.hold_incl_deg / (self.build_rate_deg_per_30m / 30.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct BuildHoldDrop {
pub build_hold: BuildHold,
pub drop_start_md: f64,
pub drop_rate_deg_per_30m: f64,
pub final_incl_deg: f64,
}
impl BuildHoldDrop {
pub fn new(
build_hold: BuildHold,
drop_start_md: f64,
drop_rate_deg_per_30m: f64,
final_incl_deg: f64,
) -> Result<BuildHoldDrop> {
if !(drop_rate_deg_per_30m.is_finite()
&& drop_rate_deg_per_30m > 0.0
&& drop_rate_deg_per_30m <= MAX_BUILD_RATE_DEG_PER_30M)
{
return Err(AlgoError::InvalidArgument(format!(
"BuildHoldDrop: drop_rate must be finite in (0, {MAX_BUILD_RATE_DEG_PER_30M}] deg/30m"
)));
}
if !(final_incl_deg.is_finite() && final_incl_deg >= 0.0)
|| final_incl_deg > build_hold.hold_incl_deg
{
return Err(AlgoError::InvalidArgument(
"BuildHoldDrop: final_incl must be finite in [0, hold_incl]".to_string(),
));
}
let build_end = build_hold.kickoff_md + build_hold.build_len();
if !(drop_start_md.is_finite() && drop_start_md >= build_end - 1e-9) {
return Err(AlgoError::InvalidArgument(
"BuildHoldDrop: drop_start_md must be finite and at/after the build end"
.to_string(),
));
}
Ok(BuildHoldDrop {
build_hold,
drop_start_md,
drop_rate_deg_per_30m,
final_incl_deg,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum WellProfile {
Vertical,
BuildHold(BuildHold),
BuildHoldDrop(BuildHoldDrop),
}
impl WellProfile {
fn angles_at(&self, md: f64) -> (f64, f64) {
match self {
WellProfile::Vertical => (0.0, 0.0),
WellProfile::BuildHold(bh) => build_hold_angles(bh, md),
WellProfile::BuildHoldDrop(bhd) => {
let bh = &bhd.build_hold;
if md <= bhd.drop_start_md {
build_hold_angles(bh, md)
} else {
let drop_len = (bh.hold_incl_deg - bhd.final_incl_deg)
/ (bhd.drop_rate_deg_per_30m / 30.0);
let incl = if md >= bhd.drop_start_md + drop_len {
bhd.final_incl_deg
} else {
bh.hold_incl_deg
- (md - bhd.drop_start_md) * (bhd.drop_rate_deg_per_30m / 30.0)
};
(incl, bh.azimuth_deg)
}
}
}
}
}
fn build_hold_angles(bh: &BuildHold, md: f64) -> (f64, f64) {
if md <= bh.kickoff_md {
(0.0, 0.0)
} else if md >= bh.kickoff_md + bh.build_len() {
(bh.hold_incl_deg, bh.azimuth_deg)
} else {
let incl = (md - bh.kickoff_md) * (bh.build_rate_deg_per_30m / 30.0);
(incl, bh.azimuth_deg)
}
}
fn normalize_azimuth(az: f64) -> f64 {
let m = az % 360.0;
if m < 0.0 {
m + 360.0
} else {
m
}
}
fn validate_build(
kickoff_md: f64,
build_rate_deg_per_30m: f64,
hold_incl_deg: f64,
azimuth_deg: f64,
) -> Result<()> {
if !(kickoff_md.is_finite() && kickoff_md >= 0.0) {
return Err(AlgoError::InvalidArgument(
"BuildHold: kickoff_md must be finite and >= 0".to_string(),
));
}
if !(build_rate_deg_per_30m.is_finite()
&& build_rate_deg_per_30m > 0.0
&& build_rate_deg_per_30m <= MAX_BUILD_RATE_DEG_PER_30M)
{
return Err(AlgoError::InvalidArgument(format!(
"BuildHold: build_rate must be finite in (0, {MAX_BUILD_RATE_DEG_PER_30M}] deg/30m"
)));
}
if !(hold_incl_deg.is_finite() && hold_incl_deg > 0.0 && hold_incl_deg < 90.0) {
return Err(AlgoError::InvalidArgument(
"BuildHold: hold_incl must be finite in (0, 90)".to_string(),
));
}
if !azimuth_deg.is_finite() {
return Err(AlgoError::InvalidArgument(
"BuildHold: azimuth must be finite".to_string(),
));
}
Ok(())
}
pub fn synth_trajectory_profile(
wellhead_xy: [f64; 2],
kb_elevation: f64,
td: f64,
md_step: f64,
profile: &WellProfile,
seed: u64,
) -> Result<Trajectory> {
let _ = seed; if let WellProfile::Vertical = profile {
return synth_trajectory(wellhead_xy, kb_elevation, td, md_step, seed);
}
if !(wellhead_xy[0].is_finite() && wellhead_xy[1].is_finite() && kb_elevation.is_finite()) {
return Err(AlgoError::InvalidArgument(
"synth_trajectory_profile: wellhead and kb_elevation must be finite".to_string(),
));
}
if !(td.is_finite() && td > 0.0) {
return Err(AlgoError::InvalidArgument(
"synth_trajectory_profile: td must be finite and > 0".to_string(),
));
}
if !(md_step.is_finite() && md_step > 0.0) {
return Err(AlgoError::InvalidArgument(
"synth_trajectory_profile: md_step must be finite and > 0".to_string(),
));
}
let n_full = (td / md_step).floor() as usize;
let mut mds: Vec<f64> = (0..=n_full).map(|k| k as f64 * md_step).collect();
if (mds.last().copied().unwrap_or(0.0) - td).abs() > 1e-9 {
mds.push(td);
}
let (mut east, mut north, mut tvd) = (0.0_f64, 0.0_f64, 0.0_f64);
let mut stations = Vec::with_capacity(mds.len());
let mut prev: Option<(f64, f64, f64)> = None; for &md in &mds {
let (incl_deg, azim_deg) = profile.angles_at(md);
let (i2, a2) = (incl_deg.to_radians(), azim_deg.to_radians());
if let Some((md1, i1, a1)) = prev {
let dmd = md - md1;
let (de, dn, dv) = min_curvature_step(dmd, i1, a1, i2, a2);
east += de;
north += dn;
tvd += dv;
}
stations.push(Station {
md,
x: wellhead_xy[0] + east,
y: wellhead_xy[1] + north,
z: kb_elevation - tvd,
tvd,
incl: incl_deg,
azim: azim_deg,
});
prev = Some((md, i2, a2));
}
Ok(Trajectory { stations })
}
fn min_curvature_step(dmd: f64, i1: f64, a1: f64, i2: f64, a2: f64) -> (f64, f64, f64) {
let cos_dl = (i1.cos() * i2.cos() + i1.sin() * i2.sin() * (a2 - a1).cos()).clamp(-1.0, 1.0);
let dl = cos_dl.acos();
let rf = if dl.abs() < 1e-9 {
1.0
} else {
(dl / 2.0).tan() / (dl / 2.0)
};
let half = dmd / 2.0 * rf;
let de = half * (i1.sin() * a1.sin() + i2.sin() * a2.sin()); let dn = half * (i1.sin() * a1.cos() + i2.sin() * a2.cos()); let dv = half * (i1.cos() + i2.cos()); (de, dn, dv)
}
pub fn max_dogleg_severity(traj: &Trajectory) -> f64 {
let mut worst = 0.0_f64;
for w in traj.stations.windows(2) {
let (s1, s2) = (&w[0], &w[1]);
let dmd = s2.md - s1.md;
if dmd <= 0.0 {
continue;
}
let (i1, a1) = (s1.incl.to_radians(), s1.azim.to_radians());
let (i2, a2) = (s2.incl.to_radians(), s2.azim.to_radians());
let cos_dl = (i1.cos() * i2.cos() + i1.sin() * i2.sin() * (a2 - a1).cos()).clamp(-1.0, 1.0);
let dls = cos_dl.acos().to_degrees() / dmd * 30.0;
worst = worst.max(dls);
}
worst
}