#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Cutoffs {
pub phi_min: f64,
pub sw_max: f64,
pub vsh_max: f64,
}
impl Default for Cutoffs {
fn default() -> Self {
Self {
phi_min: 0.08,
sw_max: 0.5,
vsh_max: 0.5,
}
}
}
impl std::fmt::Display for Cutoffs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"phi>={:.3} Sw<={:.3} Vsh<={:.3}",
self.phi_min, self.sw_max, self.vsh_max
)
}
}
pub fn net_flags(phi: &[f64], sw: &[f64], vsh: Option<&[f64]>, cut: &Cutoffs) -> Vec<bool> {
(0..phi.len())
.map(|i| {
let p = phi[i];
let s = sw.get(i).copied().unwrap_or(f64::NAN);
let v = match vsh {
Some(vsh) => vsh.get(i).copied().unwrap_or(f64::NAN),
None => 0.0, };
p >= cut.phi_min && s <= cut.sw_max && v <= cut.vsh_max && !p.is_nan() && !s.is_nan()
})
.collect()
}
fn representative_thickness(depth: &[f64]) -> Vec<f64> {
let n = depth.len();
if n < 2 {
return vec![0.0; n];
}
(0..n)
.map(|i| {
let hi = if i + 1 < n { depth[i + 1] } else { depth[i] };
let lo = if i > 0 { depth[i - 1] } else { depth[i] };
(hi - lo) / 2.0
})
.collect()
}
pub fn net_pay(depth: &[f64], net: &[bool]) -> f64 {
let t = representative_thickness(depth);
t.iter()
.zip(net)
.filter(|(_, &is_net)| is_net)
.map(|(t, _)| *t)
.sum()
}
pub fn net_to_gross(depth: &[f64], net: &[bool]) -> f64 {
if depth.len() < 2 {
return 0.0;
}
let gross = depth[depth.len() - 1] - depth[0];
if gross <= 0.0 {
return 0.0;
}
net_pay(depth, net) / gross
}
pub fn leverett_j(pc: f64, ift: f64, perm: f64, phi: f64) -> f64 {
if phi <= 0.0 {
return f64::NAN;
}
(pc / ift) * (perm / phi).sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn default_cutoffs() {
let c = Cutoffs::default();
assert_relative_eq!(c.phi_min, 0.08);
assert_relative_eq!(c.sw_max, 0.5);
assert_relative_eq!(c.vsh_max, 0.5);
}
#[test]
fn net_flags_apply_all_cutoffs() {
let phi = [0.20, 0.05, 0.20, 0.20, f64::NAN];
let sw = [0.30, 0.30, 0.80, 0.30, 0.30];
let vsh = [0.10, 0.10, 0.10, 0.90, 0.10];
let net = net_flags(&phi, &sw, Some(&vsh), &Cutoffs::default());
assert_eq!(net, vec![true, false, false, false, false]);
}
#[test]
fn net_pay_and_ntg_hand_calc() {
let depth = [2400.0, 2410.0, 2420.0, 2430.0, 2440.0];
let net = [false, true, true, true, false];
assert_relative_eq!(net_pay(&depth, &net), 30.0);
assert_relative_eq!(net_to_gross(&depth, &net), 0.75);
}
#[test]
fn net_pay_degenerate_single_sample() {
assert_relative_eq!(net_pay(&[2400.0], &[true]), 0.0);
assert_relative_eq!(net_to_gross(&[2400.0], &[true]), 0.0);
}
#[test]
fn cutoffs_display_and_serde_roundtrip() {
let c = Cutoffs {
phi_min: 0.10,
sw_max: 0.4,
vsh_max: 0.3,
};
assert_eq!(format!("{c}"), "phi>=0.100 Sw<=0.400 Vsh<=0.300");
let json = serde_json::to_string(&c).unwrap();
let back: Cutoffs = serde_json::from_str(&json).unwrap();
assert_eq!(c, back);
}
#[test]
fn leverett_j_analytic() {
assert_relative_eq!(
leverett_j(1.0e5, 0.02, 1.0e-13, 0.2),
3.535_533_9,
epsilon = 1e-6
);
assert!(leverett_j(1.0e5, 0.02, 1.0e-13, 0.0).is_nan());
}
}