use crate::array::{ArrayData, ArrayLocation, ArrayMetadata, ArrayValues};
use crate::error::{Error, ExternalDataReason, Result};
use crate::storage::EnergymlStorage;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct WindowLimits {
pub max_elements: usize,
}
impl WindowLimits {
pub fn new(max_elements: usize) -> Self {
Self { max_elements }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct Stats {
pub count: usize,
pub nan: usize,
pub min: Option<f64>,
pub max: Option<f64>,
pub mean: Option<f64>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Window {
pub whole: ArrayMetadata,
pub read: ArrayLocation,
pub data: ArrayData,
pub stats: Option<Stats>,
pub clipped: bool,
}
pub fn stats_of(data: &ArrayData) -> Option<Stats> {
if non_numeric(data.metadata.logical_type.as_deref()) {
return None;
}
Some(match &data.values {
ArrayValues::F64(v) => fold(v.iter().copied()),
ArrayValues::F32(v) => fold(v.iter().map(|x| f64::from(*x))),
ArrayValues::I64(v) => fold(v.iter().map(|x| *x as f64)),
ArrayValues::I32(v) => fold(v.iter().map(|x| f64::from(*x))),
ArrayValues::I16(v) => fold(v.iter().map(|x| f64::from(*x))),
ArrayValues::I8(v) => fold(v.iter().map(|x| f64::from(*x))),
ArrayValues::U64(v) => fold(v.iter().map(|x| *x as f64)),
ArrayValues::U32(v) => fold(v.iter().map(|x| f64::from(*x))),
ArrayValues::U16(v) => fold(v.iter().map(|x| f64::from(*x))),
ArrayValues::U8(v) => fold(v.iter().map(|x| f64::from(*x))),
})
}
fn non_numeric(logical_type: Option<&str>) -> bool {
logical_type.is_some_and(|logical| {
let logical = logical.to_ascii_lowercase();
["bool", "string", "custom"]
.iter()
.any(|word| logical.contains(word))
})
}
fn fold(values: impl Iterator<Item = f64>) -> Stats {
let (mut count, mut nan, mut valid) = (0usize, 0usize, 0usize);
let (mut min, mut max): (Option<f64>, Option<f64>) = (None, None);
let mut sum = 0.0f64;
for x in values {
count += 1;
if x.is_nan() {
nan += 1;
continue;
}
valid += 1;
sum += x;
min = Some(min.map_or(x, |m| m.min(x)));
max = Some(max.map_or(x, |m| m.max(x)));
}
Stats {
count,
nan,
min,
max,
mean: (valid > 0).then(|| sum / valid as f64),
}
}
pub async fn read_window<S: EnergymlStorage + Sync + ?Sized>(
store: &S,
location: &ArrayLocation,
limits: &WindowLimits,
) -> Result<Window> {
let whole = store.array_metadata(location).await?;
let dims = whole.dimensions.clone();
let rank = dims.len();
let refuse = |reason: String| Error::OutOfBounds {
location: address(location),
reason,
};
let too_large = |reason: String| Error::TooLarge {
location: address(location),
reason,
};
if limits.max_elements == 0 {
return Err(too_large(
"the window bound is zero elements: nothing could be read".into(),
));
}
if let Some(axis) = dims.iter().position(|d| *d < 0) {
return Err(refuse(format!(
"the store reports a negative length on axis {axis}"
)));
}
for (name, given) in [("start", &location.start), ("count", &location.count)] {
if !given.is_empty() && given.len() != rank {
return Err(refuse(format!(
"`{name}` has {} value(s), the array has {rank} axis(es) {dims:?}",
given.len()
)));
}
}
let mut starts = vec![0i64; rank];
let mut counts = vec![0i64; rank];
for (axis, &length) in dims.iter().enumerate() {
let start = location.start.get(axis).copied().unwrap_or(0);
if start < 0 || (start >= length && !(length == 0 && start == 0)) {
return Err(refuse(format!(
"start {start} on axis {axis} is outside the array (length {length})"
)));
}
let count = location.count.get(axis).copied().unwrap_or(length - start);
if count < 0 || (count == 0 && length != 0) || count > length - start {
return Err(refuse(format!(
"count {count} from start {start} on axis {axis} does not fit the array (length {length})"
)));
}
starts[axis] = start;
counts[axis] = count;
}
let mut clipped = false;
if rank > 0 {
let mut row: usize = 1;
for count in &counts[1..] {
row = row.checked_mul(*count as usize).ok_or_else(|| {
too_large("the trailing axes hold more elements than a usize can count".into())
})?;
}
if let Some(allowed) = limits.max_elements.checked_div(row) {
if allowed == 0 {
return Err(too_large(format!(
"one row along the first axis holds {row} elements, more than the bound of {}: \
narrow `count` on the trailing axes",
limits.max_elements
)));
}
if counts[0] as usize > allowed {
counts[0] = allowed as i64;
clipped = true;
}
}
}
let mut read = location.clone();
read.start = starts;
read.count = counts;
let data = store.read_array(&read).await?;
let promised = read.count.iter().map(|c| *c as usize).product::<usize>();
if data.values.len() != promised {
return Err(Error::ExternalData {
location: address(location),
reason: ExternalDataReason::ShapeDisagrees {
promised: promised as i64,
held: data.values.len(),
},
});
}
let stats = if non_numeric(whole.logical_type.as_deref()) {
None
} else {
stats_of(&data)
};
Ok(Window {
whole,
read,
data,
stats,
clipped,
})
}
fn address(location: &ArrayLocation) -> String {
match location.etp_address() {
Some((uri, path)) => format!("{uri}#{path}"),
None => format!("{}#{}", location.file, location.path_in_file),
}
}