From 22a5995ca2942551e4a119d1975388e67c380f2c Mon Sep 17 00:00:00 2001 From: spyke7 Date: Sat, 27 Dec 2025 13:35:32 +0530 Subject: [PATCH 01/17] added a simple program to export files in .vdb format --- AUTHORS | 1 + CHANGELOG | 13 +++ gridData/OpenVDB.py | 190 +++++++++++++++++++++++++++++++++++++ gridData/__init__.py | 3 +- gridData/core.py | 21 ++++ gridData/tests/test_vdb.py | 178 ++++++++++++++++++++++++++++++++++ 6 files changed, 405 insertions(+), 1 deletion(-) create mode 100644 gridData/OpenVDB.py create mode 100644 gridData/tests/test_vdb.py diff --git a/AUTHORS b/AUTHORS index 26a9fbb..0b6dad0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -27,3 +27,4 @@ Contributors: * Zhiyi Wu * Olivier Languin-Cattoën * Andrés Montoya (logo) +* Shreejan Dolai diff --git a/CHANGELOG b/CHANGELOG index 7741a9e..df21398 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,19 @@ The rules for this file: * accompany each entry with github issue/PR number (Issue #xyz) ------------------------------------------------------------------------------ +27/12/2025 IAlibay, spyke7, orbeckst + * 1.1.0 + + Changes + + * Added `OpenVDB.py` inside `gridData` to simply export and write in .vdb format + * Added `test_vdb.py` inside `gridData\tests` + + Fixes + + * Adding openVDB formats (Issue #141) + + ??/??/???? IAlibay, ollyfutur, conradolandia, orbeckst * 1.1.0 diff --git a/gridData/OpenVDB.py b/gridData/OpenVDB.py new file mode 100644 index 0000000..9afbb6b --- /dev/null +++ b/gridData/OpenVDB.py @@ -0,0 +1,190 @@ +r""" +:mod:`~gridData.OpenVDB` --- routines to write OpenVDB files +============================================================= + +The OpenVDB format is used by Blender and other VFX software for +volumetric data. See https://www.openvdb.org + +.. Note:: This module implements a simple writer for 3D regular grids, + sufficient to export density data for visualization in Blender. + +The OpenVDB format uses a sparse tree structure to efficiently store +volumetric data. It is the native format for Blender's volume system. + + +Writing OpenVDB files +--------------------- + +If you have a :class:`~gridData.core.Grid` object, you can write it to +OpenVDB format:: + + from gridData import Grid + g = Grid("data.dx") + g.export("data.vdb") + +This will create a file that can be imported directly into Blender +(File -> Import -> OpenVDB). + + +Building an OpenVDB field from a numpy array +--------------------------------------------- + +Requires: + +grid + numpy 3D array +origin + cartesian coordinates of the center of the (0,0,0) grid cell +delta + n x n array with the length of a grid cell along each axis + +Example:: + + import OpenVDB + vdb_field = OpenVDB.field('density') + vdb_field.populate(grid, origin, delta) + vdb_field.write('output.vdb') + + +Classes and functions +--------------------- + +""" + +import numpy +import warnings + +try: + import pyopenvdb as vdb +except ImportError: + vdb = None + + +class field(object): + """OpenVDB field object for writing volumetric data. + + This class provides a simple interface to write 3D grid data to + OpenVDB format, which can be imported into Blender and other + VFX software. + + The field object holds grid data and metadata, and can write it + to a .vdb file. + + Example + ------- + Create a field and write it:: + + vdb_field = OpenVDB.field('density') + vdb_field.populate(grid, origin, delta) + vdb_field.write('output.vdb') + + Or use directly from Grid:: + + g = Grid(...) + g.export('output.vdb', format='vdb') + + """ + + def __init__(self, name='density'): + """Initialize an OpenVDB field. + + Parameters + ---------- + name : str + Name of the grid (will be visible in Blender) + + """ + if vdb is None: + raise ImportError( + "pyopenvdb is required to write VDB files. " + ) + self.name = name + self.grid = None + self.origin = None + self.delta = None + + def populate(self, grid, origin, delta): + """Populate the field with grid data. + + Parameters + ---------- + grid : numpy.ndarray + 3D numpy array with the data + origin : numpy.ndarray + Coordinates of the center of grid cell [0,0,0] + delta : numpy.ndarray + Grid spacing (can be 1D array or diagonal matrix) + + Raises + ------ + ValueError + If grid is not 3D + + """ + grid = numpy.asarray(grid) + if grid.ndim != 3: + raise ValueError( + "OpenVDB only supports 3D grids, got {}D".format(grid.ndim)) + + self.grid = grid.astype(numpy.float32) # OpenVDB uses float32 + self.origin = numpy.asarray(origin) + + # Handle delta: could be 1D array or diagonal matrix + delta = numpy.asarray(delta) + if delta.ndim == 2: + # Extract diagonal if it's a matrix + self.delta = numpy.array([delta[i, i] for i in range(3)]) + else: + self.delta = delta + + def write(self, filename): + """Write the field to an OpenVDB file. + + Parameters + ---------- + filename : str + Output filename (should end in .vdb) + + """ + if self.grid is None: + raise ValueError("No data to write. Use populate() first.") + + # Create OpenVDB grid + vdb_grid = vdb.FloatGrid() + vdb_grid.name = self.name + + # Set up transform (voxel size and position) + # Check for uniform spacing + if not numpy.allclose(self.delta, self.delta[0]): + warnings.warn( + "Non-uniform grid spacing {}. Using average spacing.".format( + self.delta)) + voxel_size = float(numpy.mean(self.delta)) + else: + voxel_size = float(self.delta[0]) + + # Create linear transform with uniform voxel size + transform = vdb.createLinearTransform(voxelSize=voxel_size) + + # OpenVDB transform is at corner of voxel [0,0,0], + # but GridDataFormats origin is at center of voxel [0,0,0] + corner_origin = self.origin - 0.5 * self.delta + transform.translate(corner_origin) + vdb_grid.transform = transform + + # Set background value for sparse storage + vdb_grid.background = 0.0 + + # Populate the grid + + accessor = vdb_grid.getAccessor() + threshold = 1e-10 + + for i in range(self.grid.shape[0]): + for j in range(self.grid.shape[1]): + for k in range(self.grid.shape[2]): + value = float(self.grid[i, j, k]) + if abs(value) > threshold: + accessor.setValueOn((i, j, k), value) + + vdb.write(filename, grids=[vdb_grid]) \ No newline at end of file diff --git a/gridData/__init__.py b/gridData/__init__.py index bfe79a1..c5aaa0e 100644 --- a/gridData/__init__.py +++ b/gridData/__init__.py @@ -110,8 +110,9 @@ from . import OpenDX from . import gOpenMol from . import mrc +from . import OpenVDB -__all__ = ['Grid', 'OpenDX', 'gOpenMol', 'mrc'] +__all__ = ['Grid', 'OpenDX', 'gOpenMol', 'mrc', 'OpenVDB'] from importlib.metadata import version __version__ = version("GridDataFormats") diff --git a/gridData/core.py b/gridData/core.py index 3395d62..b120179 100644 --- a/gridData/core.py +++ b/gridData/core.py @@ -35,6 +35,7 @@ from . import OpenDX from . import gOpenMol from . import mrc +from . import OpenVDB def _grid(x): @@ -203,6 +204,7 @@ def __init__(self, grid=None, edges=None, origin=None, delta=None, 'PKL': self._export_python, 'PICKLE': self._export_python, # compatibility 'PYTHON': self._export_python, # compatibility + 'VDB': self._export_vdb, } self._loaders = { 'CCP4': self._load_mrc, @@ -676,7 +678,26 @@ def _export_dx(self, filename, type=None, typequote='"', **kwargs): if ext == '.gz': filename = root + ext dx.write(filename) + + def _export_vdb(self, filename, **kwargs): + """Export the density grid to an OpenVDB file. + The file format is compatible with Blender's volume system. + Only 3D grids are supported. + + For the file format see https://www.openvdb.org + """ + if self.grid.ndim != 3: + raise ValueError( + "OpenVDB export requires a 3D grid, got {}D".format(self.grid.ndim)) + + # Get grid name from metadata if available + grid_name = self.metadata.get('name', 'density') + + # Create and populate VDB field + vdb_field = OpenVDB.field(grid_name) + vdb_field.populate(self.grid, self.origin, self.delta) + vdb_field.write(filename) def save(self, filename): """Save a grid object to `filename` and add ".pickle" extension. diff --git a/gridData/tests/test_vdb.py b/gridData/tests/test_vdb.py new file mode 100644 index 0000000..dcd9abb --- /dev/null +++ b/gridData/tests/test_vdb.py @@ -0,0 +1,178 @@ +import numpy as np +from numpy.testing import (assert_array_equal, assert_array_almost_equal, + assert_almost_equal) + +import pytest + +from gridData import Grid + +def f_arithmetic(g): + return g + g - 2.5 * g / (g + 5.3) + +@pytest.fixture(scope="class") +def data(): + d = dict( + griddata=np.arange(1, 28).reshape(3, 3, 3), + origin=np.zeros(3), + delta=np.ones(3)) + d['grid'] = Grid(d['griddata'], origin=d['origin'], + delta=d['delta']) + return d + +class TestGrid(object): + @pytest.fixture + def pklfile(self, data, tmpdir): + g = data['grid'] + fn = tmpdir.mkdir('grid').join('grid.dat') + g.save(fn) # always saves as pkl + return fn + + def test_init(self, data): + g = Grid(data['griddata'], origin=data['origin'], + delta=1) + assert_array_equal(g.delta, data['delta']) + + def test_init_wrong_origin(self, data): + with pytest.raises(TypeError): + Grid(data['griddata'], origin=np.ones(4), delta=data['delta']) + + def test_init_wrong_delta(self, data): + with pytest.raises(TypeError): + Grid(data['griddata'], origin=data['origin'], delta=np.ones(4)) + + def test_init_missing_delta_ValueError(self, data): + with pytest.raises(ValueError): + Grid(data['griddata'], origin=data['origin']) + + def test_init_missing_origin_ValueError(self, data): + with pytest.raises(ValueError): + Grid(data['griddata'], delta=data['delta']) + + def test_init_wrong_data_exception(self): + with pytest.raises(IOError): + Grid("__does_not_exist__") + + def test_load_wrong_fileformat_ValueError(self): + with pytest.raises(ValueError): + Grid(grid=True, file_format="xxx") + + def test_equality(self, data): + assert data['grid'] == data['grid'] + assert data['grid'] != 'foo' + g = Grid(data['griddata'], origin=data['origin'] + 1, delta=data['delta']) + assert data['grid'] != g + + def test_compatibility_type(self, data): + assert data['grid'].check_compatible(data['grid']) + assert data['grid'].check_compatible(3) + g = Grid(data['griddata'], origin=data['origin'], delta=data['delta']) + assert data['grid'].check_compatible(g) + assert data['grid'].check_compatible(g.grid) + + def test_wrong_compatibile_type(self, data): + g = Grid(data['griddata'], origin=data['origin'] + 1, delta=data['delta']) + with pytest.raises(TypeError): + data['grid'].check_compatible(g) + + arr = np.zeros(data['griddata'].shape[-1] + 1) # Not broadcastable + with pytest.raises(TypeError): + data['grid'].check_compatible(arr) + + def test_non_orthonormal_boxes(self, data): + delta = np.eye(3) + with pytest.raises(NotImplementedError): + Grid(data['griddata'], origin=data['origin'], delta=delta) + + def test_centers(self, data): + g = Grid(data['griddata'], origin=np.ones(3), delta=data['delta']) + centers = np.array(list(g.centers())) + assert_array_equal(centers[0], g.origin) + assert_array_equal(centers[-1] - g.origin, + (np.array(g.grid.shape) - 1) * data['delta']) + + def test_resample_factor_failure(self, data): + pytest.importorskip('scipy') + + with pytest.raises(ValueError): + g = data['grid'].resample_factor(0) + + def test_resample_factor(self, data): + pytest.importorskip('scipy') + + g = data['grid'].resample_factor(2) + assert_array_equal(g.delta, np.ones(3) * .5) + + assert_array_equal(g.grid.shape, np.ones(3) * 5) + + assert_array_almost_equal(g.grid[::2, ::2, ::2], + data['grid'].grid) + + def test_load_pickle(self, data, tmpdir): + g = data['grid'] + fn = str(tmpdir.mkdir('grid').join('grid.pkl')) + g.save(fn) + + h = Grid() + h.load(fn) + + assert h == g + + def test_init_pickle_pathobjects(self, data, tmpdir): + g = data['grid'] + fn = tmpdir.mkdir('grid').join('grid.pickle') + g.save(fn) + + h = Grid(fn) + + assert h == g + + @pytest.mark.parametrize("fileformat", ("pkl", "PKL", "pickle", "python")) + def test_load_fileformat(self, data, pklfile, fileformat): + h = Grid(pklfile, file_format="pkl") + assert h == data['grid'] + + @pytest.mark.xfail + @pytest.mark.parametrize("fileformat", ("ccp4", "plt", "dx")) + def test_load_wrong_fileformat(self, data, pklfile, fileformat): + with pytest.raises('ValueError'): + Grid(pklfile, file_format=fileformat) + + @pytest.mark.parametrize("fileformat", ("dx", "pkl")) + def test_export(self, data, fileformat, tmpdir): + g = data['grid'] + fn = tmpdir.mkdir('grid_export').join("grid.{}".format(fileformat)) + g.export(fn) + h = Grid(fn) + assert g == h + + @pytest.mark.parametrize("fileformat", ("ccp4", "plt")) + def test_export_not_supported(self, data, fileformat, tmpdir): + g = data['grid'] + fn = tmpdir.mkdir('grid_export').join("grid.{}".format(fileformat)) + with pytest.raises(ValueError): + g.export(fn) + + +def test_inheritance(data): + class DerivedGrid(Grid): + pass + + dg = DerivedGrid(data['griddata'], origin=data['origin'], + delta=data['delta']) + result = f_arithmetic(dg) + + assert isinstance(result, DerivedGrid) + + ref = f_arithmetic(data['grid']) + assert_almost_equal(result.grid, ref.grid) + +def test_anyarray(data): + ma = np.ma.MaskedArray(data['griddata']) + mg = Grid(ma, origin=data['origin'], delta=data['delta']) + + assert isinstance(mg.grid, ma.__class__) + + result = f_arithmetic(mg) + ref = f_arithmetic(data['grid']) + + assert_almost_equal(result.grid, ref.grid) From d8091987310495418a96dbb16bc65ab8eee5f8f3 Mon Sep 17 00:00:00 2001 From: spyke7 Date: Sun, 18 Jan 2026 12:04:04 +0530 Subject: [PATCH 02/17] updated changelog and OpenVDB.py --- CHANGELOG | 17 +++-------------- gridData/OpenVDB.py | 21 ++++++++++++++------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index df21398..0307c1d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,24 +13,12 @@ The rules for this file: * accompany each entry with github issue/PR number (Issue #xyz) ------------------------------------------------------------------------------ -27/12/2025 IAlibay, spyke7, orbeckst - * 1.1.0 - - Changes - - * Added `OpenVDB.py` inside `gridData` to simply export and write in .vdb format - * Added `test_vdb.py` inside `gridData\tests` - - Fixes - - * Adding openVDB formats (Issue #141) - - -??/??/???? IAlibay, ollyfutur, conradolandia, orbeckst +??/??/???? IAlibay, ollyfutur, conradolandia, orbeckst, spyke7 * 1.1.0 Changes + * Added OpenVDB module for exporting to .vdb format (PR #148) * update logo from https://github.com/MDAnalysis/mdanalysis-subprojects-branding (issue #143) * Python 3.13 and 3.14 are now supported (PR #140) @@ -38,6 +26,7 @@ The rules for this file: Enhancements + * Added openVDB format exports (Issue #141) * `Grid` now accepts binary operations with any operand that can be broadcasted to the grid's shape according to `numpy` broadcasting rules (PR #142) diff --git a/gridData/OpenVDB.py b/gridData/OpenVDB.py index 9afbb6b..2cd97d5 100644 --- a/gridData/OpenVDB.py +++ b/gridData/OpenVDB.py @@ -5,6 +5,8 @@ The OpenVDB format is used by Blender and other VFX software for volumetric data. See https://www.openvdb.org +pyopenvdb: https://github.com/AcademySoftwareFoundation/openvdb + .. Note:: This module implements a simple writer for 3D regular grids, sufficient to export density data for visualization in Blender. @@ -57,7 +59,10 @@ try: import pyopenvdb as vdb except ImportError: - vdb = None + try: + import openvdb as vdb + except ImportError: + vdb = None class field(object): @@ -97,6 +102,7 @@ def __init__(self, name='density'): if vdb is None: raise ImportError( "pyopenvdb is required to write VDB files. " + "Install it with: conda install -c conda-forge openvdb" ) self.name = name self.grid = None @@ -180,11 +186,12 @@ def write(self, filename): accessor = vdb_grid.getAccessor() threshold = 1e-10 - for i in range(self.grid.shape[0]): - for j in range(self.grid.shape[1]): - for k in range(self.grid.shape[2]): - value = float(self.grid[i, j, k]) - if abs(value) > threshold: - accessor.setValueOn((i, j, k), value) + mask = numpy.abs(slef.grid) > threshold + indices = numpy.argwhere(mask) + + for idx in indices: + i, j, k = idx + value = float(self.grid[i, j, k]) + accessor.setValueOn((i, j, k), value) vdb.write(filename, grids=[vdb_grid]) \ No newline at end of file From 79c96de2423e1553d28f2cf162261626f3adec69 Mon Sep 17 00:00:00 2001 From: spyke7 Date: Sun, 18 Jan 2026 12:16:01 +0530 Subject: [PATCH 03/17] fixed core.py errors --- gridData/core.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/gridData/core.py b/gridData/core.py index 8c2ecc3..f92db91 100644 --- a/gridData/core.py +++ b/gridData/core.py @@ -704,6 +704,23 @@ def _export_dx(self, filename, type=None, typequote='"', **kwargs): def _export_vdb(self, filename, **kwargs): """Export the density grid to an OpenVDB file. + + The file format is compatible with Blender's volume system. + Only 3D grids are supported. + + For the file format see https://www.openvdb.org + """ + if self.grid.ndim != 3: + raise ValueError( + "OpenVDB export requires a 3D grid, got {}D".format(self.grid.ndim)) + + # Get grid name from metadata if available + grid_name = self.metadata.get('name', 'density') + + # Create and populate VDB field + vdb_field = OpenVDB.field(grid_name) + vdb_field.populate(self.grid, self.origin, self.delta) + vdb_field.write(filename) def _export_mrc(self, filename, **kwargs): """Export the density grid to an MRC/CCP4 file. @@ -740,22 +757,6 @@ def _export_mrc(self, filename, **kwargs): # Write to file mrc_file.write(filename) - The file format is compatible with Blender's volume system. - Only 3D grids are supported. - - For the file format see https://www.openvdb.org - """ - if self.grid.ndim != 3: - raise ValueError( - "OpenVDB export requires a 3D grid, got {}D".format(self.grid.ndim)) - - # Get grid name from metadata if available - grid_name = self.metadata.get('name', 'density') - - # Create and populate VDB field - vdb_field = OpenVDB.field(grid_name) - vdb_field.populate(self.grid, self.origin, self.delta) - vdb_field.write(filename) def save(self, filename): """Save a grid object to `filename` and add ".pickle" extension. From 0649210b2ec1acf35cdbfc449475dd412427753c Mon Sep 17 00:00:00 2001 From: spyke7 Date: Sun, 18 Jan 2026 13:19:24 +0530 Subject: [PATCH 04/17] fixed typo in OpenVDB.py --- gridData/OpenVDB.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gridData/OpenVDB.py b/gridData/OpenVDB.py index 2cd97d5..3ab119e 100644 --- a/gridData/OpenVDB.py +++ b/gridData/OpenVDB.py @@ -186,7 +186,7 @@ def write(self, filename): accessor = vdb_grid.getAccessor() threshold = 1e-10 - mask = numpy.abs(slef.grid) > threshold + mask = numpy.abs(self.grid) > threshold indices = numpy.argwhere(mask) for idx in indices: From f83f7090c6c9ba409360a891a04b3ce844e8cb00 Mon Sep 17 00:00:00 2001 From: spyke7 Date: Mon, 19 Jan 2026 18:42:34 +0530 Subject: [PATCH 05/17] updated OpenVDB.py --- gridData/OpenVDB.py | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/gridData/OpenVDB.py b/gridData/OpenVDB.py index 3ab119e..c865b07 100644 --- a/gridData/OpenVDB.py +++ b/gridData/OpenVDB.py @@ -132,13 +132,12 @@ def populate(self, grid, origin, delta): raise ValueError( "OpenVDB only supports 3D grids, got {}D".format(grid.ndim)) - self.grid = grid.astype(numpy.float32) # OpenVDB uses float32 + self.grid = numpy.transpose(grid, (2, 1, 0)).astype(numpy.float32) self.origin = numpy.asarray(origin) # Handle delta: could be 1D array or diagonal matrix delta = numpy.asarray(delta) if delta.ndim == 2: - # Extract diagonal if it's a matrix self.delta = numpy.array([delta[i, i] for i in range(3)]) else: self.delta = delta @@ -159,30 +158,24 @@ def write(self, filename): vdb_grid = vdb.FloatGrid() vdb_grid.name = self.name - # Set up transform (voxel size and position) - # Check for uniform spacing - if not numpy.allclose(self.delta, self.delta[0]): - warnings.warn( - "Non-uniform grid spacing {}. Using average spacing.".format( - self.delta)) - voxel_size = float(numpy.mean(self.delta)) - else: - voxel_size = float(self.delta[0]) + # this is an explicit linear transform using per-axis voxel sizes + # world = diag(delta) * index + corner_origin + corner_origin = (self.origin - 0.5 * self.delta).astype(float) - # Create linear transform with uniform voxel size - transform = vdb.createLinearTransform(voxelSize=voxel_size) + # Constructing 4x4 row-major matrix where the last row is the translation + matrix = [ + [float(self.delta[0]), 0.0, 0.0, 0.0], + [0.0, float(self.delta[1]), 0.0, 0.0], + [0.0, 0.0, float(self.delta[2]), 0.0], + [float(corner_origin[0]), float(corner_origin[1]), float(corner_origin[2]), 1.0] + ] - # OpenVDB transform is at corner of voxel [0,0,0], - # but GridDataFormats origin is at center of voxel [0,0,0] - corner_origin = self.origin - 0.5 * self.delta - transform.translate(corner_origin) + transform = vdb.createLinearTransform(matrix) vdb_grid.transform = transform - # Set background value for sparse storage vdb_grid.background = 0.0 # Populate the grid - accessor = vdb_grid.getAccessor() threshold = 1e-10 From 09b88a3489667a6a3ea073692ef235125806c0ef Mon Sep 17 00:00:00 2001 From: spyke7 Date: Tue, 20 Jan 2026 00:56:54 +0530 Subject: [PATCH 06/17] removed transpose inside populate() --- gridData/OpenVDB.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gridData/OpenVDB.py b/gridData/OpenVDB.py index c865b07..55e8fa5 100644 --- a/gridData/OpenVDB.py +++ b/gridData/OpenVDB.py @@ -132,7 +132,7 @@ def populate(self, grid, origin, delta): raise ValueError( "OpenVDB only supports 3D grids, got {}D".format(grid.ndim)) - self.grid = numpy.transpose(grid, (2, 1, 0)).astype(numpy.float32) + self.grid = grid.astype(numpy.float32) self.origin = numpy.asarray(origin) # Handle delta: could be 1D array or diagonal matrix From 3c93f8da83d5abcb32c2310a4eb850f73bcb1e36 Mon Sep 17 00:00:00 2001 From: spyke7 Date: Sat, 24 Jan 2026 00:47:27 +0530 Subject: [PATCH 07/17] test_vdb.py updated --- gridData/tests/test_vdb.py | 247 +++++++++++++------------------------ 1 file changed, 88 insertions(+), 159 deletions(-) diff --git a/gridData/tests/test_vdb.py b/gridData/tests/test_vdb.py index dcd9abb..fd4eaef 100644 --- a/gridData/tests/test_vdb.py +++ b/gridData/tests/test_vdb.py @@ -1,178 +1,107 @@ import numpy as np -from numpy.testing import (assert_array_equal, assert_array_almost_equal, - assert_almost_equal) +from numpy.testing import assert_allclose, assert_equal import pytest +import gridData.OpenVDB from gridData import Grid -def f_arithmetic(g): - return g + g - 2.5 * g / (g + 5.3) +from . import datafiles -@pytest.fixture(scope="class") -def data(): - d = dict( - griddata=np.arange(1, 28).reshape(3, 3, 3), - origin=np.zeros(3), - delta=np.ones(3)) - d['grid'] = Grid(d['griddata'], origin=d['origin'], - delta=d['delta']) - return d -class TestGrid(object): - @pytest.fixture - def pklfile(self, data, tmpdir): - g = data['grid'] - fn = tmpdir.mkdir('grid').join('grid.dat') - g.save(fn) # always saves as pkl - return fn +try: + import pyopenvdb as vdb + HAS_OPENVDB = True +except ImportError: + try: + import openvdb as vdb + HAS_OPENVDB = True + except ImportError: + HAS_OPENVDB = False - def test_init(self, data): - g = Grid(data['griddata'], origin=data['origin'], - delta=1) - assert_array_equal(g.delta, data['delta']) - def test_init_wrong_origin(self, data): - with pytest.raises(TypeError): - Grid(data['griddata'], origin=np.ones(4), delta=data['delta']) - - def test_init_wrong_delta(self, data): - with pytest.raises(TypeError): - Grid(data['griddata'], origin=data['origin'], delta=np.ones(4)) - - def test_init_missing_delta_ValueError(self, data): - with pytest.raises(ValueError): - Grid(data['griddata'], origin=data['origin']) - - def test_init_missing_origin_ValueError(self, data): - with pytest.raises(ValueError): - Grid(data['griddata'], delta=data['delta']) - - def test_init_wrong_data_exception(self): - with pytest.raises(IOError): - Grid("__does_not_exist__") - - def test_load_wrong_fileformat_ValueError(self): - with pytest.raises(ValueError): - Grid(grid=True, file_format="xxx") - - def test_equality(self, data): - assert data['grid'] == data['grid'] - assert data['grid'] != 'foo' - g = Grid(data['griddata'], origin=data['origin'] + 1, delta=data['delta']) - assert data['grid'] != g - - def test_compatibility_type(self, data): - assert data['grid'].check_compatible(data['grid']) - assert data['grid'].check_compatible(3) - g = Grid(data['griddata'], origin=data['origin'], delta=data['delta']) - assert data['grid'].check_compatible(g) - assert data['grid'].check_compatible(g.grid) - - def test_wrong_compatibile_type(self, data): - g = Grid(data['griddata'], origin=data['origin'] + 1, delta=data['delta']) - with pytest.raises(TypeError): - data['grid'].check_compatible(g) - - arr = np.zeros(data['griddata'].shape[-1] + 1) # Not broadcastable - with pytest.raises(TypeError): - data['grid'].check_compatible(arr) - - def test_non_orthonormal_boxes(self, data): - delta = np.eye(3) - with pytest.raises(NotImplementedError): - Grid(data['griddata'], origin=data['origin'], delta=delta) - - def test_centers(self, data): - g = Grid(data['griddata'], origin=np.ones(3), delta=data['delta']) - centers = np.array(list(g.centers())) - assert_array_equal(centers[0], g.origin) - assert_array_equal(centers[-1] - g.origin, - (np.array(g.grid.shape) - 1) * data['delta']) - - def test_resample_factor_failure(self, data): - pytest.importorskip('scipy') - - with pytest.raises(ValueError): - g = data['grid'].resample_factor(0) - - def test_resample_factor(self, data): - pytest.importorskip('scipy') - - g = data['grid'].resample_factor(2) - assert_array_equal(g.delta, np.ones(3) * .5) +@pytest.mark.skipif(not HAS_OPENVDB, reason="pyopenvdb/openvdb not installed") +class TestVDBWrite: + def test_write_vdb_from_grid(self, tmpdir): + data = np.arange(1, 28).reshape(3, 3, 3).astype(np.float32) + g = Grid(data, origin=np.zeros(3), delta=np.ones(3)) - assert_array_equal(g.grid.shape, np.ones(3) * 5) + outfile = str(tmpdir / "test.vdb") + g.export(outfile, file_format='VDB') - assert_array_almost_equal(g.grid[::2, ::2, ::2], - data['grid'].grid) - - def test_load_pickle(self, data, tmpdir): - g = data['grid'] - fn = str(tmpdir.mkdir('grid').join('grid.pkl')) - g.save(fn) - - h = Grid() - h.load(fn) - - assert h == g - - def test_init_pickle_pathobjects(self, data, tmpdir): - g = data['grid'] - fn = tmpdir.mkdir('grid').join('grid.pickle') - g.save(fn) - - h = Grid(fn) - - assert h == g - - @pytest.mark.parametrize("fileformat", ("pkl", "PKL", "pickle", "python")) - def test_load_fileformat(self, data, pklfile, fileformat): - h = Grid(pklfile, file_format="pkl") - assert h == data['grid'] - - @pytest.mark.xfail - @pytest.mark.parametrize("fileformat", ("ccp4", "plt", "dx")) - def test_load_wrong_fileformat(self, data, pklfile, fileformat): - with pytest.raises('ValueError'): - Grid(pklfile, file_format=fileformat) - - @pytest.mark.parametrize("fileformat", ("dx", "pkl")) - def test_export(self, data, fileformat, tmpdir): - g = data['grid'] - fn = tmpdir.mkdir('grid_export').join("grid.{}".format(fileformat)) - g.export(fn) - h = Grid(fn) - assert g == h - - @pytest.mark.parametrize("fileformat", ("ccp4", "plt")) - def test_export_not_supported(self, data, fileformat, tmpdir): - g = data['grid'] - fn = tmpdir.mkdir('grid_export').join("grid.{}".format(fileformat)) - with pytest.raises(ValueError): - g.export(fn) - - -def test_inheritance(data): - class DerivedGrid(Grid): - pass + assert tmpdir.join("test.vdb").exists() + + grids, metadata = vdb.readAll(outfile) + assert len(grids) == 1 + assert grids[0].name == 'density' - dg = DerivedGrid(data['griddata'], origin=data['origin'], - delta=data['delta']) - result = f_arithmetic(dg) + def test_write_vdb_autodetect_extension(self, tmpdir): + data = np.arange(24).reshape(2, 3, 4).astype(np.float32) + g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) + + outfile = str(tmpdir / "auto.vdb") + g.export(outfile) + + assert tmpdir.join("auto.vdb").exists() - assert isinstance(result, DerivedGrid) + def test_write_vdb_with_metadata(self, tmpdir): + data = np.ones((3, 3, 3), dtype=np.float32) + g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) + g.metadata['name'] = 'test_density' + + outfile = str(tmpdir / "metadata.vdb") + g.export(outfile) + + grids, metadata = vdb.readAll(outfile) + assert grids[0].name == 'test_density' - ref = f_arithmetic(data['grid']) - assert_almost_equal(result.grid, ref.grid) + def test_write_vdb_origin_and_spacing(self, tmpdir): + data = np.ones((4, 4, 4), dtype=np.float32) + origin = np.array([10.0, 20.0, 30.0]) + delta = np.array([0.5, 0.5, 0.5]) + + g = Grid(data, origin=origin, delta=delta) + outfile = str(tmpdir / "transform.vdb") + g.export(outfile) + + grids, metadata = vdb.readAll(outfile) + grid_vdb = grids[0] + + voxel_size = grid_vdb.transform.voxelSize() + assert_allclose([voxel_size[i] for i in range(3)], delta, rtol=1e-5) -def test_anyarray(data): - ma = np.ma.MaskedArray(data['griddata']) - mg = Grid(ma, origin=data['origin'], delta=data['delta']) + def test_write_vdb_from_ccp4(self, tmpdir): + g = Grid(datafiles.CCP4) + outfile = str(tmpdir / "from_ccp4.vdb") + + g.export(outfile, file_format='VDB') + + assert tmpdir.join("from_ccp4.vdb").exists() + grids, metadata = vdb.readAll(outfile) + assert len(grids) == 1 - assert isinstance(mg.grid, ma.__class__) + def test_vdb_field_direct(self, tmpdir): + data = np.arange(27).reshape(3, 3, 3).astype(np.float32) + + vdb_field = gridData.OpenVDB.field('direct_test') + vdb_field.populate(data, origin=[0, 0, 0], delta=[1, 1, 1]) + + outfile = str(tmpdir / "direct.vdb") + vdb_field.write(outfile) + + grids, metadata = vdb.readAll(outfile) + assert grids[0].name == 'direct_test' - result = f_arithmetic(mg) - ref = f_arithmetic(data['grid']) + def test_vdb_field_no_data_raises(self, tmpdir): + vdb_field = gridData.OpenVDB.field('empty') + + outfile = str(tmpdir / "empty.vdb") + with pytest.raises(ValueError, match="No data to write"): + vdb_field.write(outfile) - assert_almost_equal(result.grid, ref.grid) + def test_vdb_field_2d_raises(self): + data_2d = np.arange(12).reshape(3, 4) + vdb_field = gridData.OpenVDB.field('test') + + with pytest.raises(ValueError, match="3D grids"): + vdb_field.populate(data_2d, origin=[0, 0], delta=[1, 1]) \ No newline at end of file From 31831cb374a4f88f4a3aabb48ff7335453373b72 Mon Sep 17 00:00:00 2001 From: spyke7 Date: Sat, 24 Jan 2026 12:11:38 +0530 Subject: [PATCH 08/17] modified test_vdb.py for coverage --- gridData/tests/test_vdb.py | 56 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/gridData/tests/test_vdb.py b/gridData/tests/test_vdb.py index fd4eaef..4fe3ca6 100644 --- a/gridData/tests/test_vdb.py +++ b/gridData/tests/test_vdb.py @@ -34,6 +34,16 @@ def test_write_vdb_from_grid(self, tmpdir): grids, metadata = vdb.readAll(outfile) assert len(grids) == 1 assert grids[0].name == 'density' + + def test_write_vdb_default_grid_name(self, tmpdir): + data = np.ones((3, 3, 3), dtype=np.float32) + g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) + g.metadata = {} + + outfile = str(tmpdir / "default_name.vdb") + g.export(outfile) + grids, metadata = vdb.readAll(outfile) + assert grids[0].name == 'density' def test_write_vdb_autodetect_extension(self, tmpdir): data = np.arange(24).reshape(2, 3, 4).astype(np.float32) @@ -94,8 +104,8 @@ def test_vdb_field_direct(self, tmpdir): def test_vdb_field_no_data_raises(self, tmpdir): vdb_field = gridData.OpenVDB.field('empty') - outfile = str(tmpdir / "empty.vdb") + with pytest.raises(ValueError, match="No data to write"): vdb_field.write(outfile) @@ -104,4 +114,46 @@ def test_vdb_field_2d_raises(self): vdb_field = gridData.OpenVDB.field('test') with pytest.raises(ValueError, match="3D grids"): - vdb_field.populate(data_2d, origin=[0, 0], delta=[1, 1]) \ No newline at end of file + vdb_field.populate(data_2d, origin=[0, 0], delta=[1, 1]) + + def test_write_vdb_nonuniform_spacing_warning(self, tmpdir): + data = np.ones((3, 3, 3), dtype=np.float32) + delta = np.array([0.5, 1.0, 1.5]) + g = Grid(data, origin=[0, 0, 0], delta=delta) + + outfile = str(tmpdir / "nonuniform.vdb") + g.export(outfile) + assert tmpdir.join("nonuniform.vdb").exists() + + def test_write_vdb_with_delta_matrix(self, tmpdir): + data = np.ones((3, 3, 3), dtype=np.float32) + delta = np.diag([1.0, 2.0, 3.0]) + + vdb_field = gridData.OpenVDB.field('matrix_delta') + vdb_field.populate(data, origin=[0, 0, 0], delta=delta) + + outfile = str(tmpdir / "matrix_delta.vdb") + vdb_field.write(outfile) + assert tmpdir.join("matrix_delta.vdb").exists() + + def test_write_vdb_sparse_data(self, tmpdir): + data = np.zeros((10, 10, 10), dtype=np.float32) + data[2, 3, 4] = 5.0 + data[7, 8, 9] = 10.0 + + g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) + outfile = str(tmpdir / "sparse.vdb") + g.export(outfile) + + assert tmpdir.join("sparse.vdb").exists() + grids, metadata = vdb.readAll(outfile) + assert len(grids) == 1 + + def test_write_vdb_zero_threshold(self, tmpdir): + data = np.ones((3, 3, 3), dtype=np.float32) * 1e-11 + data[1, 1, 1] = 1.0 + + g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) + outfile = str(tmpdir / "threshold.vdb") + g.export(outfile) + assert tmpdir.join("threshold.vdb").exists() \ No newline at end of file From c55366b72b603144a619a2006bbc5b306aa42d28 Mon Sep 17 00:00:00 2001 From: spyke7 Date: Sat, 24 Jan 2026 12:31:41 +0530 Subject: [PATCH 09/17] updated test_vdb.py --- gridData/tests/test_vdb.py | 134 +++++++++++++++++++++++-------------- 1 file changed, 85 insertions(+), 49 deletions(-) diff --git a/gridData/tests/test_vdb.py b/gridData/tests/test_vdb.py index 4fe3ca6..af7f654 100644 --- a/gridData/tests/test_vdb.py +++ b/gridData/tests/test_vdb.py @@ -22,7 +22,10 @@ @pytest.mark.skipif(not HAS_OPENVDB, reason="pyopenvdb/openvdb not installed") class TestVDBWrite: + """Test OpenVDB file format writing""" + def test_write_vdb_from_grid(self, tmpdir): + """Test basic VDB export from Grid""" data = np.arange(1, 28).reshape(3, 3, 3).astype(np.float32) g = Grid(data, origin=np.zeros(3), delta=np.ones(3)) @@ -30,12 +33,21 @@ def test_write_vdb_from_grid(self, tmpdir): g.export(outfile, file_format='VDB') assert tmpdir.join("test.vdb").exists() - grids, metadata = vdb.readAll(outfile) assert len(grids) == 1 assert grids[0].name == 'density' + + def test_write_vdb_autodetect_extension(self, tmpdir): + """Test that .vdb extension is auto-detected""" + data = np.arange(24).reshape(2, 3, 4).astype(np.float32) + g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) + outfile = str(tmpdir / "auto.vdb") + g.export(outfile) + assert tmpdir.join("auto.vdb").exists() + def test_write_vdb_default_grid_name(self, tmpdir): + """Test that default grid name is used when no metadata""" data = np.ones((3, 3, 3), dtype=np.float32) g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) g.metadata = {} @@ -45,27 +57,19 @@ def test_write_vdb_default_grid_name(self, tmpdir): grids, metadata = vdb.readAll(outfile) assert grids[0].name == 'density' - def test_write_vdb_autodetect_extension(self, tmpdir): - data = np.arange(24).reshape(2, 3, 4).astype(np.float32) - g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) - - outfile = str(tmpdir / "auto.vdb") - g.export(outfile) - - assert tmpdir.join("auto.vdb").exists() - def test_write_vdb_with_metadata(self, tmpdir): + """Test that grid name from metadata is used""" data = np.ones((3, 3, 3), dtype=np.float32) g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) g.metadata['name'] = 'test_density' outfile = str(tmpdir / "metadata.vdb") g.export(outfile) - grids, metadata = vdb.readAll(outfile) assert grids[0].name == 'test_density' def test_write_vdb_origin_and_spacing(self, tmpdir): + """Test that origin and spacing are correctly written""" data = np.ones((4, 4, 4), dtype=np.float32) origin = np.array([10.0, 20.0, 30.0]) delta = np.array([0.5, 0.5, 0.5]) @@ -76,11 +80,11 @@ def test_write_vdb_origin_and_spacing(self, tmpdir): grids, metadata = vdb.readAll(outfile) grid_vdb = grids[0] - voxel_size = grid_vdb.transform.voxelSize() assert_allclose([voxel_size[i] for i in range(3)], delta, rtol=1e-5) def test_write_vdb_from_ccp4(self, tmpdir): + """Test exporting CCP4 file to VDB""" g = Grid(datafiles.CCP4) outfile = str(tmpdir / "from_ccp4.vdb") @@ -90,42 +94,17 @@ def test_write_vdb_from_ccp4(self, tmpdir): grids, metadata = vdb.readAll(outfile) assert len(grids) == 1 - def test_vdb_field_direct(self, tmpdir): - data = np.arange(27).reshape(3, 3, 3).astype(np.float32) - - vdb_field = gridData.OpenVDB.field('direct_test') - vdb_field.populate(data, origin=[0, 0, 0], delta=[1, 1, 1]) - - outfile = str(tmpdir / "direct.vdb") - vdb_field.write(outfile) - - grids, metadata = vdb.readAll(outfile) - assert grids[0].name == 'direct_test' - - def test_vdb_field_no_data_raises(self, tmpdir): - vdb_field = gridData.OpenVDB.field('empty') - outfile = str(tmpdir / "empty.vdb") - - with pytest.raises(ValueError, match="No data to write"): - vdb_field.write(outfile) - - def test_vdb_field_2d_raises(self): + def test_write_vdb_non3d_raises(self, tmpdir): + """Test that non-3D grids raise ValueError""" data_2d = np.arange(12).reshape(3, 4) - vdb_field = gridData.OpenVDB.field('test') - - with pytest.raises(ValueError, match="3D grids"): - vdb_field.populate(data_2d, origin=[0, 0], delta=[1, 1]) - - def test_write_vdb_nonuniform_spacing_warning(self, tmpdir): - data = np.ones((3, 3, 3), dtype=np.float32) - delta = np.array([0.5, 1.0, 1.5]) - g = Grid(data, origin=[0, 0, 0], delta=delta) - - outfile = str(tmpdir / "nonuniform.vdb") - g.export(outfile) - assert tmpdir.join("nonuniform.vdb").exists() + g = Grid(data_2d, origin=[0, 0], delta=[1, 1]) + outfile = str(tmpdir / "invalid.vdb") + with pytest.raises(ValueError, match="3D grid"): + g.export(outfile, file_format='VDB') + def test_write_vdb_with_delta_matrix(self, tmpdir): + """Test writing with delta as diagonal matrix""" data = np.ones((3, 3, 3), dtype=np.float32) delta = np.diag([1.0, 2.0, 3.0]) @@ -135,8 +114,19 @@ def test_write_vdb_with_delta_matrix(self, tmpdir): outfile = str(tmpdir / "matrix_delta.vdb") vdb_field.write(outfile) assert tmpdir.join("matrix_delta.vdb").exists() - + + def test_write_vdb_nonuniform_spacing(self, tmpdir): + """Test writing with non-uniform spacing""" + data = np.ones((3, 3, 3), dtype=np.float32) + delta = np.array([0.5, 1.0, 1.5]) + g = Grid(data, origin=[0, 0, 0], delta=delta) + + outfile = str(tmpdir / "nonuniform.vdb") + g.export(outfile) + assert tmpdir.join("nonuniform.vdb").exists() + def test_write_vdb_sparse_data(self, tmpdir): + """Test writing sparse data""" data = np.zeros((10, 10, 10), dtype=np.float32) data[2, 3, 4] = 5.0 data[7, 8, 9] = 10.0 @@ -148,12 +138,58 @@ def test_write_vdb_sparse_data(self, tmpdir): assert tmpdir.join("sparse.vdb").exists() grids, metadata = vdb.readAll(outfile) assert len(grids) == 1 + + def test_write_vdb_negative_values(self, tmpdir): + """Test writing data with negative values""" + data = np.array([[[1.0, -2.0, 3.0], + [-4.0, 5.0, -6.0], + [7.0, -8.0, 9.0]]], dtype=np.float32) + g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) + outfile = str(tmpdir / "negative.vdb") + g.export(outfile) + assert tmpdir.join("negative.vdb").exists() + def test_write_vdb_zero_threshold(self, tmpdir): - data = np.ones((3, 3, 3), dtype=np.float32) * 1e-11 - data[1, 1, 1] = 1.0 + """Test that very small values near zero are treated as background""" + data = np.ones((3, 3, 3), dtype=np.float32) * 1e-11 + data[1, 1, 1] = 1.0 g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) outfile = str(tmpdir / "threshold.vdb") g.export(outfile) - assert tmpdir.join("threshold.vdb").exists() \ No newline at end of file + assert tmpdir.join("threshold.vdb").exists() + + def test_vdb_field_direct(self, tmpdir): + """Test using OpenVDB.field directly""" + data = np.arange(27).reshape(3, 3, 3).astype(np.float32) + + vdb_field = gridData.OpenVDB.field('direct_test') + vdb_field.populate(data, origin=[0, 0, 0], delta=[1, 1, 1]) + + outfile = str(tmpdir / "direct.vdb") + vdb_field.write(outfile) + + grids, metadata = vdb.readAll(outfile) + assert grids[0].name == 'direct_test' + + def test_vdb_field_no_data_raises(self, tmpdir): + """Test that writing without data raises ValueError""" + vdb_field = gridData.OpenVDB.field('empty') + outfile = str(tmpdir / "empty.vdb") + with pytest.raises(ValueError, match="No data to write"): + vdb_field.write(outfile) + + def test_vdb_field_2d_raises(self): + """Test that 2D data raises ValueError in populate""" + data_2d = np.arange(12).reshape(3, 4) + vdb_field = gridData.OpenVDB.field('test') + + with pytest.raises(ValueError, match="3D grids"): + vdb_field.populate(data_2d, origin=[0, 0], delta=[1, 1]) + + +@pytest.mark.skipif(HAS_OPENVDB, reason="Testing import error handling") +def test_vdb_import_error(): + with pytest.raises(ImportError, match="pyopenvdb is required"): + gridData.OpenVDB.field('test') \ No newline at end of file From ea446d761217a0eb6c81895397292d8ad5841a72 Mon Sep 17 00:00:00 2001 From: spyke7 Date: Sun, 25 Jan 2026 23:20:32 +0530 Subject: [PATCH 10/17] updated CHANGELOG and gh-ci.yaml --- .github/workflows/gh-ci.yaml | 2 +- CHANGELOG | 17 ++++++++++++----- gridData/tests/test_vdb.py | 16 ---------------- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/.github/workflows/gh-ci.yaml b/.github/workflows/gh-ci.yaml index e464321..35fdd24 100644 --- a/.github/workflows/gh-ci.yaml +++ b/.github/workflows/gh-ci.yaml @@ -49,7 +49,7 @@ jobs: - name: install package deps run: | - micromamba install numpy scipy mrcfile pytest pytest-cov codecov + micromamba install numpy scipy mrcfile pytest pytest-cov codecov openvdb - name: check install run: | diff --git a/CHANGELOG b/CHANGELOG index 34717b9..28e6280 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,11 +16,20 @@ The rules for this file: 01/16/2026 IAlibay, ollyfutur, conradolandia, orbeckst, PlethoraChutney, Pradyumn-cloud, spyke7 ------------------------------------------------------------------------------- -??/??/???? orbeckst +??/??/???? orbeckst, BradyAJohnston, spyke7 - * 1.1.1 + * 1.1.1 - Fixes + Changes + + * Added OpenVDB module for exporting to .vdb format (PR #148) + + Enhancements + + * Added openVDB format exports (Issue #141, PR #148) + + + Fixes 01/22/2026 IAlibay, ollyfutur, conradolandia, orbeckst, PlethoraChutney, @@ -30,7 +39,6 @@ The rules for this file: Changes - * Added OpenVDB module for exporting to .vdb format (PR #148) * update logo from https://github.com/MDAnalysis/mdanalysis-subprojects-branding (issue #143) * Python 3.13 and 3.14 are now supported (PR #140) @@ -39,7 +47,6 @@ The rules for this file: Enhancements - * Added openVDB format exports (Issue #141) * `Grid` now accepts binary operations with any operand that can be * `Grid` now accepts binary operations with any operand that can be broadcasted to the grid's shape according to `numpy` broadcasting rules diff --git a/gridData/tests/test_vdb.py b/gridData/tests/test_vdb.py index af7f654..dd57fd9 100644 --- a/gridData/tests/test_vdb.py +++ b/gridData/tests/test_vdb.py @@ -22,8 +22,6 @@ @pytest.mark.skipif(not HAS_OPENVDB, reason="pyopenvdb/openvdb not installed") class TestVDBWrite: - """Test OpenVDB file format writing""" - def test_write_vdb_from_grid(self, tmpdir): """Test basic VDB export from Grid""" data = np.arange(1, 28).reshape(3, 3, 3).astype(np.float32) @@ -38,7 +36,6 @@ def test_write_vdb_from_grid(self, tmpdir): assert grids[0].name == 'density' def test_write_vdb_autodetect_extension(self, tmpdir): - """Test that .vdb extension is auto-detected""" data = np.arange(24).reshape(2, 3, 4).astype(np.float32) g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) @@ -47,7 +44,6 @@ def test_write_vdb_autodetect_extension(self, tmpdir): assert tmpdir.join("auto.vdb").exists() def test_write_vdb_default_grid_name(self, tmpdir): - """Test that default grid name is used when no metadata""" data = np.ones((3, 3, 3), dtype=np.float32) g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) g.metadata = {} @@ -58,7 +54,6 @@ def test_write_vdb_default_grid_name(self, tmpdir): assert grids[0].name == 'density' def test_write_vdb_with_metadata(self, tmpdir): - """Test that grid name from metadata is used""" data = np.ones((3, 3, 3), dtype=np.float32) g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) g.metadata['name'] = 'test_density' @@ -69,7 +64,6 @@ def test_write_vdb_with_metadata(self, tmpdir): assert grids[0].name == 'test_density' def test_write_vdb_origin_and_spacing(self, tmpdir): - """Test that origin and spacing are correctly written""" data = np.ones((4, 4, 4), dtype=np.float32) origin = np.array([10.0, 20.0, 30.0]) delta = np.array([0.5, 0.5, 0.5]) @@ -84,7 +78,6 @@ def test_write_vdb_origin_and_spacing(self, tmpdir): assert_allclose([voxel_size[i] for i in range(3)], delta, rtol=1e-5) def test_write_vdb_from_ccp4(self, tmpdir): - """Test exporting CCP4 file to VDB""" g = Grid(datafiles.CCP4) outfile = str(tmpdir / "from_ccp4.vdb") @@ -95,7 +88,6 @@ def test_write_vdb_from_ccp4(self, tmpdir): assert len(grids) == 1 def test_write_vdb_non3d_raises(self, tmpdir): - """Test that non-3D grids raise ValueError""" data_2d = np.arange(12).reshape(3, 4) g = Grid(data_2d, origin=[0, 0], delta=[1, 1]) @@ -104,7 +96,6 @@ def test_write_vdb_non3d_raises(self, tmpdir): g.export(outfile, file_format='VDB') def test_write_vdb_with_delta_matrix(self, tmpdir): - """Test writing with delta as diagonal matrix""" data = np.ones((3, 3, 3), dtype=np.float32) delta = np.diag([1.0, 2.0, 3.0]) @@ -116,7 +107,6 @@ def test_write_vdb_with_delta_matrix(self, tmpdir): assert tmpdir.join("matrix_delta.vdb").exists() def test_write_vdb_nonuniform_spacing(self, tmpdir): - """Test writing with non-uniform spacing""" data = np.ones((3, 3, 3), dtype=np.float32) delta = np.array([0.5, 1.0, 1.5]) g = Grid(data, origin=[0, 0, 0], delta=delta) @@ -126,7 +116,6 @@ def test_write_vdb_nonuniform_spacing(self, tmpdir): assert tmpdir.join("nonuniform.vdb").exists() def test_write_vdb_sparse_data(self, tmpdir): - """Test writing sparse data""" data = np.zeros((10, 10, 10), dtype=np.float32) data[2, 3, 4] = 5.0 data[7, 8, 9] = 10.0 @@ -140,7 +129,6 @@ def test_write_vdb_sparse_data(self, tmpdir): assert len(grids) == 1 def test_write_vdb_negative_values(self, tmpdir): - """Test writing data with negative values""" data = np.array([[[1.0, -2.0, 3.0], [-4.0, 5.0, -6.0], [7.0, -8.0, 9.0]]], dtype=np.float32) @@ -151,7 +139,6 @@ def test_write_vdb_negative_values(self, tmpdir): assert tmpdir.join("negative.vdb").exists() def test_write_vdb_zero_threshold(self, tmpdir): - """Test that very small values near zero are treated as background""" data = np.ones((3, 3, 3), dtype=np.float32) * 1e-11 data[1, 1, 1] = 1.0 @@ -161,7 +148,6 @@ def test_write_vdb_zero_threshold(self, tmpdir): assert tmpdir.join("threshold.vdb").exists() def test_vdb_field_direct(self, tmpdir): - """Test using OpenVDB.field directly""" data = np.arange(27).reshape(3, 3, 3).astype(np.float32) vdb_field = gridData.OpenVDB.field('direct_test') @@ -174,14 +160,12 @@ def test_vdb_field_direct(self, tmpdir): assert grids[0].name == 'direct_test' def test_vdb_field_no_data_raises(self, tmpdir): - """Test that writing without data raises ValueError""" vdb_field = gridData.OpenVDB.field('empty') outfile = str(tmpdir / "empty.vdb") with pytest.raises(ValueError, match="No data to write"): vdb_field.write(outfile) def test_vdb_field_2d_raises(self): - """Test that 2D data raises ValueError in populate""" data_2d = np.arange(12).reshape(3, 4) vdb_field = gridData.OpenVDB.field('test') From 1e2fd80bf7b98afddd9f051029f846094f4d139a Mon Sep 17 00:00:00 2001 From: spyke7 Date: Mon, 26 Jan 2026 19:09:51 +0530 Subject: [PATCH 11/17] Updated docs, test_vdb.py and OpenVDB.py --- AUTHORS | 2 +- CHANGELOG | 12 +-- doc/source/gridData/formats.rst | 3 + doc/source/gridData/formats/OpenVDB.rst | 2 + gridData/OpenVDB.py | 113 +++++++++++++-------- gridData/core.py | 10 +- gridData/tests/test_vdb.py | 126 ++++++++++++------------ 7 files changed, 150 insertions(+), 118 deletions(-) create mode 100644 doc/source/gridData/formats/OpenVDB.rst diff --git a/AUTHORS b/AUTHORS index d33a46b..922488c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -27,6 +27,6 @@ Contributors: * Zhiyi Wu * Olivier Languin-Cattoën * Andrés Montoya (logo) -* Shreejan Dolai * Rich Waldo * Pradyumn Prasad +* Shreejan Dolai diff --git a/CHANGELOG b/CHANGELOG index 28e6280..dad3361 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,17 +16,13 @@ The rules for this file: 01/16/2026 IAlibay, ollyfutur, conradolandia, orbeckst, PlethoraChutney, Pradyumn-cloud, spyke7 ------------------------------------------------------------------------------- -??/??/???? orbeckst, BradyAJohnston, spyke7 +??/??/???? orbeckst, spyke7 - * 1.1.1 + * 1.2.0 - Changes - - * Added OpenVDB module for exporting to .vdb format (PR #148) - - Enhancements + Enhancements - * Added openVDB format exports (Issue #141, PR #148) + * Added openVDB format exports (Issue #141, PR #148) Fixes diff --git a/doc/source/gridData/formats.rst b/doc/source/gridData/formats.rst index 0680ac8..a44a15d 100644 --- a/doc/source/gridData/formats.rst +++ b/doc/source/gridData/formats.rst @@ -29,6 +29,7 @@ small number of file formats is directly supported. :mod:`~gridData.gOpenMol` gOpenMol_ plt x :mod:`~gridData.mrc` CCP4_ ccp4,mrc x x subset implemented :class:`~gridData.core.Grid` pickle pickle x x standard Python pickle of the Grid class + :mod:`~gridData.OpenVDB` OpenVDB_ vdb x implemented for Blender visualization ============================ ========== ========= ===== ===== ========================================= @@ -39,6 +40,7 @@ small number of file formats is directly supported. .. _OpenDX: http://www.opendx.org/ .. _gOpenMol: http://www.csc.fi/gopenmol/ .. _CCP4: http://www.ccpem.ac.uk/mrc_format/mrc2014.php +.. _OpenVDB: https://www.openvdb.org/ Format-specific modules @@ -50,3 +52,4 @@ Format-specific modules formats/OpenDX formats/gOpenMol formats/mrc + formats/OpenVDB diff --git a/doc/source/gridData/formats/OpenVDB.rst b/doc/source/gridData/formats/OpenVDB.rst new file mode 100644 index 0000000..4f457ca --- /dev/null +++ b/doc/source/gridData/formats/OpenVDB.rst @@ -0,0 +1,2 @@ +.. automodule:: gridData.OpenVDb + diff --git a/gridData/OpenVDB.py b/gridData/OpenVDB.py index 55e8fa5..e06887c 100644 --- a/gridData/OpenVDB.py +++ b/gridData/OpenVDB.py @@ -2,13 +2,21 @@ :mod:`~gridData.OpenVDB` --- routines to write OpenVDB files ============================================================= -The OpenVDB format is used by Blender and other VFX software for -volumetric data. See https://www.openvdb.org +The `OpenVDB format`_ is used by Blender_ and other VFX software for +volumetric data. -pyopenvdb: https://github.com/AcademySoftwareFoundation/openvdb +.. _`OpenVDB format`: https://www.openvdb.org +.. _Blender: https://www.blender.org/ + +This module uses the pyopenvdb_ library to write OpenVDB files. + +.. _pyopenvdb: https://github.com/AcademySoftwareFoundation/openvdb .. Note:: This module implements a simple writer for 3D regular grids, - sufficient to export density data for visualization in Blender. + sufficient to export density data for visualization in Blender_. + See the `Blender volume docs`_ for details on importing VDB files. + +.. _`Blender volume docs`: https://docs.blender.org/manual/en/latest/modeling/volumes/introduction.html The OpenVDB format uses a sparse tree structure to efficiently store volumetric data. It is the native format for Blender's volume system. @@ -25,12 +33,18 @@ g.export("data.vdb") This will create a file that can be imported directly into Blender -(File -> Import -> OpenVDB). +(File -> Import -> OpenVDB) or (shift+A -> Volume -> Import OpenVDB). See `importing VDB in Blender`_ for details. + +.. _`importing VDB in Blender`: https://docs.blender.org/manual/en/latest/modeling/geometry_nodes/input/import/vdb.html Building an OpenVDB field from a numpy array --------------------------------------------- +If you want to create VDB files without using the Grid class, +you can directly use the OpenVDB field API. This is useful +for custom workflows or when integrating with other libraries. + Requires: grid @@ -65,7 +79,7 @@ vdb = None -class field(object): +class OpenVDBField(object): """OpenVDB field object for writing volumetric data. This class provides a simple interface to write 3D grid data to @@ -90,13 +104,30 @@ class field(object): """ - def __init__(self, name='density'): + def __init__(self, grid, origin, delta, name='density', threshold=1e-10): """Initialize an OpenVDB field. Parameters ---------- + grid : numpy.ndarray + 3D numpy array with the data + origin : numpy.ndarray + Coordinates of the center of grid cell [0,0,0] + delta : numpy.ndarray + Grid spacing (can be 1D array or diagonal matrix) name : str - Name of the grid (will be visible in Blender) + Name of the grid (will be visible in Blender), default 'density' + threshold : float + Values below this threshold are treated as background (sparse), + default 1e-10 + + Raises + ------ + ImportError + If pyopenvdb is not installed + ValueError + If grid is not 3D, or if delta is not 1D/2D or describes + non-orthorhombic cell """ if vdb is None: @@ -105,11 +136,10 @@ def __init__(self, name='density'): "Install it with: conda install -c conda-forge openvdb" ) self.name = name - self.grid = None - self.origin = None - self.delta = None + self.threshold = threshold + self._populate(grid, origin, delta) - def populate(self, grid, origin, delta): + def _populate(self, grid, origin, delta): """Populate the field with grid data. Parameters @@ -124,13 +154,14 @@ def populate(self, grid, origin, delta): Raises ------ ValueError - If grid is not 3D + If grid is not 3D, or if delta is not 1D/2D or describes + non-orthorhombic cell """ grid = numpy.asarray(grid) if grid.ndim != 3: raise ValueError( - "OpenVDB only supports 3D grids, got {}D".format(grid.ndim)) + f"OpenVDB only supports 3D grids, got {grid.ndim}D") self.grid = grid.astype(numpy.float32) self.origin = numpy.asarray(origin) @@ -138,9 +169,23 @@ def populate(self, grid, origin, delta): # Handle delta: could be 1D array or diagonal matrix delta = numpy.asarray(delta) if delta.ndim == 2: - self.delta = numpy.array([delta[i, i] for i in range(3)]) + if (delta.shape != (3,3)): + raise ValueError("delta as a matrix must be 3x3") + + if not numpy.allclose(delta, numpy.diag(numpy.diag(delta))): + raise ValueError("Non-orthorhombic cells are not supported") + + self.delta = numpy.diag(delta) + + elif delta.ndim == 1: + if (len(delta) != 3): + raise ValueError("delta must have length 3 for 3D grids") + self.delta=delta + else: - self.delta = delta + raise ValueError( + "delta must be either a length-3 vector or a 3x3 diagonal matrix" + ) def write(self, filename): """Write the field to an OpenVDB file. @@ -150,41 +195,27 @@ def write(self, filename): filename : str Output filename (should end in .vdb) - """ - if self.grid is None: - raise ValueError("No data to write. Use populate() first.") - - # Create OpenVDB grid + """ + self.grid=numpy.ascontiguousarray(self.grid, dtype=numpy.float32) + vdb_grid = vdb.FloatGrid() vdb_grid.name = self.name # this is an explicit linear transform using per-axis voxel sizes # world = diag(delta) * index + corner_origin - corner_origin = (self.origin - 0.5 * self.delta).astype(float) + corner_origin = (self.origin - 0.5 * self.delta) - # Constructing 4x4 row-major matrix where the last row is the translation matrix = [ - [float(self.delta[0]), 0.0, 0.0, 0.0], - [0.0, float(self.delta[1]), 0.0, 0.0], - [0.0, 0.0, float(self.delta[2]), 0.0], - [float(corner_origin[0]), float(corner_origin[1]), float(corner_origin[2]), 1.0] + [self.delta[0], 0.0, 0.0, 0.0], + [0.0, self.delta[1], 0.0, 0.0], + [0.0, 0.0, self.delta[2], 0.0], + [corner_origin[0], corner_origin[1], corner_origin[2], 1.0] ] - transform = vdb.createLinearTransform(matrix) - vdb_grid.transform = transform - vdb_grid.background = 0.0 - - # Populate the grid - accessor = vdb_grid.getAccessor() - threshold = 1e-10 - - mask = numpy.abs(self.grid) > threshold - indices = numpy.argwhere(mask) + vdb_grid.transform = vdb.createLinearTransform(matrix) - for idx in indices: - i, j, k = idx - value = float(self.grid[i, j, k]) - accessor.setValueOn((i, j, k), value) + vdb_grid.copyFromArray(self.grid, tolerance=self.threshold) + vdb_grid.prune() vdb.write(filename, grids=[vdb_grid]) \ No newline at end of file diff --git a/gridData/core.py b/gridData/core.py index f92db91..4b7506d 100644 --- a/gridData/core.py +++ b/gridData/core.py @@ -714,12 +714,14 @@ def _export_vdb(self, filename, **kwargs): raise ValueError( "OpenVDB export requires a 3D grid, got {}D".format(self.grid.ndim)) - # Get grid name from metadata if available grid_name = self.metadata.get('name', 'density') - # Create and populate VDB field - vdb_field = OpenVDB.field(grid_name) - vdb_field.populate(self.grid, self.origin, self.delta) + vdb_field = OpenVDB.OpenVDBField( + grid=self.grid, + origin=self.origin, + delta=self.delta, + name=grid_name + ) vdb_field.write(filename) def _export_mrc(self, filename, **kwargs): diff --git a/gridData/tests/test_vdb.py b/gridData/tests/test_vdb.py index dd57fd9..0c501b4 100644 --- a/gridData/tests/test_vdb.py +++ b/gridData/tests/test_vdb.py @@ -23,7 +23,6 @@ @pytest.mark.skipif(not HAS_OPENVDB, reason="pyopenvdb/openvdb not installed") class TestVDBWrite: def test_write_vdb_from_grid(self, tmpdir): - """Test basic VDB export from Grid""" data = np.arange(1, 28).reshape(3, 3, 3).astype(np.float32) g = Grid(data, origin=np.zeros(3), delta=np.ones(3)) @@ -31,18 +30,11 @@ def test_write_vdb_from_grid(self, tmpdir): g.export(outfile, file_format='VDB') assert tmpdir.join("test.vdb").exists() + grids, metadata = vdb.readAll(outfile) assert len(grids) == 1 assert grids[0].name == 'density' - - def test_write_vdb_autodetect_extension(self, tmpdir): - data = np.arange(24).reshape(2, 3, 4).astype(np.float32) - g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) - outfile = str(tmpdir / "auto.vdb") - g.export(outfile) - assert tmpdir.join("auto.vdb").exists() - def test_write_vdb_default_grid_name(self, tmpdir): data = np.ones((3, 3, 3), dtype=np.float32) g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) @@ -53,6 +45,15 @@ def test_write_vdb_default_grid_name(self, tmpdir): grids, metadata = vdb.readAll(outfile) assert grids[0].name == 'density' + def test_write_vdb_autodetect_extension(self, tmpdir): + data = np.arange(24).reshape(2, 3, 4).astype(np.float32) + g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) + + outfile = str(tmpdir / "auto.vdb") + g.export(outfile) + + assert tmpdir.join("auto.vdb").exists() + def test_write_vdb_with_metadata(self, tmpdir): data = np.ones((3, 3, 3), dtype=np.float32) g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) @@ -60,6 +61,7 @@ def test_write_vdb_with_metadata(self, tmpdir): outfile = str(tmpdir / "metadata.vdb") g.export(outfile) + grids, metadata = vdb.readAll(outfile) assert grids[0].name == 'test_density' @@ -74,6 +76,7 @@ def test_write_vdb_origin_and_spacing(self, tmpdir): grids, metadata = vdb.readAll(outfile) grid_vdb = grids[0] + voxel_size = grid_vdb.transform.voxelSize() assert_allclose([voxel_size[i] for i in range(3)], delta, rtol=1e-5) @@ -87,26 +90,28 @@ def test_write_vdb_from_ccp4(self, tmpdir): grids, metadata = vdb.readAll(outfile) assert len(grids) == 1 - def test_write_vdb_non3d_raises(self, tmpdir): - data_2d = np.arange(12).reshape(3, 4) - g = Grid(data_2d, origin=[0, 0], delta=[1, 1]) - - outfile = str(tmpdir / "invalid.vdb") - with pytest.raises(ValueError, match="3D grid"): - g.export(outfile, file_format='VDB') - - def test_write_vdb_with_delta_matrix(self, tmpdir): - data = np.ones((3, 3, 3), dtype=np.float32) - delta = np.diag([1.0, 2.0, 3.0]) + def test_vdb_field_direct(self, tmpdir): + data = np.arange(27).reshape(3, 3, 3).astype(np.float32) - vdb_field = gridData.OpenVDB.field('matrix_delta') - vdb_field.populate(data, origin=[0, 0, 0], delta=delta) + vdb_field = gridData.OpenVDB.OpenVDBField(data, origin=[0,0,0], delta=[1,1,1], name='direct_test') - outfile = str(tmpdir / "matrix_delta.vdb") + outfile = str(tmpdir / "direct.vdb") vdb_field.write(outfile) - assert tmpdir.join("matrix_delta.vdb").exists() + + grids, metadata = vdb.readAll(outfile) + assert grids[0].name == 'direct_test' - def test_write_vdb_nonuniform_spacing(self, tmpdir): + def test_vdb_field_2d_raises(self): + data_2d = np.arange(12).reshape(3, 4) + + with pytest.raises(ValueError, match="3D grids"): + gridData.OpenVDB.OpenVDBField( + data_2d, + origin=[0, 0], + delta=[1, 1] + ) + + def test_write_vdb_nonuniform_spacing_warning(self, tmpdir): data = np.ones((3, 3, 3), dtype=np.float32) delta = np.array([0.5, 1.0, 1.5]) g = Grid(data, origin=[0, 0, 0], delta=delta) @@ -114,7 +119,17 @@ def test_write_vdb_nonuniform_spacing(self, tmpdir): outfile = str(tmpdir / "nonuniform.vdb") g.export(outfile) assert tmpdir.join("nonuniform.vdb").exists() - + + def test_write_vdb_with_delta_matrix(self, tmpdir): + data = np.ones((3, 3, 3), dtype=np.float32) + delta = np.diag([1.0, 2.0, 3.0]) + + vdb_field = gridData.OpenVDB.OpenVDBField(data, origin=[0,0,0], delta=delta, name='matrix_delta') + + outfile = str(tmpdir / "matrix_delta.vdb") + vdb_field.write(outfile) + assert tmpdir.join("matrix_delta.vdb").exists() + def test_write_vdb_sparse_data(self, tmpdir): data = np.zeros((10, 10, 10), dtype=np.float32) data[2, 3, 4] = 5.0 @@ -127,53 +142,36 @@ def test_write_vdb_sparse_data(self, tmpdir): assert tmpdir.join("sparse.vdb").exists() grids, metadata = vdb.readAll(outfile) assert len(grids) == 1 - - def test_write_vdb_negative_values(self, tmpdir): - data = np.array([[[1.0, -2.0, 3.0], - [-4.0, 5.0, -6.0], - [7.0, -8.0, 9.0]]], dtype=np.float32) - g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) - outfile = str(tmpdir / "negative.vdb") - g.export(outfile) - assert tmpdir.join("negative.vdb").exists() - def test_write_vdb_zero_threshold(self, tmpdir): - data = np.ones((3, 3, 3), dtype=np.float32) * 1e-11 - data[1, 1, 1] = 1.0 + data = np.ones((3, 3, 3), dtype=np.float32) * 1e-11 + data[1, 1, 1] = 1.0 g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) outfile = str(tmpdir / "threshold.vdb") g.export(outfile) assert tmpdir.join("threshold.vdb").exists() - - def test_vdb_field_direct(self, tmpdir): - data = np.arange(27).reshape(3, 3, 3).astype(np.float32) - - vdb_field = gridData.OpenVDB.field('direct_test') - vdb_field.populate(data, origin=[0, 0, 0], delta=[1, 1, 1]) - outfile = str(tmpdir / "direct.vdb") - vdb_field.write(outfile) - - grids, metadata = vdb.readAll(outfile) - assert grids[0].name == 'direct_test' - - def test_vdb_field_no_data_raises(self, tmpdir): - vdb_field = gridData.OpenVDB.field('empty') - outfile = str(tmpdir / "empty.vdb") - with pytest.raises(ValueError, match="No data to write"): - vdb_field.write(outfile) - - def test_vdb_field_2d_raises(self): - data_2d = np.arange(12).reshape(3, 4) - vdb_field = gridData.OpenVDB.field('test') + def test_vdb_non_orthrhombic_raises(self): + data=np.ones((3,3,3), dtype=np.float32) + delta = np.array([ + [1,0.1,0], + [0,1,0], + [0,0,1], + ]) + + with pytest.raises(ValueError, match="Non-orthorhombic"): + gridData.OpenVDB.OpenVDBField( + data, + origin=[0, 0, 0], + delta=delta + ) - with pytest.raises(ValueError, match="3D grids"): - vdb_field.populate(data_2d, origin=[0, 0], delta=[1, 1]) - - @pytest.mark.skipif(HAS_OPENVDB, reason="Testing import error handling") def test_vdb_import_error(): with pytest.raises(ImportError, match="pyopenvdb is required"): - gridData.OpenVDB.field('test') \ No newline at end of file + gridData.OpenVDB.OpenVDBField( + np.ones((3, 3, 3)), + origin=[0, 0, 0], + delta=[1, 1, 1] + ) \ No newline at end of file From 145a1dd2e8fee4280433f435dbce1e55aaf0768f Mon Sep 17 00:00:00 2001 From: spyke7 Date: Mon, 26 Jan 2026 20:14:30 +0530 Subject: [PATCH 12/17] Updated tests --- gridData/OpenVDB.py | 2 +- gridData/tests/test_vdb.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/gridData/OpenVDB.py b/gridData/OpenVDB.py index e06887c..e515298 100644 --- a/gridData/OpenVDB.py +++ b/gridData/OpenVDB.py @@ -179,7 +179,7 @@ def _populate(self, grid, origin, delta): elif delta.ndim == 1: if (len(delta) != 3): - raise ValueError("delta must have length 3 for 3D grids") + raise ValueError("delta must have length-3 for 3D grids") self.delta=delta else: diff --git a/gridData/tests/test_vdb.py b/gridData/tests/test_vdb.py index 0c501b4..a17bd79 100644 --- a/gridData/tests/test_vdb.py +++ b/gridData/tests/test_vdb.py @@ -152,6 +152,7 @@ def test_write_vdb_zero_threshold(self, tmpdir): g.export(outfile) assert tmpdir.join("threshold.vdb").exists() + def test_vdb_non_orthrhombic_raises(self): data=np.ones((3,3,3), dtype=np.float32) delta = np.array([ @@ -166,6 +167,30 @@ def test_vdb_non_orthrhombic_raises(self): origin=[0, 0, 0], delta=delta ) + + def test_delta_matrix_wrong_shape_raises(self): + data = np.ones((3, 3, 3), dtype=np.float32) + origin = [0.0, 0.0, 0.0] + bad_delta = np.eye(2) + + with pytest.raises(ValueError, match="delta as a matrix must be 3x3"): + gridData.OpenVDB.OpenVDBField(data, origin, bad_delta) + + def test_delta_scalar_raises(self): + data = np.ones((3, 3, 3), dtype=np.float32) + origin = [0.0, 0.0, 0.0] + bad_delta = np.array(1.0) + + with pytest.raises(ValueError, match="delta must be either a length-3 vector or a 3x3 diagonal matrix"): + gridData.OpenVDB.OpenVDBField(data, origin, bad_delta) + + def test_delta_1d_wrong_length_raises(self): + data = np.ones((3, 3, 3), dtype=np.float32) + origin = [0.0, 0.0, 0.0] + bad_delta = np.array([1.0, 2.0]) + + with pytest.raises(ValueError, match="must have length-3"): + gridData.OpenVDB.OpenVDBField(data, origin, bad_delta) @pytest.mark.skipif(HAS_OPENVDB, reason="Testing import error handling") def test_vdb_import_error(): From 4bd1d8ad980d59fe74cbad23494cc017500594f4 Mon Sep 17 00:00:00 2001 From: spyke7 Date: Wed, 28 Jan 2026 13:06:10 +0530 Subject: [PATCH 13/17] updated test_vdb.py --- gridData/core.py | 2 +- gridData/tests/test_vdb.py | 70 ++++++++++++++++++++++++++++++++------ 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/gridData/core.py b/gridData/core.py index 4b7506d..23516c6 100644 --- a/gridData/core.py +++ b/gridData/core.py @@ -712,7 +712,7 @@ def _export_vdb(self, filename, **kwargs): """ if self.grid.ndim != 3: raise ValueError( - "OpenVDB export requires a 3D grid, got {}D".format(self.grid.ndim)) + f"OpenVDB export requires a 3D grid, got {self.grid.ndim}D") grid_name = self.metadata.get('name', 'density') diff --git a/gridData/tests/test_vdb.py b/gridData/tests/test_vdb.py index a17bd79..5ff84d8 100644 --- a/gridData/tests/test_vdb.py +++ b/gridData/tests/test_vdb.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.testing import assert_allclose, assert_equal +from numpy.testing import assert_allclose import pytest @@ -8,7 +8,6 @@ from . import datafiles - try: import pyopenvdb as vdb HAS_OPENVDB = True @@ -19,12 +18,16 @@ except ImportError: HAS_OPENVDB = False +@pytest.fixture +def grid345(): + data = np.arange(1, 61, dtype=np.float32).reshape((3, 4, 5)) + g = Grid(data.copy(), origin=np.zeros(3), delta=np.ones(3)) + return data, g @pytest.mark.skipif(not HAS_OPENVDB, reason="pyopenvdb/openvdb not installed") class TestVDBWrite: - def test_write_vdb_from_grid(self, tmpdir): - data = np.arange(1, 28).reshape(3, 3, 3).astype(np.float32) - g = Grid(data, origin=np.zeros(3), delta=np.ones(3)) + def test_write_vdb_from_grid(self, tmpdir, grid345): + data,g = grid345 outfile = str(tmpdir / "test.vdb") g.export(outfile, file_format='VDB') @@ -35,6 +38,20 @@ def test_write_vdb_from_grid(self, tmpdir): assert len(grids) == 1 assert grids[0].name == 'density' + grid_vdb = grids[0] + acc = grid_vdb.getAccessor() + + assert grid_vdb.evalActiveVoxelDim() == data.shape + + corners = [ + (0, 0, 0), + (data.shape[0] - 1, data.shape[1] - 1, data.shape[2] - 1), + (1, 2, 3) + ] + for (i, j, k) in corners: + got = acc.getValue((i, j, k)) + assert got == pytest.approx(float(data[i, j, k])) + def test_write_vdb_default_grid_name(self, tmpdir): data = np.ones((3, 3, 3), dtype=np.float32) g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) @@ -45,9 +62,8 @@ def test_write_vdb_default_grid_name(self, tmpdir): grids, metadata = vdb.readAll(outfile) assert grids[0].name == 'density' - def test_write_vdb_autodetect_extension(self, tmpdir): - data = np.arange(24).reshape(2, 3, 4).astype(np.float32) - g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) + def test_write_vdb_autodetect_extension(self, tmpdir, grid345): + data, g = grid345 outfile = str(tmpdir / "auto.vdb") g.export(outfile) @@ -78,7 +94,9 @@ def test_write_vdb_origin_and_spacing(self, tmpdir): grid_vdb = grids[0] voxel_size = grid_vdb.transform.voxelSize() - assert_allclose([voxel_size[i] for i in range(3)], delta, rtol=1e-5) + + spacing=[voxel_size[0], voxel_size[1], voxel_size[2]] + assert_allclose(spacing, delta, rtol=1e-5) def test_write_vdb_from_ccp4(self, tmpdir): g = Grid(datafiles.CCP4) @@ -111,7 +129,7 @@ def test_vdb_field_2d_raises(self): delta=[1, 1] ) - def test_write_vdb_nonuniform_spacing_warning(self, tmpdir): + def test_write_vdb_nonuniform_spacing(self, tmpdir): data = np.ones((3, 3, 3), dtype=np.float32) delta = np.array([0.5, 1.0, 1.5]) g = Grid(data, origin=[0, 0, 0], delta=delta) @@ -120,6 +138,14 @@ def test_write_vdb_nonuniform_spacing_warning(self, tmpdir): g.export(outfile) assert tmpdir.join("nonuniform.vdb").exists() + grids, metadata = vdb.readAll(outfile) + grid_vdb = grids[0] + voxel_size = grid_vdb.transform.voxelSize() + + spacing = [voxel_size[0], voxel_size[1], voxel_size[2]] + + assert_allclose(spacing, delta, rtol=1e-5) + def test_write_vdb_with_delta_matrix(self, tmpdir): data = np.ones((3, 3, 3), dtype=np.float32) delta = np.diag([1.0, 2.0, 3.0]) @@ -129,7 +155,15 @@ def test_write_vdb_with_delta_matrix(self, tmpdir): outfile = str(tmpdir / "matrix_delta.vdb") vdb_field.write(outfile) assert tmpdir.join("matrix_delta.vdb").exists() - + + grids, metadata = vdb.readAll(outfile) + grid_vdb = grids[0] + voxel_size = grid_vdb.transform.voxelSize() + + spacing = [voxel_size[0], voxel_size[1], voxel_size[2]] + + assert_allclose(spacing, [1.0, 2.0, 3.0], rtol=1e-5) + def test_write_vdb_sparse_data(self, tmpdir): data = np.zeros((10, 10, 10), dtype=np.float32) data[2, 3, 4] = 5.0 @@ -143,6 +177,11 @@ def test_write_vdb_sparse_data(self, tmpdir): grids, metadata = vdb.readAll(outfile) assert len(grids) == 1 + grid_vdb = grids[0] + acc = grid_vdb.getAccessor() + assert acc.getValue((2, 3, 4)) == pytest.approx(5.0) + assert acc.getValue((7, 8, 9)) == pytest.approx(10.0) + def test_write_vdb_zero_threshold(self, tmpdir): data = np.ones((3, 3, 3), dtype=np.float32) * 1e-11 data[1, 1, 1] = 1.0 @@ -152,6 +191,15 @@ def test_write_vdb_zero_threshold(self, tmpdir): g.export(outfile) assert tmpdir.join("threshold.vdb").exists() + grids, metadata = vdb.readAll(outfile) + grid_vdb = grids[0] + acc = grid_vdb.getAccessor() + + assert acc.getValue((1, 1, 1)) == pytest.approx(1.0) + + val, is_active = grid_vdb.getConstAccessor().probeValue((0, 0, 0)) + + assert (not is_active ) or (val == pytest.approx(0.0)) def test_vdb_non_orthrhombic_raises(self): data=np.ones((3,3,3), dtype=np.float32) From d01039121174477987e16962d32a98cbe87487c6 Mon Sep 17 00:00:00 2001 From: spyke7 Date: Thu, 29 Jan 2026 15:54:53 +0530 Subject: [PATCH 14/17] updated tests and OpenVDB.py --- gridData/OpenVDB.py | 25 ++++++++++++------------- gridData/tests/test_vdb.py | 24 ++++++++++-------------- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/gridData/OpenVDB.py b/gridData/OpenVDB.py index e515298..3fdea9d 100644 --- a/gridData/OpenVDB.py +++ b/gridData/OpenVDB.py @@ -8,9 +8,9 @@ .. _`OpenVDB format`: https://www.openvdb.org .. _Blender: https://www.blender.org/ -This module uses the pyopenvdb_ library to write OpenVDB files. +This module uses the openvdb_ library to write OpenVDB files. -.. _pyopenvdb: https://github.com/AcademySoftwareFoundation/openvdb +.. _openvdb: https://github.com/AcademySoftwareFoundation/openvdb .. Note:: This module implements a simple writer for 3D regular grids, sufficient to export density data for visualization in Blender_. @@ -71,12 +71,10 @@ import warnings try: - import pyopenvdb as vdb + import openvdb as vdb + except ImportError: - try: - import openvdb as vdb - except ImportError: - vdb = None + vdb = None class OpenVDBField(object): @@ -104,7 +102,7 @@ class OpenVDBField(object): """ - def __init__(self, grid, origin, delta, name='density', threshold=1e-10): + def __init__(self, grid, origin, delta, name='density', tolerance=1e-10): """Initialize an OpenVDB field. Parameters @@ -124,7 +122,7 @@ def __init__(self, grid, origin, delta, name='density', threshold=1e-10): Raises ------ ImportError - If pyopenvdb is not installed + If openvdb is not installed ValueError If grid is not 3D, or if delta is not 1D/2D or describes non-orthorhombic cell @@ -132,11 +130,11 @@ def __init__(self, grid, origin, delta, name='density', threshold=1e-10): """ if vdb is None: raise ImportError( - "pyopenvdb is required to write VDB files. " + "openvdb is required to write VDB files. " "Install it with: conda install -c conda-forge openvdb" ) self.name = name - self.threshold = threshold + self.tolerance = tolerance self._populate(grid, origin, delta) def _populate(self, grid, origin, delta): @@ -164,6 +162,8 @@ def _populate(self, grid, origin, delta): f"OpenVDB only supports 3D grids, got {grid.ndim}D") self.grid = grid.astype(numpy.float32) + self.grid=numpy.ascontiguousarray(self.grid, dtype=numpy.float32) + self.origin = numpy.asarray(origin) # Handle delta: could be 1D array or diagonal matrix @@ -196,7 +196,6 @@ def write(self, filename): Output filename (should end in .vdb) """ - self.grid=numpy.ascontiguousarray(self.grid, dtype=numpy.float32) vdb_grid = vdb.FloatGrid() vdb_grid.name = self.name @@ -215,7 +214,7 @@ def write(self, filename): vdb_grid.background = 0.0 vdb_grid.transform = vdb.createLinearTransform(matrix) - vdb_grid.copyFromArray(self.grid, tolerance=self.threshold) + vdb_grid.copyFromArray(self.grid, tolerance=self.tolerance) vdb_grid.prune() vdb.write(filename, grids=[vdb_grid]) \ No newline at end of file diff --git a/gridData/tests/test_vdb.py b/gridData/tests/test_vdb.py index 5ff84d8..d925cb2 100644 --- a/gridData/tests/test_vdb.py +++ b/gridData/tests/test_vdb.py @@ -2,6 +2,7 @@ from numpy.testing import assert_allclose import pytest +from unittest.mock import patch import gridData.OpenVDB from gridData import Grid @@ -9,14 +10,10 @@ from . import datafiles try: - import pyopenvdb as vdb + import openvdb as vdb HAS_OPENVDB = True except ImportError: - try: - import openvdb as vdb - HAS_OPENVDB = True - except ImportError: - HAS_OPENVDB = False + HAS_OPENVDB = False @pytest.fixture def grid345(): @@ -24,7 +21,6 @@ def grid345(): g = Grid(data.copy(), origin=np.zeros(3), delta=np.ones(3)) return data, g -@pytest.mark.skipif(not HAS_OPENVDB, reason="pyopenvdb/openvdb not installed") class TestVDBWrite: def test_write_vdb_from_grid(self, tmpdir, grid345): data,g = grid345 @@ -240,11 +236,11 @@ def test_delta_1d_wrong_length_raises(self): with pytest.raises(ValueError, match="must have length-3"): gridData.OpenVDB.OpenVDBField(data, origin, bad_delta) -@pytest.mark.skipif(HAS_OPENVDB, reason="Testing import error handling") def test_vdb_import_error(): - with pytest.raises(ImportError, match="pyopenvdb is required"): - gridData.OpenVDB.OpenVDBField( - np.ones((3, 3, 3)), - origin=[0, 0, 0], - delta=[1, 1, 1] - ) \ No newline at end of file + with patch('gridData.OpenVDB.vdb', None): + with pytest.raises(ImportError, match="openvdb is required"): + gridData.OpenVDB.OpenVDBField( + np.ones((3, 3, 3)), + origin=[0, 0, 0], + delta=[1, 1, 1] + ) \ No newline at end of file From c8e643dce95b25adfaedfe58679be60e7443f603 Mon Sep 17 00:00:00 2001 From: spyke7 Date: Thu, 29 Jan 2026 15:55:17 +0530 Subject: [PATCH 15/17] updated tests and OpenVDB.py --- gridData/OpenVDB.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gridData/OpenVDB.py b/gridData/OpenVDB.py index 3fdea9d..163b269 100644 --- a/gridData/OpenVDB.py +++ b/gridData/OpenVDB.py @@ -68,7 +68,6 @@ """ import numpy -import warnings try: import openvdb as vdb From f87d3d8633ee90214eaa080248673e99d9ff6c30 Mon Sep 17 00:00:00 2001 From: spyke7 Date: Tue, 3 Feb 2026 17:27:47 +0530 Subject: [PATCH 16/17] Test updated and reformatted with black --- gridData/OpenVDB.py | 35 +++---- gridData/tests/test_vdb.py | 205 +++++++++++++++++++------------------ 2 files changed, 124 insertions(+), 116 deletions(-) diff --git a/gridData/OpenVDB.py b/gridData/OpenVDB.py index 163b269..49f5332 100644 --- a/gridData/OpenVDB.py +++ b/gridData/OpenVDB.py @@ -101,7 +101,7 @@ class OpenVDBField(object): """ - def __init__(self, grid, origin, delta, name='density', tolerance=1e-10): + def __init__(self, grid, origin, delta, name="density", tolerance=1e-10): """Initialize an OpenVDB field. Parameters @@ -157,30 +157,29 @@ def _populate(self, grid, origin, delta): """ grid = numpy.asarray(grid) if grid.ndim != 3: - raise ValueError( - f"OpenVDB only supports 3D grids, got {grid.ndim}D") + raise ValueError(f"OpenVDB only supports 3D grids, got {grid.ndim}D") self.grid = grid.astype(numpy.float32) - self.grid=numpy.ascontiguousarray(self.grid, dtype=numpy.float32) - + self.grid = numpy.ascontiguousarray(self.grid, dtype=numpy.float32) + self.origin = numpy.asarray(origin) # Handle delta: could be 1D array or diagonal matrix delta = numpy.asarray(delta) if delta.ndim == 2: - if (delta.shape != (3,3)): + if delta.shape != (3, 3): raise ValueError("delta as a matrix must be 3x3") - + if not numpy.allclose(delta, numpy.diag(numpy.diag(delta))): raise ValueError("Non-orthorhombic cells are not supported") - + self.delta = numpy.diag(delta) - + elif delta.ndim == 1: - if (len(delta) != 3): + if len(delta) != 3: raise ValueError("delta must have length-3 for 3D grids") - self.delta=delta - + self.delta = delta + else: raise ValueError( "delta must be either a length-3 vector or a 3x3 diagonal matrix" @@ -194,26 +193,26 @@ def write(self, filename): filename : str Output filename (should end in .vdb) - """ - + """ + vdb_grid = vdb.FloatGrid() vdb_grid.name = self.name # this is an explicit linear transform using per-axis voxel sizes # world = diag(delta) * index + corner_origin - corner_origin = (self.origin - 0.5 * self.delta) + corner_origin = self.origin - 0.5 * self.delta matrix = [ [self.delta[0], 0.0, 0.0, 0.0], [0.0, self.delta[1], 0.0, 0.0], [0.0, 0.0, self.delta[2], 0.0], - [corner_origin[0], corner_origin[1], corner_origin[2], 1.0] + [corner_origin[0], corner_origin[1], corner_origin[2], 1.0], ] vdb_grid.background = 0.0 vdb_grid.transform = vdb.createLinearTransform(matrix) - + vdb_grid.copyFromArray(self.grid, tolerance=self.tolerance) vdb_grid.prune() - vdb.write(filename, grids=[vdb_grid]) \ No newline at end of file + vdb.write(filename, grids=[vdb_grid]) diff --git a/gridData/tests/test_vdb.py b/gridData/tests/test_vdb.py index d925cb2..b10dfee 100644 --- a/gridData/tests/test_vdb.py +++ b/gridData/tests/test_vdb.py @@ -11,143 +11,152 @@ try: import openvdb as vdb + HAS_OPENVDB = True except ImportError: HAS_OPENVDB = False + @pytest.fixture def grid345(): data = np.arange(1, 61, dtype=np.float32).reshape((3, 4, 5)) g = Grid(data.copy(), origin=np.zeros(3), delta=np.ones(3)) return data, g + class TestVDBWrite: def test_write_vdb_from_grid(self, tmpdir, grid345): - data,g = grid345 - + data, g = grid345 + outfile = str(tmpdir / "test.vdb") - g.export(outfile, file_format='VDB') - + g.export(outfile, file_format="VDB") + assert tmpdir.join("test.vdb").exists() - + grids, metadata = vdb.readAll(outfile) assert len(grids) == 1 - assert grids[0].name == 'density' - + assert grids[0].name == "density" + grid_vdb = grids[0] acc = grid_vdb.getAccessor() assert grid_vdb.evalActiveVoxelDim() == data.shape - + corners = [ - (0, 0, 0), - (data.shape[0] - 1, data.shape[1] - 1, data.shape[2] - 1), - (1, 2, 3) + (0, 0, 0), + (data.shape[0] - 1, data.shape[1] - 1, data.shape[2] - 1), + (1, 2, 3), ] - for (i, j, k) in corners: + for i, j, k in corners: got = acc.getValue((i, j, k)) assert got == pytest.approx(float(data[i, j, k])) - - def test_write_vdb_default_grid_name(self, tmpdir): - data = np.ones((3, 3, 3), dtype=np.float32) - g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) + + def test_write_vdb_default_grid_name(self, tmpdir, grid345): + data, g = grid345 g.metadata = {} - + outfile = str(tmpdir / "default_name.vdb") g.export(outfile) grids, metadata = vdb.readAll(outfile) - assert grids[0].name == 'density' + assert grids[0].name == "density" def test_write_vdb_autodetect_extension(self, tmpdir, grid345): data, g = grid345 - + outfile = str(tmpdir / "auto.vdb") - g.export(outfile) - + g.export(outfile) + assert tmpdir.join("auto.vdb").exists() - def test_write_vdb_with_metadata(self, tmpdir): - data = np.ones((3, 3, 3), dtype=np.float32) - g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) - g.metadata['name'] = 'test_density' - + def test_write_vdb_with_metadata(self, tmpdir, grid345): + data, g = grid345 + g.metadata["name"] = "test_density" + outfile = str(tmpdir / "metadata.vdb") g.export(outfile) - + grids, metadata = vdb.readAll(outfile) - assert grids[0].name == 'test_density' + assert grids[0].name == "test_density" def test_write_vdb_origin_and_spacing(self, tmpdir): data = np.ones((4, 4, 4), dtype=np.float32) origin = np.array([10.0, 20.0, 30.0]) delta = np.array([0.5, 0.5, 0.5]) - + g = Grid(data, origin=origin, delta=delta) outfile = str(tmpdir / "transform.vdb") g.export(outfile) - + grids, metadata = vdb.readAll(outfile) grid_vdb = grids[0] - + voxel_size = grid_vdb.transform.voxelSize() - - spacing=[voxel_size[0], voxel_size[1], voxel_size[2]] + + spacing = [voxel_size[0], voxel_size[1], voxel_size[2]] assert_allclose(spacing, delta, rtol=1e-5) def test_write_vdb_from_ccp4(self, tmpdir): g = Grid(datafiles.CCP4) outfile = str(tmpdir / "from_ccp4.vdb") - - g.export(outfile, file_format='VDB') - + + g.export(outfile, file_format="VDB") + assert tmpdir.join("from_ccp4.vdb").exists() grids, metadata = vdb.readAll(outfile) assert len(grids) == 1 def test_vdb_field_direct(self, tmpdir): data = np.arange(27).reshape(3, 3, 3).astype(np.float32) - - vdb_field = gridData.OpenVDB.OpenVDBField(data, origin=[0,0,0], delta=[1,1,1], name='direct_test') - + + vdb_field = gridData.OpenVDB.OpenVDBField( + data, origin=[0, 0, 0], delta=[1, 1, 1], name="direct_test" + ) + outfile = str(tmpdir / "direct.vdb") vdb_field.write(outfile) - + grids, metadata = vdb.readAll(outfile) - assert grids[0].name == 'direct_test' + assert grids[0].name == "direct_test" + + grid_vdb = grids[0] + assert grid_vdb.evalActiveVoxelDim() == data.shape + acc = grid_vdb.getAccessor() + + assert acc.getValue((0, 0, 0)) == pytest.approx(float(data[0, 0, 0])) + assert acc.getValue((1, 1, 1)) == pytest.approx(float(data[1, 1, 1])) + assert acc.getValue((2, 2, 2)) == pytest.approx(float(data[2, 2, 2])) def test_vdb_field_2d_raises(self): data_2d = np.arange(12).reshape(3, 4) - + with pytest.raises(ValueError, match="3D grids"): - gridData.OpenVDB.OpenVDBField( - data_2d, - origin=[0, 0], - delta=[1, 1] - ) - + gridData.OpenVDB.OpenVDBField(data_2d, origin=[0, 0], delta=[1, 1]) + def test_write_vdb_nonuniform_spacing(self, tmpdir): data = np.ones((3, 3, 3), dtype=np.float32) delta = np.array([0.5, 1.0, 1.5]) g = Grid(data, origin=[0, 0, 0], delta=delta) - + outfile = str(tmpdir / "nonuniform.vdb") g.export(outfile) assert tmpdir.join("nonuniform.vdb").exists() - + grids, metadata = vdb.readAll(outfile) grid_vdb = grids[0] voxel_size = grid_vdb.transform.voxelSize() - + spacing = [voxel_size[0], voxel_size[1], voxel_size[2]] - + assert_allclose(spacing, delta, rtol=1e-5) - + def test_write_vdb_with_delta_matrix(self, tmpdir): data = np.ones((3, 3, 3), dtype=np.float32) delta = np.diag([1.0, 2.0, 3.0]) - - vdb_field = gridData.OpenVDB.OpenVDBField(data, origin=[0,0,0], delta=delta, name='matrix_delta') - + + vdb_field = gridData.OpenVDB.OpenVDBField( + data, origin=[0, 0, 0], delta=delta, name="matrix_delta" + ) + outfile = str(tmpdir / "matrix_delta.vdb") vdb_field.write(outfile) assert tmpdir.join("matrix_delta.vdb").exists() @@ -155,63 +164,61 @@ def test_write_vdb_with_delta_matrix(self, tmpdir): grids, metadata = vdb.readAll(outfile) grid_vdb = grids[0] voxel_size = grid_vdb.transform.voxelSize() - + spacing = [voxel_size[0], voxel_size[1], voxel_size[2]] - - assert_allclose(spacing, [1.0, 2.0, 3.0], rtol=1e-5) - + + assert_allclose(spacing, np.diag(delta), rtol=1e-5) + def test_write_vdb_sparse_data(self, tmpdir): data = np.zeros((10, 10, 10), dtype=np.float32) data[2, 3, 4] = 5.0 data[7, 8, 9] = 10.0 - + g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) outfile = str(tmpdir / "sparse.vdb") g.export(outfile) - + assert tmpdir.join("sparse.vdb").exists() grids, metadata = vdb.readAll(outfile) assert len(grids) == 1 - + grid_vdb = grids[0] acc = grid_vdb.getAccessor() - assert acc.getValue((2, 3, 4)) == pytest.approx(5.0) - assert acc.getValue((7, 8, 9)) == pytest.approx(10.0) - + assert acc.getValue((2, 3, 4)) == pytest.approx(data[2, 3, 4]) + assert acc.getValue((7, 8, 9)) == pytest.approx(data[7, 8, 9]) + def test_write_vdb_zero_threshold(self, tmpdir): data = np.ones((3, 3, 3), dtype=np.float32) * 1e-11 - data[1, 1, 1] = 1.0 - + data[1, 1, 1] = 1.0 + g = Grid(data, origin=[0, 0, 0], delta=[1, 1, 1]) outfile = str(tmpdir / "threshold.vdb") g.export(outfile) assert tmpdir.join("threshold.vdb").exists() - + grids, metadata = vdb.readAll(outfile) grid_vdb = grids[0] acc = grid_vdb.getAccessor() - - assert acc.getValue((1, 1, 1)) == pytest.approx(1.0) - val, is_active = grid_vdb.getConstAccessor().probeValue((0, 0, 0)) - - assert (not is_active ) or (val == pytest.approx(0.0)) - + assert acc.getValue((1, 1, 1)) == pytest.approx(data[1, 1, 1]) + + val, is_active = grid_vdb.getConstAccessor().probeValue((0, 0, 0)) + + assert (not is_active) or (val == pytest.approx(0.0)) + def test_vdb_non_orthrhombic_raises(self): - data=np.ones((3,3,3), dtype=np.float32) - delta = np.array([ - [1,0.1,0], - [0,1,0], - [0,0,1], - ]) - + data = np.ones((3, 3, 3), dtype=np.float32) + delta = np.array( + [ + [1, 0.1, 0], + [0, 1, 0], + [0, 0, 1], + ] + ) + with pytest.raises(ValueError, match="Non-orthorhombic"): - gridData.OpenVDB.OpenVDBField( - data, - origin=[0, 0, 0], - delta=delta - ) - + gridData.OpenVDB.OpenVDBField(data, origin=[0, 0, 0], delta=delta) + def test_delta_matrix_wrong_shape_raises(self): data = np.ones((3, 3, 3), dtype=np.float32) origin = [0.0, 0.0, 0.0] @@ -219,28 +226,30 @@ def test_delta_matrix_wrong_shape_raises(self): with pytest.raises(ValueError, match="delta as a matrix must be 3x3"): gridData.OpenVDB.OpenVDBField(data, origin, bad_delta) - + def test_delta_scalar_raises(self): data = np.ones((3, 3, 3), dtype=np.float32) origin = [0.0, 0.0, 0.0] bad_delta = np.array(1.0) - with pytest.raises(ValueError, match="delta must be either a length-3 vector or a 3x3 diagonal matrix"): + with pytest.raises( + ValueError, + match="delta must be either a length-3 vector or a 3x3 diagonal matrix", + ): gridData.OpenVDB.OpenVDBField(data, origin, bad_delta) - + def test_delta_1d_wrong_length_raises(self): data = np.ones((3, 3, 3), dtype=np.float32) origin = [0.0, 0.0, 0.0] - bad_delta = np.array([1.0, 2.0]) + bad_delta = np.array([1.0, 2.0]) with pytest.raises(ValueError, match="must have length-3"): gridData.OpenVDB.OpenVDBField(data, origin, bad_delta) - + + def test_vdb_import_error(): - with patch('gridData.OpenVDB.vdb', None): + with patch("gridData.OpenVDB.vdb", None): with pytest.raises(ImportError, match="openvdb is required"): gridData.OpenVDB.OpenVDBField( - np.ones((3, 3, 3)), - origin=[0, 0, 0], - delta=[1, 1, 1] - ) \ No newline at end of file + np.ones((3, 3, 3)), origin=[0, 0, 0], delta=[1, 1, 1] + ) From 97aef4b86b265fd2f5fe78caffb17d7f4f84540a Mon Sep 17 00:00:00 2001 From: spyke7 Date: Tue, 3 Feb 2026 17:35:07 +0530 Subject: [PATCH 17/17] added skip if --- gridData/tests/test_vdb.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gridData/tests/test_vdb.py b/gridData/tests/test_vdb.py index b10dfee..aace12e 100644 --- a/gridData/tests/test_vdb.py +++ b/gridData/tests/test_vdb.py @@ -24,6 +24,7 @@ def grid345(): return data, g +@pytest.mark.skipif(not HAS_OPENVDB, reason="openvdb not installed") class TestVDBWrite: def test_write_vdb_from_grid(self, tmpdir, grid345): data, g = grid345 @@ -247,6 +248,7 @@ def test_delta_1d_wrong_length_raises(self): gridData.OpenVDB.OpenVDBField(data, origin, bad_delta) +@pytest.mark.skipif(not HAS_OPENVDB, reason="Need openvdb to test import error handling") def test_vdb_import_error(): with patch("gridData.OpenVDB.vdb", None): with pytest.raises(ImportError, match="openvdb is required"):