use crate::{
core::{
observer::{Observer, SnapshotSink},
state::{FieldHandle, State},
storage::StorageBackend,
},
geometry::{
cartesian::{CartesianGrid, for_each_interior},
grid::{BlockId, Grid},
},
io::ObserverError,
};
use parquet::{
basic::Compression,
data_type::{DoubleType, Int64Type},
file::{properties::WriterProperties, writer::SerializedFileWriter},
schema::parser::parse_message_type,
};
use std::{fs::File, path::PathBuf, sync::Arc};
const COORD_NAMES: [&str; 3] = ["x", "y", "z"];
const ROWS_PER_GROUP: usize = 1 << 22;
pub struct ParquetObserver<const D: usize = 2> {
grid: CartesianGrid<D>,
dynamic: Vec<(&'static str, FieldHandle<f64>)>,
statics: Vec<(&'static str, FieldHandle<f64>)>,
every: u64,
dir: PathBuf,
props: Arc<WriterProperties>,
epoch: u64,
static_written: bool,
}
impl<const D: usize> ParquetObserver<D> {
pub fn new(
grid: CartesianGrid<D>,
fields: Vec<(&'static str, FieldHandle<f64>)>,
every: u64,
dir: impl Into<PathBuf>,
) -> Result<Self, ObserverError> {
if !(D >= 1 && D <= 3) {
return Err(ObserverError::InvalidDimension(D));
}
if fields.is_empty() {
return Err(ObserverError::NoValidFields);
}
let dir = dir.into();
std::fs::create_dir_all(&dir)?;
Ok(Self {
grid,
dynamic: fields,
statics: Vec::new(),
every,
dir,
props: Arc::new(
WriterProperties::builder()
.set_compression(Compression::SNAPPY)
.build(),
),
epoch: 0,
static_written: false,
})
}
#[must_use]
pub fn with_static(mut self, fields: Vec<(&'static str, FieldHandle<f64>)>) -> Self {
self.statics = fields;
self
}
fn schema(cols: &[String]) -> Result<Arc<parquet::schema::types::Type>, ObserverError> {
let body: String = cols.concat();
Ok(Arc::new(parse_message_type(&format!(
"message snapshot {{ {body} }}"
))?))
}
fn write_streamed<S, P>(
&self,
path: &PathBuf,
schema: Arc<parquet::schema::types::Type>,
state: &State<f64, S>,
with_coords: bool,
fields: &[(&'static str, FieldHandle<f64>)],
prefix: P,
) -> Result<(), ObserverError>
where
S: StorageBackend<f64>,
P: Fn(
&mut parquet::file::writer::SerializedRowGroupWriter<'_, File>,
usize,
) -> Result<(), ObserverError>,
{
let file = File::create(path)?;
let mut writer = SerializedFileWriter::new(file, schema, self.props.clone())?;
let block_cells: usize = self.grid.block_cells().iter().product();
let cap = ROWS_PER_GROUP + block_cells;
let n_coords = if with_coords { D } else { 0 };
let mut bufs: Vec<Vec<f64>> = (0..n_coords + fields.len())
.map(|_| Vec::with_capacity(cap))
.collect();
let mut flush = |bufs: &mut Vec<Vec<f64>>| -> Result<(), ObserverError> {
let n = bufs[0].len();
if n == 0 {
return Ok(());
}
let mut rg = writer.next_row_group()?;
prefix(&mut rg, n)?;
for data in bufs.iter() {
let mut col = rg
.next_column()?
.ok_or_else(|| ObserverError::Internal("missing column"))?;
col.typed::<DoubleType>().write_batch(data, None, None)?;
col.close()?;
}
rg.close()?;
for b in bufs.iter_mut() {
b.clear();
}
Ok(())
};
for b in 0..self.grid.num_blocks() {
let block = BlockId(b as u32);
if with_coords {
for_each_interior(self.grid.block_cells(), |idx| {
let center = self.grid.cell_center(block, idx);
for (d, buf) in bufs[..D].iter_mut().enumerate() {
buf.push(center[d]);
}
});
}
for ((_, handle), buf) in fields.iter().zip(&mut bufs[n_coords..]) {
let v = state.view(&self.grid, block, *handle);
for_each_interior(self.grid.block_cells(), |idx| buf.push(v.get(idx)));
}
if bufs[0].len() >= ROWS_PER_GROUP {
flush(&mut bufs)?;
}
}
flush(&mut bufs)?;
writer.close()?;
Ok(())
}
fn write_static<S: StorageBackend<f64>>(
&self,
state: &State<f64, S>,
) -> Result<(), ObserverError> {
let mut cols: Vec<String> = COORD_NAMES[..D]
.iter()
.map(|n| format!("required double {n};\n"))
.collect();
cols.extend(
self.statics
.iter()
.map(|(n, _)| format!("required double {n};\n")),
);
let path = self.dir.join(format!("static_{:07}.parquet", self.epoch));
self.write_streamed(
&path,
Self::schema(&cols)?,
state,
true,
&self.statics,
|_rg, _n| Ok(()),
)
}
fn write_dynamic<S: StorageBackend<f64>>(
&self,
step: u64,
t: f64,
state: &State<f64, S>,
) -> Result<(), ObserverError> {
let mut cols = vec![
"required int64 step;\n".to_string(),
"required double t;\n".to_string(),
"required int64 epoch;\n".to_string(),
];
cols.extend(
self.dynamic
.iter()
.map(|(n, _)| format!("required double {n};\n")),
);
let path = self.dir.join(format!("snap_{step:07}.parquet"));
let epoch = self.epoch;
self.write_streamed(
&path,
Self::schema(&cols)?,
state,
false,
&self.dynamic,
move |rg, n| {
let steps = vec![step as i64; n];
let ts = vec![t; n];
let epochs = vec![epoch as i64; n];
let mut col = rg
.next_column()?
.ok_or_else(|| ObserverError::Internal("missing step column"))?;
col.typed::<Int64Type>().write_batch(&steps, None, None)?;
col.close()?;
let mut col = rg
.next_column()?
.ok_or_else(|| ObserverError::Internal("missing t column"))?;
col.typed::<DoubleType>().write_batch(&ts, None, None)?;
col.close()?;
let mut col = rg
.next_column()?
.ok_or_else(|| ObserverError::Internal("missing epoch column"))?;
col.typed::<Int64Type>().write_batch(&epochs, None, None)?;
col.close()?;
Ok(())
},
)
}
fn write_snapshot<S: StorageBackend<f64>>(
&mut self,
step: u64,
t: f64,
state: &State<f64, S>,
) -> Result<(), ObserverError> {
if !self.static_written {
self.write_static(state)?;
self.static_written = true;
}
self.write_dynamic(step, t, state)
}
}
impl<S: StorageBackend<f64>, const D: usize> Observer<f64, S> for ParquetObserver<D> {
fn observe(&mut self, step: u64, t: f64, state: &State<f64, S>) {
if step != 1 && !step.is_multiple_of(self.every) {
return;
}
if let Err(e) = self.write_snapshot(step, t, state) {
tracing::error!(step, "parquet snapshot failed: {e:#}");
}
}
}
impl<S: StorageBackend<f64>, const D: usize> SnapshotSink<f64, S> for ParquetObserver<D> {
fn consume(&mut self, step: u64, t: f64, state: &State<f64, S>) {
if let Err(e) = self.write_snapshot(step, t, state) {
tracing::error!(step, "parquet snapshot failed: {e:#}");
}
}
}