1616NumPy-aware API:
1717 compressed, info = zmat.compress(arr, info=True)
1818 restored_arr = zmat.decompress(compressed, info=info)
19+
20+ compressed, info = zmat.zmat(arr, info=True) # low-level with info
21+ restored_arr = zmat.zmat(compressed, info=info) # low-level restore
1922"""
2023
2124from _zmat import compress as _compress
2629
2730__all__ = ["compress" , "decompress" , "encode" , "decode" , "zmat" ]
2831
32+
33+ def _byte_shuffle (data_bytes , typesize ):
34+ """Regroup bytes by position within each element (byte-shuffle filter).
35+
36+ Transforms [b0_e1 b1_e1 ... b0_e2 b1_e2 ...] into
37+ [b0_e1 b0_e2 ... b1_e1 b1_e2 ...], improving compressibility of
38+ structured numerical arrays with any codec.
39+ """
40+ import numpy as np
41+ arr = np .frombuffer (data_bytes , dtype = np .uint8 ).reshape (- 1 , typesize )
42+ return arr .flatten (order = 'F' ).tobytes ()
43+
44+
45+ def _byte_unshuffle (data_bytes , typesize ):
46+ """Reverse of _byte_shuffle."""
47+ import numpy as np
48+ nelems = len (data_bytes ) // typesize
49+ arr = np .frombuffer (data_bytes , dtype = np .uint8 ).reshape (typesize , nelems )
50+ return arr .flatten (order = 'F' ).tobytes ()
51+
2952__version__ = "1.0.2"
3053
31- def compress (data , method = "zlib" , level = 1 , info = False ):
54+ def compress (data , method = "zlib" , level = 1 , info = False , shuffle = 0 ):
3255 """Compress *data* using the requested algorithm.
3356
3457 Parameters
@@ -57,10 +80,19 @@ def compress(data, method="zlib", level=1, info=False):
5780 - ``'byte'`` — bytes per element (``data.itemsize``)
5881 - ``'method'`` — the compression method used
5982 - ``'order'`` — ``'F'`` for Fortran-contiguous, ``'C'`` otherwise
83+ - ``'shuffle'``— byte-shuffle level applied (0 = none, 1 = byte)
84+ - ``'typesize'``— element size in bytes used for shuffle
6085
6186 When *info=True* but *data* is not an ndarray, the tuple
6287 ``(compressed_bytes, None)`` is returned so callers can always
6388 unpack two values.
89+ shuffle : int, optional
90+ Byte-shuffle level for non-blosc2 codecs (default ``0`` = disabled,
91+ ``1`` = byte-shuffle). Requires *info=True* so that the shuffle
92+ state can be recorded and reversed on decompression. For blosc2
93+ methods the shuffle is handled by the C layer and this parameter
94+ is ignored. Has no effect when *data* is not a
95+ :class:`numpy.ndarray`.
6496
6597 Returns
6698 -------
@@ -76,29 +108,36 @@ def compress(data, method="zlib", level=1, info=False):
76108 compressed = zmat.compress(b"hello " * 1000)
77109 original = zmat.decompress(compressed)
78110
79- NumPy array round-trip::
111+ NumPy array round-trip with byte-shuffle ::
80112
81113 import numpy as np
82114 arr = np.random.rand(100, 100)
83- compressed, info = zmat.compress(arr, info=True)
84- restored = zmat.decompress(compressed, info=info) # ndarray
115+ compressed, info = zmat.compress(arr, method='lz4', info=True, shuffle=1 )
116+ restored = zmat.decompress(compressed, info=info)
85117 assert np.array_equal(restored, arr)
86118 """
119+ _use_shuffle = (shuffle > 0 and "blosc2" not in method and method != "base64" )
120+
87121 if info :
88122 try :
89123 import numpy as np
90124
91125 if isinstance (data , np .ndarray ):
92126 order = "F" if np .isfortran (data ) else "C"
127+ ts = data .itemsize
128+ apply_shuffle = _use_shuffle and ts > 1
93129 arr_info = {
94130 "type" : str (data .dtype ),
95131 "shape" : tuple (data .shape ),
96- "byte" : data . itemsize ,
132+ "byte" : ts ,
97133 "method" : method ,
98134 "order" : order ,
135+ "shuffle" : shuffle if apply_shuffle else 0 ,
136+ "typesize" : ts ,
99137 }
100- # always serialise as C-contiguous bytes
101138 flat = np .ascontiguousarray (data ).tobytes ()
139+ if apply_shuffle :
140+ flat = _byte_shuffle (flat , ts )
102141 compressed = _compress (flat , method = method , level = level )
103142 return compressed , arr_info
104143 except ImportError :
@@ -150,6 +189,13 @@ def decompress(data, method="zlib", info=None):
150189 if info is not None :
151190 actual_method = info .get ("method" , method )
152191 raw = _decompress (data , method = actual_method )
192+
193+ # unshuffle if compression applied wrapper-level byte shuffle
194+ shuf = info .get ("shuffle" , 0 )
195+ ts = info .get ("typesize" , 1 )
196+ if shuf > 0 and ts > 1 and "blosc2" not in actual_method :
197+ raw = _byte_unshuffle (raw , ts )
198+
153199 try :
154200 import numpy as np
155201
@@ -167,17 +213,21 @@ def decompress(data, method="zlib", info=None):
167213 return _decompress (data , method = method )
168214
169215
170- def zmat (data , iscompress = 1 , method = "zlib" , nthread = 1 , shuffle = 1 , typesize = 4 ):
216+ def zmat (data , iscompress = 1 , method = "zlib" , nthread = 1 , shuffle = 1 , typesize = 4 , info = False ):
171217 """Low-level compression/decompression interface with full parameter control.
172218
219+ Mirrors the MATLAB ``[ss, info] = zmat(arr)`` / ``zmat(ss, info)`` pattern
220+ when *info* is used.
221+
173222 Parameters
174223 ----------
175- data : bytes, bytearray, or buffer-protocol object
224+ data : bytes, bytearray, buffer-protocol object, or numpy.ndarray
176225 Input data.
177226 iscompress : int
178227 ``1`` to compress at the default level (default), ``0`` to
179228 decompress, or a negative integer to set the compression level
180- (e.g. ``-9`` for maximum compression).
229+ (e.g. ``-9`` for maximum compression). Ignored when *info* is a
230+ dict — decompression is performed automatically in that case.
181231 method : str
182232 Compression algorithm (default ``'zlib'``).
183233 nthread : int
@@ -188,12 +238,105 @@ def zmat(data, iscompress=1, method="zlib", nthread=1, shuffle=1, typesize=4):
188238 typesize : int
189239 Element byte size used by the blosc2 byte-shuffle filter
190240 (default ``4``).
241+ info : bool or dict, optional
242+ * ``False`` (default) — plain bytes in, bytes out.
243+ * ``True`` — when *data* is a :class:`numpy.ndarray`, capture its
244+ dtype, shape, and memory order and return ``(compressed_bytes,
245+ info_dict)``. When *data* is not an ndarray, returns
246+ ``(compressed_bytes, None)``.
247+ * ``dict`` — treat as the info dict previously returned by this
248+ function or by :func:`compress`. Decompresses *data* and
249+ reconstructs the original :class:`numpy.ndarray` using the
250+ stored metadata. The method is taken from ``info['method']``;
251+ the *method* argument is used only as a fallback.
191252
192253 Returns
193254 -------
194255 bytes
195- Compressed or decompressed data.
256+ Compressed or decompressed data when *info* is ``False``.
257+ tuple[bytes, dict | None]
258+ ``(compressed_bytes, info_dict)`` when *info=True*.
259+ numpy.ndarray
260+ Reconstructed array when *info* is a dict and NumPy is available.
261+
262+ Examples
263+ --------
264+ NumPy array round-trip::
265+
266+ import numpy as np
267+ arr = np.random.rand(100, 100).astype(np.float32)
268+ compressed, info = zmat.zmat(arr, info=True)
269+ restored = zmat.zmat(compressed, info=info)
270+ assert np.array_equal(restored, arr)
271+
272+ blosc2 with multi-threading::
273+
274+ out = zmat.zmat(data, iscompress=1, method='blosc2zstd',
275+ nthread=4, shuffle=1, typesize=8)
196276 """
277+ _use_shuffle = (shuffle > 0 and "blosc2" not in method and method != "base64" )
278+
279+ # info dict supplied → decompress and reconstruct numpy array
280+ if isinstance (info , dict ):
281+ actual_method = info .get ("method" , method )
282+ # blosc2 shuffle is handled by the C layer; pass it through unchanged
283+ c_shuffle = shuffle if "blosc2" in actual_method else 0
284+ raw = _zmat_c (data , iscompress = 0 , method = actual_method ,
285+ nthread = nthread , shuffle = c_shuffle , typesize = typesize )
286+
287+ # unshuffle if wrapper-level shuffle was recorded in info
288+ shuf = info .get ("shuffle" , 0 )
289+ ts = info .get ("typesize" , 1 )
290+ if shuf > 0 and ts > 1 and "blosc2" not in actual_method :
291+ raw = _byte_unshuffle (raw , ts )
292+
293+ try :
294+ import numpy as np
295+
296+ dtype = np .dtype (info ["type" ])
297+ shape = tuple (info ["shape" ])
298+ order = info .get ("order" , "C" )
299+ arr = np .frombuffer (raw , dtype = dtype ).copy ().reshape (shape )
300+ if order == "F" :
301+ arr = np .asfortranarray (arr )
302+ return arr
303+ except ImportError :
304+ return raw
305+
306+ # info=True → capture numpy array metadata before compressing
307+ if info is True :
308+ try :
309+ import numpy as np
310+
311+ if isinstance (data , np .ndarray ):
312+ order = "F" if np .isfortran (data ) else "C"
313+ ts = data .itemsize
314+ apply_shuffle = _use_shuffle and ts > 1
315+ arr_info = {
316+ "type" : str (data .dtype ),
317+ "shape" : tuple (data .shape ),
318+ "byte" : ts ,
319+ "method" : method ,
320+ "order" : order ,
321+ "shuffle" : shuffle if apply_shuffle else 0 ,
322+ "typesize" : ts ,
323+ }
324+ flat = np .ascontiguousarray (data ).tobytes ()
325+ if apply_shuffle :
326+ flat = _byte_shuffle (flat , ts )
327+ # for blosc2, pass shuffle/typesize to C; for others, already done
328+ c_shuffle = shuffle if "blosc2" in method else 0
329+ c_typesize = typesize if "blosc2" in method else 1
330+ compressed = _zmat_c (flat , iscompress = iscompress , method = method ,
331+ nthread = nthread , shuffle = c_shuffle , typesize = c_typesize )
332+ return compressed , arr_info
333+ except ImportError :
334+ pass
335+
336+ # non-ndarray with info=True: compress normally, return (bytes, None)
337+ return _zmat_c (data , iscompress = iscompress , method = method ,
338+ nthread = nthread , shuffle = shuffle , typesize = typesize ), None
339+
197340 return _zmat_c (
198341 data ,
199342 iscompress = iscompress ,
0 commit comments