Coverage for /usr/lib/python3/dist-packages/h5py/_debian_h5py_serial/_hl/filters.py: 45%
197 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
1# This file is part of h5py, a Python interface to the HDF5 library.
2#
3# http://www.h5py.org
4#
5# Copyright 2008-2013 Andrew Collette and contributors
6#
7# License: Standard 3-clause BSD; see "license.txt" for full license terms
8# and contributor agreement.
10"""
11 Implements support for HDF5 compression filters via the high-level
12 interface. The following types of filter are available:
14 "gzip"
15 Standard DEFLATE-based compression, at integer levels from 0 to 9.
16 Built-in to all public versions of HDF5. Use this if you want a
17 decent-to-good ratio, good portability, and don't mind waiting.
19 "lzf"
20 Custom compression filter for h5py. This filter is much, much faster
21 than gzip (roughly 10x in compression vs. gzip level 4, and 3x faster
22 in decompressing), but at the cost of a worse compression ratio. Use
23 this if you want cheap compression and portability is not a concern.
25 "szip"
26 Access to the HDF5 SZIP encoder. SZIP is a non-mainstream compression
27 format used in space science on integer and float datasets. SZIP is
28 subject to license requirements, which means the encoder is not
29 guaranteed to be always available. However, it is also much faster
30 than gzip.
32 The following constants in this module are also useful:
34 decode
35 Tuple of available filter names for decoding
37 encode
38 Tuple of available filter names for encoding
39"""
40from collections.abc import Mapping
41import operator
43import numpy as np
44from .base import product
45from .compat import filename_encode
46from .. import h5z, h5p, h5d, h5f
49_COMP_FILTERS = {'gzip': h5z.FILTER_DEFLATE,
50 'szip': h5z.FILTER_SZIP,
51 'lzf': h5z.FILTER_LZF,
52 'shuffle': h5z.FILTER_SHUFFLE,
53 'fletcher32': h5z.FILTER_FLETCHER32,
54 'scaleoffset': h5z.FILTER_SCALEOFFSET }
56DEFAULT_GZIP = 4
57DEFAULT_SZIP = ('nn', 8)
59def _gen_filter_tuples():
60 """ Bootstrap function to figure out what filters are available. """
61 dec = []
62 enc = []
63 for name, code in _COMP_FILTERS.items():
64 if h5z.filter_avail(code):
65 info = h5z.get_filter_info(code)
66 if info & h5z.FILTER_CONFIG_ENCODE_ENABLED:
67 enc.append(name)
68 if info & h5z.FILTER_CONFIG_DECODE_ENABLED:
69 dec.append(name)
71 return tuple(dec), tuple(enc)
73decode, encode = _gen_filter_tuples()
75def _external_entry(entry):
76 """ Check for and return a well-formed entry tuple for
77 a call to h5p.set_external. """
78 # We require only an iterable entry but also want to guard against
79 # raising a confusing exception from unpacking below a str or bytes that
80 # was mistakenly passed as an entry. We go further than that and accept
81 # only a tuple, which allows simpler documentation and exception
82 # messages.
83 if not isinstance(entry, tuple):
84 raise TypeError(
85 "Each external entry must be a tuple of (name, offset, size)")
86 name, offset, size = entry # raise ValueError without three elements
87 name = filename_encode(name)
88 offset = operator.index(offset)
89 size = operator.index(size)
90 return (name, offset, size)
92def _normalize_external(external):
93 """ Normalize external into a well-formed list of tuples and return. """
94 if external is None:
95 return []
96 try:
97 # Accept a solitary name---a str, bytes, or os.PathLike acceptable to
98 # filename_encode.
99 return [_external_entry((external, 0, h5f.UNLIMITED))]
100 except TypeError:
101 pass
102 # Check and rebuild each entry to be well-formed.
103 return [_external_entry(entry) for entry in external]
105class FilterRefBase(Mapping):
106 """Base class for referring to an HDF5 and describing its options
108 Your subclass must define filter_id, and may define a filter_options tuple.
109 """
110 filter_id = None
111 filter_options = ()
113 # Mapping interface supports using instances as **kwargs for compatibility
114 # with older versions of h5py
115 @property
116 def _kwargs(self):
117 return {
118 'compression': self.filter_id,
119 'compression_opts': self.filter_options
120 }
122 def __hash__(self):
123 return hash((self.filter_id, self.filter_options))
125 def __eq__(self, other):
126 return (
127 isinstance(other, FilterRefBase)
128 and self.filter_id == other.filter_id
129 and self.filter_options == other.filter_options
130 )
132 def __len__(self):
133 return len(self._kwargs)
135 def __iter__(self):
136 return iter(self._kwargs)
138 def __getitem__(self, item):
139 return self._kwargs[item]
141class Gzip(FilterRefBase):
142 filter_id = h5z.FILTER_DEFLATE
144 def __init__(self, level=DEFAULT_GZIP):
145 self.filter_options = (level,)
147def fill_dcpl(plist, shape, dtype, chunks, compression, compression_opts,
148 shuffle, fletcher32, maxshape, scaleoffset, external,
149 allow_unknown_filter=False):
150 """ Generate a dataset creation property list.
152 Undocumented and subject to change without warning.
153 """
155 if shape is None or shape == ():
156 shapetype = 'Empty' if shape is None else 'Scalar'
157 if any((chunks, compression, compression_opts, shuffle, fletcher32,
158 scaleoffset is not None)):
159 raise TypeError(
160 f"{shapetype} datasets don't support chunk/filter options"
161 )
162 if maxshape and maxshape != ():
163 raise TypeError(f"{shapetype} datasets cannot be extended")
164 return h5p.create(h5p.DATASET_CREATE)
166 def rq_tuple(tpl, name):
167 """ Check if chunks/maxshape match dataset rank """
168 if tpl in (None, True):
169 return
170 try:
171 tpl = tuple(tpl)
172 except TypeError:
173 raise TypeError('"%s" argument must be None or a sequence object' % name)
174 if len(tpl) != len(shape):
175 raise ValueError('"%s" must have same rank as dataset shape' % name)
177 rq_tuple(chunks, 'chunks')
178 rq_tuple(maxshape, 'maxshape')
180 if compression is not None:
181 if isinstance(compression, FilterRefBase):
182 compression_opts = compression.filter_options
183 compression = compression.filter_id
185 if compression not in encode and not isinstance(compression, int):
186 raise ValueError('Compression filter "%s" is unavailable' % compression)
188 if compression == 'gzip':
189 if compression_opts is None:
190 gzip_level = DEFAULT_GZIP
191 elif compression_opts in range(10):
192 gzip_level = compression_opts
193 else:
194 raise ValueError("GZIP setting must be an integer from 0-9, not %r" % compression_opts)
196 elif compression == 'lzf':
197 if compression_opts is not None:
198 raise ValueError("LZF compression filter accepts no options")
200 elif compression == 'szip':
201 if compression_opts is None:
202 compression_opts = DEFAULT_SZIP
204 err = "SZIP options must be a 2-tuple ('ec'|'nn', even integer 0-32)"
205 try:
206 szmethod, szpix = compression_opts
207 except TypeError:
208 raise TypeError(err)
209 if szmethod not in ('ec', 'nn'):
210 raise ValueError(err)
211 if not (0<szpix<=32 and szpix%2 == 0):
212 raise ValueError(err)
214 elif compression_opts is not None:
215 # Can't specify just compression_opts by itself.
216 raise TypeError("Compression method must be specified")
218 if scaleoffset is not None:
219 # scaleoffset must be an integer when it is not None or False,
220 # except for integral data, for which scaleoffset == True is
221 # permissible (will use SO_INT_MINBITS_DEFAULT)
223 if scaleoffset < 0:
224 raise ValueError('scale factor must be >= 0')
226 if dtype.kind == 'f':
227 if scaleoffset is True:
228 raise ValueError('integer scaleoffset must be provided for '
229 'floating point types')
230 elif dtype.kind in ('u', 'i'):
231 if scaleoffset is True:
232 scaleoffset = h5z.SO_INT_MINBITS_DEFAULT
233 else:
234 raise TypeError('scale/offset filter only supported for integer '
235 'and floating-point types')
237 # Scale/offset following fletcher32 in the filter chain will (almost?)
238 # always triggers a read error, as most scale/offset settings are
239 # lossy. Since fletcher32 must come first (see comment below) we
240 # simply prohibit the combination of fletcher32 and scale/offset.
241 if fletcher32:
242 raise ValueError('fletcher32 cannot be used with potentially lossy'
243 ' scale/offset filter')
245 external = _normalize_external(external)
246 # End argument validation
248 if (chunks is True) or \
249 (chunks is None and any((shuffle, fletcher32, compression, maxshape,
250 scaleoffset is not None))):
251 chunks = guess_chunk(shape, maxshape, dtype.itemsize)
253 if maxshape is True:
254 maxshape = (None,)*len(shape)
256 if chunks is not None:
257 plist.set_chunk(chunks)
258 plist.set_fill_time(h5d.FILL_TIME_ALLOC) # prevent resize glitch
260 # scale-offset must come before shuffle and compression
261 if scaleoffset is not None:
262 if dtype.kind in ('u', 'i'):
263 plist.set_scaleoffset(h5z.SO_INT, scaleoffset)
264 else: # dtype.kind == 'f'
265 plist.set_scaleoffset(h5z.SO_FLOAT_DSCALE, scaleoffset)
267 for item in external:
268 plist.set_external(*item)
270 if shuffle:
271 plist.set_shuffle()
273 if compression == 'gzip':
274 plist.set_deflate(gzip_level)
275 elif compression == 'lzf':
276 plist.set_filter(h5z.FILTER_LZF, h5z.FLAG_OPTIONAL)
277 elif compression == 'szip':
278 opts = {'ec': h5z.SZIP_EC_OPTION_MASK, 'nn': h5z.SZIP_NN_OPTION_MASK}
279 plist.set_szip(opts[szmethod], szpix)
280 elif isinstance(compression, int):
281 if not allow_unknown_filter and not h5z.filter_avail(compression):
282 raise ValueError("Unknown compression filter number: %s" % compression)
284 plist.set_filter(compression, h5z.FLAG_OPTIONAL, compression_opts)
286 # `fletcher32` must come after `compression`, otherwise, if `compression`
287 # is "szip" and the data is 64bit, the fletcher32 checksum will be wrong
288 # (see GitHub issue #953).
289 if fletcher32:
290 plist.set_fletcher32()
292 return plist
294def get_filters(plist):
295 """ Extract a dictionary of active filters from a DCPL, along with
296 their settings.
298 Undocumented and subject to change without warning.
299 """
301 filters = {h5z.FILTER_DEFLATE: 'gzip', h5z.FILTER_SZIP: 'szip',
302 h5z.FILTER_SHUFFLE: 'shuffle', h5z.FILTER_FLETCHER32: 'fletcher32',
303 h5z.FILTER_LZF: 'lzf', h5z.FILTER_SCALEOFFSET: 'scaleoffset'}
305 pipeline = {}
307 nfilters = plist.get_nfilters()
309 for i in range(nfilters):
311 code, _, vals, _ = plist.get_filter(i)
313 if code == h5z.FILTER_DEFLATE:
314 vals = vals[0] # gzip level
316 elif code == h5z.FILTER_SZIP:
317 mask, pixels = vals[0:2]
318 if mask & h5z.SZIP_EC_OPTION_MASK:
319 mask = 'ec'
320 elif mask & h5z.SZIP_NN_OPTION_MASK:
321 mask = 'nn'
322 else:
323 raise TypeError("Unknown SZIP configuration")
324 vals = (mask, pixels)
325 elif code == h5z.FILTER_LZF:
326 vals = None
327 else:
328 if len(vals) == 0:
329 vals = None
331 pipeline[filters.get(code, str(code))] = vals
333 return pipeline
335CHUNK_BASE = 16*1024 # Multiplier by which chunks are adjusted
336CHUNK_MIN = 8*1024 # Soft lower limit (8k)
337CHUNK_MAX = 1024*1024 # Hard upper limit (1M)
339def guess_chunk(shape, maxshape, typesize):
340 """ Guess an appropriate chunk layout for a dataset, given its shape and
341 the size of each element in bytes. Will allocate chunks only as large
342 as MAX_SIZE. Chunks are generally close to some power-of-2 fraction of
343 each axis, slightly favoring bigger values for the last index.
345 Undocumented and subject to change without warning.
346 """
347 # pylint: disable=unused-argument
349 # For unlimited dimensions we have to guess 1024
350 shape = tuple((x if x!=0 else 1024) for i, x in enumerate(shape))
352 ndims = len(shape)
353 if ndims == 0:
354 raise ValueError("Chunks not allowed for scalar datasets.")
356 chunks = np.array(shape, dtype='=f8')
357 if not np.all(np.isfinite(chunks)):
358 raise ValueError("Illegal value in chunk tuple")
360 # Determine the optimal chunk size in bytes using a PyTables expression.
361 # This is kept as a float.
362 dset_size = product(chunks)*typesize
363 target_size = CHUNK_BASE * (2**np.log10(dset_size/(1024.*1024)))
365 if target_size > CHUNK_MAX:
366 target_size = CHUNK_MAX
367 elif target_size < CHUNK_MIN:
368 target_size = CHUNK_MIN
370 idx = 0
371 while True:
372 # Repeatedly loop over the axes, dividing them by 2. Stop when:
373 # 1a. We're smaller than the target chunk size, OR
374 # 1b. We're within 50% of the target chunk size, AND
375 # 2. The chunk is smaller than the maximum chunk size
377 chunk_bytes = product(chunks)*typesize
379 if (chunk_bytes < target_size or \
380 abs(chunk_bytes-target_size)/target_size < 0.5) and \
381 chunk_bytes < CHUNK_MAX:
382 break
384 if product(chunks) == 1:
385 break # Element size larger than CHUNK_MAX
387 chunks[idx%ndims] = np.ceil(chunks[idx%ndims] / 2.0)
388 idx += 1
390 return tuple(int(x) for x in chunks)