use crate::foundation::{GeoError, Result, Stats};
use ndarray::Array1;
use std::borrow::Cow;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LogKind {
#[default]
Log,
Core,
}
#[derive(Debug, Clone)]
pub struct Log {
pub mnemonic: String,
pub unit: String,
kind: LogKind,
md: Array1<f64>,
values: Array1<f64>,
}
impl Log {
pub fn new(
mnemonic: impl Into<String>,
unit: impl Into<String>,
md: Vec<f64>,
values: Vec<f64>,
) -> Result<Log> {
if md.len() != values.len() {
return Err(GeoError::Parse(format!(
"log '{}': md len {} != values len {}",
mnemonic.into(),
md.len(),
values.len()
)));
}
Ok(Log {
mnemonic: mnemonic.into(),
unit: unit.into(),
kind: LogKind::Log,
md: Array1::from(md),
values: Array1::from(values),
})
}
pub fn kind(&self) -> LogKind {
self.kind
}
pub fn with_kind(mut self, kind: LogKind) -> Self {
self.kind = kind;
self
}
pub fn load_las_all(path: impl AsRef<Path>) -> Result<Vec<Log>> {
let d = crate::io::las::load(path.as_ref())?;
let md = d.index;
d.curves
.into_iter()
.map(|c| Log::new(c.mnemonic, c.unit, md.clone(), c.values))
.collect()
}
pub fn load_las(path: impl AsRef<Path>, mnemonic: &str) -> Result<Log> {
let d = crate::io::las::load(path.as_ref())?;
let c = d
.curves
.into_iter()
.find(|c| c.mnemonic.eq_ignore_ascii_case(mnemonic))
.ok_or_else(|| GeoError::NotFound(format!("LAS curve '{mnemonic}'")))?;
Log::new(c.mnemonic, c.unit, d.index, c.values)
}
pub fn len(&self) -> usize {
self.md.len()
}
pub fn is_empty(&self) -> bool {
self.md.is_empty()
}
pub(crate) fn md_slice(&self) -> &[f64] {
self.md.as_slice().expect("log md is contiguous")
}
pub(crate) fn values_slice(&self) -> &[f64] {
self.values.as_slice().expect("log values is contiguous")
}
pub fn view(&self) -> LogView<'_> {
LogView::borrowed(self.md_slice(), self.values_slice())
}
pub(crate) fn clip(&self, top_md: f64, base_md: f64) -> LogView<'_> {
let md = self.md_slice();
let lo = md.partition_point(|&m| m < top_md);
let hi = md.partition_point(|&m| m < base_md);
let hi = hi.max(lo);
LogView::borrowed(&md[lo..hi], &self.values_slice()[lo..hi])
}
}
#[derive(Debug, Clone)]
pub struct LogView<'a> {
md: Cow<'a, [f64]>,
values: Cow<'a, [f64]>,
}
impl<'a> LogView<'a> {
pub(crate) fn borrowed(md: &'a [f64], values: &'a [f64]) -> LogView<'a> {
LogView {
md: Cow::Borrowed(md),
values: Cow::Borrowed(values),
}
}
pub fn stats(&self) -> Stats {
Stats::of(&self.values)
}
pub fn stats_weighted(&self, by: &LogView) -> Stats {
let n = self.values.len().min(by.values.len());
Stats::weighted(&self.values[..n], &by.values[..n])
}
pub fn filter(&self, pred: impl Fn(f64) -> bool) -> LogView<'a> {
let mut md = Vec::new();
let mut values = Vec::new();
for (&m, &v) in self.md.iter().zip(self.values.iter()) {
if pred(v) {
md.push(m);
values.push(v);
}
}
LogView {
md: Cow::Owned(md),
values: Cow::Owned(values),
}
}
pub fn at_md(&self, md: f64) -> Option<f64> {
let m = &self.md;
let v = &self.values;
if m.is_empty() || md.is_nan() || md < m[0] || md > m[m.len() - 1] {
return None;
}
for i in 1..m.len() {
if md <= m[i] {
let span = m[i] - m[i - 1];
if span <= 0.0 {
return Some(v[i - 1]);
}
let t = (md - m[i - 1]) / span;
return Some(v[i - 1] + (v[i] - v[i - 1]) * t);
}
}
Some(v[m.len() - 1])
}
pub fn resample(&self, step: f64) -> Log {
let m = &self.md;
if m.is_empty() || step <= 0.0 {
return Log {
mnemonic: String::new(),
unit: String::new(),
kind: LogKind::Log,
md: Array1::from(Vec::new()),
values: Array1::from(Vec::new()),
};
}
let (lo, hi) = (m[0], m[m.len() - 1]);
let n = ((hi - lo) / step).floor() as usize + 1;
let mut md = Vec::with_capacity(n);
let mut values = Vec::with_capacity(n);
for k in 0..n {
let d = lo + step * k as f64;
md.push(d);
values.push(self.at_md(d).unwrap_or(f64::NAN));
}
Log {
mnemonic: String::new(),
unit: String::new(),
kind: LogKind::Log,
md: Array1::from(md),
values: Array1::from(values),
}
}
pub fn values(&self) -> &[f64] {
&self.values
}
pub fn md(&self) -> &[f64] {
&self.md
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn log() -> Log {
Log::new(
"NTG",
"v/v",
vec![100.0, 110.0, 120.0, 130.0, 140.0],
vec![0.2, 0.4, f64::NAN, 0.8, 1.0],
)
.unwrap()
}
#[test]
fn new_rejects_mismatched_lengths() {
assert!(Log::new("X", "u", vec![1.0, 2.0], vec![1.0]).is_err());
}
#[test]
fn view_stats_skip_nan() {
let l = log();
let s = l.view().stats();
assert_eq!(s.count, 4); assert_relative_eq!(s.sum, 0.2 + 0.4 + 0.8 + 1.0);
assert_relative_eq!(s.mean, 2.4 / 4.0);
assert_relative_eq!(s.min, 0.2);
assert_relative_eq!(s.max, 1.0);
}
#[test]
fn clip_selects_half_open_window() {
let l = log();
let v = l.clip(110.0, 130.0);
assert_eq!(v.md(), &[110.0, 120.0]);
assert_eq!(v.values()[0], 0.4);
assert!(v.values()[1].is_nan());
assert_eq!(v.stats().count, 1);
}
#[test]
fn clip_to_td_includes_last_sample() {
let l = log();
let v = l.clip(130.0, 1e9);
assert_eq!(v.md(), &[130.0, 140.0]);
}
#[test]
fn filter_keeps_predicate_subset() {
let l = log();
let v = l.view().filter(|x| x >= 0.5);
assert_eq!(v.values(), &[0.8, 1.0]);
assert_eq!(v.md(), &[130.0, 140.0]);
}
#[test]
fn at_md_interpolates_and_bounds() {
let l = Log::new("X", "u", vec![100.0, 200.0], vec![0.0, 10.0]).unwrap();
let v = l.view();
assert_relative_eq!(v.at_md(150.0).unwrap(), 5.0);
assert_relative_eq!(v.at_md(100.0).unwrap(), 0.0);
assert_relative_eq!(v.at_md(200.0).unwrap(), 10.0);
assert!(v.at_md(99.0).is_none());
assert!(v.at_md(201.0).is_none());
}
#[test]
fn resample_node_count_and_values() {
let l = Log::new("X", "u", vec![100.0, 200.0], vec![0.0, 10.0]).unwrap();
let r = l.view().resample(25.0);
assert_eq!(r.len(), 5);
assert_eq!(r.md_slice(), &[100.0, 125.0, 150.0, 175.0, 200.0]);
assert_relative_eq!(r.values_slice()[1], 2.5);
assert_relative_eq!(r.values_slice()[4], 10.0);
}
#[test]
fn stats_weighted_pv_vs_hand_calc() {
let sw = Log::new("SW", "v/v", vec![10.0, 20.0, 30.0], vec![0.2, 0.5, 0.8]).unwrap();
let pv = Log::new("PV", "m3", vec![10.0, 20.0, 30.0], vec![1.0, 1.0, 2.0]).unwrap();
let s = sw.view().stats_weighted(&pv.view());
assert_relative_eq!(s.sum, 2.3);
assert_relative_eq!(s.mean, 0.575);
assert_eq!(s.count, 3);
}
}