use crate::error::StaticError;
use crate::grid::{Grid, Property};
use crate::model::trend::TrendSurface;
use crate::petro::{upscale_porosity, upscale_sw, SwSample, WeightedSample};
use crate::volumetrics::{populate_constant, validate_fraction, ConstantPriors, NTG, PORO, SW};
pub type PetroSample = (f64, f64, f64);
pub(crate) fn sort_by_tvd(mut samples: Vec<PetroSample>) -> Vec<PetroSample> {
samples.sort_by(|a, b| a.0.total_cmp(&b.0));
samples
}
pub(crate) fn populate(
grid: &mut Grid,
priors: ConstantPriors,
logs: Option<&[PetroSample]>,
trend: Option<&TrendSurface>,
) -> Result<(), StaticError> {
match logs {
None => populate_constant(grid, priors),
Some(samples) => populate_from_logs(grid, priors, samples),
}?;
if let Some(trend) = trend {
apply_areal_trend(grid, trend)?;
}
Ok(())
}
fn apply_areal_trend(grid: &mut Grid, trend: &TrendSurface) -> Result<(), StaticError> {
let dims = grid.dims();
let (ni, nj, nk) = (dims.ni, dims.nj, dims.nk);
let lattice = crate::model::pipeline::areal_lattice(grid)?;
let mult = trend.column_multipliers_on(&lattice)?;
let mut targets = vec![NTG];
if trend.applies_to_porosity() {
targets.push(PORO);
}
for name in targets {
let mut prop = grid.properties().get(name).cloned().ok_or_else(|| {
StaticError::InvalidInput(format!("areal trend target cube '{name}' is not populated"))
})?;
for k in 0..nk {
for j in 0..nj {
for i in 0..ni {
let idx = (k * nj + j) * ni + i;
prop.values[idx] = (prop.values[idx] * mult[j * ni + i]).clamp(0.0, 1.0);
}
}
}
grid.properties_mut().set(prop)?;
}
Ok(())
}
pub(crate) fn override_zone_priors(
grid: &mut Grid,
priors: ConstantPriors,
k_range: core::ops::Range<usize>,
) -> Result<(), StaticError> {
validate_fraction("porosity", priors.porosity)?;
validate_fraction("net_to_gross", priors.net_to_gross)?;
validate_fraction("water_saturation", priors.water_saturation)?;
let dims = grid.dims();
let (ni, nj) = (dims.ni, dims.nj);
for (name, val) in [
(PORO, priors.porosity),
(SW, priors.water_saturation),
(NTG, priors.net_to_gross),
] {
let mut prop = grid.properties().get(name).cloned().ok_or_else(|| {
StaticError::InvalidInput(format!("zone-priors target cube '{name}' is not populated"))
})?;
for k in k_range.clone() {
for j in 0..nj {
for i in 0..ni {
prop.values[(k * nj + j) * ni + i] = val;
}
}
}
grid.properties_mut().set(prop)?;
}
Ok(())
}
fn populate_from_logs(
grid: &mut Grid,
priors: ConstantPriors,
samples: &[PetroSample],
) -> Result<(), StaticError> {
validate_fraction("porosity", priors.porosity)?;
validate_fraction("net_to_gross", priors.net_to_gross)?;
validate_fraction("water_saturation", priors.water_saturation)?;
let n = grid.cell_count();
let (mut poro, mut sw, mut ntg) = {
let props = grid.properties_mut();
(
props.take_values(PORO),
props.take_values(SW),
props.take_values(NTG),
)
};
poro.clear();
poro.reserve(n);
sw.clear();
sw.reserve(n);
ntg.clear();
ntg.reserve(n);
for cell in grid.cells() {
let (lo, hi) = {
let (t, b) = (cell.top_depth(), cell.bottom_depth());
(t.min(b), t.max(b))
};
let start = samples.partition_point(|(tvd, _, _)| *tvd < lo);
let end = samples.partition_point(|(tvd, _, _)| *tvd <= hi);
let in_range = &samples[start..end];
if in_range.is_empty() {
poro.push(priors.porosity);
sw.push(priors.water_saturation);
} else {
let phi: Vec<WeightedSample> = in_range
.iter()
.map(|(_, p, _)| WeightedSample::new(1.0, *p))
.collect();
poro.push(upscale_porosity(&phi)?);
let sws: Vec<SwSample> = in_range
.iter()
.map(|(_, p, s)| SwSample {
length: 1.0,
porosity: *p,
water_saturation: *s,
})
.collect();
sw.push(upscale_sw(&sws)?);
}
ntg.push(priors.net_to_gross);
}
let props = grid.properties_mut();
props.set(Property {
name: PORO.to_string(),
values: poro,
})?;
props.set(Property {
name: SW.to_string(),
values: sw,
})?;
props.set(Property {
name: NTG.to_string(),
values: ntg,
})?;
Ok(())
}