use super::payload::{bytes, invalid, row_count};
use crate::DatasetError;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub struct StructureChunkPayload {
positions: Arc<[[f32; 3]]>,
elements: Arc<[u16]>,
residues: Arc<[u32]>,
radii: Arc<[f32]>,
}
impl StructureChunkPayload {
pub fn new(
positions: Arc<[[f32; 3]]>,
elements: Arc<[u16]>,
residues: Arc<[u32]>,
radii: Arc<[f32]>,
) -> Result<Self, DatasetError> {
let rows = row_count(positions.len())?;
if rows == 0
|| elements.len() != positions.len()
|| residues.len() != positions.len()
|| radii.len() != positions.len()
{
return Err(invalid("structure columns must be non-empty and aligned"));
}
if positions.iter().flatten().any(|value| !value.is_finite())
|| radii
.iter()
.any(|radius| !radius.is_finite() || *radius < 0.0)
{
return Err(invalid("structure coordinates and radii must be finite"));
}
Ok(Self {
positions,
elements,
residues,
radii,
})
}
#[must_use]
pub fn positions(&self) -> &Arc<[[f32; 3]]> {
&self.positions
}
#[must_use]
pub fn elements(&self) -> &Arc<[u16]> {
&self.elements
}
#[must_use]
pub fn residues(&self) -> &Arc<[u32]> {
&self.residues
}
#[must_use]
pub fn radii(&self) -> &Arc<[f32]> {
&self.radii
}
pub(super) fn minimum_host_bytes(&self) -> Result<u64, DatasetError> {
let positions = bytes(self.positions.len(), 12)?;
let elements = bytes(self.elements.len(), 2)?;
let residues = bytes(self.residues.len(), 4)?;
let radii = bytes(self.radii.len(), 4)?;
positions
.checked_add(elements)
.and_then(|value| value.checked_add(residues))
.and_then(|value| value.checked_add(radii))
.ok_or(DatasetError::PayloadByteSizeOverflow)
}
}
#[derive(Clone, Debug)]
pub struct ScalarChunkPayload {
values: Arc<[f32]>,
}
impl ScalarChunkPayload {
pub fn new(values: Arc<[f32]>) -> Result<Self, DatasetError> {
row_count(values.len())?;
if values.is_empty() || values.iter().any(|value| value.is_infinite()) {
return Err(invalid("scalar values must be non-empty and not infinite"));
}
Ok(Self { values })
}
#[must_use]
pub fn values(&self) -> &Arc<[f32]> {
&self.values
}
}
#[derive(Clone, Debug)]
pub enum PropertyValues {
Boolean {
rows: u32,
words: Arc<[u64]>,
},
Integer(Arc<[i64]>),
Real(Arc<[f64]>),
Symbol(Arc<[u32]>),
}
#[derive(Clone, Debug)]
pub struct PropertyChunkPayload {
values: PropertyValues,
validity: Option<Arc<[u64]>>,
rows: u32,
}
impl PropertyChunkPayload {
pub fn new(values: PropertyValues, validity: Option<Arc<[u64]>>) -> Result<Self, DatasetError> {
let rows = property_rows(&values)?;
let expected_words = packed_word_count(rows)?;
if let PropertyValues::Boolean { words, .. } = &values {
validate_words(words, rows, expected_words)?;
}
if let Some(mask) = &validity {
validate_words(mask, rows, expected_words)?;
}
if let PropertyValues::Real(values) = &values {
validate_reals(values, validity.as_deref())?;
}
Ok(Self {
values,
validity,
rows,
})
}
#[must_use]
pub const fn row_count(&self) -> u32 {
self.rows
}
#[must_use]
pub const fn values(&self) -> &PropertyValues {
&self.values
}
#[must_use]
pub fn validity(&self) -> Option<&Arc<[u64]>> {
self.validity.as_ref()
}
pub(super) fn minimum_host_bytes(&self) -> Result<u64, DatasetError> {
let values = match &self.values {
PropertyValues::Boolean { words, .. } => bytes(words.len(), 8)?,
PropertyValues::Integer(values) => bytes(values.len(), 8)?,
PropertyValues::Real(values) => bytes(values.len(), 8)?,
PropertyValues::Symbol(values) => bytes(values.len(), 4)?,
};
let validity = match &self.validity {
Some(mask) => bytes(mask.len(), 8)?,
None => 0,
};
values
.checked_add(validity)
.ok_or(DatasetError::PayloadByteSizeOverflow)
}
}
#[derive(Clone, Debug)]
pub struct TrajectoryChunkPayload {
frame: u64,
time_seconds: f64,
positions: Arc<[[f32; 3]]>,
}
impl TrajectoryChunkPayload {
pub fn new(
frame: u64,
time_seconds: f64,
positions: Arc<[[f32; 3]]>,
) -> Result<Self, DatasetError> {
row_count(positions.len())?;
if positions.is_empty()
|| !time_seconds.is_finite()
|| positions.iter().flatten().any(|value| !value.is_finite())
{
return Err(invalid("trajectory time and positions must be finite"));
}
Ok(Self {
frame,
time_seconds,
positions,
})
}
#[must_use]
pub const fn frame(&self) -> u64 {
self.frame
}
#[must_use]
pub const fn time_seconds(&self) -> f64 {
self.time_seconds
}
#[must_use]
pub fn positions(&self) -> &Arc<[[f32; 3]]> {
&self.positions
}
}
#[derive(Clone, Debug)]
pub struct TrajectoryFramesPayload {
first_frame: u64,
times_seconds: Arc<[f64]>,
rows_per_frame: u32,
positions: Arc<[[f32; 3]]>,
}
impl TrajectoryFramesPayload {
pub fn new(
first_frame: u64,
times_seconds: Arc<[f64]>,
rows_per_frame: u32,
positions: Arc<[[f32; 3]]>,
) -> Result<Self, DatasetError> {
let frames = row_count(times_seconds.len())?;
if frames == 0 || rows_per_frame == 0 {
return Err(invalid("trajectory frame batches must be non-empty"));
}
first_frame
.checked_add(u64::from(frames - 1))
.ok_or_else(|| invalid("trajectory frame identity range overflows u64"))?;
let expected = u64::from(frames)
.checked_mul(u64::from(rows_per_frame))
.and_then(|value| usize::try_from(value).ok())
.ok_or(DatasetError::PayloadByteSizeOverflow)?;
if positions.len() != expected
|| times_seconds.iter().any(|value| !value.is_finite())
|| positions.iter().flatten().any(|value| !value.is_finite())
{
return Err(invalid(
"trajectory frame times and packed coordinates must be finite and aligned",
));
}
Ok(Self {
first_frame,
times_seconds,
rows_per_frame,
positions,
})
}
#[must_use]
pub const fn first_frame(&self) -> u64 {
self.first_frame
}
#[must_use]
pub fn times_seconds(&self) -> &Arc<[f64]> {
&self.times_seconds
}
#[must_use]
pub const fn rows_per_frame(&self) -> u32 {
self.rows_per_frame
}
#[must_use]
pub fn positions(&self) -> &Arc<[[f32; 3]]> {
&self.positions
}
pub(super) fn minimum_host_bytes(&self) -> Result<u64, DatasetError> {
bytes(self.times_seconds.len(), 8)?
.checked_add(bytes(self.positions.len(), 12)?)
.ok_or(DatasetError::PayloadByteSizeOverflow)
}
}
fn property_rows(values: &PropertyValues) -> Result<u32, DatasetError> {
let rows = match values {
PropertyValues::Boolean { rows, .. } => *rows,
PropertyValues::Integer(values) => row_count(values.len())?,
PropertyValues::Real(values) => row_count(values.len())?,
PropertyValues::Symbol(values) => row_count(values.len())?,
};
if rows == 0 {
return Err(invalid("property columns must be non-empty"));
}
Ok(rows)
}
fn packed_word_count(rows: u32) -> Result<usize, DatasetError> {
u64::from(rows)
.checked_add(63)
.map(|value| value / 64)
.and_then(|value| usize::try_from(value).ok())
.ok_or(DatasetError::PayloadByteSizeOverflow)
}
fn validate_words(words: &[u64], rows: u32, expected: usize) -> Result<(), DatasetError> {
if words.len() != expected {
return Err(invalid(
"bit-packed property storage has the wrong word count",
));
}
let used = rows % 64;
let Some(last) = words.last().copied() else {
return Err(invalid("bit-packed property storage must be non-empty"));
};
if used != 0 && last >> used != 0 {
return Err(invalid("unused property bits must be zero"));
}
Ok(())
}
fn validate_reals(values: &[f64], validity: Option<&[u64]>) -> Result<(), DatasetError> {
for (index, value) in values.iter().enumerate() {
let valid = match validity {
Some(mask) => (mask[index / 64] & (1u64 << (index % 64))) != 0,
None => true,
};
if valid && !value.is_finite() {
return Err(invalid("valid real property values must be finite"));
}
}
Ok(())
}