use crate::foundation::{GeoError, Point3, Result};
use std::collections::{BTreeSet, HashMap};
const SMALL_BETA: f64 = 1e-4;
pub fn tangent(inc_deg: f64, azi_deg: f64) -> [f64; 3] {
let (i, a) = (inc_deg.to_radians(), azi_deg.to_radians());
[i.sin() * a.cos(), i.sin() * a.sin(), i.cos()]
}
pub fn dogleg(t1: [f64; 3], t2: [f64; 3]) -> f64 {
(t1[0] * t2[0] + t1[1] * t2[1] + t1[2] * t2[2])
.clamp(-1.0, 1.0)
.acos()
}
pub fn ratio_factor(beta: f64) -> f64 {
if beta < SMALL_BETA {
1.0 + beta * beta / 12.0
} else {
(2.0 / beta) * (beta / 2.0).tan()
}
}
pub fn arc_point(pa: Point3, t1: [f64; 3], t2: [f64; 3], f: f64, dmd: f64) -> Point3 {
let beta = dogleg(t1, t2);
let tf = if beta < SMALL_BETA {
let l = [
t1[0] + (t2[0] - t1[0]) * f,
t1[1] + (t2[1] - t1[1]) * f,
t1[2] + (t2[2] - t1[2]) * f,
];
let n = (l[0] * l[0] + l[1] * l[1] + l[2] * l[2]).sqrt().max(1e-300);
[l[0] / n, l[1] / n, l[2] / n]
} else {
let s = beta.sin();
let (w1, w2) = (((1.0 - f) * beta).sin() / s, (f * beta).sin() / s);
[
w1 * t1[0] + w2 * t2[0],
w1 * t1[1] + w2 * t2[1],
w1 * t1[2] + w2 * t2[2],
]
};
let half = 0.5 * (f * dmd) * ratio_factor(f * beta);
Point3::new(
pa.x + half * (t1[1] + tf[1]),
pa.y + half * (t1[0] + tf[0]),
pa.z + half * (t1[2] + tf[2]),
)
}
pub fn survey_positions(
stations: &[(f64, f64, f64)],
head: (f64, f64),
kb: f64,
) -> Result<Vec<(Point3, [f64; 3])>> {
let &(md0, inc0, azi0) = stations
.first()
.ok_or_else(|| GeoError::OutOfRange("trajectory needs at least one station".into()))?;
let mut pos = Point3::new(head.0, head.1, md0 - kb);
let mut t_prev = tangent(inc0, azi0);
let mut out = Vec::with_capacity(stations.len());
out.push((pos, t_prev));
for w in stations.windows(2) {
let (md_a, _, _) = w[0];
let (md_b, inc_b, azi_b) = w[1];
let dmd = md_b - md_a;
if dmd <= 0.0 {
return Err(GeoError::OutOfRange(
"station measured depth must strictly increase".into(),
));
}
let t_b = tangent(inc_b, azi_b);
pos = arc_point(pos, t_prev, t_b, 1.0, dmd);
out.push((pos, t_b));
t_prev = t_b;
}
Ok(out)
}
pub fn merge_strat_order(wells: &[Vec<(f64, &str)>]) -> Vec<String> {
let mut appearance: Vec<&str> = Vec::new();
let mut index: HashMap<&str, usize> = HashMap::new();
for well in wells {
for &(_, name) in well {
index.entry(name).or_insert_with(|| {
appearance.push(name);
appearance.len() - 1
});
}
}
let n = appearance.len();
if n == 0 {
return Vec::new();
}
let mut edges: BTreeSet<(usize, usize)> = BTreeSet::new();
for well in wells {
for (i, &(md_a, a)) in well.iter().enumerate() {
for &(md_b, b) in &well[i + 1..] {
if a == b {
continue;
}
let (ia, ib) = (index[a], index[b]);
if md_a < md_b {
edges.insert((ia, ib));
} else if md_b < md_a {
edges.insert((ib, ia));
} }
}
}
let mut indeg = vec![0usize; n];
let mut succ: Vec<Vec<usize>> = vec![Vec::new(); n];
for &(from, to) in &edges {
succ[from].push(to);
indeg[to] += 1;
}
let mut placed = vec![false; n];
let mut order: Vec<usize> = Vec::with_capacity(n);
while order.len() < n {
let k = (0..n)
.find(|&k| !placed[k] && indeg[k] == 0)
.or_else(|| (0..n).find(|&k| !placed[k]))
.expect("an unplaced node exists while order.len() < n");
placed[k] = true;
order.push(k);
for &m in &succ[k] {
if !placed[m] && indeg[m] > 0 {
indeg[m] -= 1;
}
}
}
order
.into_iter()
.map(|k| appearance[k].to_string())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use std::f64::consts::{FRAC_PI_2, PI};
#[test]
fn ratio_factor_limits() {
assert_relative_eq!(ratio_factor(0.0), 1.0, epsilon = 1e-12);
assert_relative_eq!(ratio_factor(1e-6), 1.0, epsilon = 1e-10);
assert_relative_eq!(ratio_factor(FRAC_PI_2), 4.0 / PI, epsilon = 1e-12);
}
#[test]
fn tangent_and_dogleg() {
assert_relative_eq!(tangent(0.0, 0.0)[2], 1.0); let east = tangent(90.0, 90.0);
assert_relative_eq!(east[1], 1.0, epsilon = 1e-12); assert_relative_eq!(
dogleg(tangent(0.0, 0.0), tangent(90.0, 0.0)),
FRAC_PI_2,
epsilon = 1e-12
);
}
#[test]
fn arc_point_endpoints_and_vertical() {
let pa = Point3::new(10.0, 20.0, 100.0);
let down = [0.0, 0.0, 1.0];
let p0 = arc_point(pa, down, down, 0.0, 50.0);
assert_relative_eq!(p0.z, 100.0, epsilon = 1e-12);
let p1 = arc_point(pa, down, down, 1.0, 50.0);
assert_relative_eq!(p1.z, 150.0, epsilon = 1e-12);
assert_relative_eq!(p1.x, 10.0, epsilon = 1e-12);
}
#[test]
fn survey_vertical_is_md_minus_kb() {
let pos =
survey_positions(&[(0.0, 0.0, 0.0), (1000.0, 0.0, 0.0)], (5.0, 6.0), 30.0).unwrap();
assert_relative_eq!(pos[0].0.z, -30.0, epsilon = 1e-12);
assert_relative_eq!(pos[1].0.z, 970.0, epsilon = 1e-12); assert_relative_eq!(pos[1].0.x, 5.0, epsilon = 1e-12);
}
#[test]
fn survey_rejects_non_increasing_md() {
assert!(
survey_positions(&[(100.0, 0.0, 0.0), (100.0, 5.0, 0.0)], (0.0, 0.0), 0.0).is_err()
);
assert!(survey_positions(&[], (0.0, 0.0), 0.0).is_err());
}
#[test]
fn strat_empty_is_empty() {
assert!(merge_strat_order(&[]).is_empty());
assert!(merge_strat_order(&[vec![]]).is_empty());
}
#[test]
fn strat_single_well_recovers_md_order_from_any_file_order() {
let w = vec![(30.0, "C"), (10.0, "A"), (20.0, "B")];
assert_eq!(merge_strat_order(&[w]), ["A", "B", "C"]);
}
#[test]
fn strat_separation_resolves_a_coincident_tie() {
let w1 = vec![(10.0, "A"), (20.0, "B"), (20.0, "C")];
let w2 = vec![(10.0, "A"), (20.0, "B"), (30.0, "C")];
assert_eq!(merge_strat_order(&[w1, w2]), ["A", "B", "C"]);
}
#[test]
fn strat_tie_everywhere_breaks_by_first_appearance() {
let xy = vec![vec![(10.0, "X"), (10.0, "Y")], vec![(5.0, "X"), (5.0, "Y")]];
assert_eq!(merge_strat_order(&xy), ["X", "Y"]);
let yx = vec![vec![(10.0, "Y"), (10.0, "X")], vec![(5.0, "Y"), (5.0, "X")]];
assert_eq!(merge_strat_order(&yx), ["Y", "X"]);
}
#[test]
fn strat_contradiction_is_deterministic_and_never_hangs() {
let w1 = vec![(10.0, "P"), (20.0, "Q")];
let w2 = vec![(10.0, "Q"), (20.0, "P")];
assert_eq!(merge_strat_order(&[w1, w2]), ["P", "Q"]);
}
#[test]
fn strat_field_shape_duva_cerisa_west() {
let absent = vec![(100.0, "Top"), (120.0, "Mid"), (120.0, "Lower")];
let dev = vec![(100.0, "Top"), (120.0, "Mid"), (130.0, "Lower")];
let sand = vec![(100.0, "Top"), (110.0, "Sand"), (120.0, "Mid")];
let order = merge_strat_order(&[absent, dev, sand]);
assert_eq!(order, ["Top", "Sand", "Mid", "Lower"]);
}
}