use crate::error::StaticError;
use crate::model::draw::RealizationDraw;
use crate::model::model::StaticModel;
use crate::model::template::StaticModelTemplate;
use crate::spill::{spill_grid_to, unique_spill_path};
use crate::volumetrics::{GasFvf, OilFvf};
use petektools::sampling::{
aggregate, reservoir_summary, seeded_rng, Clamped, Correlation, ReservoirSummary, Sampler,
};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(default)]
pub struct McSettings {
pub n: usize,
pub seed: u64,
pub workers: usize,
pub spill_dir: Option<PathBuf>,
}
impl Default for McSettings {
fn default() -> Self {
Self {
n: 1,
seed: 0,
workers: 1,
spill_dir: None,
}
}
}
impl McSettings {
#[must_use]
pub fn new(n: usize, seed: u64) -> Self {
Self {
n,
seed,
..Self::default()
}
}
#[must_use]
pub fn with_workers(mut self, workers: usize) -> Self {
self.workers = workers;
self
}
#[must_use]
pub fn with_spill_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.spill_dir = Some(dir.into());
self
}
}
pub fn run_mc(
tmpl: &mut StaticModelTemplate,
inputs: &McInputs,
settings: &McSettings,
) -> Result<McResult, StaticError> {
let realized = inputs.realize(settings.n, settings.seed)?;
let n = settings.n;
let dir = settings.spill_dir.as_deref();
let workers = settings.workers.clamp(1, n);
if workers == 1 {
let triples = run_draws_impl(tmpl, &realized, 0..n, dir)?;
return Ok(assemble(triples, realized));
}
let _ = run_draws_impl(tmpl, &realized, 0..1, dir)?;
let ranges = shard_ranges(n, workers);
let realized_ref = &realized;
let template_ref = &*tmpl;
let shards: Result<Vec<Vec<OutTriple>>, StaticError> = ranges
.into_par_iter()
.map(|r| {
let mut t = template_ref.clone();
run_draws_impl(&mut t, realized_ref, r, dir)
})
.collect();
let triples: Vec<OutTriple> = shards?.into_iter().flatten().collect();
Ok(assemble(triples, realized))
}
type OutTriple = (f64, f64, f64);
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Input {
Plain(Sampler),
Clamped(Clamped),
}
impl Input {
#[must_use]
pub fn plain(sampler: Sampler) -> Self {
Input::Plain(sampler)
}
pub fn clamped(sampler: Sampler, lo: f64, hi: f64) -> Result<Self, StaticError> {
Ok(Input::Clamped(sampler.clamped(lo, hi)?))
}
fn sample_n<R: rand::Rng>(&self, n: usize, rng: &mut R) -> Vec<f64> {
match self {
Input::Plain(s) => s.sample_n(n, rng),
Input::Clamped(c) => c.sample_n(n, rng),
}
}
}
impl From<Sampler> for Input {
fn from(s: Sampler) -> Self {
Input::Plain(s)
}
}
impl From<Clamped> for Input {
fn from(c: Clamped) -> Self {
Input::Clamped(c)
}
}
#[derive(Debug, Clone)]
pub struct McInputs {
pub area_m2: Input,
pub gross_height_m: Input,
pub contact_depth_m: Input,
pub goc_depth_m: Option<Input>,
pub porosity: Input,
pub net_to_gross: Input,
pub water_saturation: Input,
pub sw_gas: Option<Input>,
pub boi: Input,
pub bgi: Option<Input>,
pub property_shifts: Vec<(String, Input)>,
}
impl McInputs {
#[must_use]
pub fn new(
area_m2: Input,
gross_height_m: Input,
contact_depth_m: Input,
porosity: Input,
net_to_gross: Input,
water_saturation: Input,
boi: Input,
) -> Self {
Self {
area_m2,
gross_height_m,
contact_depth_m,
goc_depth_m: None,
porosity,
net_to_gross,
water_saturation,
sw_gas: None,
boi,
bgi: None,
property_shifts: Vec::new(),
}
}
#[must_use]
pub fn with_goc(mut self, goc_depth_m: Input) -> Self {
self.goc_depth_m = Some(goc_depth_m);
self
}
#[must_use]
pub fn with_sw_gas(mut self, sw_gas: Input) -> Self {
self.sw_gas = Some(sw_gas);
self
}
#[must_use]
pub fn with_bgi(mut self, bgi: Input) -> Self {
self.bgi = Some(bgi);
self
}
#[must_use]
pub fn with_property_shift(mut self, property: impl Into<String>, shift: Input) -> Self {
let property = property.into();
self.property_shifts.retain(|(p, _)| p != &property);
self.property_shifts.push((property, shift));
self
}
pub fn realize(&self, n: usize, seed: u64) -> Result<RealizedInputs, StaticError> {
if n == 0 {
return Err(StaticError::InvalidInput(
"structured MC needs at least one draw (n = 0)".into(),
));
}
let mut rng = seeded_rng(seed);
let area_m2 = self.area_m2.sample_n(n, &mut rng);
let gross_height_m = self.gross_height_m.sample_n(n, &mut rng);
let contact_depth_m = self.contact_depth_m.sample_n(n, &mut rng);
let goc_depth_m = self.goc_depth_m.as_ref().map(|s| s.sample_n(n, &mut rng));
let porosity = self.porosity.sample_n(n, &mut rng);
let net_to_gross = self.net_to_gross.sample_n(n, &mut rng);
let water_saturation = self.water_saturation.sample_n(n, &mut rng);
let sw_gas = self.sw_gas.as_ref().map(|s| s.sample_n(n, &mut rng));
let boi = self.boi.sample_n(n, &mut rng);
let bgi = self.bgi.as_ref().map(|s| s.sample_n(n, &mut rng));
let property_shifts = self
.property_shifts
.iter()
.map(|(name, s)| (name.clone(), s.sample_n(n, &mut rng)))
.collect();
Ok(RealizedInputs {
area_m2,
gross_height_m,
contact_depth_m,
goc_depth_m,
porosity,
net_to_gross,
water_saturation,
sw_gas,
boi,
bgi,
property_shifts,
seed,
})
}
}
#[derive(Debug, Clone)]
pub struct RealizedInputs {
pub area_m2: Vec<f64>,
pub gross_height_m: Vec<f64>,
pub contact_depth_m: Vec<f64>,
pub goc_depth_m: Option<Vec<f64>>,
pub porosity: Vec<f64>,
pub net_to_gross: Vec<f64>,
pub water_saturation: Vec<f64>,
pub sw_gas: Option<Vec<f64>>,
pub boi: Vec<f64>,
pub bgi: Option<Vec<f64>>,
pub property_shifts: Vec<(String, Vec<f64>)>,
seed: u64,
}
impl RealizedInputs {
#[must_use]
pub fn len(&self) -> usize {
self.area_m2.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.area_m2.is_empty()
}
fn draw_at(&self, i: usize) -> RealizationDraw {
let mut d = RealizationDraw::new(
self.area_m2[i],
self.gross_height_m[i],
self.contact_depth_m[i],
self.porosity[i],
self.net_to_gross[i],
self.water_saturation[i],
self.seed.wrapping_add(i as u64),
);
if let Some(g) = &self.goc_depth_m {
d = d.with_goc(g[i]);
}
if let Some(s) = &self.sw_gas {
d = d.with_sw_gas(s[i]);
}
for (name, deltas) in &self.property_shifts {
d = d.with_property_shift(name.clone(), deltas[i]);
}
d
}
}
#[derive(Debug, Clone)]
pub struct McResult {
pub oil_sm3: Vec<f64>,
pub gas_sm3: Vec<f64>,
pub grv_m3: Vec<f64>,
realized: RealizedInputs,
}
impl McResult {
#[must_use]
pub fn len(&self) -> usize {
self.oil_sm3.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.oil_sm3.is_empty()
}
#[must_use]
pub fn realized_inputs(&self) -> &RealizedInputs {
&self.realized
}
pub fn summary(&self) -> Result<ReservoirSummary, StaticError> {
Ok(reservoir_summary(&self.oil_sm3)?)
}
pub fn gas_summary(&self) -> Result<ReservoirSummary, StaticError> {
Ok(reservoir_summary(&self.gas_sm3)?)
}
pub fn grv_summary(&self) -> Result<ReservoirSummary, StaticError> {
Ok(reservoir_summary(&self.grv_m3)?)
}
}
#[deprecated(since = "0.1.0", note = "use `run_mc` with `McSettings::new(n, seed)`")]
pub fn run_structured_mc(
tmpl: &mut StaticModelTemplate,
inputs: &McInputs,
n: usize,
seed: u64,
) -> Result<McResult, StaticError> {
run_mc(tmpl, inputs, &McSettings::new(n, seed))
}
#[must_use]
pub fn default_mc_workers() -> usize {
let cores = std::thread::available_parallelism()
.map(std::num::NonZeroUsize::get)
.unwrap_or(1);
cores.clamp(1, 6)
}
#[deprecated(
since = "0.1.0",
note = "use `run_mc` with `McSettings::new(n, seed).with_workers(workers)`"
)]
pub fn run_structured_mc_parallel(
tmpl: &mut StaticModelTemplate,
inputs: &McInputs,
n: usize,
seed: u64,
workers: usize,
) -> Result<McResult, StaticError> {
run_mc(
tmpl,
inputs,
&McSettings::new(n, seed).with_workers(workers),
)
}
#[deprecated(
since = "0.1.0",
note = "use `run_mc` with `McSettings::new(n, seed).with_spill_dir(dir)` \
(pass `std::env::temp_dir()` for the old `None` behaviour)"
)]
pub fn run_structured_mc_spilled(
tmpl: &mut StaticModelTemplate,
inputs: &McInputs,
n: usize,
seed: u64,
spill_dir: Option<PathBuf>,
) -> Result<McResult, StaticError> {
let dir = spill_dir.unwrap_or_else(std::env::temp_dir);
run_mc(tmpl, inputs, &McSettings::new(n, seed).with_spill_dir(dir))
}
#[deprecated(
since = "0.1.0",
note = "use `run_mc` with `McSettings::new(n, seed).with_workers(workers).with_spill_dir(dir)`"
)]
pub fn run_structured_mc_parallel_spilled(
tmpl: &mut StaticModelTemplate,
inputs: &McInputs,
n: usize,
seed: u64,
workers: usize,
spill_dir: Option<PathBuf>,
) -> Result<McResult, StaticError> {
let dir = spill_dir.unwrap_or_else(std::env::temp_dir);
run_mc(
tmpl,
inputs,
&McSettings::new(n, seed)
.with_workers(workers)
.with_spill_dir(dir),
)
}
fn shard_ranges(n: usize, workers: usize) -> Vec<std::ops::Range<usize>> {
let base = n / workers;
let rem = n % workers;
let mut ranges = Vec::with_capacity(workers);
let mut start = 0;
for w in 0..workers {
let len = base + usize::from(w < rem);
ranges.push(start..start + len);
start += len;
}
ranges
}
fn run_draws_impl(
tmpl: &mut StaticModelTemplate,
realized: &RealizedInputs,
range: std::ops::Range<usize>,
spill_dir: Option<&Path>,
) -> Result<Vec<OutTriple>, StaticError> {
let mut out = Vec::with_capacity(range.len());
let mut model = tmpl.reusable_model();
let shard_store: Option<PathBuf> = spill_dir.map(unique_spill_path);
for i in range {
let draw = realized.draw_at(i);
tmpl.realize_into(&draw, &mut model)
.map_err(|e| at_draw(i, e))?;
let boi = realized.boi[i];
let bgi = realized.bgi.as_ref().map(|b| b[i]);
let triple = match &shard_store {
None => outputs(&model, boi, bgi).map_err(|e| at_draw(i, e))?,
Some(path) => {
let backing =
spill_grid_to(model.grid(), path, false).map_err(|e| at_draw(i, e))?;
model.set_spill(Some(Arc::new(backing)));
let triple = outputs(&model, boi, bgi).map_err(|e| at_draw(i, e));
model.set_spill(None);
triple?
}
};
out.push(triple);
}
if let Some(path) = &shard_store {
let _ = std::fs::remove_file(path); }
Ok(out)
}
fn assemble(triples: Vec<OutTriple>, realized: RealizedInputs) -> McResult {
let n = triples.len();
let mut oil_sm3 = Vec::with_capacity(n);
let mut gas_sm3 = Vec::with_capacity(n);
let mut grv_m3 = Vec::with_capacity(n);
for (oil, gas, grv) in triples {
oil_sm3.push(oil);
gas_sm3.push(gas);
grv_m3.push(grv);
}
McResult {
oil_sm3,
gas_sm3,
grv_m3,
realized,
}
}
#[must_use]
pub fn aggregate_field(segments: &[&McResult], corr: Correlation) -> Vec<f64> {
let vecs: Vec<&[f64]> = segments.iter().map(|s| s.oil_sm3.as_slice()).collect();
aggregate(&vecs, corr)
}
pub(crate) fn outputs(
model: &StaticModel,
boi: f64,
bgi: Option<f64>,
) -> Result<(f64, f64, f64), StaticError> {
let ip = model.in_place_summary()?;
let two_contact = ip.gas.is_some() && ip.oil.is_some();
let oil = if two_contact {
ip.oil_zone_ooip_sm3(OilFvf::new(boi)?)
} else {
ip.ooip_sm3(OilFvf::new(boi)?)
};
let gas = match (two_contact, bgi) {
(true, Some(bgi)) => ip.gas_zone_ogip_sm3(GasFvf::new(bgi)?),
_ => 0.0,
};
Ok((oil, gas, ip.grv_m3))
}
fn at_draw(index: usize, source: StaticError) -> StaticError {
StaticError::McDraw {
index,
source: Box::new(source),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gridder::{Conformity, SolveOpts};
use crate::model::{
BuildOpts, ConstantPriors, Gaussian, McMode, PropertyPipeline, UpscaleMethod, WellLog,
};
use crate::wireframe::{
Boundary, Contact, ContactKind, GriddedDepth, Hardness, Horizon, HorizonRole, Wireframe,
};
fn flat_wireframe(n: usize, depth_m: f64, owc_m: f64) -> Wireframe {
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: std::sync::Arc::new(vec![Horizon {
name: "top".into(),
role: HorizonRole::Top,
surface: GriddedDepth {
ncol: n,
nrow: n,
depth_m: vec![depth_m; n * n],
is_control: vec![true; n * n],
},
}]),
contacts: vec![Contact {
kind: ContactKind::Owc,
depth_m: owc_m,
hardness: Hardness::Hard,
}],
}
}
fn opts() -> BuildOpts {
BuildOpts {
area_m2: 100.0,
gross_height_m: 50.0,
nk: 5,
conformity: Conformity::Proportional,
solve_opts: SolveOpts::default(),
priors: ConstantPriors {
porosity: 0.25,
net_to_gross: 0.8,
water_saturation: 0.3,
},
}
}
fn tri(min: f64, mode: f64, max: f64) -> Input {
Input::plain(Sampler::new_triangular(min, mode, max).unwrap())
}
fn nominal_inputs() -> McInputs {
McInputs::new(
tri(90.0, 100.0, 110.0), tri(45.0, 50.0, 55.0), tri(5020.0, 5025.0, 5030.0), tri(0.18, 0.22, 0.26), tri(0.70, 0.80, 0.90), tri(0.25, 0.30, 0.35), tri(1.20, 1.30, 1.45), )
}
#[test]
fn run_is_bit_reproducible() {
let wf = flat_wireframe(11, 5000.0, 5025.0);
let inputs = nominal_inputs();
let mut t1 = StaticModelTemplate::new(&wf, opts()).unwrap();
let mut t2 = StaticModelTemplate::new(&wf, opts()).unwrap();
let a = run_mc(&mut t1, &inputs, &McSettings::new(200, 42)).unwrap();
let b = run_mc(&mut t2, &inputs, &McSettings::new(200, 42)).unwrap();
assert_eq!(a.oil_sm3, b.oil_sm3, "oil vector not bit-reproducible");
assert_eq!(a.gas_sm3, b.gas_sm3);
assert_eq!(a.grv_m3, b.grv_m3);
let c = run_mc(&mut t1, &inputs, &McSettings::new(200, 43)).unwrap();
assert_ne!(a.oil_sm3, c.oil_sm3);
}
#[test]
fn sharded_mc_matches_serial_and_is_worker_invariant() {
let wf = flat_wireframe(11, 5000.0, 5025.0);
let inputs = nominal_inputs();
let (n, seed) = (250usize, 42u64);
let mut ts = StaticModelTemplate::new(&wf, opts()).unwrap();
let serial = run_mc(&mut ts, &inputs, &McSettings::new(n, seed)).unwrap();
for workers in [1usize, 2, 3, 5, 8] {
let mut t = StaticModelTemplate::new(&wf, opts()).unwrap();
let par = run_mc(
&mut t,
&inputs,
&McSettings::new(n, seed).with_workers(workers),
)
.unwrap();
assert_eq!(par.len(), n, "workers={workers} length");
assert_eq!(
par.oil_sm3, serial.oil_sm3,
"workers={workers}: oil vector diverged from serial"
);
assert_eq!(par.gas_sm3, serial.gas_sm3, "workers={workers}: gas");
assert_eq!(par.grv_m3, serial.grv_m3, "workers={workers}: grv");
}
let mut ta = StaticModelTemplate::new(&wf, opts()).unwrap();
let mut tb = StaticModelTemplate::new(&wf, opts()).unwrap();
let a = run_mc(&mut ta, &inputs, &McSettings::new(n, seed).with_workers(4)).unwrap();
let b = run_mc(&mut tb, &inputs, &McSettings::new(n, seed).with_workers(4)).unwrap();
assert_eq!(
a.oil_sm3, b.oil_sm3,
"same (seed,n,workers) not reproducible"
);
let mut tc = StaticModelTemplate::new(&wf, opts()).unwrap();
let over = run_mc(
&mut tc,
&inputs,
&McSettings::new(n, seed).with_workers(10_000),
)
.unwrap();
assert_eq!(over.oil_sm3, serial.oil_sm3, "over-large worker count");
}
#[test]
fn result_vectors_have_length_n() {
let wf = flat_wireframe(11, 5000.0, 5025.0);
let mut t = StaticModelTemplate::new(&wf, opts()).unwrap();
let r = run_mc(&mut t, &nominal_inputs(), &McSettings::new(137, 1)).unwrap();
assert_eq!(r.len(), 137);
assert_eq!(r.oil_sm3.len(), 137);
assert_eq!(r.gas_sm3.len(), 137);
assert_eq!(r.grv_m3.len(), 137);
assert!(r.oil_sm3.iter().all(|v| v.is_finite() && *v > 0.0));
let s = r.summary().unwrap();
assert!(s.p90 <= s.p50 && s.p50 <= s.p10, "P-curve ordered: {s:?}");
}
#[test]
fn clamped_garbage_sampler_never_aborts() {
let wild = |mean: f64, sd: f64, lo: f64, hi: f64| {
Input::clamped(Sampler::new_normal(mean, sd).unwrap(), lo, hi).unwrap()
};
let inputs = McInputs::new(
wild(100.0, 50.0, 10.0, 200.0), wild(50.0, 40.0, 5.0, 120.0), tri(5020.0, 5025.0, 5030.0), wild(0.20, 0.30, 0.02, 0.35), wild(0.60, 0.40, 0.05, 0.95), wild(0.30, 0.30, 0.02, 0.60), wild(1.30, 0.50, 1.05, 1.80), );
let wf = flat_wireframe(11, 5000.0, 5025.0);
let mut t = StaticModelTemplate::new(&wf, opts()).unwrap();
let r = run_mc(&mut t, &inputs, &McSettings::new(1000, 7)).unwrap();
assert_eq!(r.len(), 1000);
assert!(r.oil_sm3.iter().all(|v| v.is_finite() && *v > 0.0));
}
#[test]
fn two_contact_run_produces_gas_and_oil_vectors() {
let wf = flat_wireframe(11, 5000.0, 5040.0);
let inputs = McInputs::new(
tri(90.0, 100.0, 110.0),
tri(45.0, 50.0, 55.0),
tri(5038.0, 5040.0, 5042.0), tri(0.18, 0.22, 0.26),
tri(0.70, 0.80, 0.90),
tri(0.25, 0.30, 0.35),
tri(1.20, 1.30, 1.45),
)
.with_goc(tri(5018.0, 5020.0, 5022.0))
.with_bgi(tri(0.0035, 0.004, 0.0045));
let mut t = StaticModelTemplate::new(&wf, opts()).unwrap();
let r = run_mc(&mut t, &inputs, &McSettings::new(100, 11)).unwrap();
assert!(r.oil_sm3.iter().all(|v| *v > 0.0), "oil leg positive");
assert!(r.gas_sm3.iter().all(|v| *v > 0.0), "gas cap positive");
}
#[test]
fn level_shift_and_resimulate_both_drive_through_the_loop() {
use petektools::{Variogram, VariogramModel};
let wf = flat_wireframe(11, 5000.0, 5025.0);
let poro = |seed: u64| {
let low = WellLog::new(
0.5,
0.5,
vec![
(5005.0, 0.10),
(5015.0, 0.12),
(5025.0, 0.14),
(5035.0, 0.16),
(5045.0, 0.18),
],
);
let high = WellLog::new(
9.5,
9.5,
vec![
(5005.0, 0.26),
(5015.0, 0.25),
(5025.0, 0.24),
(5035.0, 0.23),
(5045.0, 0.22),
],
);
let vgm = Variogram::new(VariogramModel::Spherical, 0.0, 1.0, 5.0).unwrap();
PropertyPipeline::new("PORO")
.upscale(vec![low, high], UpscaleMethod::Arithmetic)
.propagate(Gaussian::new(vgm, seed))
};
let mut tl = StaticModelTemplate::new(&wf, opts())
.unwrap()
.with_property(poro(42));
let ls_inputs = nominal_inputs().with_property_shift("PORO", tri(-0.02, 0.0, 0.02));
let realized = ls_inputs.realize(100, 3).unwrap();
assert!(
(realized.draw_at(0).property_shift("PORO") - realized.property_shifts[0].1[0]).abs()
< 1e-15,
"the sampled PORO shift must reach the draw"
);
let ls = run_mc(&mut tl, &ls_inputs, &McSettings::new(100, 3)).unwrap();
assert_eq!(ls.len(), 100);
assert!(ls.oil_sm3.iter().all(|v| v.is_finite() && *v > 0.0));
let mut tr = StaticModelTemplate::new(&wf, opts())
.unwrap()
.with_property_mode(poro(42), McMode::Resimulate);
let rs = run_mc(&mut tr, &nominal_inputs(), &McSettings::new(100, 3)).unwrap();
assert_eq!(rs.len(), 100);
assert!(rs.oil_sm3.iter().all(|v| v.is_finite() && *v > 0.0));
assert_ne!(ls.oil_sm3, rs.oil_sm3);
}
#[test]
fn aggregation_brackets_are_ordered_sensibly() {
let wf = flat_wireframe(11, 5000.0, 5025.0);
let mut t1 = StaticModelTemplate::new(&wf, opts()).unwrap();
let mut t2 = StaticModelTemplate::new(&wf, opts()).unwrap();
let seg1 = run_mc(&mut t1, &nominal_inputs(), &McSettings::new(500, 101)).unwrap();
let seg2 = run_mc(&mut t2, &nominal_inputs(), &McSettings::new(500, 202)).unwrap();
let ind = aggregate_field(&[&seg1, &seg2], Correlation::Independent);
let com = aggregate_field(&[&seg1, &seg2], Correlation::Comonotonic);
let s_ind = reservoir_summary(&ind).unwrap();
let s_com = reservoir_summary(&com).unwrap();
assert!((s_ind.mean - s_com.mean).abs() < 1e-6, "means must match");
assert!(
s_com.p90 <= s_ind.p90 + 1e-9,
"comonotonic P90 not <= independent"
);
assert!(
s_com.p10 >= s_ind.p10 - 1e-9,
"comonotonic P10 not >= independent"
);
}
#[test]
fn n_zero_is_a_typed_error() {
let wf = flat_wireframe(9, 5000.0, 5025.0);
let mut t = StaticModelTemplate::new(&wf, opts()).unwrap();
let err = run_mc(&mut t, &nominal_inputs(), &McSettings::new(0, 1)).unwrap_err();
assert!(matches!(err, StaticError::InvalidInput(_)));
}
#[test]
fn bad_draw_surfaces_as_typed_mcdraw_with_index() {
let inputs = McInputs::new(
tri(90.0, 100.0, 110.0),
tri(45.0, 50.0, 55.0),
tri(5020.0, 5025.0, 5030.0),
Input::plain(Sampler::new_uniform(0.9, 1.5).unwrap()), tri(0.70, 0.80, 0.90),
tri(0.25, 0.30, 0.35),
tri(1.20, 1.30, 1.45),
);
let wf = flat_wireframe(9, 5000.0, 5025.0);
let mut t = StaticModelTemplate::new(&wf, opts()).unwrap();
let err = run_mc(&mut t, &inputs, &McSettings::new(500, 5)).unwrap_err();
match err {
StaticError::McDraw { index, source } => {
assert!(index < 500, "index in range");
assert!(matches!(*source, StaticError::InvalidInput(_)));
}
other => panic!("expected McDraw, got {other}"),
}
}
#[test]
fn smoke_10k_draws_with_timing() {
let wf = flat_wireframe(11, 5000.0, 5025.0);
let mut t = StaticModelTemplate::new(&wf, opts()).unwrap();
let start = std::time::Instant::now();
let r = run_mc(&mut t, &nominal_inputs(), &McSettings::new(10_000, 2024)).unwrap();
let dt = start.elapsed();
assert_eq!(r.len(), 10_000);
let s = r.summary().unwrap();
assert!(s.p90 <= s.p50 && s.p50 <= s.p10);
eprintln!(
"[mc smoke] 10k draws (11x11x5) in {:.3?} ({:.1} us/draw); oil P90/P50/P10 = {:.3}/{:.3}/{:.3} Sm3",
dt,
dt.as_secs_f64() * 1e6 / 10_000.0,
s.p90,
s.p50,
s.p10
);
}
#[test]
#[allow(deprecated)]
fn deprecated_wrappers_match_run_mc() {
let wf = flat_wireframe(11, 5000.0, 5025.0);
let inputs = nominal_inputs();
let (n, seed) = (60usize, 9u64);
let dir = std::env::temp_dir();
let mut a = StaticModelTemplate::new(&wf, opts()).unwrap();
let mut b = StaticModelTemplate::new(&wf, opts()).unwrap();
let new = run_mc(&mut a, &inputs, &McSettings::new(n, seed)).unwrap();
let old = run_structured_mc(&mut b, &inputs, n, seed).unwrap();
assert_eq!(new.oil_sm3, old.oil_sm3, "serial wrapper");
let mut a = StaticModelTemplate::new(&wf, opts()).unwrap();
let mut b = StaticModelTemplate::new(&wf, opts()).unwrap();
let new = run_mc(&mut a, &inputs, &McSettings::new(n, seed).with_workers(3)).unwrap();
let old = run_structured_mc_parallel(&mut b, &inputs, n, seed, 3).unwrap();
assert_eq!(new.oil_sm3, old.oil_sm3, "parallel wrapper");
let mut a = StaticModelTemplate::new(&wf, opts()).unwrap();
let mut b = StaticModelTemplate::new(&wf, opts()).unwrap();
let new = run_mc(
&mut a,
&inputs,
&McSettings::new(n, seed).with_spill_dir(dir.clone()),
)
.unwrap();
let old = run_structured_mc_spilled(&mut b, &inputs, n, seed, None).unwrap();
assert_eq!(new.oil_sm3, old.oil_sm3, "spilled wrapper");
let mut a = StaticModelTemplate::new(&wf, opts()).unwrap();
let mut b = StaticModelTemplate::new(&wf, opts()).unwrap();
let new = run_mc(
&mut a,
&inputs,
&McSettings::new(n, seed).with_workers(2).with_spill_dir(dir),
)
.unwrap();
let old = run_structured_mc_parallel_spilled(&mut b, &inputs, n, seed, 2, None).unwrap();
assert_eq!(new.oil_sm3, old.oil_sm3, "parallel-spilled wrapper");
}
}