use crate::error::StaticError;
use crate::grid::{Dims, Grid};
use crate::volumetrics::grv::{InPlace, ZoneVolumes};
use crate::volumetrics::names::{NTG, PORO, SW};
use crate::volumetrics::valid::validate_fraction;
use core::ops::Range;
pub trait SlabSource {
fn dims(&self) -> Dims;
fn require_cubes(&self) -> Result<(), StaticError>;
type Slab<'a>: CellSlab
where
Self: 'a;
fn slab(&self, k: usize) -> Result<Self::Slab<'_>, StaticError>;
}
pub trait CellSlab {
fn centroid_z(&self, local: usize) -> f64;
fn cell_volume(&self, local: usize) -> f64;
fn poro(&self, local: usize) -> f64;
fn ntg(&self, local: usize) -> f64;
fn sw(&self, local: usize) -> f64;
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Clip {
Bulk,
Single(f64),
Two {
goc: f64,
owc: f64,
sw_gas: Option<f64>,
},
}
impl Clip {
#[must_use]
pub fn needs_cubes(&self) -> bool {
!matches!(self, Clip::Bulk)
}
}
#[inline]
fn cell_hcpv(
volume: f64,
poro: f64,
ntg: f64,
sw: f64,
sw_override: Option<f64>,
) -> Result<f64, StaticError> {
let phi = frac(PORO, poro)?;
let n = frac(NTG, ntg)?;
let water = match sw_override {
Some(s) => s,
None => frac(SW, sw)?,
};
Ok(volume * n * phi * (1.0 - water))
}
#[inline]
fn frac(what: &str, x: f64) -> Result<f64, StaticError> {
validate_fraction(what, x)?;
Ok(x)
}
pub fn compute_clipped<S: SlabSource>(
src: &S,
clip: Clip,
k_range: Range<usize>,
per_cell: bool,
) -> Result<InPlace, StaticError> {
match clip {
Clip::Bulk => stream_bulk(src, k_range),
Clip::Single(depth) => {
if !depth.is_finite() {
return Err(StaticError::OutOfRange(format!(
"contact depth must be finite, got {depth}"
)));
}
src.require_cubes()?;
stream_single(src, depth, k_range, per_cell)
}
Clip::Two { goc, owc, sw_gas } => {
if !goc.is_finite() || !owc.is_finite() {
return Err(StaticError::OutOfRange(format!(
"contact depths must be finite, got GOC {goc}, OWC {owc}"
)));
}
if goc > owc {
return Err(StaticError::InvalidInput(format!(
"GOC ({goc}) must be shallower than OWC ({owc})"
)));
}
if let Some(s) = sw_gas {
validate_fraction("sw_gas", s)?;
}
src.require_cubes()?;
stream_two(src, goc, owc, sw_gas, k_range, per_cell)
}
}
}
fn stream_single<S: SlabSource>(
src: &S,
contact_depth_m: f64,
k_range: Range<usize>,
per_cell: bool,
) -> Result<InPlace, StaticError> {
let dims = src.dims();
let per_slab = dims.ni * dims.nj;
let mut per_cell_hcpv = if per_cell {
vec![0.0; dims.cell_count()]
} else {
Vec::new()
};
let (mut grv, mut hcpv, mut cells) = (0.0f64, 0.0f64, 0usize);
for k in k_range {
let slab = src.slab(k)?;
let kbase = k * per_slab;
for local in 0..per_slab {
if slab.centroid_z(local) >= contact_depth_m {
continue; }
let v = slab.cell_volume(local);
if v <= 0.0 {
continue; }
let h = cell_hcpv(v, slab.poro(local), slab.ntg(local), slab.sw(local), None)?;
if per_cell {
per_cell_hcpv[kbase + local] = h;
}
grv += v;
hcpv += h;
cells += 1;
}
}
Ok(InPlace {
grv_m3: grv,
hcpv_m3: hcpv,
cells_in_column: cells,
per_cell_hcpv,
gas: None,
oil: None,
})
}
fn stream_two<S: SlabSource>(
src: &S,
goc_m: f64,
owc_m: f64,
sw_gas: Option<f64>,
k_range: Range<usize>,
per_cell: bool,
) -> Result<InPlace, StaticError> {
let dims = src.dims();
let per_slab = dims.ni * dims.nj;
let mut per_cell_hcpv = if per_cell {
vec![0.0; dims.cell_count()]
} else {
Vec::new()
};
let (mut gg, mut gh, mut gc) = (0.0f64, 0.0f64, 0usize);
let (mut og, mut oh, mut oc) = (0.0f64, 0.0f64, 0usize);
for k in k_range {
let slab = src.slab(k)?;
let kbase = k * per_slab;
for local in 0..per_slab {
let z = slab.centroid_z(local);
if z >= owc_m {
continue; }
let v = slab.cell_volume(local);
if v <= 0.0 {
continue;
}
let is_gas = z < goc_m;
let h = cell_hcpv(
v,
slab.poro(local),
slab.ntg(local),
slab.sw(local),
if is_gas { sw_gas } else { None },
)?;
if per_cell {
per_cell_hcpv[kbase + local] = h;
}
if is_gas {
gg += v;
gh += h;
gc += 1;
} else {
og += v;
oh += h;
oc += 1;
}
}
}
Ok(InPlace {
grv_m3: gg + og,
hcpv_m3: gh + oh,
cells_in_column: gc + oc,
per_cell_hcpv,
gas: Some(ZoneVolumes {
grv_m3: gg,
hcpv_m3: gh,
cells: gc,
}),
oil: Some(ZoneVolumes {
grv_m3: og,
hcpv_m3: oh,
cells: oc,
}),
})
}
fn stream_bulk<S: SlabSource>(src: &S, k_range: Range<usize>) -> Result<InPlace, StaticError> {
let dims = src.dims();
let per_slab = dims.ni * dims.nj;
let (mut grv, mut cells) = (0.0f64, 0usize);
for k in k_range {
let slab = src.slab(k)?;
for local in 0..per_slab {
let v = slab.cell_volume(local);
if v <= 0.0 {
continue;
}
grv += v;
cells += 1;
}
}
Ok(InPlace {
grv_m3: grv,
hcpv_m3: 0.0,
cells_in_column: cells,
per_cell_hcpv: Vec::new(),
gas: None,
oil: None,
})
}
pub struct GridSource<'a> {
grid: &'a Grid,
poro: Option<&'a [f64]>,
ntg: Option<&'a [f64]>,
sw: Option<&'a [f64]>,
}
impl<'a> GridSource<'a> {
#[must_use]
pub fn new(grid: &'a Grid) -> Self {
let get = |n: &str| grid.properties().get(n).map(|p| p.values.as_slice());
Self {
grid,
poro: get(PORO),
ntg: get(NTG),
sw: get(SW),
}
}
}
pub struct GridSlab<'a> {
grid: &'a Grid,
poro: Option<&'a [f64]>,
ntg: Option<&'a [f64]>,
sw: Option<&'a [f64]>,
kbase: usize,
}
impl SlabSource for GridSource<'_> {
fn dims(&self) -> Dims {
self.grid.dims()
}
fn require_cubes(&self) -> Result<(), StaticError> {
for (name, present) in [
(PORO, self.poro.is_some()),
(NTG, self.ntg.is_some()),
(SW, self.sw.is_some()),
] {
if !present {
return Err(StaticError::InvalidInput(format!(
"grid is missing required property '{name}'"
)));
}
}
Ok(())
}
type Slab<'s>
= GridSlab<'s>
where
Self: 's;
fn slab(&self, k: usize) -> Result<GridSlab<'_>, StaticError> {
let d = self.grid.dims();
Ok(GridSlab {
grid: self.grid,
poro: self.poro,
ntg: self.ntg,
sw: self.sw,
kbase: k * d.ni * d.nj,
})
}
}
impl CellSlab for GridSlab<'_> {
#[inline]
fn centroid_z(&self, local: usize) -> f64 {
self.grid.cell_centroid_z_at(self.kbase + local)
}
#[inline]
fn cell_volume(&self, local: usize) -> f64 {
self.grid.cell_volume_at(self.kbase + local)
}
#[inline]
fn poro(&self, local: usize) -> f64 {
self.poro.expect("require_cubes verified PORO")[self.kbase + local]
}
#[inline]
fn ntg(&self, local: usize) -> f64 {
self.ntg.expect("require_cubes verified NTG")[self.kbase + local]
}
#[inline]
fn sw(&self, local: usize) -> f64 {
self.sw.expect("require_cubes verified SW")[self.kbase + local]
}
}