use crate::core::log::{Log, LogView};
use crate::core::tops::{Interval, Top};
use crate::core::trajectory::{Trajectory, TrajectoryInput};
use crate::foundation::{GeoError, Point3, Result, Stats};
use indexmap::IndexMap;
const MAIN: &str = "";
pub struct Well {
pub id: String,
pub head: (f64, f64),
pub kb: f64,
crs: Option<String>,
sidetracks: IndexMap<String, Sidetrack>,
}
impl Well {
pub fn new(id: impl Into<String>, head: (f64, f64), kb: f64) -> Well {
let mut sidetracks = IndexMap::new();
sidetracks.insert(MAIN.to_string(), Sidetrack::new(MAIN.to_string(), head, kb));
Well {
id: id.into(),
head,
kb,
crs: None,
sidetracks,
}
}
pub fn crs(&self) -> Option<&str> {
self.crs.as_deref()
}
pub fn set_crs(&mut self, crs: impl Into<String>) {
self.crs = Some(crs.into());
}
pub fn sidetrack(&self, label: &str) -> Option<&Sidetrack> {
self.sidetracks.get(label)
}
pub fn sidetrack_mut(&mut self, label: &str) -> &mut Sidetrack {
let (head, kb) = (self.head, self.kb);
self.sidetracks
.entry(label.to_string())
.or_insert_with(|| Sidetrack::new(label.to_string(), head, kb))
}
pub fn main(&self) -> &Sidetrack {
self.sidetracks
.get(MAIN)
.expect("the main sidetrack is always present")
}
pub fn sidetracks(&self) -> impl Iterator<Item = &Sidetrack> {
self.sidetracks.values()
}
pub fn xyz(&self, md: f64) -> Option<Point3> {
self.main().xyz(md)
}
pub fn tvd(&self, md: f64) -> Option<f64> {
self.main().tvd(md)
}
pub fn md_at_tvd(&self, tvd: f64) -> Option<f64> {
self.main().md_at_tvd(tvd)
}
pub fn top(&self, name: &str) -> Option<Interval<'_>> {
self.main().top(name)
}
pub fn log(&self, mnemonic: &str) -> Option<LogView<'_>> {
self.main().log(mnemonic)
}
pub fn logs(&self) -> impl Iterator<Item = &Log> {
self.main().logs()
}
pub fn mnemonics(&self) -> Vec<&str> {
self.main().logs().map(|l| l.mnemonic.as_str()).collect()
}
pub fn zones(&self) -> Vec<Interval<'_>> {
self.main().zones()
}
pub fn zone_stats(&self, mnemonic: &str) -> Vec<(String, Stats)> {
self.main().zone_stats(mnemonic)
}
}
pub struct Sidetrack {
pub label: String,
head: (f64, f64),
kb: f64,
trajectories: Vec<Trajectory>,
active: usize,
logs: Vec<Log>,
tops: Vec<Top>,
}
impl Sidetrack {
fn new(label: String, head: (f64, f64), kb: f64) -> Sidetrack {
Sidetrack {
label,
head,
kb,
trajectories: Vec::new(),
active: 0,
logs: Vec::new(),
tops: Vec::new(),
}
}
pub fn add_trajectory(&mut self, input: TrajectoryInput) -> Result<&mut Trajectory> {
let traj = Trajectory::from_input(input, self.head, self.kb)?;
self.trajectories.push(traj);
self.active = self.trajectories.len() - 1;
Ok(self
.trajectories
.last_mut()
.expect("just pushed a trajectory"))
}
pub fn set_active(&mut self, index: usize) -> Result<()> {
if index >= self.trajectories.len() {
return Err(GeoError::OutOfRange(format!(
"trajectory index {index} out of range (have {})",
self.trajectories.len()
)));
}
self.active = index;
Ok(())
}
pub fn active(&self) -> &Trajectory {
self.trajectories
.get(self.active)
.expect("active() requires at least one trajectory")
}
pub fn trajectories(&self) -> &[Trajectory] {
&self.trajectories
}
pub fn xyz(&self, md: f64) -> Option<Point3> {
self.trajectories.get(self.active).and_then(|t| t.xyz(md))
}
pub fn tvd(&self, md: f64) -> Option<f64> {
self.trajectories.get(self.active).and_then(|t| t.tvd(md))
}
pub fn md_at_tvd(&self, tvd: f64) -> Option<f64> {
self.trajectories
.get(self.active)
.and_then(|t| t.md_at_tvd(tvd))
}
pub fn add_log(&mut self, log: Log) {
self.logs.push(log);
}
pub fn add_tops(&mut self, tops: Vec<Top>) {
self.tops.extend(tops);
self.tops.sort_by(|a, b| a.md.total_cmp(&b.md));
}
pub fn top(&self, name: &str) -> Option<Interval<'_>> {
let i = self
.tops
.iter()
.position(|t| t.name.eq_ignore_ascii_case(name))?;
let top = &self.tops[i];
let base = self
.tops
.get(i + 1)
.map(|n| n.md)
.or_else(|| self.trajectories.get(self.active).map(|t| t.md_range().1))
.unwrap_or(f64::NAN);
Some(Interval::new(top.name.clone(), top.md, base, &self.logs))
}
pub fn log(&self, mnemonic: &str) -> Option<LogView<'_>> {
self.logs
.iter()
.find(|l| l.mnemonic.eq_ignore_ascii_case(mnemonic))
.map(|l| l.view())
}
pub fn logs(&self) -> impl Iterator<Item = &Log> {
self.logs.iter()
}
pub fn zones(&self) -> Vec<Interval<'_>> {
let td = self.trajectories.get(self.active).map(|t| t.md_range().1);
self.tops
.iter()
.enumerate()
.map(|(i, top)| {
let base = self
.tops
.get(i + 1)
.map(|n| n.md)
.or(td)
.unwrap_or(f64::NAN);
Interval::new(top.name.clone(), top.md, base, &self.logs)
})
.collect()
}
pub fn zone_stats(&self, mnemonic: &str) -> Vec<(String, Stats)> {
self.zones()
.into_iter()
.filter_map(|z| z.log(mnemonic).map(|lv| (z.name.clone(), lv.stats())))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::trajectory::Station;
use approx::assert_relative_eq;
fn vertical(md_top: f64, md_bot: f64) -> TrajectoryInput {
TrajectoryInput::Stations(vec![
Station::new(md_top, 0.0, 0.0),
Station::new(md_bot, 0.0, 0.0),
])
}
#[test]
fn new_well_has_empty_main() {
let w = Well::new("15/9-A1", (1000.0, 2000.0), 80.0);
assert_eq!(w.id, "15/9-A1");
assert_eq!(w.main().label, "");
assert!(w.main().trajectories().is_empty());
assert!(w.xyz(1000.0).is_none());
}
#[test]
fn add_trajectory_makes_newest_active() {
let mut w = Well::new("w", (0.0, 0.0), 0.0);
let st = w.sidetrack_mut("");
st.add_trajectory(vertical(0.0, 1000.0)).unwrap();
st.add_trajectory(vertical(0.0, 2000.0)).unwrap();
assert_eq!(st.trajectories().len(), 2);
assert_eq!(st.active().md_range(), (0.0, 2000.0));
assert!(st.xyz(1500.0).is_some());
}
#[test]
fn set_active_switches_and_bounds_check() {
let mut w = Well::new("w", (0.0, 0.0), 0.0);
let st = w.sidetrack_mut("");
st.add_trajectory(vertical(0.0, 1000.0)).unwrap();
st.add_trajectory(vertical(0.0, 2000.0)).unwrap();
st.set_active(0).unwrap();
assert_eq!(st.active().md_range(), (0.0, 1000.0));
assert!(st.set_active(5).is_err());
}
#[test]
fn well_delegates_to_main_active() {
let mut w = Well::new("w", (500.0, 600.0), 30.0);
w.sidetrack_mut("")
.add_trajectory(vertical(0.0, 1000.0))
.unwrap();
let p = w.xyz(400.0).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, 400.0 - 30.0, epsilon = 1e-9); assert_relative_eq!(w.tvd(400.0).unwrap(), 370.0, epsilon = 1e-9);
assert_relative_eq!(w.md_at_tvd(370.0).unwrap(), 400.0, epsilon = 1e-9);
}
fn ntg_log() -> Log {
Log::new(
"NTG",
"v/v",
vec![
2400.0, 2410.0, 2420.0, 2430.0, 2440.0, 2450.0, 2460.0, 2470.0, 2480.0, 2490.0,
2500.0,
],
vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0],
)
.unwrap()
}
#[test]
fn top_resolves_interval_to_next_top() {
let mut w = Well::new("w", (0.0, 0.0), 0.0);
let st = w.sidetrack_mut("");
st.add_trajectory(vertical(0.0, 3000.0)).unwrap();
st.add_tops(vec![Top::new("Brent", 2400.0), Top::new("Dunlin", 2450.0)]);
st.add_log(ntg_log());
let brent = w.top("Brent").unwrap();
assert_eq!(brent.top_md, 2400.0);
assert_eq!(brent.base_md, 2450.0); assert_eq!(brent.thickness_md(), 50.0);
let v = brent.log("NTG").unwrap();
assert_eq!(v.md(), &[2400.0, 2410.0, 2420.0, 2430.0, 2440.0]);
let s = v.stats();
assert_eq!(s.count, 5);
assert_relative_eq!(s.mean, 0.3, epsilon = 1e-12);
}
#[test]
fn zone_stats_average_and_sum_per_zone() {
let mut w = Well::new("w", (0.0, 0.0), 0.0);
let st = w.sidetrack_mut("");
st.add_trajectory(vertical(0.0, 2500.0)).unwrap(); st.add_tops(vec![Top::new("Brent", 2400.0), Top::new("Dunlin", 2450.0)]);
st.add_log(ntg_log());
let zs = w.zone_stats("NTG");
assert_eq!(zs.len(), 2);
assert_eq!(zs[0].0, "Brent");
assert_relative_eq!(zs[0].1.mean, 0.3, epsilon = 1e-12);
assert_relative_eq!(zs[0].1.sum, 1.5, epsilon = 1e-12);
assert_eq!(zs[1].0, "Dunlin");
assert_relative_eq!(zs[1].1.mean, 0.8, epsilon = 1e-12);
assert_relative_eq!(zs[1].1.sum, 4.0, epsilon = 1e-12);
let zones = w.zones();
assert_eq!(zones.len(), 2);
assert_eq!(zones[0].name, "Brent");
}
#[test]
fn deepest_top_runs_to_td() {
let mut w = Well::new("w", (0.0, 0.0), 0.0);
let st = w.sidetrack_mut("");
st.add_trajectory(vertical(0.0, 2500.0)).unwrap(); st.add_tops(vec![Top::new("Brent", 2400.0), Top::new("Dunlin", 2450.0)]);
let dunlin = w.top("Dunlin").unwrap();
assert_eq!(dunlin.top_md, 2450.0);
assert_eq!(dunlin.base_md, 2500.0); }
#[test]
fn enumerate_logs_and_mnemonics() {
let mut w = Well::new("w", (0.0, 0.0), 0.0);
let st = w.sidetrack_mut("");
st.add_log(ntg_log());
st.add_log(Log::new("GR", "GAPI", vec![2400.0, 2410.0], vec![40.0, 60.0]).unwrap());
assert_eq!(w.logs().count(), 2);
assert_eq!(w.mnemonics(), vec!["NTG", "GR"]); }
#[test]
fn ergonomic_chain_stats() {
let mut w = Well::new("15/9-A1", (0.0, 0.0), 0.0);
let st = w.sidetrack_mut("");
st.add_trajectory(vertical(0.0, 3000.0)).unwrap();
st.add_tops(vec![Top::new("Brent", 2400.0), Top::new("Dunlin", 2450.0)]);
st.add_log(ntg_log());
let stats = w.top("Brent").unwrap().log("NTG").unwrap().stats();
assert_relative_eq!(stats.mean, 0.3, epsilon = 1e-12);
assert!(w.top("brent").unwrap().log("ntg").is_some());
assert!(w.top("Nope").is_none());
}
#[test]
fn well_log_returns_full_curve() {
let mut w = Well::new("w", (0.0, 0.0), 0.0);
let st = w.sidetrack_mut("");
st.add_log(ntg_log());
let v = w.log("NTG").unwrap();
assert_eq!(v.md().len(), 11);
assert!(w.log("GR").is_none());
}
#[test]
fn sidetrack_mut_creates_named_bore_lazily() {
let mut w = Well::new("w", (0.0, 0.0), 0.0);
assert!(w.sidetrack("T2").is_none());
w.sidetrack_mut("T2")
.add_trajectory(vertical(0.0, 500.0))
.unwrap();
assert_eq!(w.sidetrack("T2").unwrap().label, "T2");
assert_eq!(w.sidetracks().count(), 2);
assert!(w.sidetrack("T2").unwrap().xyz(250.0).is_some());
assert!(w.xyz(250.0).is_none()); }
}