use crate::core::inplace::Fluid;
use crate::units::SrsError;
use petekstatic::gridder::{Conformity, SolveOpts};
use petekstatic::model::{BuildOpts, StaticModel, StaticModelBuilder};
use petekstatic::volumetrics::{ConstantPriors, GasFvf, OilFvf};
use petekstatic::wireframe::Wireframe;
pub type PetroSample = petekstatic::model::PetroSample;
#[derive(Debug, Clone)]
pub struct RefiningModel {
builder: StaticModelBuilder,
fluid: Fluid,
fvf: f64,
}
#[derive(Debug, Clone)]
pub struct Refined {
pub model: StaticModel,
pub in_place: f64,
pub grv_mcm: f64,
pub controls: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct WireframeSeed {
pub area_m2: f64,
pub gross_height_m: f64,
pub nk: usize,
pub priors: ConstantPriors,
pub fluid: Fluid,
pub fvf: f64,
}
impl RefiningModel {
#[allow(clippy::too_many_arguments)]
pub fn new(
area_m2: f64,
gross_height_m: f64,
ni: usize,
nj: usize,
nk: usize,
top_depth_m: f64,
contact_depth_m: f64,
priors: ConstantPriors,
fluid: Fluid,
fvf: f64,
) -> Result<Self, SrsError> {
let builder = StaticModelBuilder::flat(
ni,
nj,
top_depth_m,
contact_depth_m,
BuildOpts {
area_m2,
gross_height_m,
nk,
conformity: Conformity::Proportional,
solve_opts: SolveOpts::default(),
priors,
},
)?;
Ok(Self {
builder,
fluid,
fvf,
})
}
pub fn from_wireframe(wf: &Wireframe, seed: WireframeSeed) -> Result<Self, SrsError> {
let builder = StaticModelBuilder::from_wireframe(
wf,
BuildOpts {
area_m2: seed.area_m2,
gross_height_m: seed.gross_height_m,
nk: seed.nk,
conformity: Conformity::Proportional,
solve_opts: SolveOpts::default(),
priors: seed.priors,
},
)?;
Ok(Self {
builder,
fluid: seed.fluid,
fvf: seed.fvf,
})
}
#[must_use]
pub fn with_logs(mut self, samples: Vec<PetroSample>) -> Self {
self.builder = self.builder.with_logs(samples);
self
}
pub fn add_top_control(&mut self, ip: usize, jp: usize, depth_m: f64) {
self.builder.add_top_control(ip, jp, depth_m);
}
pub fn solve(&self) -> Result<Refined, SrsError> {
let model = self.builder.build()?;
let res = model.in_place()?;
let in_place = match self.fluid {
Fluid::Oil => res.ooip_sm3(OilFvf::new(self.fvf)?),
Fluid::Gas => res.ogip_sm3(GasFvf::new(self.fvf)?),
};
Ok(Refined {
model,
in_place,
grv_mcm: res.grv_mcm(),
controls: self.builder.control_count(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn model_with_contact(contact_depth_m: f64) -> RefiningModel {
RefiningModel::new(
400_000.0,
15.0,
10,
10,
5,
1500.0,
contact_depth_m,
ConstantPriors {
porosity: 0.25,
net_to_gross: 0.8,
water_saturation: 0.3,
},
Fluid::Oil,
1.25,
)
.unwrap()
}
fn model() -> RefiningModel {
model_with_contact(1530.0)
}
#[test]
fn flat_model_matches_box_volume() {
let r = model().solve().unwrap();
let expected_grv_mcm = 6.0;
assert!((r.grv_mcm - expected_grv_mcm).abs() / expected_grv_mcm < 1e-6);
assert!(r.in_place > 0.0);
}
#[test]
fn adding_a_structural_high_changes_the_answer() {
let mut m = model_with_contact(1507.5); let before = m.solve().unwrap();
m.add_top_control(5, 5, 1494.0); let after = m.solve().unwrap();
assert!(after.controls == before.controls + 1);
assert!(
after.in_place > before.in_place,
"structural high should raise in-place: {} -> {}",
before.in_place,
after.in_place
);
}
#[test]
fn pure_dip_conserves_gross_volume() {
let mut m = model();
let flat = m.solve().unwrap();
m.add_top_control(5, 5, 1488.0);
let tilted = m.solve().unwrap();
assert!((tilted.grv_mcm - flat.grv_mcm).abs() / flat.grv_mcm < 1e-6);
}
use petekstatic::wireframe::{
Boundary, Contact, ContactKind, GriddedDepth, Hardness, Horizon, HorizonRole, Wireframe,
};
fn flat_wireframe(n: usize, depth_m: f64, owc_m: f64) -> Wireframe {
let depth = vec![depth_m; n * n];
Wireframe {
boundary: Boundary {
ring: vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]],
hardness: Hardness::Interpolated,
},
horizons: vec![Horizon {
name: "top".into(),
role: HorizonRole::Top,
surface: GriddedDepth {
ncol: n,
nrow: n,
depth_m: depth,
is_control: vec![true; n * n],
},
}]
.into(),
contacts: vec![Contact {
kind: ContactKind::Owc,
depth_m: owc_m,
hardness: Hardness::Hard,
}],
}
}
fn seed() -> WireframeSeed {
WireframeSeed {
area_m2: 400_000.0,
gross_height_m: 15.0,
nk: 5,
priors: ConstantPriors {
porosity: 0.25,
net_to_gross: 0.8,
water_saturation: 0.3,
},
fluid: Fluid::Oil,
fvf: 1.25,
}
}
#[test]
fn flat_wireframe_matches_box_volume() {
let wf = flat_wireframe(11, 1500.0, 1530.0);
let m = RefiningModel::from_wireframe(&wf, seed()).unwrap();
let r = m.solve().unwrap();
let expected_mcm = 6.0; assert!((r.grv_mcm - expected_mcm).abs() / expected_mcm < 1e-6);
assert!(r.in_place > 0.0);
assert_eq!(r.controls, 11 * 11);
}
#[test]
fn structural_high_in_the_wireframe_raises_in_place() {
let mut wf = flat_wireframe(11, 1500.0, 1507.5);
let flat = RefiningModel::from_wireframe(&wf, seed())
.unwrap()
.solve()
.unwrap();
std::sync::Arc::make_mut(&mut wf.horizons)[0]
.surface
.depth_m[5 * 11 + 5] = 1494.0;
let crest = RefiningModel::from_wireframe(&wf, seed())
.unwrap()
.solve()
.unwrap();
assert!(
crest.in_place > flat.in_place,
"structural high should raise in-place: {} -> {}",
flat.in_place,
crest.in_place
);
}
#[test]
fn logs_populate_cells_in_their_depth_range() {
let wf = flat_wireframe(11, 1500.0, 1530.0);
let samples: Vec<PetroSample> = (0..=75)
.map(|i| (1500.0 + 0.1 * f64::from(i), 0.30, 0.20)) .collect();
let r = RefiningModel::from_wireframe(&wf, seed())
.unwrap()
.with_logs(samples)
.solve()
.unwrap();
let poro = r.model.grid().properties().get("PORO").unwrap();
assert!(
poro.values.iter().any(|&v| (v - 0.30).abs() < 1e-9),
"log-driven cells"
);
assert!(
poro.values.iter().any(|&v| (v - 0.25).abs() < 1e-9),
"prior cells below logs"
);
let sw = r.model.grid().properties().get("SW").unwrap();
assert!(
sw.values.iter().any(|&v| (v - 0.20).abs() < 1e-9),
"log Sw applied"
);
}
#[test]
fn empty_logs_fall_back_to_priors() {
let wf = flat_wireframe(11, 1500.0, 1530.0);
let r = RefiningModel::from_wireframe(&wf, seed())
.unwrap()
.with_logs(vec![]) .solve()
.unwrap();
let poro = r.model.grid().properties().get("PORO").unwrap();
assert!(
poro.values.iter().all(|&v| (v - 0.25).abs() < 1e-9),
"all prior porosity"
);
}
#[test]
fn from_wireframe_needs_a_top_and_a_contact() {
let mut wf = flat_wireframe(4, 1500.0, 1530.0);
std::sync::Arc::make_mut(&mut wf.horizons).clear();
assert!(RefiningModel::from_wireframe(&wf, seed()).is_err());
let mut wf2 = flat_wireframe(4, 1500.0, 1530.0);
wf2.contacts.clear();
assert!(RefiningModel::from_wireframe(&wf2, seed()).is_err());
}
}