Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,21 @@ The rules for this file:
* accompany each entry with github issue/PR number (Issue #xyz)

-------------------------------------------------------------------------------
MM/DD/2026 orbeckst
MM/DD/2026 orbeckst, spyke7

* 1.3.0

Changes

* update init signature for OpenDX.DXClass to include name and components
as optional kwargs to streamline init in child classes (PR #179)

Enhancements

* Implemented loading of a Grid from a native object for OpenVDB (Issue #162,
PR #170)

Fixes


05/22/2026 orbeckst, spyke7
Expand Down
112 changes: 90 additions & 22 deletions gridData/OpenVDB.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,20 +109,21 @@
@dataclass
class DownCastTo:
""":func:`~dataclasses.dataclass` decorator serving as a marker for a downcast.

This function is used to create a proxy for an OpenVDB grid type.
The field :attr:`gridType` contains the OpenVDB grid type that it represents.
:meth:`OpenVDBField._get_best_grid_type` selects a OpenVDB grid that best matches
the numpy dtype of the data but in some cases, only target OpenVDB grid types are
available that loose precision. In this case, this class wraps the orginal OpenVDB
class to indicate that the downcast. For example, ::

np.dtype("int32"): ["Int32Grid", DownCastTo("FloatGrid")]

indicates that NumPy int32 data should be represented by a :class:`openvdb.Int32Grid`
but if this is not available, a :class:`openvdb.FloatGrid` is used instead,
which, however, is only able to represent a subset of all 32-bit integers.
"""

gridType: str


Expand All @@ -142,7 +143,7 @@ class OpenVDBField(object):

import gridData.OpenVDB as OpenVDB

vdb_field = OpenVDB.OpenVDBField(grid=np.ones((3, 4, 5)),
vdb_field = OpenVDB.OpenVDBField(grid=np.ones((3, 4, 5)),
origin=np.array([1.5, 0, 0]),
delta=np.array([0.5, 0.5, 0.25]),
name='density')
Expand All @@ -154,6 +155,32 @@ class OpenVDBField(object):
g.export('output.vdb', format='vdb')
"""

# dtype maps for numpy to vdb
_DTYPES_NP2VDB = {
np.dtype("bool"): ["BoolGrid"],
np.dtype("int8"): ["Int32Grid", "FloatGrid"],
np.dtype("uint8"): ["Int32Grid", "FloatGrid"],
np.dtype("int16"): ["Int32Grid", "FloatGrid"],
np.dtype("uint16"): ["Int32Grid", "FloatGrid"],
np.dtype("int32"): ["Int32Grid", DownCastTo("FloatGrid")],
np.dtype("uint32"): [DownCastTo("Int32Grid"), DownCastTo("FloatGrid")],
np.dtype("int64"): ["Int64Grid", DownCastTo("FloatGrid")],
np.dtype("uint64"): ["Int64Grid", DownCastTo("FloatGrid")],
np.dtype("float16"): ["HalfGrid", "FloatGrid"],
np.dtype("float32"): ["FloatGrid"],
np.dtype("float64"): ["DoubleGrid", DownCastTo("FloatGrid")],
}

# dtype maps for vdb to numpy
_DTYPES_VDB2NP = {
"BoolGrid": np.dtype("bool"),
"Int32Grid": np.dtype("int32"),
"Int64Grid": np.dtype("int64"),
"FloatGrid": np.dtype("float32"),
"DoubleGrid": np.dtype("float64"),
"HalfGrid": np.dtype("float16"),
}

def __init__(
self,
grid=None,
Expand Down Expand Up @@ -205,8 +232,13 @@ def __init__(
self.metadata = {}

if grid is not None:
self._populate(grid, origin, delta)
self.vdb_grid = self._create_openvdb_grid()
if isinstance(grid, vdb.GridBase):
self.vdb_grid = grid
self._extract_from_vdb_grid()
else:
self._populate(grid, origin, delta)
self.vdb_grid = self._create_openvdb_grid()

else:
self.grid = None
self.origin = None
Expand Down Expand Up @@ -272,6 +304,57 @@ def native(self):
"""
return self.vdb_grid

def _extract_from_vdb_grid(self):
"""Extract numpy array, origin, delta from stored VDB grid.

This method converts the sparse VDB grid to a dense numpy array
and extracts the transform information.
"""
for key in self.vdb_grid.metadata:
try:
self.metadata[key] = self.vdb_grid[key]
except (TypeError, ValueError) as e:
warnings.warn(
f"Could not read metadata key '{key}' from VDB grid: {e}",
UserWarning,
)

transformation = self.vdb_grid.transform

v_origin = np.array(transformation.indexToWorld([0, 0, 0]))
v_delta = np.array(transformation.indexToWorld([1, 1, 1])) - v_origin

self.origin = v_origin
self.delta = v_delta

try:
dtype = self._DTYPES_VDB2NP[type(self.vdb_grid).__name__]
except KeyError:
raise TypeError(
f"Unknown VDB grid type '{type(self.vdb_grid).__name__}', cannot map to a numpy dtype."
)

bbox = self.vdb_grid.evalActiveVoxelBoundingBox()

if bbox is None or any(bbox[1][i] < bbox[0][i] for i in range(3)):
warnings.warn(
"VDB grid has no active voxels (empty bounding box). Returning an empty array of shape (0, 0, 0).",
RuntimeWarning,
)
self.grid = np.zeros((0, 0, 0), dtype=dtype)
return

shape = tuple(np.array(bbox[1]) - np.array(bbox[0]) + 1)

self.grid = np.zeros(shape, dtype=dtype)
print(dtype)
self.vdb_grid.copyToArray(self.grid, ijk=bbox[0])

if not np.all(np.array(bbox[0]) == 0):
self.origin = np.array(
transformation.indexToWorld(np.array(bbox[0]).tolist())
)

def _populate(self, grid, origin, delta):
"""Populate the field with grid data.

Expand Down Expand Up @@ -331,23 +414,8 @@ def _get_best_grid_type(self):
TypeError
If dtype is not supported or no suitable grid type is available
"""
datatypes = {
np.dtype("bool"): ["BoolGrid"],
np.dtype("int8"): ["Int32Grid", "FloatGrid"],
np.dtype("uint8"): ["Int32Grid", "FloatGrid"],
np.dtype("int16"): ["Int32Grid", "FloatGrid"],
np.dtype("uint16"): ["Int32Grid", "FloatGrid"],
np.dtype("int32"): ["Int32Grid", DownCastTo("FloatGrid")],
np.dtype("uint32"): [DownCastTo("Int32Grid"), DownCastTo("FloatGrid")],
np.dtype("int64"): ["Int64Grid", DownCastTo("FloatGrid")],
np.dtype("uint64"): ["Int64Grid", DownCastTo("FloatGrid")],
np.dtype("float16"): ["HalfGrid", "FloatGrid"],
np.dtype("float32"): ["FloatGrid"],
np.dtype("float64"): ["DoubleGrid", DownCastTo("FloatGrid")],
}

try:
vdb_gridtypes = datatypes[self.grid.dtype]
vdb_gridtypes = self._DTYPES_NP2VDB[self.grid.dtype]
except KeyError:
raise TypeError(f"Data type {self.grid.dtype} not supported for VDB")

Expand Down
15 changes: 15 additions & 0 deletions gridData/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,21 @@ def __init__(self, grid=None, edges=None, origin=None, delta=None,
self.interpolation_cval = None # default to using min(grid)

if grid is not None:
try:
# if a openvdb native grid is passed
import openvdb as vdb
if isinstance(grid, vdb.GridBase):
vdb_field = OpenVDB.OpenVDBField(grid=grid)
self.metadata = vdb_field.metadata
self._load(
grid=vdb_field.grid,
origin=vdb_field.origin,
delta=vdb_field.delta,
metadata=vdb_field.metadata,
)
return
except ImportError:
pass
if isinstance(grid, str):
# can probably safely try to load() it...
filename = grid
Expand Down
98 changes: 98 additions & 0 deletions gridData/tests/test_vdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,104 @@ def test_from_native_grid_shape_values_and_dimension(self, grid345):
world = native_grid.transform.indexToWorld((0, 0, 0))
assert_allclose([world[0], world[1], world[2]], g.origin, rtol=1e-5)

def test_file_roundtrip_native_vdbgrid(self, tmpdir, grid345):
data, g = grid345
g.metadata["name"] = "new_density"

outfile = str(tmpdir / "roundtrip.vdb")
g.export(outfile)

grids, _ = vdb.readAll(outfile)
assert len(grids) == 1

grid_vdb = grids[0]
new_vdb_grid = Grid(grid=grid_vdb)
assert_allclose(new_vdb_grid.grid, g.grid, rtol=1e-5)
assert_allclose(new_vdb_grid.origin, g.origin, rtol=1e-5)
assert_allclose(new_vdb_grid.delta, g.delta, rtol=1e-5)
assert new_vdb_grid.metadata["name"] == "new_density"
assert new_vdb_grid.grid.dtype == np.dtype("float32")
assert_allclose(new_vdb_grid.grid, data, rtol=1e-5)

def test_extract_from_vdb_grid(self, grid345):
data, g = grid345
g.metadata["name"] = "new_density"

native = g.convert_to("vdb")
new_vdb_grid = Grid(grid=native)

assert_allclose(new_vdb_grid.grid, g.grid, rtol=1e-5)
assert_allclose(new_vdb_grid.origin, g.origin, rtol=1e-5)
assert_allclose(new_vdb_grid.delta, g.delta, rtol=1e-5)
assert new_vdb_grid.metadata["name"] == "new_density"
assert new_vdb_grid.grid.dtype == np.dtype("float32")
assert_allclose(new_vdb_grid.grid, data, rtol=1e-5)

@pytest.mark.parametrize(
"vdb_type,np_dtype", list(gridData.OpenVDB.OpenVDBField._DTYPES_VDB2NP.items())
)
def test_extract_dtype_roundtrip(self, vdb_type, np_dtype):
vdb_cls = getattr(vdb, vdb_type, None)
if vdb_cls is None:
pytest.skip(f"{vdb_type} not available in this openvdb build")

native = vdb_cls()
native.name = "test"
acc = native.getAccessor()
acc.setValueOn((0, 0, 0), True if np_dtype == np.dtype("bool") else 1)
acc.setValueOn((1, 1, 1), True if np_dtype == np.dtype("bool") else 2)

field = gridData.OpenVDB.OpenVDBField(grid=native)

assert field.grid.dtype == np_dtype
assert field.grid.shape != (0, 0, 0)

def test_extract_unknown_vdb_type_warns(self):
native = vdb.FloatGrid()
acc = native.getAccessor()
acc.setValueOn((0, 0, 0), 1.0)

patched = {
k: v
for k, v in gridData.OpenVDB.OpenVDBField._DTYPES_VDB2NP.items()
if k != "FloatGrid"
}
with patch.object(gridData.OpenVDB.OpenVDBField, "_DTYPES_VDB2NP", patched):
with pytest.raises(TypeError, match="Unknown VDB grid type"):
field = gridData.OpenVDB.OpenVDBField(grid=native)

def test_extract_empty_vdb_grid_warns(self):
native = vdb.FloatGrid()
native.clear()

with pytest.warns(RuntimeWarning, match="no active voxels"):
field = gridData.OpenVDB.OpenVDBField(grid=native)

assert field.grid.shape == (0, 0, 0)

def test_extract_unreadable_metadata_warns(self, grid345):
data, g = grid345
g.metadata["readable"] = "hello"
g.metadata["factor"] = 42

native = g.convert_to("vdb")

original_getitem = native.__class__.__getitem__

def patched_getitem(self, key):
if key == "factor":
raise TypeError("unsupported metadata type")
return original_getitem(self, key)

with patch.object(native.__class__, "__getitem__", patched_getitem):
with pytest.warns(
UserWarning, match="Could not read metadata key 'factor'"
):
field = gridData.OpenVDB.OpenVDBField(grid=native)

assert field.metadata.get("readable") == "hello"
assert "factor" not in field.metadata


@pytest.mark.skipif(
not HAS_OPENVDB, reason="Need openvdb to test import error handling"
Expand Down
Loading