Skip to content

Commit 2a2a934

Browse files
committed
[feat] enable shuffle for all codecs,accept info in zmat.zmat
1 parent 2da9980 commit 2a2a934

2 files changed

Lines changed: 198 additions & 12 deletions

File tree

python/zmat/__init__.py

Lines changed: 153 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
NumPy-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

2124
from _zmat import compress as _compress
@@ -26,9 +29,29 @@
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,

zmat.m

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@
4646
% options: a series of ('name', value) pairs, supported options include
4747
% 'nthread': followed by an integer specifying number of threads for blosc2 meta-compressors
4848
% 'typesize': followed by an integer specifying the number of bytes per data element (used for shuffle)
49-
% 'shuffle': shuffle methods in blosc2 meta-compressor, 0 disable, 1, byte-shuffle
49+
% 'shuffle': 0 to disable (default for non-blosc2), 1 to enable byte-shuffle.
50+
% For blosc2 methods the shuffle is applied inside the C layer.
51+
% For all other methods, when [output,info]=zmat(...) is called,
52+
% the shuffle is applied in MATLAB before compression and reversed
53+
% after decompression; the info struct records 'shuffle' and
54+
% 'typesize' so that zmat(compressed, info) restores the original array.
5055
%
5156
% output:
5257
% output: a uint8 row vector, storing the compressed or decompressed data;
@@ -220,7 +225,34 @@
220225
iscompress = -9;
221226
end
222227

223-
[varargout{1:max(1, nargout)}] = zipmat(input, iscompress, zipmethod, nthread, shuffle, typesize);
228+
%% wrapper-level byte shuffle: applies to non-blosc2 codecs when info is requested
229+
do_wrapper_shuffle = (nargout > 1 && shuffle > 0 && iscompress ~= 0 && ...
230+
isempty(strfind(zipmethod, 'blosc2')) && ...
231+
~strcmp(zipmethod, 'base64') && ...
232+
isempty(specialtype) && typesize > 1);
233+
234+
if (do_wrapper_shuffle)
235+
%% convert input to bytes, apply byte shuffle, then compress
236+
orig_class = class(input);
237+
orig_size = size(input);
238+
if (~isa(input, 'uint8'))
239+
raw_bytes = typecast(input(:)', 'uint8');
240+
else
241+
raw_bytes = input(:)';
242+
end
243+
nelems = numel(raw_bytes) / typesize;
244+
M = reshape(raw_bytes, typesize, nelems); % typesize x nelems: col = one element
245+
shuffled_bytes = reshape(M', 1, []); % flatten row-major: all byte-0s, then byte-1s...
246+
[varargout{1:max(1, nargout)}] = zipmat(shuffled_bytes, iscompress, zipmethod, nthread, 0, 1);
247+
%% overwrite info with original array metadata and record shuffle state
248+
varargout{2}.type = orig_class;
249+
varargout{2}.size = orig_size;
250+
varargout{2}.byte = typesize;
251+
varargout{2}.shuffle = shuffle;
252+
varargout{2}.typesize = typesize;
253+
else
254+
[varargout{1:max(1, nargout)}] = zipmat(input, iscompress, zipmethod, nthread, shuffle, typesize);
255+
end
224256

225257
%% store special matrix type info in the output info struct
226258
if (nargout > 1 && ~isempty(specialtype))
@@ -295,6 +327,17 @@
295327
end
296328
end
297329

330+
%% unshuffle if compression applied wrapper-level byte shuffle
331+
if (isfield(inputinfo, 'shuffle') && inputinfo.shuffle > 0 && ...
332+
isfield(inputinfo, 'typesize') && inputinfo.typesize > 1 && ...
333+
isempty(strfind(inputinfo.method, 'blosc2')))
334+
ts = inputinfo.typesize;
335+
raw_bytes = varargout{1}(:)';
336+
nelems = numel(raw_bytes) / ts;
337+
M = reshape(raw_bytes, nelems, ts); % nelems x ts: col = one byte position
338+
varargout{1} = reshape(M', 1, []); % flatten row-major: bytes of e1, then e2...
339+
end
340+
298341
if (strcmp(inputinfo.type, 'logical'))
299342
varargout{1} = logical(varargout{1});
300343
else

0 commit comments

Comments
 (0)