1#![allow(clippy::multiple_crate_versions)] use std::{borrow::Cow, fmt};
37
38use ndarray::{Array, Array1, ArrayView, Dimension, Zip};
39use numcodecs::{
40 AnyArray, AnyArrayAssignError, AnyArrayDType, AnyArrayView, AnyArrayViewMut, AnyCowArray,
41 Codec, StaticCodec, StaticCodecConfig, StaticCodecVersion,
42};
43use schemars::JsonSchema;
44use serde::{Deserialize, Serialize};
45use thiserror::Error;
46
47#[cfg(test)]
48use ::serde_json as _;
49
50mod ffi;
51
52type ZfpCodecVersion = StaticCodecVersion<0, 1, 0>;
53
54#[derive(Clone, Serialize, Deserialize, JsonSchema)]
55#[schemars(deny_unknown_fields)]
57pub struct ZfpCodec {
59 #[serde(flatten)]
61 pub mode: ZfpCompressionMode,
62 #[serde(default, rename = "_version")]
64 pub version: ZfpCodecVersion,
65}
66
67#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
68#[serde(tag = "mode")]
69#[serde(deny_unknown_fields)]
70pub enum ZfpCompressionMode {
72 #[serde(rename = "expert")]
73 Expert {
75 min_bits: u32,
77 max_bits: u32,
79 max_prec: u32,
81 min_exp: i32,
86 },
87 #[serde(rename = "fixed-rate")]
92 FixedRate {
93 rate: f64,
95 },
96 #[serde(rename = "fixed-precision")]
100 FixedPrecision {
101 precision: u32,
103 },
104 #[serde(rename = "fixed-accuracy")]
109 FixedAccuracy {
110 tolerance: f64,
112 },
113 #[serde(rename = "reversible")]
116 Reversible,
117}
118
119impl Codec for ZfpCodec {
120 type Error = ZfpCodecError;
121
122 fn encode(&self, data: AnyCowArray) -> Result<AnyArray, Self::Error> {
123 if matches!(data.dtype(), AnyArrayDType::I32 | AnyArrayDType::I64)
124 && matches!(
125 self.mode,
126 ZfpCompressionMode::FixedAccuracy { tolerance: _ }
127 )
128 {
129 return Err(ZfpCodecError::FixedAccuracyModeIntegerData);
130 }
131
132 match data {
133 AnyCowArray::I32(data) => Ok(AnyArray::U8(
134 Array1::from(compress(data.view(), &self.mode)?).into_dyn(),
135 )),
136 AnyCowArray::I64(data) => Ok(AnyArray::U8(
137 Array1::from(compress(data.view(), &self.mode)?).into_dyn(),
138 )),
139 AnyCowArray::F32(data) => Ok(AnyArray::U8(
140 Array1::from(compress(data.view(), &self.mode)?).into_dyn(),
141 )),
142 AnyCowArray::F64(data) => Ok(AnyArray::U8(
143 Array1::from(compress(data.view(), &self.mode)?).into_dyn(),
144 )),
145 encoded => Err(ZfpCodecError::UnsupportedDtype(encoded.dtype())),
146 }
147 }
148
149 fn decode(&self, encoded: AnyCowArray) -> Result<AnyArray, Self::Error> {
150 let AnyCowArray::U8(encoded) = encoded else {
151 return Err(ZfpCodecError::EncodedDataNotBytes {
152 dtype: encoded.dtype(),
153 });
154 };
155
156 if !matches!(encoded.shape(), [_]) {
157 return Err(ZfpCodecError::EncodedDataNotOneDimensional {
158 shape: encoded.shape().to_vec(),
159 });
160 }
161
162 decompress(&AnyCowArray::U8(encoded).as_bytes())
163 }
164
165 fn decode_into(
166 &self,
167 encoded: AnyArrayView,
168 decoded: AnyArrayViewMut,
169 ) -> Result<(), Self::Error> {
170 let AnyArrayView::U8(encoded) = encoded else {
171 return Err(ZfpCodecError::EncodedDataNotBytes {
172 dtype: encoded.dtype(),
173 });
174 };
175
176 if !matches!(encoded.shape(), [_]) {
177 return Err(ZfpCodecError::EncodedDataNotOneDimensional {
178 shape: encoded.shape().to_vec(),
179 });
180 }
181
182 decompress_into(&AnyArrayView::U8(encoded).as_bytes(), decoded)
183 }
184}
185
186impl StaticCodec for ZfpCodec {
187 const CODEC_ID: &'static str = "zfp.rs";
188
189 type Config<'de> = Self;
190
191 fn from_config(config: Self::Config<'_>) -> Self {
192 config
193 }
194
195 fn get_config(&self) -> StaticCodecConfig<Self> {
196 StaticCodecConfig::from(self)
197 }
198}
199
200#[derive(Debug, Error)]
201pub enum ZfpCodecError {
203 #[error("Zfp does not support the dtype {0}")]
205 UnsupportedDtype(AnyArrayDType),
206 #[error("Zfp does not support the fixed accuracy mode for integer data")]
208 FixedAccuracyModeIntegerData,
209 #[error("Zfp only supports 1-4 dimensional data but found shape {shape:?}")]
211 ExcessiveDimensionality {
212 shape: Vec<usize>,
214 },
215 #[error("Zfp was configured with an invalid expert mode {mode:?}")]
217 InvalidExpertMode {
218 mode: ZfpCompressionMode,
220 },
221 #[error(
224 "Zfp does not support non-finite (infinite or NaN) floating point data in non-reversible lossy compression"
225 )]
226 NonFiniteData,
227 #[error("Zfp failed to encode the header")]
229 HeaderEncodeFailed,
230 #[error("Zfp failed to encode the array metadata header")]
232 MetaHeaderEncodeFailed {
233 source: ZfpHeaderError,
235 },
236 #[error("Zfp failed to encode the data")]
238 ZfpEncodeFailed,
239 #[error(
242 "Zfp can only decode one-dimensional byte arrays but received an array of dtype {dtype}"
243 )]
244 EncodedDataNotBytes {
245 dtype: AnyArrayDType,
247 },
248 #[error(
251 "Zfp can only decode one-dimensional byte arrays but received a byte array of shape {shape:?}"
252 )]
253 EncodedDataNotOneDimensional {
254 shape: Vec<usize>,
256 },
257 #[error("Zfp failed to decode the header")]
259 HeaderDecodeFailed,
260 #[error("Zfp failed to decode the array metadata header")]
262 MetaHeaderDecodeFailed {
263 source: ZfpHeaderError,
265 },
266 #[error("ZfpCodec cannot decode into the provided array")]
268 MismatchedDecodeIntoArray {
269 #[from]
271 source: AnyArrayAssignError,
272 },
273 #[error("Zfp failed to decode the data")]
275 ZfpDecodeFailed,
276}
277
278#[derive(Debug, Error)]
279#[error(transparent)]
280pub struct ZfpHeaderError(postcard::Error);
282
283pub fn compress<T: ffi::ZfpCompressible, D: Dimension>(
299 data: ArrayView<T, D>,
300 mode: &ZfpCompressionMode,
301) -> Result<Vec<u8>, ZfpCodecError> {
302 if !matches!(mode, ZfpCompressionMode::Reversible) && !Zip::from(&data).all(|x| x.is_finite()) {
303 return Err(ZfpCodecError::NonFiniteData);
304 }
305
306 let mut encoded = postcard::to_extend(
307 &CompressionHeader {
308 dtype: <T as ffi::ZfpCompressible>::D_TYPE,
309 shape: Cow::Borrowed(data.shape()),
310 version: StaticCodecVersion,
311 },
312 Vec::new(),
313 )
314 .map_err(|err| ZfpCodecError::MetaHeaderEncodeFailed {
315 source: ZfpHeaderError(err),
316 })?;
317
318 if data.is_empty() {
320 return Ok(encoded);
321 }
322
323 let field = ffi::ZfpField::new(data.into_dyn().squeeze())?;
326 let stream = ffi::ZfpCompressionStream::new(&field, mode)?;
327
328 let stream = stream.with_bitstream(field, &mut encoded);
331
332 let stream = stream.write_header()?;
334
335 stream.compress()?;
337
338 Ok(encoded)
339}
340
341pub fn decompress(encoded: &[u8]) -> Result<AnyArray, ZfpCodecError> {
351 let (header, encoded) =
352 postcard::take_from_bytes::<CompressionHeader>(encoded).map_err(|err| {
353 ZfpCodecError::MetaHeaderDecodeFailed {
354 source: ZfpHeaderError(err),
355 }
356 })?;
357
358 if header.shape.iter().copied().product::<usize>() == 0 {
360 let decoded = match header.dtype {
361 ZfpDType::I32 => AnyArray::I32(Array::zeros(&*header.shape)),
362 ZfpDType::I64 => AnyArray::I64(Array::zeros(&*header.shape)),
363 ZfpDType::F32 => AnyArray::F32(Array::zeros(&*header.shape)),
364 ZfpDType::F64 => AnyArray::F64(Array::zeros(&*header.shape)),
365 };
366 return Ok(decoded);
367 }
368
369 let stream = ffi::ZfpDecompressionStream::new(encoded);
371
372 let stream = stream.read_header()?;
374
375 match header.dtype {
377 ZfpDType::I32 => {
378 let mut decompressed = Array::zeros(&*header.shape);
379 stream.decompress_into(decompressed.view_mut().squeeze())?;
380 Ok(AnyArray::I32(decompressed))
381 }
382 ZfpDType::I64 => {
383 let mut decompressed = Array::zeros(&*header.shape);
384 stream.decompress_into(decompressed.view_mut().squeeze())?;
385 Ok(AnyArray::I64(decompressed))
386 }
387 ZfpDType::F32 => {
388 let mut decompressed = Array::zeros(&*header.shape);
389 stream.decompress_into(decompressed.view_mut().squeeze())?;
390 Ok(AnyArray::F32(decompressed))
391 }
392 ZfpDType::F64 => {
393 let mut decompressed = Array::zeros(&*header.shape);
394 stream.decompress_into(decompressed.view_mut().squeeze())?;
395 Ok(AnyArray::F64(decompressed))
396 }
397 }
398}
399
400pub fn decompress_into(encoded: &[u8], decoded: AnyArrayViewMut) -> Result<(), ZfpCodecError> {
412 let (header, encoded) =
413 postcard::take_from_bytes::<CompressionHeader>(encoded).map_err(|err| {
414 ZfpCodecError::MetaHeaderDecodeFailed {
415 source: ZfpHeaderError(err),
416 }
417 })?;
418
419 if decoded.shape() != &*header.shape {
420 return Err(ZfpCodecError::MismatchedDecodeIntoArray {
421 source: AnyArrayAssignError::ShapeMismatch {
422 src: header.shape.into_owned(),
423 dst: decoded.shape().to_vec(),
424 },
425 });
426 }
427
428 if decoded.is_empty() {
430 return Ok(());
431 }
432
433 let stream = ffi::ZfpDecompressionStream::new(encoded);
435
436 let stream = stream.read_header()?;
438
439 match (decoded, header.dtype) {
441 (AnyArrayViewMut::I32(decoded), ZfpDType::I32) => stream.decompress_into(decoded.squeeze()),
442 (AnyArrayViewMut::I64(decoded), ZfpDType::I64) => stream.decompress_into(decoded.squeeze()),
443 (AnyArrayViewMut::F32(decoded), ZfpDType::F32) => stream.decompress_into(decoded.squeeze()),
444 (AnyArrayViewMut::F64(decoded), ZfpDType::F64) => stream.decompress_into(decoded.squeeze()),
445 (decoded, dtype) => Err(ZfpCodecError::MismatchedDecodeIntoArray {
446 source: AnyArrayAssignError::DTypeMismatch {
447 src: dtype.into_dtype(),
448 dst: decoded.dtype(),
449 },
450 }),
451 }
452}
453
454#[derive(Serialize, Deserialize)]
455struct CompressionHeader<'a> {
456 dtype: ZfpDType,
457 #[serde(borrow)]
458 shape: Cow<'a, [usize]>,
459 version: ZfpCodecVersion,
460}
461
462#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
464#[expect(missing_docs)]
465pub enum ZfpDType {
466 #[serde(rename = "i32", alias = "int32")]
467 I32,
468 #[serde(rename = "i64", alias = "int64")]
469 I64,
470 #[serde(rename = "f32", alias = "float32")]
471 F32,
472 #[serde(rename = "f64", alias = "float64")]
473 F64,
474}
475
476impl ZfpDType {
477 #[must_use]
479 pub const fn into_dtype(self) -> AnyArrayDType {
480 match self {
481 Self::I32 => AnyArrayDType::I32,
482 Self::I64 => AnyArrayDType::I64,
483 Self::F32 => AnyArrayDType::F32,
484 Self::F64 => AnyArrayDType::F64,
485 }
486 }
487}
488
489impl fmt::Display for ZfpDType {
490 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
491 fmt.write_str(match self {
492 Self::I32 => "i32",
493 Self::I64 => "i64",
494 Self::F32 => "f32",
495 Self::F64 => "f64",
496 })
497 }
498}
499
500#[cfg(test)]
501#[allow(clippy::unwrap_used)]
502mod tests {
503 use ndarray::ArrayView1;
504
505 use super::*;
506
507 #[test]
508 fn zero_length() {
509 let encoded = compress(
510 Array::<f32, _>::from_shape_vec([1, 27, 0].as_slice(), vec![])
511 .unwrap()
512 .view(),
513 &ZfpCompressionMode::FixedPrecision { precision: 7 },
514 )
515 .unwrap();
516 let decoded = decompress(&encoded).unwrap();
517
518 assert_eq!(decoded.dtype(), AnyArrayDType::F32);
519 assert!(decoded.is_empty());
520 assert_eq!(decoded.shape(), &[1, 27, 0]);
521 }
522
523 #[test]
524 fn one_dimension() {
525 let data = Array::from_shape_vec(
526 [2_usize, 1, 2, 1, 1, 1].as_slice(),
527 vec![1.0, 2.0, 3.0, 4.0],
528 )
529 .unwrap();
530
531 let encoded = compress(
532 data.view(),
533 &ZfpCompressionMode::FixedAccuracy { tolerance: 0.1 },
534 )
535 .unwrap();
536 let decoded = decompress(&encoded).unwrap();
537
538 assert_eq!(decoded, AnyArray::F32(data));
539 }
540
541 #[test]
542 fn small_state() {
543 for data in [
544 &[][..],
545 &[0.0],
546 &[0.0, 1.0],
547 &[0.0, 1.0, 0.0],
548 &[0.0, 1.0, 0.0, 1.0],
549 ] {
550 let encoded = compress(
551 ArrayView1::from(data),
552 &ZfpCompressionMode::FixedAccuracy { tolerance: 0.1 },
553 )
554 .unwrap();
555 let decoded = decompress(&encoded).unwrap();
556
557 assert_eq!(
558 decoded,
559 AnyArray::F64(Array1::from_vec(data.to_vec()).into_dyn())
560 );
561 }
562 }
563}