use crate::error::StaticError;
use crate::grid::{hexahedron_volume, Dims, Grid, Pillar, Point3};
use crate::volumetrics::{compute_clipped, CellSlab, Clip, SlabSource, NTG, PORO, SW};
use petektools::store::{Dtype, LaneSpec, Store, StoreSchema, StoreWriter};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
const ZCORN: &str = "ZCORN";
const COORD: &str = "COORD";
const COORD_ELEMS_PER_PILLAR: usize = 6;
pub fn unique_spill_path(dir: &Path) -> PathBuf {
static SEQ: AtomicU64 = AtomicU64::new(0);
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
dir.join(format!("petekstatic-spill-{pid}-{seq}-{nanos}.pts"))
}
fn cube_lane_names(grid: &Grid) -> Vec<String> {
let mut names: Vec<String> = grid.properties().names().map(str::to_owned).collect();
names.sort(); names
}
pub fn spill_grid(grid: &Grid, dir: &Path, cleanup: bool) -> Result<SpillBacking, StaticError> {
std::fs::create_dir_all(dir).map_err(|e| StaticError::Algo(e.into()))?;
let path = unique_spill_path(dir);
spill_grid_to(grid, &path, cleanup)
}
pub fn spill_grid_to(grid: &Grid, path: &Path, cleanup: bool) -> Result<SpillBacking, StaticError> {
let geom = grid.geometry();
if !geom.is_vertical() {
return Err(StaticError::InvalidInput(
"out-of-core spill supports vertical lattices only (every grid today)".into(),
));
}
let dims = grid.dims();
let per_slab_zcorn = dims.ni * dims.nj * 8;
let per_slab_cube = dims.ni * dims.nj;
let zcorn = geom.zcorn();
let cube_names = cube_lane_names(grid);
let cubes: Vec<(&str, &[f64])> = cube_names
.iter()
.map(|n| {
(
n.as_str(),
grid.properties()
.get(n)
.map(|p| p.values.as_slice())
.expect("named cube present"),
)
})
.collect();
spill_streaming(
path,
dims,
geom.coord(),
&cube_names,
cleanup,
|k, out| {
let z0 = k * per_slab_zcorn;
for (dst, src) in out.iter_mut().zip(&zcorn[z0..z0 + per_slab_zcorn]) {
*dst = *src as f32;
}
Ok(())
},
|name, k, out| {
let vals = cubes
.iter()
.find(|(n, _)| *n == name)
.map(|(_, v)| *v)
.expect("named cube present");
let c0 = k * per_slab_cube;
for (dst, src) in out.iter_mut().zip(&vals[c0..c0 + per_slab_cube]) {
*dst = *src as f32;
}
Ok(())
},
)
}
fn store_schema(dims: Dims, pillar_count: usize, cube_names: &[String]) -> StoreSchema {
let per_slab_zcorn = dims.ni * dims.nj * 8;
let per_slab_cube = dims.ni * dims.nj;
let mut lanes = vec![
LaneSpec::slab(ZCORN, Dtype::F32, per_slab_zcorn as u64),
LaneSpec::flat(
COORD,
Dtype::F64,
(pillar_count * COORD_ELEMS_PER_PILLAR) as u64,
),
];
for name in cube_names {
lanes.push(LaneSpec::slab(name, Dtype::F32, per_slab_cube as u64));
}
let app = serde_json::json!({
"kind": "petekstatic-static-model",
"ni": dims.ni, "nj": dims.nj, "nk": dims.nk,
"cubes": cube_names,
});
StoreSchema::new(dims.nk as u64, lanes).with_app(app)
}
#[allow(clippy::too_many_arguments)]
pub fn spill_streaming(
path: &Path,
dims: Dims,
coord: &[Pillar],
cube_names: &[String],
cleanup: bool,
mut fill_zcorn: impl FnMut(usize, &mut [f32]) -> Result<(), StaticError>,
mut fill_cube: impl FnMut(&str, usize, &mut [f32]) -> Result<(), StaticError>,
) -> Result<SpillBacking, StaticError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| StaticError::Algo(e.into()))?;
}
let schema = store_schema(dims, coord.len(), cube_names);
let mut w = StoreWriter::create(path, schema)?;
{
let flat = w.flat_mut_f64(COORD)?;
for (p, slot) in coord
.iter()
.zip(flat.chunks_exact_mut(COORD_ELEMS_PER_PILLAR))
{
slot.copy_from_slice(&[
p.top.x, p.top.y, p.top.z, p.bottom.x, p.bottom.y, p.bottom.z,
]);
}
}
for k in 0..dims.nk {
fill_zcorn(k, w.slab_mut_f32(ZCORN, k as u64)?)?;
for name in cube_names {
fill_cube(name, k, w.slab_mut_f32(name, k as u64)?)?;
}
}
w.finalize()?;
let store = petektools::store::open(path)?;
Ok(SpillBacking {
store,
path: path.to_path_buf(),
cleanup,
dims,
coord: coord.to_vec(),
cube_names: cube_names.to_vec(),
})
}
#[derive(Debug)]
pub struct SpillBacking {
store: Store,
path: PathBuf,
cleanup: bool,
dims: Dims,
coord: Vec<Pillar>,
cube_names: Vec<String>,
}
impl Drop for SpillBacking {
fn drop(&mut self) {
if self.cleanup {
let _ = std::fs::remove_file(&self.path);
}
}
}
impl SpillBacking {
#[must_use]
pub fn dims(&self) -> Dims {
self.dims
}
#[must_use]
pub fn store_path(&self) -> &Path {
&self.path
}
pub fn detach(&mut self) {
self.cleanup = false;
}
#[must_use]
pub fn cube_names(&self) -> &[String] {
&self.cube_names
}
fn zcorn_slab(&self, k: usize) -> Result<&[f32], StaticError> {
Ok(self.store.slab_f32(ZCORN, k as u64)?)
}
fn cube_slab_opt(&self, name: &str, k: usize) -> Result<Option<&[f32]>, StaticError> {
if self.cube_names.iter().any(|n| n == name) {
Ok(Some(self.store.slab_f32(name, k as u64)?))
} else {
Ok(None)
}
}
#[must_use]
pub fn source(&self) -> SpillSource<'_> {
SpillSource { backing: self }
}
pub fn bulk_volume(&self) -> Result<f64, StaticError> {
Ok(compute_clipped(&self.source(), Clip::Bulk, 0..self.dims.nk, false)?.grv_m3)
}
pub fn to_in_core_grid(&self) -> Result<Grid, StaticError> {
let dims = self.dims;
let mut grid = crate::grid::build_box(crate::grid::BoxSpec::square(
1.0,
1.0,
Dims::new(1, 1, 1).expect("1x1x1 dims are valid"),
))?;
let mut zcorn = Vec::with_capacity(dims.cell_count() * 8);
for k in 0..dims.nk {
zcorn.extend(self.zcorn_slab(k)?.iter().map(|&z| z as f64));
}
grid.install_geometry(dims, self.coord.clone(), zcorn);
for name in self.cube_names().to_vec() {
let mut values = Vec::with_capacity(dims.cell_count());
for k in 0..dims.nk {
let slab = self
.cube_slab_opt(&name, k)?
.expect("cube listed in cube_names is spilled");
values.extend(slab.iter().map(|&v| v as f64));
}
grid.properties_mut()
.set(crate::grid::Property { name, values })?;
}
Ok(grid)
}
}
pub struct SpillSource<'a> {
backing: &'a SpillBacking,
}
impl SlabSource for SpillSource<'_> {
fn dims(&self) -> Dims {
self.backing.dims
}
fn require_cubes(&self) -> Result<(), StaticError> {
for name in [PORO, NTG, SW] {
if !self.backing.cube_names.iter().any(|n| n == name) {
return Err(StaticError::InvalidInput(format!(
"spilled grid is missing required property '{name}'"
)));
}
}
Ok(())
}
type Slab<'s>
= SpillSlab<'s>
where
Self: 's;
fn slab(&self, k: usize) -> Result<SpillSlab<'_>, StaticError> {
Ok(SpillSlab {
zslab: self.backing.zcorn_slab(k)?,
poro: self.backing.cube_slab_opt(PORO, k)?,
ntg: self.backing.cube_slab_opt(NTG, k)?,
sw: self.backing.cube_slab_opt(SW, k)?,
coord: &self.backing.coord,
dims: self.backing.dims,
})
}
}
pub struct SpillSlab<'a> {
zslab: &'a [f32],
poro: Option<&'a [f32]>,
ntg: Option<&'a [f32]>,
sw: Option<&'a [f32]>,
coord: &'a [Pillar],
dims: Dims,
}
impl SpillSlab<'_> {
#[inline]
fn corners(&self, local: usize) -> [Point3; 8] {
let ni = self.dims.ni;
let j = local / ni;
let i = local % ni;
let z = &self.zslab[local * 8..local * 8 + 8];
let mut corners = [Point3::new(0.0, 0.0, 0.0); 8];
for (idx, slot) in corners.iter_mut().enumerate() {
let di = idx & 1;
let dj = (idx >> 1) & 1;
let top = self.coord[self.dims.pillar_linear(i + di, j + dj)].top;
*slot = Point3::new(top.x, top.y, z[idx] as f64);
}
corners
}
}
impl CellSlab for SpillSlab<'_> {
#[inline]
fn centroid_z(&self, local: usize) -> f64 {
let z = &self.zslab[local * 8..local * 8 + 8];
let s: f64 = z.iter().map(|&v| v as f64).sum();
s / 8.0
}
#[inline]
fn cell_volume(&self, local: usize) -> f64 {
hexahedron_volume(&self.corners(local))
}
#[inline]
fn poro(&self, local: usize) -> f64 {
self.poro.expect("require_cubes verified PORO")[local] as f64
}
#[inline]
fn ntg(&self, local: usize) -> f64 {
self.ntg.expect("require_cubes verified NTG")[local] as f64
}
#[inline]
fn sw(&self, local: usize) -> f64 {
self.sw.expect("require_cubes verified SW")[local] as f64
}
}