From 3e33fe18c1acf7e4960313595647d62f0146a77e Mon Sep 17 00:00:00 2001 From: "w.pomp" Date: Thu, 22 Oct 2026 19:50:20 +0200 Subject: [PATCH] - improve Imread slicing - can_open function for Reader trait --- .../py/ndbioimage/ndbioimage_rs/__init__.pyi | 182 +++-- ndbioimage/pyproject.toml | 2 +- ndbioimage/src/axes.rs | 17 +- ndbioimage/src/py/imread.rs | 671 +++++++++++++----- ndbioimage/src/readers.rs | 34 + ndbioimage/src/readers/bioformats_java.rs | 7 + ndbioimage/src/readers/bioformats_rust.rs | 7 + ndbioimage/src/readers/czi.rs | 7 + ndbioimage/src/readers/tiff.rs | 44 +- ndbioimage/src/readers/tiffseq.rs | 20 +- ndbioimage/src/view.rs | 37 + ndbioimage/tests/test_slicing.py | 6 +- ndbioimage/tests/test_ufuncs.py | 38 +- 13 files changed, 808 insertions(+), 264 deletions(-) diff --git a/ndbioimage/py/ndbioimage/ndbioimage_rs/__init__.pyi b/ndbioimage/py/ndbioimage/ndbioimage_rs/__init__.pyi index f8b4cfe..11f57f8 100644 --- a/ndbioimage/py/ndbioimage/ndbioimage_rs/__init__.pyi +++ b/ndbioimage/py/ndbioimage/ndbioimage_rs/__init__.pyi @@ -52,6 +52,8 @@ class Flags: def __new__(cls) -> Flags: ... def __getnewargs__(self) -> None: ... def __getitem__(self, item: builtins.str) -> builtins.bool: ... + def __str__(self) -> builtins.str: ... + def __repr__(self) -> builtins.str: ... class Imread: r""" @@ -211,6 +213,16 @@ class Imread: the name of the tube lens """ @property + def channel_names(self) -> builtins.dict[builtins.int, builtins.str]: + r""" + names of the channels in order + """ + @property + def summary(self) -> builtins.str: + r""" + gives a helpful summary of the recorded experiment + """ + @property def interpolation_order(self) -> typing.Optional[builtins.int]: r""" the bspline order used for interpolation when using transforms @@ -293,7 +305,9 @@ class Imread: r""" reshape the view with a new axis order """ - def squeeze(self) -> numpy.ndarray | int | float: ... + def squeeze( + self, axes: str | typing.Sequence[int | str] = None + ) -> Imread | float | int: ... def close(self) -> None: r""" close the file: does nothing as this is handled automatically @@ -306,7 +320,19 @@ class Imread: r""" change the data type of the view: (u)int(8/16/32) or float(32/64) """ - def __getitem__(self, n: typing.Any) -> typing.Any: + def __call__( + self, + c: typing.Optional[int | slice | numpy.ArrayLike] = None, + z: typing.Optional[int | slice | numpy.ArrayLike] = None, + t: typing.Optional[int | slice | numpy.ArrayLike] = None, + y: typing.Optional[int | slice | numpy.ArrayLike] = None, + x: typing.Optional[int | slice | numpy.ArrayLike] = None, + ) -> typing.Optional[int | Imread]: + r""" + same as im[] but allowing keyword axes, but slices need to made with slice() + also, no ellipsis or new axes + """ + def __getitem__(self, n: typing.Any) -> typing.Optional[int | Imread]: r""" slice the view and return a new view or a single number """ @@ -314,7 +340,7 @@ class Imread: self, dtype: typing.Optional[numpy.typing.DTypeLike] = None, copy: typing.Optional[builtins.bool] = None, - ) -> typing.Any: + ) -> numpy.typing.NDArray: r""" convert to a numpy array, optionally with a different dtype """ @@ -322,139 +348,199 @@ class Imread: r""" check if an item is contained in the view (not implemented) """ - def __lt__(self, other: typing.Any) -> typing.Any: + def __lt__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | bool: r""" element-wise less than comparison """ - def __le__(self, other: typing.Any) -> typing.Any: + def __le__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | bool: r""" element-wise less than or equal comparison """ - def __eq__(self, other: typing.Any) -> typing.Any: + def __eq__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | bool: r""" element-wise equality comparison """ - def __ne__(self, other: typing.Any) -> typing.Any: + def __ne__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | bool: r""" element-wise not equal comparison """ - def __gt__(self, other: typing.Any) -> typing.Any: + def __gt__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | bool: r""" element-wise greater than comparison """ - def __ge__(self, other: typing.Any) -> typing.Any: + def __ge__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | bool: r""" element-wise greater than or equal comparison """ - def __add__(self, other: typing.Any) -> typing.Any: + def __add__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise addition """ - def __radd__(self, other: typing.Any) -> typing.Any: + def __radd__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise addition (reflected) """ - def __sub__(self, other: typing.Any) -> typing.Any: + def __sub__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise subtraction """ - def __rsub__(self, other: typing.Any) -> typing.Any: + def __rsub__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise subtraction (reflected) """ - def __mul__(self, other: typing.Any) -> typing.Any: + def __mul__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise multiplication """ - def __rmul__(self, other: typing.Any) -> typing.Any: + def __rmul__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise multiplication (reflected) """ - def __truediv__(self, other: typing.Any) -> typing.Any: + def __truediv__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise true division """ - def __rtruediv__(self, other: typing.Any) -> typing.Any: + def __rtruediv__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise true division (reflected) """ - def __floordiv__(self, other: typing.Any) -> typing.Any: + def __floordiv__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise floor division """ - def __rfloordiv__(self, other: typing.Any) -> typing.Any: + def __rfloordiv__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise floor division (reflected) """ - def __mod__(self, other: typing.Any) -> typing.Any: + def __mod__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise modulo """ - def __rmod__(self, other: typing.Any) -> typing.Any: + def __rmod__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise modulo (reflected) """ - def __matmul__(self, other: typing.Any) -> typing.Any: + def __matmul__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise matrix multiplication """ - def __rmatmul__(self, other: typing.Any) -> typing.Any: + def __rmatmul__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise matrix multiplication (reflected) """ - def __and__(self, other: typing.Any) -> typing.Any: + def __and__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | bool: r""" element-wise bitwise AND """ - def __rand__(self, other: typing.Any) -> typing.Any: + def __rand__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | bool: r""" element-wise bitwise AND (reflected) """ - def __or__(self, other: typing.Any) -> typing.Any: + def __or__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | bool: r""" element-wise bitwise OR """ - def __ror__(self, other: typing.Any) -> typing.Any: + def __ror__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | bool: r""" element-wise bitwise OR (reflected) """ - def __xor__(self, other: typing.Any) -> typing.Any: + def __xor__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | bool: r""" element-wise bitwise XOR """ - def __rxor__(self, other: typing.Any) -> typing.Any: + def __rxor__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | bool: r""" element-wise bitwise XOR (reflected) """ - def __lshift__(self, other: typing.Any) -> typing.Any: + def __lshift__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise left shift """ - def __rlshift__(self, other: typing.Any) -> typing.Any: + def __rlshift__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise left shift (reflected) """ - def __rshift__(self, other: typing.Any) -> typing.Any: + def __rshift__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise right shift """ - def __rrshift__(self, other: typing.Any) -> typing.Any: + def __rrshift__( + self, other: float | numpy.ArrayLike | Imread + ) -> numpy.typing.NDArray | float: r""" element-wise right shift (reflected) """ - def __neg__(self) -> typing.Any: + def __neg__(self) -> numpy.typing.NDArray | float: r""" element-wise negation """ - def __pos__(self) -> typing.Any: + def __pos__(self) -> numpy.typing.NDArray | float: r""" element-wise positive """ - def __abs__(self) -> typing.Any: + def __abs__(self) -> numpy.typing.NDArray | float: r""" element-wise absolute value """ - def __invert__(self) -> typing.Any: + def __invert__(self) -> numpy.typing.NDArray | float: r""" element-wise bitwise inversion """ @@ -507,13 +593,19 @@ class Imread: r""" the file path as a string """ + def frame( + self, c: builtins.int, z: builtins.int, t: builtins.int + ) -> numpy.typing.NDArray | float: + r""" + retrieve a single frame at czt, sliced accordingly, alias of get_frame + """ def get_frame( self, c: builtins.int, z: builtins.int, t: builtins.int - ) -> typing.Any: + ) -> numpy.typing.NDArray | float: r""" retrieve a single frame at czt, sliced accordingly """ - def flatten(self) -> typing.Any: + def flatten(self) -> numpy.typing.NDArray | float: r""" flatten the view into a 1D numpy array """ @@ -529,6 +621,10 @@ class Imread: r""" find the position of an axis """ + def swapaxes(self, ax0: int | str, ax1: int | str) -> Imread: + r""" + swap two axes + """ def swap_axes(self, ax0: int | str, ax1: int | str) -> Imread: r""" swap two axes @@ -539,7 +635,7 @@ class Imread: r""" permute the order of the axes """ - def as_array(self) -> typing.Any: + def as_array(self) -> numpy.typing.NDArray | float: r""" collect data into a numpy array """ @@ -573,10 +669,6 @@ class Imread: r""" the name of a particular channel """ - def summary(self) -> builtins.str: - r""" - gives a helpful summary of the recorded experiment - """ @staticmethod def get_available_series( path: str | pathlib.Path | Imread, reader: typing.Optional[builtins.str] = None @@ -793,7 +885,7 @@ class Shape: arguments for pickling """ def __getitem__( - self, idx: str | int | None | Ellipsis | slice | list[int] | tuple[int] + self, idx: str | int | None | Ellipsis | slice | typing.Sequence[int] ) -> typing.Optional[int | tuple[int]]: r""" get dimension size by index or axis name diff --git a/ndbioimage/pyproject.toml b/ndbioimage/pyproject.toml index 97652a9..4c3af06 100644 --- a/ndbioimage/pyproject.toml +++ b/ndbioimage/pyproject.toml @@ -35,7 +35,7 @@ ndbioimage_generate_stub = "ndbioimage:ndbioimage_generate_stub" [tool.maturin] python-source = "py" -features = ["python", "bioformats_java", "gpl-formats", "czi", "tiff", "tiffseq", "tiffwrite", "movie", "transforms"] +features = ["python", "bioformats_java", "gpl-formats", "bioformats_rust", "czi", "tiff", "tiffseq", "tiffwrite", "movie", "transforms"] no-default-features = true module-name = "ndbioimage.ndbioimage_rs" include = ["py/ndbioimage/jassets/j4rs*", "py/ndbioimage/deps/libj4rs*"] diff --git a/ndbioimage/src/axes.rs b/ndbioimage/src/axes.rs index 6c9c1be..0c6d9f2 100644 --- a/ndbioimage/src/axes.rs +++ b/ndbioimage/src/axes.rs @@ -314,16 +314,16 @@ impl Index<&Axis> for Shape { impl Index for Shape { type Output = usize; - fn index(&self, dim: usize) -> &Self::Output { - &self[self.order[dim % self.order.len()]] + fn index(&self, index: usize) -> &Self::Output { + &self[self.order[index]] } } impl Index<&usize> for Shape { type Output = usize; - fn index(&self, dim: &usize) -> &Self::Output { - &self[self.order[dim % self.order.len()]] + fn index(&self, index: &usize) -> &Self::Output { + &self[self.order[*index]] } } @@ -406,6 +406,10 @@ impl Shape { } } + pub fn get(&self, index: &usize) -> Option<&usize> { + self.order.get(*index).map(|axis| &self[axis]) + } + /// the number of axes in the shape pub fn len(&self) -> usize { self.order.len() @@ -421,6 +425,11 @@ impl Shape { self.order.iter().map(|axis| self[axis]).collect() } + /// the sizes in cztyx order + pub fn cztyx(&self) -> [usize; 5] { + [self.c, self.z, self.t, self.y, self.x] + } + /// the sizes along the axes as a map from axis to size pub fn to_hashmap(&self) -> HashMap { let mut map = HashMap::new(); diff --git a/ndbioimage/src/py/imread.rs b/ndbioimage/src/py/imread.rs index 5bae44e..3088925 100644 --- a/ndbioimage/src/py/imread.rs +++ b/ndbioimage/src/py/imread.rs @@ -6,7 +6,7 @@ use crate::movie::MovieOptions; use crate::readers::{DynReader, PixelType, Reader}; use crate::view::{Item, View}; use itertools::Itertools; -use ndarray::{Ix0, Ix1, IxDyn, SliceInfoElem}; +use ndarray::{Ix0, Ix1, IxDyn, SliceInfo, SliceInfoElem}; use numpy::{ AllowTypeChange, IntoPyArray, PyArray, PyArrayDescr, PyArrayLike0, PyArrayLike1, PyArrayMethods, dtype, @@ -17,15 +17,15 @@ use pyo3::exceptions::{ PyIndexError, PyKeyError, PyNotImplementedError, PyTypeError, PyValueError, }; use pyo3::prelude::*; -use pyo3::types::{PyBytes, PyEllipsis, PyInt, PyList, PyNone, PySlice, PyString, PyTuple}; +use pyo3::types::{PyBytes, PyEllipsis, PyInt, PyNone, PySlice, PyString, PyTuple}; pub(crate) use pyo3::{ - Bound, IntoPyObject, IntoPyObjectExt, PyAny, PyErr, PyRef, PyResult, Python, pyclass, - pyfunction, pymethods, + Bound, IntoPyObject, IntoPyObjectExt, PyAny, PyRef, PyResult, Python, pyclass, pyfunction, + pymethods, }; use pyo3_stub_gen::derive::*; use pyo3_stub_gen::inventory::submit; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; /// class to read image files, while taking good care of important metadata, @@ -304,9 +304,52 @@ impl PyView { todo!() } - #[gen_stub(override_return_type(type_repr="numpy.ndarray | int | float", imports=("numpy")))] - fn squeeze<'py>(&self, py: Python<'py>) -> PyResult> { - let view = self.view.squeeze()?; + #[gen_stub(override_return_type(type_repr="Imread | float | int", imports=("numpy", "numpy.typing")))] + #[pyo3(signature = (axes = None))] + fn squeeze<'py>( + &self, + py: Python<'py>, + #[gen_stub( + override_type(type_repr="str | typing.Sequence[int | str]", imports=("typing")) + )] + axes: Option>, + ) -> PyResult> { + let view = if let Some(axes) = axes + && !axes.is_none() + { + let view_axes = self.view.axes(); + let axes = axes + .try_iter()? + .map(|a| { + let axis = a?; + if axis.is_instance_of::() { + Ok(axis + .cast_into::()? + .extract::()? + .parse::() + .map_err(Error::from)?) + } else if axis.is_instance_of::() { + let mut index = axis.cast_into::()?.extract::()?; + if index < 0 { + index += view_axes.len() as isize; + } + if index > view_axes.len() as isize { + return Err(PyIndexError::new_err(format!( + "index {} is out of bounds for size {}", + index, + view_axes.len() + ))); + } + Ok(view_axes[index as usize]) + } else { + Err(PyValueError::new_err("cannot convert to axis".to_string())) + } + }) + .collect::>()?; + self.view.squeeze_axes(axes)? + } else { + self.view.squeeze()? + }; if view.ndim() == 0 { Ok(match self.dtype { PixelType::Bool => todo!(), @@ -423,135 +466,112 @@ impl PyView { self.as_type(py, dtype) } + /// same as im[] but allowing keyword axes, but slices need to made with slice() + /// also, no ellipsis or new axes + #[gen_stub(override_return_type(type_repr="typing.Optional[int | Imread]", imports=("typing")))] + #[pyo3(signature = (c = None, z = None, t = None, y = None, x = None))] + fn __call__<'py>( + &self, + py: Python<'py>, + #[gen_stub(override_type(type_repr="typing.Optional[int | slice | numpy.ArrayLike]", imports=("typing")))] + c: Option>, + #[gen_stub(override_type(type_repr="typing.Optional[int | slice | numpy.ArrayLike]", imports=("typing")))] + z: Option>, + #[gen_stub(override_type(type_repr="typing.Optional[int | slice | numpy.ArrayLike]", imports=("typing")))] + t: Option>, + #[gen_stub(override_type(type_repr="typing.Optional[int | slice | numpy.ArrayLike]", imports=("typing")))] + y: Option>, + #[gen_stub(override_type(type_repr="typing.Optional[int | slice | numpy.ArrayLike]", imports=("typing")))] + x: Option>, + ) -> PyResult> { + let mut new_slice = [SliceInfoElem::Slice { + start: 0, + end: None, + step: 1, + }; 5]; + let mut ellipsis = true; + for (i, (n, (s, t))) in new_slice + .iter_mut() + .zip([c, z, t, y, x].iter().zip(self.view.shape().cztyx())) + .enumerate() + { + if let Some(s) = s + && !s.is_none() + { + if s.is_instance_of::() { + return Err(PyValueError::new_err("ellipsis cannot be used as input")); + } else { + *n = parse_slice(s, t, i, 0, &mut ellipsis)?.0[0]; + } + } + } + let slice = SliceInfo::try_from(new_slice).map_err(Error::from)?; + let view = self.view.slice_cztyx(slice)?; + let axes = view.axes(); + let axes = self + .view + .axes() + .iter() + .filter(|ax| axes.contains(ax)) + .copied() + .collect::>(); + let view = view.permute_axes(&axes)?; + if view.ndim() == 0 { + Self::item(py, view, self.dtype) + } else { + PyView { + view, + dtype: self.dtype, + ome: self.ome.clone(), + index: 0, + } + .into_bound_py_any(py) + } + } + /// slice the view and return a new view or a single number + #[gen_stub(override_return_type(type_repr="typing.Optional[int | Imread]", imports=("typing")))] fn __getitem__<'py>( &self, py: Python<'py>, n: Bound<'py, PyAny>, ) -> PyResult> { - // TODO: newaxis let slice: Vec<_> = if n.is_instance_of::() { n.cast_into::()?.into_iter().collect() - } else if n.is_instance_of::() { - n.cast_into::()?.into_iter().collect() } else { vec![n] }; let mut new_slice = Vec::new(); - let mut ellipsis = None; let shape = self.view.shape(); - for (i, (s, t)) in slice.iter().zip(shape.iter()).enumerate() { - if s.is_none() { - new_slice.push(SliceInfoElem::Slice { - start: 0, - end: None, - step: 1, - }); - } else if s.is_instance_of::() { - new_slice.push(SliceInfoElem::Index(s.cast::()?.extract::()?)); - } else if s.is_instance_of::() { - let u = s.cast::()?.indices(*t as isize)?; - new_slice.push(SliceInfoElem::Slice { - start: u.start, - end: Some(u.stop), - step: u.step, - }); - } else if s.is_instance_of::() { - if ellipsis.is_some() { - return Err(PyErr::new::( - "cannot have more than one ellipsis".to_string(), - )); - } - let _ = ellipsis.insert(i); - } else if let Ok(arr_like) = s.extract::>() { - let pyarr: &Bound> = &arr_like; - let mut index = *pyarr.readonly().as_array().into_scalar(); - let index0 = index; - if index < 0 { - index += *t as isize; - } - if (index < 0) || (index >= *t as isize) { - return Err(PyIndexError::new_err(format!( - "index {} is out of bounds for axis {} with size {}", - index0, i, t - ))); - } - new_slice.push(SliceInfoElem::Index(index)); - } else if let Ok(arr_like) = s.extract::>() { - let pyarr: &Bound> = &arr_like; - let read = pyarr.readonly(); - let mut indices = read.as_array().to_vec(); - for index in indices.iter_mut() { - let index0 = *index; - if *index < 0 { - *index += *t as isize; - } - if (*index < 0) || (*index >= *t as isize) { - return Err(PyIndexError::new_err(format!( - "index {} is out of bounds for axis {} with size {}", - index0, i, t - ))); - } - } - if indices.is_empty() { - new_slice.push(SliceInfoElem::Slice { - start: 0, - end: Some(0), - step: 1, - }) - } else { - let d = indices - .windows(2) - .map(|i| i[1] - i[0]) - .collect::>(); - if d.is_empty() { - let index = indices[0]; - new_slice.push(SliceInfoElem::Slice { - start: index, - end: Some(index + 1), - step: 1, - }); - } else if d.len() == 1 { - new_slice.push(SliceInfoElem::Slice { - start: indices[0], - end: indices.last().map(|j| j + 1), - step: d.into_iter().collect::>()[0], - }); - } else { - return Err(PyValueError::new_err("indices array must regularly spaced")); - } - }; - } else { - return Err(PyValueError::new_err(format!( - "cannot convert {:?} to slice", - s - ))); - } - } - if new_slice.len() > shape.len() { + let mut i = 0; + let n_axes = slice + .iter() + .fold(0, |i, s| if s.is_none() { i } else { i + 1 }); + let mut ellipsis = false; + if n_axes > shape.len() { return Err(PyValueError::new_err(format!( - "got more indices ({}) than dimensions ({})", - new_slice.len(), - shape.len() + "too many indices for array: array is {}-dimensional, but {} were indexed", + shape.len(), + n_axes, ))); } - while new_slice.len() < shape.len() { - if let Some(i) = ellipsis { - new_slice.insert( - i, - SliceInfoElem::Slice { - start: 0, - end: None, - step: 1, - }, - ) - } else { - new_slice.push(SliceInfoElem::Slice { - start: 0, - end: None, - step: 1, - }) - } + let n_missing = shape.len() - n_axes; + for s in slice.iter() { + let (ns, n) = parse_slice(s, shape[i], i, n_missing, &mut ellipsis)?; + new_slice.extend_from_slice(&ns); + i += n; + } + for _ in 0..shape.len() + - new_slice + .iter() + .filter(|&i| i != &SliceInfoElem::NewAxis) + .count() + { + new_slice.push(SliceInfoElem::Slice { + start: 0, + end: None, + step: 1, + }) } let view = self.view.slice(new_slice.as_slice())?; if view.ndim() == 0 { @@ -570,11 +590,15 @@ impl PyView { /// convert to a numpy array, optionally with a different dtype #[expect(unused_variables)] #[pyo3(signature = (dtype = None, copy = None))] + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray", imports=("numpy", "numpy.typing")))] fn __array__<'py>( &self, py: Python<'py>, #[gen_stub(override_type(type_repr = "typing.Optional[numpy.typing.DTypeLike]", imports=("typing", "numpy", "numpy.typing") ))] + #[gen_stub(override_type( + type_repr = "numpy.typing.DTypeLike", imports=("numpy.typing") + ))] dtype: Option>, copy: Option, ) -> PyResult> { @@ -591,9 +615,11 @@ impl PyView { } /// element-wise less than comparison + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | bool", imports=("numpy", "numpy.typing")))] fn __lt__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -603,9 +629,11 @@ impl PyView { } /// element-wise less than or equal comparison + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | bool", imports=("numpy", "numpy.typing")))] fn __le__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -615,9 +643,11 @@ impl PyView { } /// element-wise equality comparison + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | bool", imports=("numpy", "numpy.typing")))] fn __eq__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -627,9 +657,11 @@ impl PyView { } /// element-wise not equal comparison + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | bool", imports=("numpy", "numpy.typing")))] fn __ne__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -639,9 +671,11 @@ impl PyView { } /// element-wise greater than comparison + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | bool", imports=("numpy", "numpy.typing")))] fn __gt__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -651,9 +685,11 @@ impl PyView { } /// element-wise greater than or equal comparison + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | bool", imports=("numpy", "numpy.typing")))] fn __ge__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -663,9 +699,11 @@ impl PyView { } /// element-wise addition + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __add__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -675,9 +713,11 @@ impl PyView { } /// element-wise addition (reflected) + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __radd__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -687,9 +727,11 @@ impl PyView { } /// element-wise subtraction + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __sub__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -699,9 +741,11 @@ impl PyView { } /// element-wise subtraction (reflected) + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __rsub__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -711,9 +755,11 @@ impl PyView { } /// element-wise multiplication + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __mul__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -723,9 +769,11 @@ impl PyView { } /// element-wise multiplication (reflected) + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __rmul__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -735,9 +783,11 @@ impl PyView { } /// element-wise true division + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __truediv__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -747,9 +797,11 @@ impl PyView { } /// element-wise true division (reflected) + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __rtruediv__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -759,9 +811,11 @@ impl PyView { } /// element-wise floor division + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __floordiv__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -771,9 +825,11 @@ impl PyView { } /// element-wise floor division (reflected) + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __rfloordiv__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -783,9 +839,11 @@ impl PyView { } /// element-wise modulo + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __mod__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -795,9 +853,11 @@ impl PyView { } /// element-wise modulo (reflected) + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __rmod__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -843,9 +903,11 @@ impl PyView { } /// element-wise matrix multiplication + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __matmul__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -855,9 +917,11 @@ impl PyView { } /// element-wise matrix multiplication (reflected) + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __rmatmul__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -867,9 +931,11 @@ impl PyView { } /// element-wise bitwise AND + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | bool", imports=("numpy", "numpy.typing")))] fn __and__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -879,9 +945,11 @@ impl PyView { } /// element-wise bitwise AND (reflected) + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | bool", imports=("numpy", "numpy.typing")))] fn __rand__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -891,9 +959,11 @@ impl PyView { } /// element-wise bitwise OR + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | bool", imports=("numpy", "numpy.typing")))] fn __or__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -903,9 +973,11 @@ impl PyView { } /// element-wise bitwise OR (reflected) + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | bool", imports=("numpy", "numpy.typing")))] fn __ror__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -915,9 +987,11 @@ impl PyView { } /// element-wise bitwise XOR + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | bool", imports=("numpy", "numpy.typing")))] fn __xor__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -927,9 +1001,11 @@ impl PyView { } /// element-wise bitwise XOR (reflected) + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | bool", imports=("numpy", "numpy.typing")))] fn __rxor__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -939,9 +1015,11 @@ impl PyView { } /// element-wise left shift + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __lshift__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -951,9 +1029,11 @@ impl PyView { } /// element-wise left shift (reflected) + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __rlshift__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -963,9 +1043,11 @@ impl PyView { } /// element-wise right shift + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __rshift__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -975,9 +1057,11 @@ impl PyView { } /// element-wise right shift (reflected) + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __rrshift__<'py>( &self, py: Python<'py>, + #[gen_stub(override_type(type_repr="float | numpy.ArrayLike | Imread", imports=("numpy.typing")))] other: Bound<'py, PyAny>, ) -> PyResult> { let np = PyModule::import(py, "numpy")?; @@ -987,6 +1071,7 @@ impl PyView { } /// element-wise negation + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __neg__<'py>(&self, py: Python<'py>) -> PyResult> { let np = PyModule::import(py, "numpy")?; let a = self.as_array(py)?; @@ -994,6 +1079,7 @@ impl PyView { } /// element-wise positive + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __pos__<'py>(&self, py: Python<'py>) -> PyResult> { let np = PyModule::import(py, "numpy")?; let a = self.as_array(py)?; @@ -1001,6 +1087,7 @@ impl PyView { } /// element-wise absolute value + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __abs__<'py>(&self, py: Python<'py>) -> PyResult> { let np = PyModule::import(py, "numpy")?; let a = self.as_array(py)?; @@ -1008,6 +1095,7 @@ impl PyView { } /// element-wise bitwise inversion + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn __invert__<'py>(&self, py: Python<'py>) -> PyResult> { let np = PyModule::import(py, "numpy")?; let a = self.as_array(py)?; @@ -1123,7 +1211,20 @@ impl PyView { Ok(self.view.path().display().to_string()) } + /// retrieve a single frame at czt, sliced accordingly, alias of get_frame + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] + fn frame<'py>( + &self, + py: Python<'py>, + c: isize, + z: isize, + t: isize, + ) -> PyResult> { + self.get_frame(py, c, z, t) + } + /// retrieve a single frame at czt, sliced accordingly + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn get_frame<'py>( &self, py: Python<'py>, @@ -1202,6 +1303,7 @@ impl PyView { } /// flatten the view into a 1D numpy array + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn flatten<'py>(&self, py: Python<'py>) -> PyResult> { Ok(match self.dtype { PixelType::Bool => todo!(), @@ -1317,18 +1419,23 @@ impl PyView { .axes() .iter() .position(|a| *a == axis) - .ok_or_else(|| { - PyErr::new::(format!("cannot find axis {:?}", axis)) - }) + .ok_or_else(|| PyValueError::new_err(format!("cannot find axis {:?}", axis))) } else if axis.is_instance_of::() { Ok(axis.cast_into::()?.extract::()?) } else { - Err(PyErr::new::( - "cannot convert to axis".to_string(), - )) + Err(PyValueError::new_err("cannot convert to axis".to_string())) } } + /// swap two axes + fn swapaxes( + &self, + #[gen_stub(override_type(type_repr = "int | str"))] ax0: Bound, + #[gen_stub(override_type(type_repr = "int | str"))] ax1: Bound, + ) -> PyResult { + self.swap_axes(ax0, ax1) + } + /// swap two axes fn swap_axes( &self, @@ -1379,6 +1486,7 @@ impl PyView { } /// collect data into a numpy array + #[gen_stub(override_return_type(type_repr="numpy.typing.NDArray | float", imports=("numpy", "numpy.typing")))] fn as_array<'py>(&self, py: Python<'py>) -> PyResult> { Ok(match self.dtype { PixelType::Bool => todo!(), @@ -1826,7 +1934,14 @@ impl PyView { Ok(self.view.channel_name(channel).map(|s| s.to_owned())?) } + /// names of the channels in order + #[getter] + fn channel_names(&self) -> PyResult> { + Ok(self.view.channel_names()?) + } + /// gives a helpful summary of the recorded experiment + #[getter] fn summary(&self) -> PyResult { Ok(self.view.summary()?) } @@ -2025,6 +2140,130 @@ pub(crate) struct PyShape { inner: Shape, } +fn parse_slice( + s: &Bound, + t: usize, + i: usize, + n_missing: usize, + ellipsis: &mut bool, +) -> PyResult<(Vec, usize)> { + if s.is_none() { + Ok((vec![SliceInfoElem::NewAxis], 0)) + } else if s.is_instance_of::() { + let mut index = s.cast::()?.extract::()?; + if index < 0 { + index += t as isize; + } + if (index < 0) || (index >= t as isize) { + return Err(PyIndexError::new_err(format!( + "index {} is out of bounds for axis {} with size {}", + index, i, t + ))); + } + Ok((vec![SliceInfoElem::Index(index)], 1)) + } else if s.is_instance_of::() { + let u = s.cast::()?.indices(t as isize)?; + Ok(( + vec![SliceInfoElem::Slice { + start: u.start, + end: Some(u.stop), + step: u.step, + }], + 1, + )) + } else if s.is_instance_of::() { + if *ellipsis { + return Err(PyIndexError::new_err( + "an index can only have a single ellipsis ('...')", + )); + } + *ellipsis = true; + Ok(( + vec![ + SliceInfoElem::Slice { + start: 0, + end: None, + step: 1 + }; + n_missing + ], + n_missing, + )) + } else if let Ok(arr_like) = s.extract::>() { + let pyarr: &Bound> = &arr_like; + let mut index = *pyarr.readonly().as_array().into_scalar(); + let index0 = index; + if index < 0 { + index += t as isize; + } + if (index < 0) || (index >= t as isize) { + return Err(PyIndexError::new_err(format!( + "index {} is out of bounds for axis {} with size {}", + index0, i, t + ))); + } + Ok((vec![SliceInfoElem::Index(index)], 1)) + } else if let Ok(arr_like) = s.extract::>() { + let pyarr: &Bound> = &arr_like; + let read = pyarr.readonly(); + let mut indices = read.as_array().to_vec(); + for index in indices.iter_mut() { + let index0 = *index; + if *index < 0 { + *index += t as isize; + } + if (*index < 0) || (*index >= t as isize) { + return Err(PyIndexError::new_err(format!( + "index {} is out of bounds for axis {} with size {}", + index0, i, t + ))); + } + } + if indices.is_empty() { + Ok(( + vec![SliceInfoElem::Slice { + start: 0, + end: Some(0), + step: 1, + }], + 1, + )) + } else { + let d = indices + .windows(2) + .map(|i| i[1] - i[0]) + .collect::>(); + if d.is_empty() { + let index = indices[0]; + Ok(( + vec![SliceInfoElem::Slice { + start: index, + end: Some(index + 1), + step: 1, + }], + 1, + )) + } else if d.len() == 1 { + Ok(( + vec![SliceInfoElem::Slice { + start: indices[0], + end: indices.last().map(|j| j + 1), + step: d.into_iter().collect::>()[0], + }], + 1, + )) + } else { + Err(PyValueError::new_err("indices array must regularly spaced")) + } + } + } else { + Err(PyValueError::new_err(format!( + "cannot convert {:?} to slice", + s + ))) + } +} + #[gen_stub_pymethods] #[pymethods] impl PyShape { @@ -2080,14 +2319,39 @@ impl PyShape { /// string representation fn __str__(&self) -> String { - format!("{}", self.inner) + format!( + "({})", + self.inner + .order + .iter() + .map(|ax| match ax { + Axis::C => format!("c: {}", self.inner.c), + Axis::Z => format!("z: {}", self.inner.z), + Axis::T => format!("t: {}", self.inner.t), + Axis::Y => format!("y: {}", self.inner.y), + Axis::X => format!("x: {}", self.inner.x), + Axis::New => "n: 1".to_string(), + }) + .join(", ") + ) } /// detailed representation for debugging fn __repr__(&self) -> String { format!( - "Shape({}, {}, {}, {}, {})", - self.inner.c, self.inner.z, self.inner.t, self.inner.x, self.inner.y + "({})", + self.inner + .order + .iter() + .map(|ax| match ax { + Axis::C => self.inner.c, + Axis::Z => self.inner.z, + Axis::T => self.inner.t, + Axis::Y => self.inner.y, + Axis::X => self.inner.x, + Axis::New => 1, + }) + .join(", ") ) } @@ -2115,68 +2379,83 @@ impl PyShape { &self, py: Python<'py>, #[gen_stub(override_type( - type_repr = "str | int | None | Ellipsis | slice | list[int] | tuple[int]" + type_repr = "str | int | None | Ellipsis | slice | typing.Sequence[int]" ))] idx: Bound<'py, PyAny>, ) -> PyResult> { - let (idx, is_idx) = if idx.is_instance_of::() || idx.is_instance_of::() - { - ((0..self.inner.order.len()).collect(), true) - } else if idx.is_instance_of::() { - let indices = idx - .cast::()? - .indices(self.inner.order.len() as isize)?; - ( - if indices.step > 0 { - (indices.start..indices.stop) - .step_by(indices.step as usize) - .map(|i| i as usize) - .collect::>() - } else { - (indices.stop..indices.start) - .step_by(-indices.step as usize) - .map(|i| i as usize) - .collect::>() - }, - true, - ) - } else if idx.is_instance_of::() { - (idx.cast::()?.extract::>()?, true) - } else if idx.is_instance_of::() { - (idx.cast::()?.extract::>()?, true) - } else if idx.is_instance_of::() { - let s = idx.cast::()?.extract::()?; - ( - s.to_uppercase() - .chars() - .map(|i| match i { - 'C' => Ok(self.inner.c), - 'Z' => Ok(self.inner.z), - 'T' => Ok(self.inner.t), - 'Y' => Ok(self.inner.y), - 'X' => Ok(self.inner.x), - _ => Err(Error::Parse(s.to_string())), + let (idx, is_idx): (Vec, bool) = + if idx.is_instance_of::() || idx.is_instance_of::() { + ((0..self.inner.order.len()).collect(), true) + } else if idx.is_instance_of::() { + let indices = idx + .cast::()? + .indices(self.inner.order.len() as isize)?; + ( + if indices.step > 0 { + (indices.start..indices.stop) + .step_by(indices.step as usize) + .map(|i| i as usize) + .collect::>() + } else { + (indices.stop..indices.start) + .step_by(-indices.step as usize) + .map(|i| i as usize) + .collect::>() + }, + true, + ) + } else if idx.is_instance_of::() { + let s = idx.cast::()?.extract::()?; + ( + s.to_uppercase() + .chars() + .map(|i| match i { + 'C' => Ok(self.inner.c), + 'Z' => Ok(self.inner.z), + 'T' => Ok(self.inner.t), + 'Y' => Ok(self.inner.y), + 'X' => Ok(self.inner.x), + _ => Err(Error::Parse(s.to_string())), + }) + .collect::, _>>()?, + false, + ) + } else if let Ok(idx) = idx.try_iter() { + let len = self.inner.order.len() as isize; + ( + idx.map(|j| { + let mut i = j?.extract::()?; + if i < 0 { + i += len + }; + if i < 0 || i >= len { + Err(PyIndexError::new_err(format!( + "index {} is out of bounds for size {}", + i, len + ))) + } else { + Ok(i as usize) + } }) - .collect::, _>>()?, - false, - ) - } else if idx.is_instance_of::() { - let i = idx.cast::()?.extract::()?; - let len = self.inner.order.len() as isize; - let i = if i < 0 { i + len } else { i }; - if i < 0 || i >= len { - return Err(PyIndexError::new_err(format!( - "index {} is out of bounds for size {}", - i, len - ))); - } - (vec![i as usize], true) - } else { - return Err(PyErr::new::(format!( - "Unknown type: {:?}", - idx - ))); - }; + .collect::>()?, + true, + ) + } else if idx.is_instance_of::() { + let len = self.inner.order.len() as isize; + let mut i = idx.cast::()?.extract::()?; + if i < 0 { + i += len + }; + if i < 0 || i >= len { + return Err(PyIndexError::new_err(format!( + "index {} is out of bounds for size {}", + i, len + ))); + } + (vec![i as usize], true) + } else { + return Err(PyTypeError::new_err(format!("Unknown type: {:?}", idx))); + }; let shape = if is_idx { let mut shape = Vec::new(); for axis in &self.inner.order { @@ -2292,6 +2571,22 @@ impl PyFlags { } } + fn __str__(&self) -> String { + format!( + "C_CONTIGUOUS : {}\nF_CONTIGUOUS : {}\nOWNDATA : {}\nWRITEABLE : {}\nALIGNED : {}\nWRITEBACKIFCOPY : {}", + self.c_contiguous, + self.f_contiguous, + self.owndata, + self.writable, + self.aligned, + self.writebackifcopy + ) + } + + fn __repr__(&self) -> String { + self.__str__() + } + #[getter] fn fnc(&self) -> bool { self.c_contiguous && !self.f_contiguous diff --git a/ndbioimage/src/readers.rs b/ndbioimage/src/readers.rs index 2ae3a65..d87eb78 100644 --- a/ndbioimage/src/readers.rs +++ b/ndbioimage/src/readers.rs @@ -188,6 +188,12 @@ pub trait Reader: Clone + Sized + Debug + Send + Hash + Into { where P: AsRef; + /// a simple fast test to see if the reader can open a path without parsing the whole file + /// not a guarantee of success, but rather an early failure + fn can_open

(path: P) -> Result + where + P: AsRef; + /// the name of the reader type fn reader_name(&self) -> &'static str { type_name::() @@ -478,6 +484,34 @@ impl Reader for DynReader { )) } + fn can_open

(path: P) -> Result + where + P: AsRef, + { + let mut can_open = false; + #[cfg(feature = "tiff")] + { + can_open |= tiff::TiffReader::can_open(&path)?; + } + #[cfg(feature = "tiffseq")] + { + can_open |= tiffseq::TiffSeqReader::can_open(&path)?; + } + #[cfg(feature = "czi")] + { + can_open |= czi::CziReader::can_open(&path)?; + } + #[cfg(feature = "bioformats_rust")] + { + can_open |= bioformats_rust::BioFormatsRustReader::can_open(&path)?; + } + #[cfg(feature = "bioformats_java")] + { + can_open |= bioformats_java::BioFormatsJavaReader::can_open(&path)?; + } + Ok(can_open) + } + fn reader_name(&self) -> &'static str { match self { #[cfg(feature = "tiff")] diff --git a/ndbioimage/src/readers/bioformats_java.rs b/ndbioimage/src/readers/bioformats_java.rs index b7fc7a6..719fddf 100644 --- a/ndbioimage/src/readers/bioformats_java.rs +++ b/ndbioimage/src/readers/bioformats_java.rs @@ -666,6 +666,13 @@ impl Reader for BioFormatsJavaReader { Ok(new) } + fn can_open

(_path: P) -> Result + where + P: AsRef, + { + Ok(true) + } + fn metadata(&self) -> Result { self.get_ome() } diff --git a/ndbioimage/src/readers/bioformats_rust.rs b/ndbioimage/src/readers/bioformats_rust.rs index a23290d..9deca4f 100644 --- a/ndbioimage/src/readers/bioformats_rust.rs +++ b/ndbioimage/src/readers/bioformats_rust.rs @@ -274,6 +274,13 @@ impl Reader for BioFormatsRustReader { Ok(new) } + fn can_open

(path: P) -> Result + where + P: AsRef, + { + Ok(path.as_ref().extension() != Some(std::ffi::OsStr::new("czi"))) + } + fn metadata(&self) -> Result { let reader = self.get_reader()?; let metadata = reader.metadata(); diff --git a/ndbioimage/src/readers/czi.rs b/ndbioimage/src/readers/czi.rs index de9a0e0..c71aeb6 100644 --- a/ndbioimage/src/readers/czi.rs +++ b/ndbioimage/src/readers/czi.rs @@ -215,6 +215,13 @@ impl Reader for CziReader { Ok(new) } + fn can_open

(path: P) -> Result + where + P: AsRef, + { + Ok(path.as_ref().extension() == Some(std::ffi::OsStr::new("czi"))) + } + fn metadata(&self) -> Result { let mut ome = Ome::default(); let xml = xmltree::Element::parse(self.metadata_xml()?.as_bytes())?; diff --git a/ndbioimage/src/readers/tiff.rs b/ndbioimage/src/readers/tiff.rs index 8cd9797..0c26eb9 100644 --- a/ndbioimage/src/readers/tiff.rs +++ b/ndbioimage/src/readers/tiff.rs @@ -232,6 +232,24 @@ impl Reader for TiffReader { Ok(new) } + fn can_open

(path: P) -> Result + where + P: AsRef, + { + let path = path.as_ref(); + let mut file = File::open(path)?; + let mut sig = [0; 2]; + file.read_exact(&mut sig)?; + if sig == [73, 73] { + file.read_exact(&mut sig)?; + Ok((sig[0] == 42) || (sig[0] == 43)) + } else if sig == [77, 77] { + Ok((sig[1] == 42) || (sig[1] == 43)) + } else { + Ok(false) + } + } + fn metadata(&self) -> Result { let mut reader = self.get_reader()?; let image_description = reader.current_ifd().get_tag(Tag::ImageDescription); @@ -466,18 +484,36 @@ impl Reader for TiffReader { &self.pixel_type } - fn get_available_positions

(_path: P, _series: usize) -> Result, Error> + fn get_available_positions

(path: P, _series: usize) -> Result, Error> where P: AsRef, { - Ok(HashSet::from([0])) + let path = path.as_ref(); + if Self::can_open(path)? { + Ok(HashSet::from([0])) + } else { + Err(Error::InvalidReader( + "TiffReader".to_string(), + path.to_string_lossy().to_string(), + "it is not a tiff".to_string(), + )) + } } - fn get_available_series

(_path: P) -> Result, Error> + fn get_available_series

(path: P) -> Result, Error> where P: AsRef, { - Ok(HashSet::from([0])) + let path = path.as_ref(); + if Self::can_open(path)? { + Ok(HashSet::from([0])) + } else { + Err(Error::InvalidReader( + "TiffReader".to_string(), + path.to_string_lossy().to_string(), + "it is not a tiff".to_string(), + )) + } } } diff --git a/ndbioimage/src/readers/tiffseq.rs b/ndbioimage/src/readers/tiffseq.rs index 03faddb..3beffc5 100644 --- a/ndbioimage/src/readers/tiffseq.rs +++ b/ndbioimage/src/readers/tiffseq.rs @@ -239,6 +239,13 @@ impl Reader for TiffSeqReader { }) } + fn can_open

(path: P) -> Result + where + P: AsRef, + { + Ok(path.as_ref().is_dir()) + } + fn metadata(&self) -> Result { let mut ome = Ome::default(); let info = self.metadata_map.get("Info").and_then(|v| v.as_mapping()); @@ -580,11 +587,20 @@ impl Reader for TiffSeqReader { &self.pixel_type } - fn get_available_positions

(_path: P, _series: usize) -> Result, Error> + fn get_available_positions

(path: P, _series: usize) -> Result, Error> where P: AsRef, { - Ok(HashSet::from([0])) + let path = path.as_ref(); + if Self::can_open(path)? { + Ok(HashSet::from([0])) + } else { + Err(Error::InvalidReader( + "TiffSeqReader".to_string(), + path.to_string_lossy().to_string(), + "not a tiff sequence".to_string(), + )) + } } fn get_available_series

(path: P) -> Result, Error> diff --git a/ndbioimage/src/view.rs b/ndbioimage/src/view.rs index 7568838..46c435b 100644 --- a/ndbioimage/src/view.rs +++ b/ndbioimage/src/view.rs @@ -284,6 +284,21 @@ impl View { .ok_or(Error::NoChannel(channel)) } + /// names of the channels in order + pub fn channel_names(&self) -> Result, Error> { + if self.channel_names.get().is_none() { + let names = self.metadata()?.image[0] + .pixels + .channel + .iter() + .enumerate() + .map(|(i, c)| (i, c.name.clone().unwrap_or_else(|| format!("{}", i)))) + .collect::>(); + let _ = self.channel_names.set(names); + } + Ok(self.channel_names.get().cloned().unwrap_or_default()) + } + /// set the maximum number of frames the process-wide cache may hold, /// evicting the least recently used frames pub fn with_cache_capacity(self, capacity: usize) -> Self { @@ -404,6 +419,28 @@ impl View { view.slice(slice.as_slice()) } + /// remove axes of size 1 in axes + pub fn squeeze_axes(&self, axes: HashSet) -> Result, Error> { + let view = self.clone().into_dyn(); + let slice: Vec<_> = self + .shape() + .into_iter() + .zip(self.axes().iter()) + .map(|(s, a)| { + if s == 1 && axes.contains(a) { + SliceInfoElem::Index(0) + } else { + SliceInfoElem::Slice { + start: 0, + end: None, + step: 1, + } + } + }) + .collect(); + view.slice(slice.as_slice()) + } + pub(crate) fn op_axes(&self) -> Vec { self.operations.keys().cloned().collect() } diff --git a/ndbioimage/tests/test_slicing.py b/ndbioimage/tests/test_slicing.py index 3fa9a57..2130f23 100644 --- a/ndbioimage/tests/test_slicing.py +++ b/ndbioimage/tests/test_slicing.py @@ -26,8 +26,10 @@ def image(array): @pytest.mark.parametrize( "s", - combinations_with_replacement( - (0, -1, 1, slice(None), slice(0, 1), slice(-1, 0), slice(1, 1)), 5 + list( + combinations_with_replacement( + (0, -1, 1, slice(None), slice(0, 1), slice(-1, 0), slice(1, 1)), 5 + ) ), ) def test_slicing(s, image, array): diff --git a/ndbioimage/tests/test_ufuncs.py b/ndbioimage/tests/test_ufuncs.py index 15424c8..21bfeb4 100644 --- a/ndbioimage/tests/test_ufuncs.py +++ b/ndbioimage/tests/test_ufuncs.py @@ -26,24 +26,26 @@ def image(array): @pytest.mark.parametrize( "fun_and_axis", - product( - ( - np.sum, - np.nansum, - np.min, - np.nanmin, - np.max, - np.nanmax, - np.argmin, - np.argmax, - np.mean, - np.nanmean, - np.var, - np.nanvar, - np.std, - np.nanstd, - ), - (None, 0, 1, 2, 3, 4), + list( + product( + ( + np.sum, + np.nansum, + np.min, + np.nanmin, + np.max, + np.nanmax, + np.argmin, + np.argmax, + np.mean, + np.nanmean, + np.var, + np.nanvar, + np.std, + np.nanstd, + ), + (None, 0, 1, 2, 3, 4), + ) ), ) def test_ufuncs(fun_and_axis, image, array):