use std::collections::BTreeMap;
use indexmap::IndexMap;
use ndarray::ArrayD;
use serde_json::{json, Value};
use crate::array::{Index, NdArray, Slice};
use crate::h5::attrs::AttrValue;
use crate::h5::data;
use crate::json::repr_int_tuple;
use crate::labels::check_class_id;
use crate::{Error, Result};
#[derive(Debug, Clone, PartialEq)]
pub enum PayloadData {
Array(NdArray),
Strings(Vec<String>),
}
impl PayloadData {
pub fn shape(&self) -> Vec<usize> {
match self {
PayloadData::Array(a) => a.shape(),
PayloadData::Strings(s) => vec![s.len()],
}
}
pub fn nbytes(&self) -> usize {
match self {
PayloadData::Array(a) => a.nbytes(),
PayloadData::Strings(s) => s.len() * 8,
}
}
pub fn dtype_str(&self) -> String {
match self {
PayloadData::Array(a) => a.dtype().numpy_str().to_string(),
PayloadData::Strings(_) => "|O".to_string(),
}
}
pub fn as_array(&self) -> Option<&NdArray> {
match self {
PayloadData::Array(a) => Some(a),
PayloadData::Strings(_) => None,
}
}
}
impl From<NdArray> for PayloadData {
fn from(a: NdArray) -> Self {
PayloadData::Array(a)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Payload {
pub kind: String,
pub datasets: IndexMap<String, PayloadData>,
pub attrs: Vec<(String, AttrValue)>,
pub stacked_axes: usize,
pub class_ids: Vec<i64>,
}
impl Payload {
pub fn new(kind: &str) -> Payload {
Payload {
kind: kind.into(),
datasets: IndexMap::new(),
attrs: Vec::new(),
stacked_axes: 0,
class_ids: Vec::new(),
}
}
pub fn data(&self) -> Result<&NdArray> {
self.array("data")
}
pub fn array(&self, name: &str) -> Result<&NdArray> {
match self.datasets.get(name) {
Some(PayloadData::Array(a)) => Ok(a),
Some(PayloadData::Strings(_)) => Err(Error::Type(format!("{name:?} holds strings"))),
None => Err(Error::Key(crate::json::repr_str(name))),
}
}
pub fn attr(&self, name: &str) -> Option<&AttrValue> {
self.attrs.iter().find(|(k, _)| k == name).map(|(_, v)| v)
}
pub fn set_attr(&mut self, name: &str, value: AttrValue) {
if let Some(slot) = self.attrs.iter_mut().find(|(k, _)| k == name) {
slot.1 = value;
} else {
self.attrs.push((name.into(), value));
}
}
pub fn nbytes(&self) -> usize {
self.datasets.values().map(PayloadData::nbytes).sum()
}
pub fn describe(&self) -> Value {
let datasets: serde_json::Map<String, Value> = self
.datasets
.iter()
.map(|(name, d)| (name.clone(), json!({"shape": d.shape(), "dtype": d.dtype_str()})))
.collect();
json!({"kind": self.kind, "datasets": datasets, "nbytes": self.nbytes()})
}
}
pub type Masks = BTreeMap<i64, ArrayD<bool>>;
pub fn normalize_masks(masks: &Masks, spatial_shape: Option<&[usize]>) -> Result<Vec<usize>> {
normalize_mask_shapes(masks.iter().map(|(class_id, mask)| (*class_id, mask.shape())), spatial_shape)
}
pub fn normalize_mask_shapes<'a>(
masks: impl IntoIterator<Item = (i64, &'a [usize])>,
spatial_shape: Option<&[usize]>,
) -> Result<Vec<usize>> {
let mut shape: Option<Vec<usize>> = spatial_shape.map(<[usize]>::to_vec);
for (class_id, found) in masks {
match &shape {
None => shape = Some(found.to_vec()),
Some(s) if found != s.as_slice() => {
return Err(Error::coded(
"E405",
format!(
"mask for class {class_id} has shape {}, expected {}",
repr_int_tuple(found),
repr_int_tuple(s)
),
))
}
_ => {}
}
check_class_id(class_id)?;
}
shape.ok_or_else(|| Error::coded("E410", "no masks were supplied"))
}
pub const SLAB_BYTES: usize = 8 * 1024 * 1024;
pub fn slabs(shape: &[usize], itemsize: usize, prefix: &[usize]) -> Vec<Vec<Index>> {
slabs_within(shape, itemsize, prefix, SLAB_BYTES)
}
pub fn slabs_within(shape: &[usize], itemsize: usize, prefix: &[usize], budget: usize) -> Vec<Vec<Index>> {
let rest = &shape[prefix.len()..];
let mut out = Vec::new();
if rest.is_empty() || rest[0] == 0 {
return out;
}
let per_row = rest[1..].iter().product::<usize>() * itemsize;
if per_row > budget && rest.len() > 1 {
for index in 0..rest[0] {
let mut p = prefix.to_vec();
p.push(index);
out.extend(slabs_within(shape, itemsize, &p, budget));
}
return out;
}
let step = (budget / per_row.max(1)).min(rest[0]).max(1);
let mut start = 0;
while start < rest[0] {
let mut sel: Vec<Index> = prefix.iter().map(|i| Index::At(*i as i64)).collect();
sel.push(Index::Slice(Slice::new(start as i64, (start + step) as i64)));
out.push(sel);
start += step;
}
out
}
pub fn contains_value(ds: &hdf5::Dataset, value: i64) -> Result<bool> {
contains_value_within(ds, value, SLAB_BYTES)
}
pub fn contains_value_within(ds: &hdf5::Dataset, value: i64, budget: usize) -> Result<bool> {
let dtype = data::dtype(ds)?;
for sel in slabs_within(&ds.shape(), dtype.itemsize(), &[], budget) {
let block = data::read_region(ds, &sel)?;
if block.cast::<i64>().iter().any(|v| *v == value) {
return Ok(true);
}
}
Ok(false)
}
pub fn any_nonzero(ds: &hdf5::Dataset) -> Result<bool> {
let dtype = data::dtype(ds)?;
for sel in slabs(&ds.shape(), dtype.itemsize(), &[]) {
if data::read_region(ds, &sel)?.nonzero_mask().iter().any(|v| *v) {
return Ok(true);
}
}
Ok(false)
}
pub fn count_nonzero(ds: &hdf5::Dataset) -> Result<u64> {
count_nonzero_within(ds, SLAB_BYTES)
}
pub fn count_nonzero_within(ds: &hdf5::Dataset, budget: usize) -> Result<u64> {
let dtype = data::dtype(ds)?;
let mut total = 0u64;
for sel in slabs_within(&ds.shape(), dtype.itemsize(), &[], budget) {
let block = data::read_region(ds, &sel)?;
total += block.nonzero_mask().iter().filter(|v| **v).count() as u64;
}
Ok(total)
}
pub fn value_counts(ds: &hdf5::Dataset, ceiling: i64) -> Result<BTreeMap<i64, u64>> {
value_counts_within(ds, ceiling, SLAB_BYTES)
}
pub fn value_counts_within(ds: &hdf5::Dataset, ceiling: i64, budget: usize) -> Result<BTreeMap<i64, u64>> {
let dtype = data::dtype(ds)?;
let mut totals = vec![0u64; (ceiling.max(0) + 1) as usize];
for sel in slabs_within(&ds.shape(), dtype.itemsize(), &[], budget) {
let block = data::read_region(ds, &sel)?;
for v in block.cast::<i64>().iter() {
if *v >= 0 && *v <= ceiling {
totals[*v as usize] += 1;
}
}
}
Ok(totals.into_iter().enumerate().filter(|(_, c)| *c > 0).map(|(v, c)| (v as i64, c)).collect())
}
pub fn popcounts(ds: &hdf5::Dataset) -> Result<Vec<[u64; 64]>> {
popcounts_within(ds, SLAB_BYTES)
}
pub fn popcounts_within(ds: &hdf5::Dataset, budget: usize) -> Result<Vec<[u64; 64]>> {
let shape = ds.shape();
let planes = shape.first().copied().unwrap_or(0);
let mut out = vec![[0u64; 64]; planes];
for (plane, counts) in out.iter_mut().enumerate() {
for sel in slabs_within(&shape, 8, &[plane], budget) {
let block = data::read_region(ds, &sel)?;
for word in block.cast::<u64>().iter() {
let mut w = *word;
while w != 0 {
let bit = w.trailing_zeros() as usize;
counts[bit] += 1;
w &= w - 1;
}
}
}
}
Ok(out)
}
pub fn packbits(bits: impl IntoIterator<Item = bool>) -> Vec<u8> {
let mut out = Vec::new();
let mut byte = 0u8;
let mut n = 0;
for bit in bits {
byte = (byte << 1) | u8::from(bit);
n += 1;
if n == 8 {
out.push(byte);
byte = 0;
n = 0;
}
}
if n > 0 {
out.push(byte << (8 - n));
}
out
}
pub fn unpackbits(packed: &[u8], n: usize) -> Vec<bool> {
let mut out = Vec::with_capacity(n);
for byte in packed {
for k in (0..8).rev() {
if out.len() == n {
return out;
}
out.push((byte >> k) & 1 == 1);
}
}
out.resize(n, false);
out
}
#[cfg(test)]
mod tests {
use super::*;
fn selection_bytes(shape: &[usize], itemsize: usize, sel: &[Index]) -> usize {
let mut n = itemsize;
for (axis, extent) in shape.iter().enumerate() {
n *= match sel.get(axis) {
Some(Index::At(_)) => 1,
Some(Index::Slice(s)) => {
let start = s.start.unwrap_or(0).max(0) as usize;
let stop = (s.stop.unwrap_or(*extent as i64).max(0) as usize).min(*extent);
stop.saturating_sub(start)
}
_ => *extent,
};
}
n
}
#[test]
fn s14_slabs_stay_within_the_budget_and_tile_the_dataset() {
for (shape, itemsize, prefix) in [
(vec![2, 16, 32, 32], 2, vec![]), (vec![1, 16, 32, 32], 8, vec![0]), (vec![3, 7, 5], 1, vec![]), ] {
let sels = slabs_within(&shape, itemsize, &prefix, 4096);
assert!(sels.iter().all(|sel| selection_bytes(&shape, itemsize, sel) <= 4096), "{shape:?}");
let whole: usize = shape[prefix.len()..].iter().product::<usize>() * itemsize;
let read: usize = sels.iter().map(|sel| selection_bytes(&shape, itemsize, sel)).sum();
assert_eq!(read, whole, "{shape:?}");
}
}
#[test]
fn s14_plane_counts_read_within_the_slab_budget() {
let dir = tempfile::tempdir().unwrap();
let file = crate::h5::file::marked(|| hdf5::File::create(dir.path().join("planes.h5"))).unwrap();
let mut layers = ndarray::Array4::<u16>::zeros((2, 16, 32, 32));
layers.slice_mut(ndarray::s![0, 2..10, 2..20, 2..20]).fill(1);
layers.slice_mut(ndarray::s![1, .., .., 10..30]).fill(2);
let ds = file.new_dataset_builder().with_data(&layers).create("layers").unwrap();
let expected =
BTreeMap::from([(0, 2 * 16 * 32 * 32 - 8 * 18 * 18 - 16 * 32 * 20), (1, 8 * 18 * 18), (2, 16 * 32 * 20)]);
assert_eq!(value_counts_within(&ds, 2, 4096).unwrap(), expected);
assert_eq!(value_counts(&ds, 2).unwrap(), expected);
let mut words = ndarray::Array4::<u64>::zeros((1, 16, 32, 32));
words.slice_mut(ndarray::s![0, 2..10, 2..20, 2..20]).fill(0b01);
words.slice_mut(ndarray::s![0, .., .., 10..30]).mapv_inplace(|w| w | 0b10);
let ds = file.new_dataset_builder().with_data(&words).create("bitmask").unwrap();
let counts = popcounts_within(&ds, 4096).unwrap();
assert_eq!((counts[0][0], counts[0][1]), (8 * 18 * 18, 16 * 32 * 20));
assert_eq!(popcounts(&ds).unwrap(), counts);
}
#[test]
fn s7_7_scans_reach_the_last_slab() {
let dir = tempfile::tempdir().unwrap();
let file = crate::h5::file::marked(|| hdf5::File::create(dir.path().join("scan.h5"))).unwrap();
let mut labels = ndarray::Array3::<u16>::zeros((8, 4, 4));
labels[[7, 3, 3]] = 65535;
labels[[7, 3, 2]] = 1;
let ds = file.new_dataset_builder().with_data(&labels).create("labels").unwrap();
for budget in [1, 32, SLAB_BYTES] {
assert!(contains_value_within(&ds, 65535, budget).unwrap(), "budget {budget}");
assert!(!contains_value_within(&ds, 2, budget).unwrap(), "budget {budget}");
assert_eq!(count_nonzero_within(&ds, budget).unwrap(), 2, "budget {budget}");
}
}
#[test]
fn bits_pack_like_numpy() {
let bits = [true, false, true, true, false, false, false, false, true];
assert_eq!(packbits(bits), vec![176, 128]);
assert_eq!(unpackbits(&[176, 128], 9), bits.to_vec());
}
}