use std::borrow::Cow;
use std::collections::{BTreeSet, HashMap};
use std::path::Path;
use std::sync::Arc;
use zarrs::array::Array;
use zarrs::plugin::{ExtensionName, ZarrVersion};
use super::zarr_store::CacheStorage;
use super::{ArrayData, AxisSelect, DType, NativeDataset, NativeField, Reader, Selection};
use crate::cache::Cache;
use crate::error::{Error, Result};
pub struct ZarrReader;
impl ZarrReader {
pub fn new() -> Self {
ZarrReader
}
}
impl Default for ZarrReader {
fn default() -> Self {
Self::new()
}
}
fn zarr_err(detail: impl Into<String>) -> Error {
Error::Format {
format: "zarr".to_string(),
detail: detail.into(),
}
}
fn fetch_concurrency() -> usize {
#[cfg(target_arch = "wasm32")]
{
1
}
#[cfg(not(target_arch = "wasm32"))]
{
std::env::var("EARTHSCIIO_FETCH_CONCURRENCY")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.map(|v| v.clamp(1, 64))
.unwrap_or(16)
}
}
impl Reader for ZarrReader {
fn formats(&self) -> &'static [&'static str] {
&["zarr"]
}
fn extensions(&self) -> &'static [&'static str] {
&["zarr"]
}
fn read_native(
&self,
_blob: &Path,
_vars: &[String],
_sel: &Selection,
) -> Result<NativeDataset> {
Err(zarr_err(
"zarr is store-backed; the Provider calls read_store",
))
}
fn store_backed(&self) -> bool {
true
}
fn supports_selection(&self) -> bool {
true
}
fn array_shape(
&self,
cache: Arc<Cache>,
base_url: &str,
var: &str,
) -> Result<Option<Vec<usize>>> {
let storage = Arc::new(CacheStorage::new(cache, base_url));
let array = open_array(storage, var)?;
Ok(Some(array.shape().iter().map(|&s| s as usize).collect()))
}
fn read_store(
&self,
cache: Arc<Cache>,
base_url: &str,
variables: &[String],
select: &Selection,
) -> Result<NativeDataset> {
if variables.is_empty() {
return Err(zarr_err(
"the zarr reader requires an explicit list of variables (arrays); \
the store cannot be enumerated without a consolidated metadata index",
));
}
let storage = Arc::new(CacheStorage::new(cache, base_url));
let axes: Option<&[AxisSelect]> = match select {
Selection::Orthogonal(a) => Some(a.as_slice()),
_ => None,
};
read_arrays(storage, variables, axes)
}
fn supports_direct_read(&self) -> bool {
cfg!(feature = "object-store")
}
#[cfg(feature = "object-store")]
fn read_store_direct(
&self,
base_url: &str,
variables: &[String],
select: &Selection,
options: &[(String, String)],
) -> Result<NativeDataset> {
if variables.is_empty() {
return Err(zarr_err(
"the zarr reader requires an explicit list of variables (arrays); \
the store cannot be enumerated without a consolidated metadata index",
));
}
super::zarr_object_store::read_zarr_object_store_with_options(
base_url,
variables,
select,
&super::zarr_object_store::read_store_options(base_url, options),
)
}
#[cfg(feature = "object-store")]
fn array_shape_direct(
&self,
base_url: &str,
var: &str,
options: &[(String, String)],
) -> Result<Option<Vec<usize>>> {
super::zarr_object_store::array_shape_object_store(
base_url,
var,
&super::zarr_object_store::read_store_options(base_url, options),
)
.map(Some)
}
}
pub(crate) fn read_arrays<S>(
storage: Arc<S>,
variables: &[String],
axes: Option<&[AxisSelect]>,
) -> Result<NativeDataset>
where
S: zarrs::storage::ReadableStorageTraits + 'static,
{
let mut out_vars = HashMap::new();
for array_name in variables {
let array = open_array(storage.clone(), array_name)?;
let shape: Vec<usize> = array.shape().iter().map(|&s| s as usize).collect();
let ndim = shape.len();
let zeros = vec![0u64; ndim];
let chunk_shape: Vec<usize> = array
.chunk_shape_usize(&zeros)
.map_err(|e| zarr_err(format!("chunk shape of '{array_name}': {e}")))?;
let dims = dim_names(&array, ndim);
let (dtype, is_f32) = float_dtype(&array, array_name)?;
let sel_idx: Vec<Vec<usize>> = match axes {
Some(a) if a.len() == ndim => {
let mut v = Vec::with_capacity(ndim);
for d in 0..ndim {
v.push(a[d].resolve(shape[d])?);
}
v
}
_ => (0..ndim).map(|d| (0..shape[d]).collect()).collect(),
};
let out_shape: Vec<usize> = sel_idx.iter().map(Vec::len).collect();
let total: usize = out_shape.iter().product();
let mut data = vec![0.0f64; total];
let out_stride = c_strides(&out_shape);
let owners = chunk_owners(&sel_idx, &chunk_shape, &out_stride);
let cids: Vec<Vec<usize>> = needed_chunks(&sel_idx, &chunk_shape);
let fetch_one = |cid: &Vec<usize>, out: &std::sync::Mutex<&mut [f64]>| -> Result<()> {
let cid_u64: Vec<u64> = cid.iter().map(|&c| c as u64).collect();
let subset = array
.chunk_subset(&cid_u64)
.map_err(|e| zarr_err(format!("chunk subset {cid:?} of '{array_name}': {e}")))?;
let cshape: Vec<usize> = subset.shape().iter().map(|&s| s as usize).collect();
if is_f32 {
let elems = array
.retrieve_array_subset::<Vec<f32>>(&subset)
.map_err(|e| zarr_err(format!("decode chunk {cid:?} of '{array_name}': {e}")))?;
let mut g = out.lock().expect("zarr scatter mutex poisoned");
scatter_chunk(&mut g, &elems, cid, &cshape, &owners);
} else {
let elems = array
.retrieve_array_subset::<Vec<f64>>(&subset)
.map_err(|e| zarr_err(format!("decode chunk {cid:?} of '{array_name}': {e}")))?;
let mut g = out.lock().expect("zarr scatter mutex poisoned");
scatter_chunk(&mut g, &elems, cid, &cshape, &owners);
}
Ok(())
};
let workers = fetch_concurrency().min(cids.len());
let out = std::sync::Mutex::new(&mut data[..]);
if workers <= 1 {
for cid in &cids {
fetch_one(cid, &out)?;
}
} else {
use std::sync::atomic::{AtomicUsize, Ordering};
let next = AtomicUsize::new(0);
let fail: std::sync::Mutex<Option<Error>> = std::sync::Mutex::new(None);
std::thread::scope(|s| {
for _ in 0..workers {
s.spawn(|| loop {
let i = next.fetch_add(1, Ordering::Relaxed);
if i >= cids.len() {
break;
}
if let Err(e) = fetch_one(&cids[i], &out) {
*fail.lock().expect("zarr error mutex poisoned") = Some(e);
next.store(cids.len(), Ordering::Relaxed);
break;
}
});
}
});
if let Some(e) = fail.into_inner().expect("zarr error mutex poisoned") {
return Err(e);
}
}
drop(out);
out_vars.insert(
array_name.clone(),
NativeField {
dtype,
dims,
shape: out_shape,
data: ArrayData::F64(data),
fill_value: None,
},
);
}
Ok(NativeDataset {
variables: out_vars,
coords: HashMap::new(),
})
}
pub(crate) fn open_array<S>(storage: Arc<S>, name: &str) -> Result<Array<S>>
where
S: zarrs::storage::ReadableStorageTraits + 'static,
{
let path = format!("/{}", name.trim_start_matches('/'));
Array::open(storage, &path).map_err(|e| zarr_err(format!("open zarr array '{name}': {e}")))
}
fn dim_names<S>(array: &Array<S>, ndim: usize) -> Vec<String> {
if let Some(dn) = array.dimension_names() {
let names: Vec<String> = dn.iter().filter_map(|d| d.clone()).collect();
if names.len() == ndim {
return names;
}
}
if let Some(arr) = array
.attributes()
.get("_ARRAY_DIMENSIONS")
.and_then(|v| v.as_array())
{
let names: Vec<String> = arr
.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect();
if names.len() == ndim {
return names;
}
}
(0..ndim).map(|i| format!("dim_{i}")).collect()
}
fn float_dtype<S>(array: &Array<S>, name: &str) -> Result<(DType, bool)> {
let dt = array.data_type();
let n = dt.name(ZarrVersion::V3).map(|c| c.to_string()).unwrap_or_default();
match n.as_str() {
"float64" => Ok((DType::Float64, false)),
"float32" => Ok((DType::Float64, true)),
other => Err(zarr_err(format!(
"the Rust zarr reader currently supports float dtypes (float32/float64); \
array '{name}' has data type '{other}'"
))),
}
}
fn needed_chunks(sel_idx: &[Vec<usize>], chunks: &[usize]) -> Vec<Vec<usize>> {
let per_dim: Vec<Vec<usize>> = sel_idx
.iter()
.zip(chunks)
.map(|(idxs, &cl)| {
let set: BTreeSet<usize> = idxs.iter().map(|g| g / cl).collect();
set.into_iter().collect()
})
.collect();
let mut out: Vec<Vec<usize>> = vec![Vec::new()];
for dim in &per_dim {
let mut next = Vec::with_capacity(out.len() * dim.len());
for prefix in &out {
for &c in dim {
let mut p = prefix.clone();
p.push(c);
next.push(p);
}
}
out = next;
}
out
}
fn c_strides(shape: &[usize]) -> Vec<usize> {
let mut st = vec![1usize; shape.len()];
for d in (0..shape.len().saturating_sub(1)).rev() {
st[d] = st[d + 1] * shape[d + 1];
}
st
}
fn chunk_owners(
sel_idx: &[Vec<usize>],
chunk_shape: &[usize],
out_stride: &[usize],
) -> Vec<HashMap<usize, Vec<(usize, usize)>>> {
sel_idx
.iter()
.zip(chunk_shape.iter().zip(out_stride))
.map(|(idxs, (&cl, &os))| {
let mut owned: HashMap<usize, Vec<(usize, usize)>> = HashMap::new();
for (pos, &g) in idxs.iter().enumerate() {
owned.entry(g / cl).or_default().push((pos * os, g % cl));
}
owned
})
.collect()
}
fn scatter_chunk<T: Copy + Into<f64>>(
out: &mut [f64],
elems: &[T],
cid: &[usize],
cshape: &[usize],
owners: &[HashMap<usize, Vec<(usize, usize)>>],
) {
let ndim = cid.len();
let cstride = c_strides(cshape);
let mut per_dim: Vec<Cow<[(usize, usize)]>> = Vec::with_capacity(ndim);
for d in 0..ndim {
let Some(owned) = owners[d].get(&cid[d]) else {
return;
};
per_dim.push(if cstride[d] == 1 {
Cow::Borrowed(owned.as_slice())
} else {
Cow::Owned(
owned
.iter()
.map(|&(lin, local)| (lin, local * cstride[d]))
.collect(),
)
});
}
let mut counter = vec![0usize; ndim];
loop {
let mut lin = 0usize;
let mut off = 0usize;
for (dim, &c) in per_dim.iter().zip(counter.iter()) {
let (l, o) = dim[c];
lin += l;
off += o;
}
out[lin] = elems[off].into();
let mut d = ndim;
loop {
if d == 0 {
return;
}
d -= 1;
counter[d] += 1;
if counter[d] < per_dim[d].len() {
break;
}
counter[d] = 0;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn needed_chunks_orthogonal_dedup_and_skip() {
let got = needed_chunks(&[vec![1], vec![0, 250, 260], vec![0]], &[1, 100, 1]);
assert_eq!(got, vec![vec![1, 0, 0], vec![1, 2, 0]]);
}
#[test]
fn needed_chunks_never_scans_whole_array() {
let got = needed_chunks(&[vec![0], vec![50, 12345, 52000], vec![0]], &[1, 100, 52411]);
let dim1: BTreeSet<usize> = got.iter().map(|c| c[1]).collect();
assert_eq!(dim1, BTreeSet::from([0, 123, 520]));
assert_eq!(got.len(), 3); }
#[test]
fn axis_select_resolves() {
assert_eq!(AxisSelect::All.resolve(4).unwrap(), vec![0, 1, 2, 3]);
assert_eq!(
AxisSelect::Indices(vec![3, 0, 1]).resolve(4).unwrap(),
vec![3, 0, 1]
);
assert_eq!(
AxisSelect::Range { start: 1, stop: 8, step: 2 }.resolve(10).unwrap(),
vec![1, 3, 5, 7]
);
assert!(AxisSelect::Indices(vec![9]).resolve(4).is_err());
}
#[test]
fn scatter_places_selected_indices() {
let sel = vec![vec![1usize, 0]];
let owners = chunk_owners(&sel, &[2], &c_strides(&[2]));
let mut out = vec![0.0f64; 2];
scatter_chunk(&mut out, &[10.0f64, 20.0], &[0], &[2], &owners);
assert_eq!(out, vec![20.0, 10.0]);
}
struct ChunkBuf {
cstart: Vec<usize>,
cshape: Vec<usize>,
elems: Vec<f64>,
}
#[allow(clippy::needless_range_loop)]
fn assemble_reference(
sel_idx: &[Vec<usize>],
chunk_shape: &[usize],
chunks: &HashMap<Vec<usize>, ChunkBuf>,
) -> Vec<f64> {
let ndim = sel_idx.len();
let sel_shape: Vec<usize> = sel_idx.iter().map(Vec::len).collect();
let total: usize = sel_shape.iter().product();
let mut data = vec![0.0f64; total];
if total == 0 {
return data;
}
for lin in 0..total {
let mut rem = lin;
let mut midx = vec![0usize; ndim];
for d in (0..ndim).rev() {
midx[d] = rem % sel_shape[d];
rem /= sel_shape[d];
}
let global: Vec<usize> = (0..ndim).map(|d| sel_idx[d][midx[d]]).collect();
let cid: Vec<usize> = (0..ndim).map(|d| global[d] / chunk_shape[d]).collect();
if let Some(buf) = chunks.get(&cid) {
let mut off = 0usize;
for d in 0..ndim {
let w = global[d] - buf.cstart[d];
off = off * buf.cshape[d] + w;
}
data[lin] = buf.elems[off];
}
}
data
}
fn synthetic(global: &[usize]) -> f32 {
let mut acc = 0.1f32;
for (d, &g) in global.iter().enumerate() {
acc += (g as f32) * (d as f32 + 1.0) * 0.3;
}
acc
}
fn synth_chunk(
cid: &[usize],
shape: &[usize],
chunk_shape: &[usize],
) -> (Vec<usize>, Vec<usize>, Vec<f32>) {
let ndim = shape.len();
let cstart: Vec<usize> = (0..ndim).map(|d| cid[d] * chunk_shape[d]).collect();
let cshape: Vec<usize> =
(0..ndim).map(|d| chunk_shape[d].min(shape[d] - cstart[d])).collect();
let n: usize = cshape.iter().product();
let strides = c_strides(&cshape);
let mut elems = vec![0.0f32; n];
for (lin, slot) in elems.iter_mut().enumerate() {
let global: Vec<usize> =
(0..ndim).map(|d| cstart[d] + (lin / strides[d]) % cshape[d]).collect();
*slot = synthetic(&global);
}
(cstart, cshape, elems)
}
fn both_paths(
shape: &[usize],
chunk_shape: &[usize],
sel_idx: &[Vec<usize>],
) -> (Vec<f64>, Vec<f64>, usize) {
let cids = needed_chunks(sel_idx, chunk_shape);
let out_shape: Vec<usize> = sel_idx.iter().map(Vec::len).collect();
let total: usize = out_shape.iter().product();
let out_stride = c_strides(&out_shape);
let owners = chunk_owners(sel_idx, chunk_shape, &out_stride);
let mut new_out = vec![0.0f64; total];
let mut written = 0usize;
for cid in &cids {
let (_cstart, cshape, elems) = synth_chunk(cid, shape, chunk_shape);
written += (0..shape.len())
.map(|d| owners[d].get(&cid[d]).map_or(0, Vec::len))
.product::<usize>();
scatter_chunk(&mut new_out, &elems, cid, &cshape, &owners);
}
let mut chunks: HashMap<Vec<usize>, ChunkBuf> = HashMap::new();
for cid in &cids {
let (cstart, cshape, elems) = synth_chunk(cid, shape, chunk_shape);
let elems: Vec<f64> = elems.into_iter().map(|x| x as f64).collect();
chunks.insert(cid.clone(), ChunkBuf { cstart, cshape, elems });
}
let old_out = assemble_reference(sel_idx, chunk_shape, &chunks);
(new_out, old_out, written)
}
fn assert_bit_identical(new_out: &[f64], old_out: &[f64], ctx: &str) {
assert_eq!(new_out.len(), old_out.len(), "{ctx}: length");
for (i, (n, o)) in new_out.iter().zip(old_out).enumerate() {
assert_eq!(n.to_bits(), o.to_bits(), "{ctx}: element {i}: {n} vs {o}");
}
}
type ScatterCase = (Vec<usize>, Vec<usize>, Vec<Vec<usize>>);
#[test]
fn scatter_as_you_go_is_bit_identical_to_collect_then_scatter() {
let cases: Vec<ScatterCase> = vec![
(
vec![7, 5, 11],
vec![3, 2, 4],
vec![vec![6, 0, 3, 3], vec![4, 1], vec![10, 0, 5, 5, 9]],
),
(vec![7, 5, 11], vec![3, 2, 4], vec![(0..7).collect(), (0..5).collect(), (0..11).collect()]),
(vec![4, 9, 6], vec![1, 4, 3], vec![vec![2], (0..9).collect(), (0..6).collect()]),
(vec![10, 10], vec![4, 3], vec![(0..10).step_by(3).collect(), vec![9, 2, 2, 0]]),
(vec![5], vec![8], vec![vec![4, 0, 2]]),
(vec![6], vec![1], vec![vec![5, 5, 0, 3]]),
(
vec![3, 4, 2, 5],
vec![2, 3, 1, 2],
vec![vec![2, 0], vec![3, 1, 0], vec![1, 0], vec![4, 4, 1]],
),
(vec![4, 4], vec![2, 2], vec![vec![1, 2], vec![]]),
];
for (shape, chunk_shape, sel_idx) in cases {
let ctx = format!("shape={shape:?} chunks={chunk_shape:?} sel={sel_idx:?}");
let (new_out, old_out, written) = both_paths(&shape, &chunk_shape, &sel_idx);
assert_bit_identical(&new_out, &old_out, &ctx);
let total: usize = sel_idx.iter().map(Vec::len).product();
assert_eq!(written, total, "{ctx}: cells written");
}
}
#[test]
fn scatter_visits_only_the_selected_chunks() {
let sel_idx = vec![vec![1usize], vec![1, 4], vec![0, 1, 2, 3]];
let chunk_shape = vec![1usize, 2, 4];
let cids = needed_chunks(&sel_idx, &chunk_shape);
assert_eq!(cids, vec![vec![1, 0, 0], vec![1, 2, 0]]);
let owners = chunk_owners(&sel_idx, &chunk_shape, &c_strides(&[1, 2, 4]));
for cid in &cids {
for (d, own) in owners.iter().enumerate() {
assert!(
own.get(&cid[d]).is_some_and(|v| !v.is_empty()),
"chunk {cid:?} owns nothing on dim {d}"
);
}
}
let cells: usize = cids
.iter()
.map(|cid| (0..3).map(|d| owners[d][&cid[d]].len()).product::<usize>())
.sum();
assert_eq!(cells, 8); }
#[test]
fn scatter_handles_f64_and_f32_identically() {
let shape = vec![5usize, 5];
let chunk_shape = vec![2usize, 3];
let sel_idx = vec![vec![4usize, 1, 0], vec![0, 3, 4]];
let out_shape: Vec<usize> = sel_idx.iter().map(Vec::len).collect();
let out_stride = c_strides(&out_shape);
let owners = chunk_owners(&sel_idx, &chunk_shape, &out_stride);
let total: usize = out_shape.iter().product();
let mut from_f32 = vec![0.0f64; total];
let mut from_f64 = vec![0.0f64; total];
for cid in needed_chunks(&sel_idx, &chunk_shape) {
let (_cstart, cshape, elems) = synth_chunk(&cid, &shape, &chunk_shape);
let widened: Vec<f64> = elems.iter().map(|&x| x as f64).collect();
scatter_chunk(&mut from_f32, &elems, &cid, &cshape, &owners);
scatter_chunk(&mut from_f64, &widened, &cid, &cshape, &owners);
}
assert_bit_identical(&from_f32, &from_f64, "f32 vs pre-widened f64");
}
#[test]
fn scatter_preserves_nan_and_zero_bits_verbatim() {
let sel_idx = vec![vec![3usize, 0, 1, 2]];
let chunk_shape = vec![4usize];
let owners = chunk_owners(&sel_idx, &chunk_shape, &c_strides(&[4]));
let elems = vec![-0.0f64, f64::NAN, f64::INFINITY, 0.0];
let mut out = vec![0.0f64; 4];
scatter_chunk(&mut out, &elems, &[0], &[4], &owners);
assert_eq!(out[0].to_bits(), 0.0f64.to_bits()); assert_eq!(out[1].to_bits(), (-0.0f64).to_bits()); assert!(out[2].is_nan()); assert_eq!(out[3], f64::INFINITY); }
}