Skip to main content

numcodecs_ebcc/
lib.rs

1//! [![CI Status]][workflow] [![MSRV]][repo] [![Latest Version]][crates.io] [![Rust Doc Crate]][docs.rs] [![Rust Doc Main]][docs]
2//!
3//! [CI Status]: https://img.shields.io/github/actions/workflow/status/juntyr/numcodecs-rs/ci.yml?branch=main
4//! [workflow]: https://github.com/juntyr/numcodecs-rs/actions/workflows/ci.yml?query=branch%3Amain
5//!
6//! [MSRV]: https://img.shields.io/badge/MSRV-1.87.0-blue
7//! [repo]: https://github.com/juntyr/numcodecs-rs
8//!
9//! [Latest Version]: https://img.shields.io/crates/v/numcodecs-ebcc
10//! [crates.io]: https://crates.io/crates/numcodecs-ebcc
11//!
12//! [Rust Doc Crate]: https://img.shields.io/docsrs/numcodecs-ebcc
13//! [docs.rs]: https://docs.rs/numcodecs-ebcc/
14//!
15//! [Rust Doc Main]: https://img.shields.io/badge/docs-main-blue
16//! [docs]: https://juntyr.github.io/numcodecs-rs/numcodecs_ebcc
17//!
18//! EBCC codec implementation for the [`numcodecs`] API.
19
20#![allow(clippy::multiple_crate_versions)] // embedded-io
21
22#[cfg(test)]
23use ::serde_json as _;
24
25use std::borrow::Cow;
26
27use ndarray::{Array, Array1, ArrayBase, ArrayViewMut, Axis, Data, DataMut, Dimension, IxDyn};
28use num_traits::Float;
29use numcodecs::{
30    AnyArray, AnyArrayDType, AnyArrayView, AnyArrayViewMut, AnyCowArray, Codec, StaticCodec,
31    StaticCodecConfig, StaticCodecVersion,
32};
33use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
34use serde::{Deserialize, Deserializer, Serialize, Serializer};
35use thiserror::Error;
36
37type EbccCodecVersion = StaticCodecVersion<0, 1, 0>;
38
39/// Codec providing compression using EBCC.
40///
41/// EBCC combines JPEG2000 compression with error-bounded residual compression.
42///
43/// Arrays that are higher-dimensional than 3D are encoded by compressing each
44/// 3D slice with EBCC independently. Specifically, the array's shape is
45/// interpreted as `[.., depth, height, width]`. If you want to compress 3D
46/// slices along three different axes, you can swizzle the array axes
47/// beforehand.
48#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
49#[schemars(deny_unknown_fields)]
50pub struct EbccCodec {
51    /// EBCC residual compression
52    #[serde(flatten)]
53    pub residual: EbccResidualType,
54    /// JPEG2000 positive base compression ratio
55    #[serde(default = "default_base_cr")]
56    pub base_cr: Positive<f32>,
57    /// The codec's encoding format version. Do not provide this parameter explicitly.
58    #[serde(default, rename = "_version")]
59    pub version: EbccCodecVersion,
60}
61
62const fn default_base_cr() -> Positive<f32> {
63    Positive(100.0)
64}
65
66/// Residual compression types supported by EBCC.
67#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
68#[serde(tag = "residual")]
69#[serde(deny_unknown_fields)]
70pub enum EbccResidualType {
71    #[serde(rename = "jpeg2000-only")]
72    /// No residual compression - base JPEG2000 only
73    Jpeg2000Only,
74    #[serde(rename = "absolute")]
75    /// Residual compression with absolute maximum error bound
76    AbsoluteError {
77        /// The positive maximum absolute error bound
78        error: Positive<f32>,
79    },
80    #[serde(rename = "relative")]
81    /// Residual compression with relative error bound
82    RelativeError {
83        /// The positive maximum relative error bound
84        error: Positive<f32>,
85    },
86}
87
88impl Codec for EbccCodec {
89    type Error = EbccCodecError;
90
91    fn encode(&self, data: AnyCowArray) -> Result<AnyArray, Self::Error> {
92        match data {
93            AnyCowArray::F32(data) => Ok(AnyArray::U8(
94                Array1::from(compress(data, self.residual, self.base_cr)?).into_dyn(),
95            )),
96            encoded => Err(EbccCodecError::UnsupportedDtype(encoded.dtype())),
97        }
98    }
99
100    fn decode(&self, encoded: AnyCowArray) -> Result<AnyArray, Self::Error> {
101        let AnyCowArray::U8(encoded) = encoded else {
102            return Err(EbccCodecError::EncodedDataNotBytes {
103                dtype: encoded.dtype(),
104            });
105        };
106
107        if !matches!(encoded.shape(), [_]) {
108            return Err(EbccCodecError::EncodedDataNotOneDimensional {
109                shape: encoded.shape().to_vec(),
110            });
111        }
112
113        decompress(&AnyCowArray::U8(encoded).as_bytes())
114    }
115
116    fn decode_into(
117        &self,
118        encoded: AnyArrayView,
119        decoded: AnyArrayViewMut,
120    ) -> Result<(), Self::Error> {
121        let AnyArrayView::U8(encoded) = encoded else {
122            return Err(EbccCodecError::EncodedDataNotBytes {
123                dtype: encoded.dtype(),
124            });
125        };
126
127        if !matches!(encoded.shape(), [_]) {
128            return Err(EbccCodecError::EncodedDataNotOneDimensional {
129                shape: encoded.shape().to_vec(),
130            });
131        }
132
133        match decoded {
134            AnyArrayViewMut::F32(decoded) => {
135                decompress_into(&AnyArrayView::U8(encoded).as_bytes(), decoded)
136            }
137            decoded => Err(EbccCodecError::UnsupportedDtype(decoded.dtype())),
138        }
139    }
140}
141
142impl StaticCodec for EbccCodec {
143    const CODEC_ID: &'static str = "ebcc.rs";
144
145    type Config<'de> = Self;
146
147    fn from_config(config: Self::Config<'_>) -> Self {
148        config
149    }
150
151    fn get_config(&self) -> StaticCodecConfig<'_, Self> {
152        StaticCodecConfig::from(self)
153    }
154}
155
156/// Errors that may occur when applying the [`EbccCodec`].
157#[derive(Debug, thiserror::Error)]
158pub enum EbccCodecError {
159    /// [`EbccCodec`] does not support the dtype
160    #[error("Ebcc does not support the dtype {0}")]
161    UnsupportedDtype(AnyArrayDType),
162    /// [`EbccCodec`] failed to encode the header
163    #[error("Ebcc failed to encode the header")]
164    HeaderEncodeFailed {
165        /// Opaque source error
166        source: EbccHeaderError,
167    },
168    /// [`EbccCodec`] can only encode >2D data where the last two dimensions
169    /// must be at least 32x32 but received an array with an insufficient shape
170    #[error(
171        "Ebcc can only encode >2D data where the last two dimensions must be at least 32x32 but received an array of shape {shape:?}"
172    )]
173    InsufficientDimensions {
174        /// The unexpected shape of the array
175        shape: Vec<usize>,
176    },
177    /// [`EbccCodec`] failed to encode the data
178    #[error("Ebcc failed to encode the data")]
179    EbccEncodeFailed {
180        /// Opaque source error
181        source: EbccCodingError,
182    },
183    /// [`EbccCodec`] failed to encode a 3D slice
184    #[error("Ebcc failed to encode a 3D slice")]
185    SliceEncodeFailed {
186        /// Opaque source error
187        source: EbccSliceError,
188    },
189    /// [`EbccCodec`] can only decode one-dimensional byte arrays but received
190    /// an array of a different dtype
191    #[error(
192        "Ebcc can only decode one-dimensional byte arrays but received an array of dtype {dtype}"
193    )]
194    EncodedDataNotBytes {
195        /// The unexpected dtype of the encoded array
196        dtype: AnyArrayDType,
197    },
198    /// [`EbccCodec`] can only decode one-dimensional byte arrays but received
199    /// an array of a different shape
200    #[error(
201        "Ebcc can only decode one-dimensional byte arrays but received a byte array of shape {shape:?}"
202    )]
203    EncodedDataNotOneDimensional {
204        /// The unexpected shape of the encoded array
205        shape: Vec<usize>,
206    },
207    /// [`EbccCodec`] failed to decode the header
208    #[error("Ebcc failed to decode the header")]
209    HeaderDecodeFailed {
210        /// Opaque source error
211        source: EbccHeaderError,
212    },
213    /// [`EbccCodec`] cannot decode into an array with a mismatching shape
214    #[error("Ebcc cannot decode an array of shape {decoded:?} into an array of shape {array:?}")]
215    DecodeIntoShapeMismatch {
216        /// The shape of the decoded data
217        decoded: Vec<usize>,
218        /// The mismatching shape of the array to decode into
219        array: Vec<usize>,
220    },
221    /// [`EbccCodec`] failed to decode a 3D slice
222    #[error("Ebcc failed to decode a slice")]
223    SliceDecodeFailed {
224        /// Opaque source error
225        source: EbccSliceError,
226    },
227    /// [`EbccCodec`] failed to decode from an excessive number of slices
228    #[error("Ebcc failed to decode from an excessive number of slices")]
229    DecodeTooManySlices,
230    /// [`EbccCodec`] failed to decode the data
231    #[error("Ebcc failed to decode the data")]
232    EbccDecodeFailed {
233        /// Opaque source error
234        source: EbccCodingError,
235    },
236}
237
238#[expect(clippy::derive_partial_eq_without_eq)] // floats are not Eq
239#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Hash)]
240/// Positive floating point number
241pub struct Positive<T: Float>(T);
242
243impl<T: Float> PartialEq<T> for Positive<T> {
244    fn eq(&self, other: &T) -> bool {
245        self.0 == *other
246    }
247}
248
249impl Serialize for Positive<f32> {
250    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
251        serializer.serialize_f32(self.0)
252    }
253}
254
255impl<'de> Deserialize<'de> for Positive<f32> {
256    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
257        let x = f32::deserialize(deserializer)?;
258
259        if x > 0.0 {
260            Ok(Self(x))
261        } else {
262            Err(serde::de::Error::invalid_value(
263                serde::de::Unexpected::Float(f64::from(x)),
264                &"a positive value",
265            ))
266        }
267    }
268}
269
270impl JsonSchema for Positive<f32> {
271    fn schema_name() -> Cow<'static, str> {
272        Cow::Borrowed("PositiveF32")
273    }
274
275    fn schema_id() -> Cow<'static, str> {
276        Cow::Borrowed(concat!(module_path!(), "::", "Positive<f32>"))
277    }
278
279    fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
280        json_schema!({
281            "type": "number",
282            "exclusiveMinimum": 0.0
283        })
284    }
285}
286
287#[derive(Debug, Error)]
288#[error(transparent)]
289/// Opaque error for when encoding or decoding the header fails
290pub struct EbccHeaderError(postcard::Error);
291
292#[derive(Debug, Error)]
293#[error(transparent)]
294/// Opaque error for when encoding or decoding a 3D slice fails
295pub struct EbccSliceError(postcard::Error);
296
297#[derive(Debug, Error)]
298#[error(transparent)]
299/// Opaque error for when encoding or decoding with EBCC fails
300pub struct EbccCodingError(ebcc::EBCCError);
301
302/// Compress the `data` array using EBCC with the provided `residual` and
303/// `base_cr`.
304///
305/// # Errors
306///
307/// Errors with
308/// - [`EbccCodecError::HeaderEncodeFailed`] if encoding the header failed
309/// - [`EbccCodecError::InsufficientDimensions`] if the `data` has fewer than
310///   two dimensions or the last two dimensions are not at least 32x32
311/// - [`EbccCodecError::EbccEncodeFailed`] if encoding with EBCC failed
312/// - [`EbccCodecError::SliceEncodeFailed`] if encoding a 3D slice failed
313#[allow(clippy::missing_panics_doc)]
314pub fn compress<S: Data<Elem = f32>, D: Dimension>(
315    data: ArrayBase<S, D>,
316    residual: EbccResidualType,
317    base_cr: Positive<f32>,
318) -> Result<Vec<u8>, EbccCodecError> {
319    let mut encoded = postcard::to_extend(
320        &CompressionHeader {
321            dtype: EbccDType::F32,
322            shape: Cow::Borrowed(data.shape()),
323            version: StaticCodecVersion,
324        },
325        Vec::new(),
326    )
327    .map_err(|err| EbccCodecError::HeaderEncodeFailed {
328        source: EbccHeaderError(err),
329    })?;
330
331    // EBCC cannot handle zero-length dimensions
332    if data.is_empty() {
333        return Ok(encoded);
334    }
335
336    let mut chunk_size = Vec::from(data.shape());
337    let (width, height, depth) = match *chunk_size.as_mut_slice() {
338        [ref mut rest @ .., depth, height, width] => {
339            for r in rest {
340                *r = 1;
341            }
342            (width, height, depth)
343        }
344        [height, width] => (width, height, 1),
345        _ => {
346            return Err(EbccCodecError::InsufficientDimensions {
347                shape: Vec::from(data.shape()),
348            });
349        }
350    };
351
352    if (width < 32) || (height < 32) {
353        return Err(EbccCodecError::InsufficientDimensions {
354            shape: Vec::from(data.shape()),
355        });
356    }
357
358    for mut slice in data.into_dyn().exact_chunks(chunk_size.as_slice()) {
359        while slice.ndim() < 3 {
360            slice = slice.insert_axis(Axis(0));
361        }
362        #[expect(clippy::unwrap_used)]
363        // slice must now have at least three axes, and all but the last three
364        //  must be of size 1
365        let slice = slice.into_shape_with_order((depth, height, width)).unwrap();
366
367        let encoded_slice = ebcc::ebcc_encode(
368            slice,
369            &ebcc::EBCCConfig {
370                base_cr: base_cr.0,
371                residual_compression_type: match residual {
372                    EbccResidualType::Jpeg2000Only => ebcc::EBCCResidualType::Jpeg2000Only,
373                    EbccResidualType::AbsoluteError { error } => {
374                        ebcc::EBCCResidualType::AbsoluteError(error.0)
375                    }
376                    EbccResidualType::RelativeError { error } => {
377                        ebcc::EBCCResidualType::RelativeError(error.0)
378                    }
379                },
380            },
381        )
382        .map_err(|err| EbccCodecError::EbccEncodeFailed {
383            source: EbccCodingError(err),
384        })?;
385
386        encoded = postcard::to_extend(encoded_slice.as_slice(), encoded).map_err(|err| {
387            EbccCodecError::SliceEncodeFailed {
388                source: EbccSliceError(err),
389            }
390        })?;
391    }
392
393    Ok(encoded)
394}
395
396/// Decompress the `encoded` data into an array using EBCC.
397///
398/// # Errors
399///
400/// Errors with
401/// - [`EbccCodecError::HeaderDecodeFailed`] if decoding the header failed
402/// - [`EbccCodecError::SliceDecodeFailed`] if decoding a 3D slice failed
403/// - [`EbccCodecError::EbccDecodeFailed`] if decoding with EBCC failed
404/// - [`EbccCodecError::DecodeTooManySlices`] if the encoded data contains
405///   too many slices
406pub fn decompress(encoded: &[u8]) -> Result<AnyArray, EbccCodecError> {
407    fn decompress_typed(
408        encoded: &[u8],
409        shape: &[usize],
410    ) -> Result<Array<f32, IxDyn>, EbccCodecError> {
411        let mut decoded = Array::<f32, _>::zeros(shape);
412        decompress_into_typed(encoded, decoded.view_mut())?;
413        Ok(decoded)
414    }
415
416    let (header, encoded) =
417        postcard::take_from_bytes::<CompressionHeader>(encoded).map_err(|err| {
418            EbccCodecError::HeaderDecodeFailed {
419                source: EbccHeaderError(err),
420            }
421        })?;
422
423    // Return empty data for zero-size arrays
424    if header.shape.iter().copied().any(|s| s == 0) {
425        return match header.dtype {
426            EbccDType::F32 => Ok(AnyArray::F32(Array::zeros(&*header.shape))),
427        };
428    }
429
430    match header.dtype {
431        EbccDType::F32 => Ok(AnyArray::F32(decompress_typed(encoded, &header.shape)?)),
432    }
433}
434
435/// Decompress the `encoded` data into the `decoded` array using EBCC.
436///
437/// # Errors
438///
439/// Errors with
440/// - [`EbccCodecError::HeaderDecodeFailed`] if decoding the header failed
441/// - [`EbccCodecError::DecodeIntoShapeMismatch`] is the `decoded` array shape
442///   does not match the shape of the decoded data
443/// - [`EbccCodecError::SliceDecodeFailed`] if decoding a 3D slice failed
444/// - [`EbccCodecError::EbccDecodeFailed`] if decoding with EBCC failed
445/// - [`EbccCodecError::DecodeTooManySlices`] if the encoded data contains
446///   too many slices
447pub fn decompress_into<S: DataMut<Elem = f32>, D: Dimension>(
448    encoded: &[u8],
449    decoded: ArrayBase<S, D>,
450) -> Result<(), EbccCodecError> {
451    let (header, encoded) =
452        postcard::take_from_bytes::<CompressionHeader>(encoded).map_err(|err| {
453            EbccCodecError::HeaderDecodeFailed {
454                source: EbccHeaderError(err),
455            }
456        })?;
457
458    if decoded.shape() != &*header.shape {
459        return Err(EbccCodecError::DecodeIntoShapeMismatch {
460            decoded: header.shape.into_owned(),
461            array: Vec::from(decoded.shape()),
462        });
463    }
464
465    // Return empty data for zero-size arrays
466    if header.shape.iter().copied().any(|s| s == 0) {
467        return match header.dtype {
468            EbccDType::F32 => Ok(()),
469        };
470    }
471
472    match header.dtype {
473        EbccDType::F32 => decompress_into_typed(encoded, decoded.into_dyn().view_mut()),
474    }
475}
476
477fn decompress_into_typed(
478    mut encoded: &[u8],
479    mut decoded: ArrayViewMut<f32, IxDyn>,
480) -> Result<(), EbccCodecError> {
481    let mut chunk_size = Vec::from(decoded.shape());
482    let (width, height, depth) = match *chunk_size.as_mut_slice() {
483        [ref mut rest @ .., depth, height, width] => {
484            for r in rest {
485                *r = 1;
486            }
487            (width, height, depth)
488        }
489        [height, width] => (width, height, 1),
490        [width] => (width, 1, 1),
491        [] => (1, 1, 1),
492    };
493
494    for mut slice in decoded.exact_chunks_mut(chunk_size.as_slice()) {
495        let (encoded_slice, rest) =
496            postcard::take_from_bytes::<Cow<[u8]>>(encoded).map_err(|err| {
497                EbccCodecError::SliceDecodeFailed {
498                    source: EbccSliceError(err),
499                }
500            })?;
501        encoded = rest;
502
503        while slice.ndim() < 3 {
504            slice = slice.insert_axis(Axis(0));
505        }
506        #[expect(clippy::unwrap_used)]
507        // slice must now have at least three axes, and all but the last
508        //  three must be of size 1
509        let slice = slice.into_shape_with_order((depth, height, width)).unwrap();
510
511        ebcc::ebcc_decode_into(&encoded_slice, slice).map_err(|err| {
512            EbccCodecError::EbccDecodeFailed {
513                source: EbccCodingError(err),
514            }
515        })?;
516    }
517
518    if !encoded.is_empty() {
519        return Err(EbccCodecError::DecodeTooManySlices);
520    }
521
522    Ok(())
523}
524
525#[derive(Serialize, Deserialize)]
526struct CompressionHeader<'a> {
527    dtype: EbccDType,
528    #[serde(borrow)]
529    shape: Cow<'a, [usize]>,
530    version: EbccCodecVersion,
531}
532
533/// Dtypes that EBCC can compress and decompress
534#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
535enum EbccDType {
536    #[serde(rename = "f32", alias = "float32")]
537    F32,
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543
544    #[test]
545    fn test_unsupported_dtype() {
546        let codec = EbccCodec {
547            residual: EbccResidualType::Jpeg2000Only,
548            base_cr: Positive(10.0),
549            version: StaticCodecVersion,
550        };
551
552        let data = Array1::<i32>::zeros(100);
553        let result = codec.encode(AnyCowArray::I32(data.into_dyn().into()));
554
555        assert!(matches!(result, Err(EbccCodecError::UnsupportedDtype(_))));
556    }
557
558    #[test]
559    fn test_invalid_dimensions() {
560        let codec = EbccCodec {
561            residual: EbccResidualType::Jpeg2000Only,
562            base_cr: Positive(10.0),
563            version: StaticCodecVersion,
564        };
565
566        // Test dimensions too small (32 < 32x32 requirement)
567        let data = Array::zeros(32);
568        let result = codec.encode(AnyCowArray::F32(data.into_dyn().into()));
569        assert!(
570            matches!(result, Err(EbccCodecError::InsufficientDimensions { shape }) if shape == [32])
571        );
572
573        // Test dimensions too small (16x16 < 32x32 requirement)
574        let data = Array::zeros((16, 16));
575        let result = codec.encode(AnyCowArray::F32(data.into_dyn().into()));
576        assert!(
577            matches!(result, Err(EbccCodecError::InsufficientDimensions { shape }) if shape == [16, 16])
578        );
579
580        // Test mixed valid/invalid dimensions
581        let data = Array::zeros((1, 32, 16));
582        let result = codec.encode(AnyCowArray::F32(data.into_dyn().into()));
583        assert!(
584            matches!(result, Err(EbccCodecError::InsufficientDimensions { shape }) if shape == [1, 32, 16])
585        );
586
587        // Test valid dimensions
588        let data = Array::zeros((1, 32, 32));
589        let result = codec.encode(AnyCowArray::F32(data.into_dyn().into()));
590        assert!(result.is_ok());
591
592        // Test valid dimensions with slicing
593        let data = Array::zeros((2, 2, 2, 32, 32));
594        let result = codec.encode(AnyCowArray::F32(data.into_dyn().into()));
595        assert!(result.is_ok());
596    }
597
598    #[test]
599    fn test_large_array() -> Result<(), EbccCodecError> {
600        // Test with a larger array (similar to small climate dataset)
601        let height = 721; // Quarter degree resolution
602        let width = 1440;
603        let frames = 1;
604
605        #[expect(clippy::suboptimal_flops, clippy::cast_precision_loss)]
606        let data = Array::from_shape_fn((frames, height, width), |(_k, i, j)| {
607            let lat = -90.0 + (i as f32 / height as f32) * 180.0;
608            let lon = -180.0 + (j as f32 / width as f32) * 360.0;
609            #[allow(clippy::let_and_return)]
610            let temp = 273.15 + 30.0 * (1.0 - lat.abs() / 90.0) + 5.0 * (lon / 180.0).sin();
611            temp
612        });
613
614        let codec_error = 0.1;
615        let codec = EbccCodec {
616            residual: EbccResidualType::AbsoluteError {
617                error: Positive(codec_error),
618            },
619            base_cr: Positive(20.0),
620            version: StaticCodecVersion,
621        };
622
623        let encoded = codec.encode(AnyArray::F32(data.clone().into_dyn()).into_cow())?;
624        let decoded = codec.decode(encoded.cow())?;
625
626        let AnyArray::U8(encoded) = encoded else {
627            return Err(EbccCodecError::EncodedDataNotBytes {
628                dtype: encoded.dtype(),
629            });
630        };
631
632        let AnyArray::F32(decoded) = decoded else {
633            return Err(EbccCodecError::UnsupportedDtype(decoded.dtype()));
634        };
635
636        // Check compression ratio
637        let original_size = data.len() * std::mem::size_of::<f32>();
638        #[allow(clippy::cast_precision_loss)]
639        let compression_ratio = original_size as f64 / encoded.len() as f64;
640
641        assert!(
642            compression_ratio > 5.0,
643            "Compression ratio {compression_ratio} should be at least 5:1",
644        );
645
646        // Check error bound is respected
647        let max_error = data
648            .iter()
649            .zip(decoded.iter())
650            .map(|(&orig, &decomp)| (orig - decomp).abs())
651            .fold(0.0f32, f32::max);
652
653        assert!(
654            max_error <= (codec_error + 1e-6),
655            "Max error {max_error} exceeds error bound {codec_error}",
656        );
657
658        Ok(())
659    }
660}