Skip to content

Commit 37016b8

Browse files
authored
Merge pull request #911 from MiraGeoscience/GEOPY-2893
GEOPY-2893: h5 compression level seems ignore (from mira-omf)
2 parents cbdabab + c8fc08c commit 37016b8

6 files changed

Lines changed: 109 additions & 13 deletions

File tree

geoh5py/io/h5_writer.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,9 @@ def update_field(
354354
"vertices",
355355
"z_cell_delimiters",
356356
]:
357-
H5Writer.write_array_attribute(h5file, entity, attribute, **kwargs)
357+
H5Writer.write_array_attribute(
358+
h5file, entity, attribute, compression=compression, **kwargs
359+
)
358360
elif attribute == "property_groups":
359361
H5Writer.write_property_groups(h5file, entity)
360362
elif attribute == "color_map":
@@ -588,14 +590,21 @@ def write_visible(
588590

589591
@staticmethod
590592
def write_array_attribute(
591-
file: str | h5py.File, entity, attribute, values=None, **kwargs
593+
file: str | h5py.File,
594+
entity,
595+
attribute,
596+
values=None,
597+
compression: int = 9,
598+
**kwargs,
592599
) -> None:
593600
"""
594601
Add :obj:`~geoh5py.objects.object_base.ObjectBase.surveys` of an object.
595602
596603
:param file: Name or handle to a geoh5 file.
597604
:param entity: Target entity.
598605
:param attribute: Name of the attribute to be written to geoh5
606+
:param values: Values to be written to geoh5
607+
:param compression: Compression level `compression_opts` of the h5 file.
599608
"""
600609
with fetch_h5_handle(file, mode="r+") as h5file:
601610
entity_handle = H5Writer.fetch_handle(h5file, entity)
@@ -624,7 +633,7 @@ def write_array_attribute(
624633
KEY_MAP[attribute],
625634
data=values,
626635
compression="gzip",
627-
compression_opts=9,
636+
compression_opts=compression,
628637
**kwargs,
629638
)
630639

geoh5py/objects/maxwell_plate.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,18 +256,23 @@ def geometry(self, value: PlateGeometry):
256256

257257
@classmethod
258258
def create(
259-
cls, workspace, geometry: PlateGeometry | None = None, **kwargs
259+
cls,
260+
workspace,
261+
compression: int = 5,
262+
geometry: PlateGeometry | None = None,
263+
**kwargs,
260264
) -> MaxwellPlate:
261265
"""
262266
Function to create an entity.
263267
264268
:param workspace: Workspace to be added to.
269+
:param compression: Compression parameter for the Maxwell Plate.
265270
:param geometry: Geometry parameters for the Maxwell Plate.
266271
:param kwargs: List of keyword arguments defining the properties of a class.
267272
268273
:return entity: Registered Entity to the workspace.
269274
"""
270-
plate = super().create(workspace, **kwargs)
275+
plate = super().create(workspace, compression=compression, **kwargs)
271276

272277
if geometry is not None:
273278
plate.geometry = geometry

geoh5py/shared/entity.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,12 @@ def coordinate_reference_system(self, value: dict):
227227
self.metadata = {"Coordinate Reference System": coordinate_reference_system}
228228

229229
@classmethod
230-
def create(cls, workspace, **kwargs):
230+
def create(cls, workspace, compression: int = 5, **kwargs):
231231
"""
232232
Function to create an entity.
233233
234234
:param workspace: Workspace to be added to.
235+
:param compression: Compression to be applied to the entity.
235236
:param kwargs: List of keyword arguments defining the properties of a class.
236237
237238
:return entity: Registered Entity to the workspace.
@@ -242,9 +243,7 @@ def create(cls, workspace, **kwargs):
242243
else {}
243244
)
244245
new_object = workspace.create_entity(
245-
cls,
246-
entity=kwargs,
247-
entity_type=entity_type_kwargs,
246+
cls, entity=kwargs, entity_type=entity_type_kwargs, compression=compression
248247
)
249248
return new_object
250249

geoh5py/shared/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
from .entity import Entity
4747
from .entity_container import EntityContainer
4848

49-
DEFAULT_PAGE_BUF_SIZE = 16777216
49+
DEFAULT_PAGE_SIZE = 65536 # Default page_size for H5, in bytes
50+
5051
INV_KEY_MAP = {
5152
"Allow delete": "allow_delete",
5253
"Allow delete contents": "allow_delete_content",

geoh5py/workspace/workspace.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
from geoh5py.shared.entity_type import EntityType
7171
from geoh5py.shared.exceptions import Geoh5FileClosedError
7272
from geoh5py.shared.utils import (
73-
DEFAULT_PAGE_BUF_SIZE,
73+
DEFAULT_PAGE_SIZE,
7474
ClassIdentifierEnum,
7575
as_str_if_utf8_bytes,
7676
clear_array_attributes,
@@ -115,6 +115,7 @@ class Workspace(AbstractContextManager):
115115
:param name: Name of the project.
116116
:param repack: Repack the *geoh5* file after closing.
117117
:param version: Version of the project.
118+
:param page_size: Page size of the h5 file, in bytes.
118119
"""
119120

120121
_active_ref: ClassVar[ReferenceType[Workspace]] | type(None) = type(None) # type: ignore
@@ -136,6 +137,7 @@ def __init__(
136137
name: str = "GEOSCIENCE",
137138
repack: bool = False,
138139
version: float = 2.1,
140+
page_size: int = DEFAULT_PAGE_SIZE,
139141
):
140142
self._root: RootGroup
141143
self._data: dict[uuid.UUID, ReferenceType[data.Data]] = {}
@@ -152,7 +154,7 @@ def __init__(
152154
self._repack: bool = repack
153155
self._types: dict[uuid.UUID, ReferenceType[EntityType]] = {}
154156
self._version: float = version
155-
157+
self._page_size: int = validate_page_size(page_size)
156158
self._h5file = self.validate_h5file_input(h5file)
157159

158160
self.open(mode=mode)
@@ -1298,18 +1300,33 @@ def _open_h5(self, mode) -> h5py.File:
12981300
def _create_h5(self) -> h5py.File:
12991301
"""
13001302
Generate a new geoh5 file with core structure.
1303+
1304+
Default mode for ANALYST uses:
1305+
- Page size (fs_page_size) of 65536 bytes.
1306+
- Page buffer (page_buf_size) is able to hold 256 pages.
1307+
- Library version (libver) fixed with (lower, upper) bound.
13011308
"""
13021309
self._geoh5 = h5py.File(
13031310
self.h5file,
13041311
"x",
13051312
fs_strategy="page",
1306-
page_buf_size=DEFAULT_PAGE_BUF_SIZE,
1313+
page_buf_size=self._page_size * 256,
1314+
fs_page_size=self._page_size,
13071315
libver=("v110", "v114"),
13081316
)
13091317
H5Writer.init_geoh5(self._geoh5, self)
13101318

13111319
return self._geoh5
13121320

1321+
@property
1322+
def page_size(self) -> int:
1323+
"""
1324+
HDF5 page size.
1325+
1326+
Must be a multiple of 2, greater than or equal 512.
1327+
"""
1328+
return self._page_size
1329+
13131330
def promote(self, value: Any) -> Any:
13141331
"""
13151332
Promote uuid to entity as it is or in a nested structure.
@@ -1605,3 +1622,20 @@ def active_workspace(workspace: Workspace):
16051622
previous_active = previous_active_ref()
16061623
if previous_active is not None:
16071624
previous_active.activate() # pylint: disable=no-member
1625+
1626+
1627+
def validate_page_size(value: int) -> int:
1628+
"""
1629+
Check if a page size is valid value. Raise an error if not valid,
1630+
else return the value as-is.
1631+
1632+
:param value: A positive integer multiple of 2, >=512.
1633+
:return: Page size value, same as :param value:
1634+
"""
1635+
if not isinstance(value, int):
1636+
raise TypeError("Page size must be an integer.")
1637+
1638+
if value < 512 or value % 2 != 0:
1639+
raise ValueError("Page size must be an integer multiple of 2, and >=512.")
1640+
1641+
return value

tests/workspace_test.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from __future__ import annotations
2121

2222
import io
23+
import operator
24+
import os
2325
from pathlib import Path
2426

2527
import numpy as np
@@ -219,3 +221,49 @@ def test_network_drive_warning(tmp_path):
219221
network_drive.mkdir()
220222
with pytest.warns(match="Opening workspace with write access in a network drive"):
221223
_ = Workspace(network_drive / "test.geoh5")
224+
225+
226+
def test_page_size(tmp_path):
227+
228+
with pytest.raises(TypeError, match="Page size must be an integer"):
229+
Workspace.create(tmp_path / f"{__name__}.geoh5", page_size="abc")
230+
231+
with pytest.raises(
232+
ValueError, match="Page size must be an integer multiple of 2, and"
233+
):
234+
Workspace.create(tmp_path / f"{__name__}.geoh5", page_size=128)
235+
236+
with pytest.raises(
237+
ValueError, match="Page size must be an integer multiple of 2, and"
238+
):
239+
Workspace.create(tmp_path / f"{__name__}.geoh5", page_size=601)
240+
241+
with Workspace.create(tmp_path / f"{__name__}.geoh5", page_size=512) as workspace:
242+
assert workspace.page_size == 512
243+
244+
245+
@pytest.mark.parametrize(
246+
"n_values, compressions, expected_opt",
247+
[
248+
(100, (1, 5), operator.eq), # No compression since < page_size
249+
(10000, (1, 5), operator.gt),
250+
],
251+
)
252+
def test_compression_below_page(
253+
tmp_path, n_values: int, compressions: tuple, expected_opt
254+
):
255+
vertices = np.c_[np.arange(n_values), np.arange(n_values), np.arange(n_values)]
256+
values = np.arange(n_values)
257+
258+
with Workspace.create(tmp_path / "test_low.geoh5", page_size=512) as ws:
259+
pt = Points.create(ws, vertices=vertices, compression=compressions[0])
260+
pt.add_data({"values": {"values": values}}, compression=compressions[0])
261+
262+
with Workspace.create(tmp_path / "test_high.geoh5", page_size=512) as ws:
263+
pt = Points.create(ws, vertices=vertices, compression=compressions[1])
264+
pt.add_data({"values": {"values": values}}, compression=compressions[1])
265+
266+
size_low_comp = os.stat(tmp_path / "test_low.geoh5").st_size
267+
size_med_comp = os.stat(tmp_path / "test_high.geoh5").st_size
268+
269+
assert expected_opt(size_low_comp, size_med_comp)

0 commit comments

Comments
 (0)