use std::num::NonZeroU64;
use std::sync::Arc;
use zarrs::array::codec::api::{
ArrayBytes, ArrayCodecTraits, ArrayPartialDecoderTraits, ArrayToArrayCodecTraits, Codec,
CodecError, CodecMetadataOptions, CodecOptions, CodecPluginV3, CodecTraits, CodecTraitsV3,
PartialDecoderCapability, PartialEncoderCapability, RecommendedConcurrency,
};
use zarrs::array::data_type::{
Int8DataType, Int16DataType, Int32DataType, Int64DataType, UInt8DataType, UInt16DataType,
UInt32DataType, UInt64DataType,
};
use zarrs::array::{ArraySubset, DataType, FillValue, Indexer, data_type};
use zarrs::metadata::Configuration;
use zarrs::metadata::v3::MetadataV3;
use zarrs::plugin::{PluginCreateError, ZarrVersion};
use ndic_lift::NdLiftConfig;
#[derive(Clone, Debug)]
pub struct NdLiftCodec {
config: NdLiftConfig,
}
zarrs::plugin::impl_extension_aliases!(NdLiftCodec, v3: "nd_lift", []);
inventory::submit! {
CodecPluginV3::new::<NdLiftCodec>()
}
impl CodecTraitsV3 for NdLiftCodec {
fn create(metadata: &MetadataV3) -> Result<Codec, PluginCreateError> {
let configuration: Configuration = metadata.configuration().cloned().unwrap_or_default();
let codec = Arc::new(Self::new_with_configuration(&configuration)?);
Ok(Codec::ArrayToArray(codec))
}
}
impl NdLiftCodec {
pub fn new(config: NdLiftConfig) -> Result<Self, PluginCreateError> {
config
.validate_semantics()
.map_err(|err| PluginCreateError::Other(err.to_string()))?;
Ok(Self { config })
}
pub fn new_with_configuration(
configuration: &Configuration,
) -> Result<Self, PluginCreateError> {
let config: NdLiftConfig = configuration
.to_typed()
.map_err(|err| PluginCreateError::Other(format!("nd_lift configuration: {err}")))?;
Self::new(config)
}
}
enum Plane {
I32,
I64,
}
fn plane_of(data_type: &DataType) -> Result<Plane, CodecError> {
if data_type.is::<UInt8DataType>()
|| data_type.is::<Int8DataType>()
|| data_type.is::<UInt16DataType>()
|| data_type.is::<Int16DataType>()
|| data_type.is::<UInt32DataType>()
|| data_type.is::<Int32DataType>()
{
Ok(Plane::I32)
} else if data_type.is::<UInt64DataType>() || data_type.is::<Int64DataType>() {
Ok(Plane::I64)
} else {
Err(CodecError::UnsupportedDataType(
data_type.clone(),
ndic_lift::CODEC_NAME.to_string(),
))
}
}
fn shape_usize(shape: &[NonZeroU64]) -> Result<Vec<usize>, CodecError> {
shape
.iter()
.map(|d| {
usize::try_from(d.get())
.map_err(|_| CodecError::Other(format!("chunk extent {d} exceeds usize")))
})
.collect()
}
trait PlaneConvert<P>: Sized {
fn widen_bytes(bytes: &[u8]) -> Result<Vec<P>, CodecError>;
fn narrow(plane: &[P]) -> Result<Vec<Self>, CodecError>;
}
macro_rules! plane_convert_identity {
($t:ty) => {
impl PlaneConvert<$t> for $t {
fn widen_bytes(bytes: &[u8]) -> Result<Vec<$t>, CodecError> {
Ok(bytemuck::pod_collect_to_vec(bytes))
}
fn narrow(plane: &[$t]) -> Result<Vec<$t>, CodecError> {
Ok(plane.to_vec())
}
}
};
}
macro_rules! plane_convert_narrower {
($in:ty => $p:ty) => {
impl PlaneConvert<$p> for $in {
fn widen_bytes(bytes: &[u8]) -> Result<Vec<$p>, CodecError> {
Ok(bytes
.chunks_exact(size_of::<$in>())
.map(|c| <$p>::from(bytemuck::pod_read_unaligned::<$in>(c)))
.collect())
}
fn narrow(plane: &[$p]) -> Result<Vec<$in>, CodecError> {
const LO: $p = <$in>::MIN as $p;
const HI: $p = <$in>::MAX as $p;
let lo = plane.iter().copied().min().unwrap_or(LO);
let hi = plane.iter().copied().max().unwrap_or(HI);
if lo < LO || hi > HI {
return Err(narrow_error(lo.into(), hi.into(), stringify!($in)));
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let out = plane.iter().map(|&v| v as $in).collect();
Ok(out)
}
}
};
}
macro_rules! plane_convert_unsigned_full_width {
($in:ty => $p:ty) => {
impl PlaneConvert<$p> for $in {
fn widen_bytes(bytes: &[u8]) -> Result<Vec<$p>, CodecError> {
#[allow(clippy::cast_sign_loss)]
const PLANE_MAX: $in = <$p>::MAX as $in;
let decode = |c| bytemuck::pod_read_unaligned::<$in>(c);
let max = bytes
.chunks_exact(size_of::<$in>())
.map(decode)
.max()
.unwrap_or(0);
if max > PLANE_MAX {
return Err(CodecError::Other(format!(
"nd_lift overflow budget: input value {max} does not fit the widened \
{} coefficient plane",
stringify!($p),
)));
}
#[allow(clippy::cast_possible_wrap)]
let out = bytes
.chunks_exact(size_of::<$in>())
.map(|c| decode(c) as $p)
.collect();
Ok(out)
}
fn narrow(plane: &[$p]) -> Result<Vec<$in>, CodecError> {
let lo = plane.iter().copied().min().unwrap_or(0);
if lo < 0 {
let hi = plane.iter().copied().max().unwrap_or(0);
return Err(narrow_error(lo.into(), hi.into(), stringify!($in)));
}
#[allow(clippy::cast_sign_loss)]
let out = plane.iter().map(|&v| v as $in).collect();
Ok(out)
}
}
};
}
plane_convert_identity!(i32);
plane_convert_identity!(i64);
plane_convert_narrower!(u8 => i32);
plane_convert_narrower!(i8 => i32);
plane_convert_narrower!(u16 => i32);
plane_convert_narrower!(i16 => i32);
plane_convert_unsigned_full_width!(u32 => i32);
plane_convert_unsigned_full_width!(u64 => i64);
fn narrow_error(lo: i128, hi: i128, dtype: &str) -> CodecError {
CodecError::Other(format!(
"nd_lift decode: coefficient range [{lo}, {hi}] does not narrow back to {dtype} \
(corrupt or mismatched chunk)"
))
}
fn transform_bytes<In, P>(
bytes: &[u8],
shape: &[usize],
config: &NdLiftConfig,
forward: bool,
) -> Result<Vec<u8>, CodecError>
where
In: bytemuck::Pod + PlaneConvert<P>,
P: ndic_lift::PlaneSample + bytemuck::Pod,
{
let n: usize = shape.iter().product();
if forward {
if bytes.len() != n * size_of::<In>() {
return Err(CodecError::Other(format!(
"nd_lift encode: got {} bytes for {n} elements of {} bytes",
bytes.len(),
size_of::<In>()
)));
}
let mut plane = In::widen_bytes(bytes)?;
ndic_lift::forward(&mut plane, shape, &config.transforms)
.map_err(|err| CodecError::Other(err.to_string()))?;
Ok(bytemuck::cast_slice(&plane).to_vec())
} else {
if bytes.len() != n * size_of::<P>() {
return Err(CodecError::Other(format!(
"nd_lift decode: got {} bytes for {n} coefficients of {} bytes",
bytes.len(),
size_of::<P>()
)));
}
let mut plane: Vec<P> = bytemuck::pod_collect_to_vec(bytes);
ndic_lift::inverse(&mut plane, shape, &config.transforms)
.map_err(|err| CodecError::Other(err.to_string()))?;
let output = In::narrow(&plane)?;
Ok(bytemuck::cast_slice(&output).to_vec())
}
}
fn transform_dispatch(
bytes: &[u8],
shape: &[usize],
data_type: &DataType,
config: &NdLiftConfig,
forward: bool,
) -> Result<Vec<u8>, CodecError> {
if data_type.is::<UInt8DataType>() {
transform_bytes::<u8, i32>(bytes, shape, config, forward)
} else if data_type.is::<Int8DataType>() {
transform_bytes::<i8, i32>(bytes, shape, config, forward)
} else if data_type.is::<UInt16DataType>() {
transform_bytes::<u16, i32>(bytes, shape, config, forward)
} else if data_type.is::<Int16DataType>() {
transform_bytes::<i16, i32>(bytes, shape, config, forward)
} else if data_type.is::<UInt32DataType>() {
transform_bytes::<u32, i32>(bytes, shape, config, forward)
} else if data_type.is::<Int32DataType>() {
transform_bytes::<i32, i32>(bytes, shape, config, forward)
} else if data_type.is::<UInt64DataType>() {
transform_bytes::<u64, i64>(bytes, shape, config, forward)
} else if data_type.is::<Int64DataType>() {
transform_bytes::<i64, i64>(bytes, shape, config, forward)
} else {
Err(CodecError::UnsupportedDataType(
data_type.clone(),
ndic_lift::CODEC_NAME.to_string(),
))
}
}
impl CodecTraits for NdLiftCodec {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn configuration(
&self,
_version: ZarrVersion,
_options: &CodecMetadataOptions,
) -> Option<Configuration> {
match serde_json::to_value(&self.config) {
Ok(serde_json::Value::Object(map)) => Some(Configuration::from(map)),
_ => None,
}
}
fn partial_decoder_capability(&self) -> PartialDecoderCapability {
PartialDecoderCapability {
partial_read: true,
partial_decode: true,
}
}
fn partial_encoder_capability(&self) -> PartialEncoderCapability {
PartialEncoderCapability {
partial_encode: false,
}
}
}
impl ArrayCodecTraits for NdLiftCodec {
fn recommended_concurrency(
&self,
_shape: &[NonZeroU64],
_data_type: &DataType,
) -> Result<RecommendedConcurrency, CodecError> {
Ok(RecommendedConcurrency::new_maximum(1))
}
}
struct NdLiftPartialDecoder {
shape: Vec<u64>,
data_type: DataType,
chunk: ArrayBytes<'static>,
}
impl NdLiftPartialDecoder {
fn new(
codec: &NdLiftCodec,
input_handle: &dyn ArrayPartialDecoderTraits,
shape: &[NonZeroU64],
data_type: &DataType,
fill_value: &FillValue,
options: &CodecOptions,
) -> Result<Self, CodecError> {
let shape_u64: Vec<u64> = shape.iter().map(|d| d.get()).collect();
let coefficients = input_handle
.partial_decode(&ArraySubset::new_with_shape(shape_u64.clone()), options)?;
let chunk = codec
.decode(coefficients, shape, data_type, fill_value, options)?
.into_owned();
Ok(Self {
shape: shape_u64,
data_type: data_type.clone(),
chunk,
})
}
}
impl ArrayPartialDecoderTraits for NdLiftPartialDecoder {
fn data_type(&self) -> &DataType {
&self.data_type
}
fn exists(&self) -> Result<bool, zarrs::storage::StorageError> {
Ok(true)
}
fn size_held(&self) -> usize {
self.chunk.size()
}
fn partial_decode(
&self,
indexer: &dyn Indexer,
_options: &CodecOptions,
) -> Result<ArrayBytes<'_>, CodecError> {
self.chunk
.extract_array_subset(indexer, &self.shape, &self.data_type)
}
fn supports_partial_decode(&self) -> bool {
true
}
}
impl ArrayToArrayCodecTraits for NdLiftCodec {
fn into_dyn(self: Arc<Self>) -> Arc<dyn ArrayToArrayCodecTraits> {
self as Arc<dyn ArrayToArrayCodecTraits>
}
fn partial_decoder(
self: Arc<Self>,
input_handle: Arc<dyn ArrayPartialDecoderTraits>,
shape: &[NonZeroU64],
data_type: &DataType,
fill_value: &FillValue,
options: &CodecOptions,
) -> Result<Arc<dyn ArrayPartialDecoderTraits>, CodecError> {
Ok(Arc::new(NdLiftPartialDecoder::new(
&self,
&*input_handle,
shape,
data_type,
fill_value,
options,
)?))
}
fn encoded_data_type(&self, decoded_data_type: &DataType) -> Result<DataType, CodecError> {
Ok(match plane_of(decoded_data_type)? {
Plane::I32 => data_type::int32(),
Plane::I64 => data_type::int64(),
})
}
fn encoded_fill_value(
&self,
decoded_data_type: &DataType,
decoded_fill_value: &FillValue,
) -> Result<FillValue, CodecError> {
let bytes = transform_dispatch(
decoded_fill_value.as_ne_bytes(),
&[1],
decoded_data_type,
&NdLiftConfig::new(Vec::new()),
true,
)?;
Ok(FillValue::new(bytes))
}
fn encode<'a>(
&self,
bytes: ArrayBytes<'a>,
shape: &[NonZeroU64],
data_type: &DataType,
_fill_value: &FillValue,
_options: &CodecOptions,
) -> Result<ArrayBytes<'a>, CodecError> {
let shape = shape_usize(shape)?;
self.config
.validate(shape.len())
.map_err(|err| CodecError::Other(err.to_string()))?;
let fixed = bytes.into_fixed()?;
let out = transform_dispatch(&fixed, &shape, data_type, &self.config, true)?;
Ok(ArrayBytes::from(out))
}
fn decode<'a>(
&self,
bytes: ArrayBytes<'a>,
shape: &[NonZeroU64],
data_type: &DataType,
_fill_value: &FillValue,
_options: &CodecOptions,
) -> Result<ArrayBytes<'a>, CodecError> {
let shape = shape_usize(shape)?;
self.config
.validate(shape.len())
.map_err(|err| CodecError::Other(err.to_string()))?;
let fixed = bytes.into_fixed()?;
let out = transform_dispatch(&fixed, &shape, data_type, &self.config, false)?;
Ok(ArrayBytes::from(out))
}
}