From 00c8a7e9ae3ac453186510bdd20bcf353f3e3c9c Mon Sep 17 00:00:00 2001 From: Andre Pradhana Date: Wed, 2 Sep 2026 17:31:26 -0700 Subject: [PATCH] Revert "NanoVDB Python: restructure to mirror the C++ NanoVDB API (#2219)" This reverts commit 8a8ed7ca3e6726d3c7bcb13ce107e5d973fe7840 for the VDB 13.1 release. Signed-off-by: Andre Pradhana --- .github/workflows/nanovdb.yml | 3 - nanovdb/nanovdb/GridHandle.h | 57 +- nanovdb/nanovdb/Readme.md | 1 - nanovdb/nanovdb/python/BuildTypes.def | 141 -- nanovdb/nanovdb/python/CMakeLists.txt | 61 +- nanovdb/nanovdb/python/NanoVDBModule.cc | 1195 ++----------- nanovdb/nanovdb/python/PyBuildGrid.cc | 239 --- nanovdb/nanovdb/python/PyBuildGrid.h | 20 - nanovdb/nanovdb/python/PyCreateNanoGrid.cc | 556 +----- nanovdb/nanovdb/python/PyCreateNanoGrid.h | 15 - nanovdb/nanovdb/python/PyGridChecksum.cc | 61 +- nanovdb/nanovdb/python/PyGridChecksum.h | 5 - nanovdb/nanovdb/python/PyGridHandle.cc | 1 - nanovdb/nanovdb/python/PyGridHandle.h | 199 +-- nanovdb/nanovdb/python/PyGridStats.cc | 209 +-- nanovdb/nanovdb/python/PyGridStats.h | 6 - nanovdb/nanovdb/python/PyGridValidator.cc | 135 +- nanovdb/nanovdb/python/PyGridValidator.h | 6 - nanovdb/nanovdb/python/PyHostBuffer.cc | 5 +- nanovdb/nanovdb/python/PyIO.cc | 126 +- nanovdb/nanovdb/python/PyMath.cc | 512 ++---- nanovdb/nanovdb/python/PyPrimitives.cc | 379 +--- nanovdb/nanovdb/python/PySampleFromVoxels.cc | 70 +- nanovdb/nanovdb/python/PyTools.cc | 17 +- nanovdb/nanovdb/python/PyTree.cc | 121 -- nanovdb/nanovdb/python/PyTree.h | 486 ----- nanovdb/nanovdb/python/PyVoxelBlockManager.cc | 547 ------ nanovdb/nanovdb/python/PyVoxelBlockManager.h | 20 - nanovdb/nanovdb/python/__init__.py | 25 +- nanovdb/nanovdb/python/cuda/PyDeviceBuffer.cc | 5 +- .../nanovdb/python/cuda/PyDeviceGridHandle.cu | 77 +- nanovdb/nanovdb/python/cuda/PyPointsToGrid.cu | 3 +- nanovdb/nanovdb/python/examples/README.md | 48 - nanovdb/nanovdb/python/examples/build_grid.py | 80 - .../python/examples/bulk_leaf_numpy.py | 63 - .../python/examples/collide_level_set.py | 85 - .../python/examples/index_grid_channels.py | 86 - .../nanovdb/python/examples/io_roundtrip.py | 103 -- .../nanovdb/python/examples/load_inspect.py | 67 - .../python/examples/make_funny_nanovdb.py | 61 - .../python/examples/make_typed_grids.py | 72 - .../nanovdb/python/examples/node_manager.py | 50 - .../python/examples/openvdb_interop.py | 53 - nanovdb/nanovdb/python/examples/quantize.py | 59 - .../python/examples/raytrace_fog_volume.py | 95 - .../python/examples/raytrace_level_set.py | 120 -- nanovdb/nanovdb/python/examples/validate.py | 61 - nanovdb/nanovdb/python/test/TestExamples.py | 105 -- nanovdb/nanovdb/python/test/TestNanoVDB.py | 1561 +---------------- nanovdb/nanovdb/python/test/sitecustomize.py | 19 - 50 files changed, 587 insertions(+), 7504 deletions(-) delete mode 100644 nanovdb/nanovdb/python/BuildTypes.def delete mode 100644 nanovdb/nanovdb/python/PyBuildGrid.cc delete mode 100644 nanovdb/nanovdb/python/PyBuildGrid.h delete mode 100644 nanovdb/nanovdb/python/PyTree.cc delete mode 100644 nanovdb/nanovdb/python/PyTree.h delete mode 100644 nanovdb/nanovdb/python/PyVoxelBlockManager.cc delete mode 100644 nanovdb/nanovdb/python/PyVoxelBlockManager.h delete mode 100644 nanovdb/nanovdb/python/examples/README.md delete mode 100644 nanovdb/nanovdb/python/examples/build_grid.py delete mode 100644 nanovdb/nanovdb/python/examples/bulk_leaf_numpy.py delete mode 100644 nanovdb/nanovdb/python/examples/collide_level_set.py delete mode 100644 nanovdb/nanovdb/python/examples/index_grid_channels.py delete mode 100644 nanovdb/nanovdb/python/examples/io_roundtrip.py delete mode 100644 nanovdb/nanovdb/python/examples/load_inspect.py delete mode 100644 nanovdb/nanovdb/python/examples/make_funny_nanovdb.py delete mode 100644 nanovdb/nanovdb/python/examples/make_typed_grids.py delete mode 100644 nanovdb/nanovdb/python/examples/node_manager.py delete mode 100644 nanovdb/nanovdb/python/examples/openvdb_interop.py delete mode 100644 nanovdb/nanovdb/python/examples/quantize.py delete mode 100644 nanovdb/nanovdb/python/examples/raytrace_fog_volume.py delete mode 100644 nanovdb/nanovdb/python/examples/raytrace_level_set.py delete mode 100644 nanovdb/nanovdb/python/examples/validate.py delete mode 100644 nanovdb/nanovdb/python/test/TestExamples.py delete mode 100644 nanovdb/nanovdb/python/test/sitecustomize.py diff --git a/.github/workflows/nanovdb.yml b/.github/workflows/nanovdb.yml index ababa6c2ba..04fdc05d1a 100644 --- a/.github/workflows/nanovdb.yml +++ b/.github/workflows/nanovdb.yml @@ -120,9 +120,6 @@ jobs: - name: install shell: powershell run: .\ci\install_windows.ps1 - - name: install_numpy - shell: powershell - run: .\ci\install_windows_numpy.ps1 - name: build # nvcc doesn't set _WIN32 when run in bash so we need to set it manually shell: bash diff --git a/nanovdb/nanovdb/GridHandle.h b/nanovdb/nanovdb/GridHandle.h index 22b7697106..30cee4a7d2 100644 --- a/nanovdb/nanovdb/GridHandle.h +++ b/nanovdb/nanovdb/GridHandle.h @@ -623,64 +623,35 @@ splitGrids(const GridHandle &handle, const BufferT* other = nullptr) /// @brief Combines (or merges) multiple GridHandles into a single GridHandle containing all grids /// @tparam BufferT Type of the input and output grid buffers -/// @param handles array of non-owning pointers to the GridHandles to be combined -/// @param count number of pointers in @a handles +/// @param handles Vector of GridHandles to be combined /// @param pool optional pool used for allocation of output GridHandle /// @return single GridHandle containing all input grids -/// @note This overload borrows the input handles, so it can be used when the -/// handles are owned elsewhere (e.g. by a language binding) and can be -/// neither moved from nor cheaply copied. -template +template class VectorT> inline GridHandle -mergeGrids(const GridHandle* const* handles, size_t count, const BufferT* pool = nullptr) +mergeGrids(const VectorT> &handles, const BufferT* pool = nullptr) { - static_assert(!BufferHasDeviceSingle::value, - "mergeGrids requires a buffer type providing create(): cuda::copyTo HostBuffer handles first"); - static_assert(!BufferHasHostSingle::value, - "mergeGrids requires a buffer type providing create(): copy the handles to HostBuffer first"); uint64_t size = 0u; uint32_t counter = 0u, gridCount = 0u; - for (size_t i = 0; i < count; ++i) { - gridCount += handles[i]->gridCount(); - for (uint32_t n=0; ngridCount(); ++n) size += handles[i]->gridSize(n); + for (auto &h : handles) { + gridCount += h.gridCount(); + for (uint32_t n=0; n(); auto buffer = BufferT::create(size, pool); void *dst = buffer.data(); - for (size_t i = 0; i < count; ++i) { - const GridHandle &h = *handles[i]; + for (auto &h : handles) { + const void *src = h.data(); for (uint32_t n=0; nisValid()); - const uint64_t gridSize = h.gridSize(n); - std::memcpy(dst, src, gridSize); - tools::updateGridCount(reinterpret_cast(dst), counter++, gridCount); - dst = util::PtrAdd(dst, gridSize); + std::memcpy(dst, src, h.gridSize(n)); + GridData *data = reinterpret_cast(dst); + NANOVDB_ASSERT(data->isValid()); + tools::updateGridCount(data, counter++, gridCount); + dst = util::PtrAdd(dst, data->mGridSize); + src = util::PtrAdd(src, data->mGridSize); } } return GridHandle(std::move(buffer)); }// mergeGrids -/// @brief Combines (or merges) multiple GridHandles into a single GridHandle containing all grids -/// @tparam BufferT Type of the input and output grid buffers -/// @param handles Vector of GridHandles to be combined -/// @param pool optional pool used for allocation of output GridHandle -/// @return single GridHandle containing all input grids -template class VectorT> -inline GridHandle -mergeGrids(const VectorT> &handles, const BufferT* pool = nullptr) -{ - std::vector*> ptrs; - ptrs.reserve(handles.size()); - for (auto &h : handles) ptrs.push_back(&h); - return mergeGrids(ptrs.data(), ptrs.size(), pool); -}// mergeGrids - } // namespace nanovdb #if defined(__CUDACC__) diff --git a/nanovdb/nanovdb/Readme.md b/nanovdb/nanovdb/Readme.md index ee818e7970..31e15d9b22 100644 --- a/nanovdb/nanovdb/Readme.md +++ b/nanovdb/nanovdb/Readme.md @@ -8,7 +8,6 @@ A lightweight GPU friendly version of VDB initially targeting rendering applicat * [Source tree](../../doc/nanovdb/SourceTree.md) * [Examples](../../doc/nanovdb/HelloWorld.md) * [TEACHME — learn NanoVDB by prompting an LLM coding agent](../../doc/nanovdb/TEACHME/README.md) -* [Python examples](python/examples/) ### Copyright Contributors to the OpenVDB Project ### SPDX-License-Identifier: Apache-2.0 diff --git a/nanovdb/nanovdb/python/BuildTypes.def b/nanovdb/nanovdb/python/BuildTypes.def deleted file mode 100644 index bed2f199b3..0000000000 --- a/nanovdb/nanovdb/python/BuildTypes.def +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: Apache-2.0 -// -// X-macro list of NanoVDB BuildT types currently exposed to Python. -// -// NanoVDB coding style says "avoid macro functions; use inline and templates -// instead" (docs/codingstyle.txt). This file is a deliberate exception: it -// must emit top-level template instantiations and binding registrations into -// several translation units from one canonical list, which a C++ template -// cannot do. The pattern is contained to this file plus the matching -// per-consumer macro definitions; no runtime behavior is hidden behind a -// macro function. -// -// Consumers define one or more of the kind-specific macros below before -// including this file. Each macro is invoked once per matching type; any -// macro the consumer does not define is treated as a no-op and reset after -// the file is processed. This is the single point that must be edited when -// adding (or removing) a Python-visible BuildT. -// -// Macros: -// NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) -// Scalar value types with arithmetic semantics — exposed with full -// NodeInfo + setVoxel accessors. Suffix forms Python class names -// (e.g. "Float" -> "FloatGrid"). GridTypeEnum names the -// nanovdb::GridType:: enumerator a grid of this BuildT carries -// (used by the polymorphic handle.grid(n) dispatch). -// -// NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) -// Vector value types — exposed with a setVoxel accessor but no -// NodeInfo. AccessorName is passed explicitly because the legacy -// Python class names for the original two (Vec3f, RGBA8) are -// inconsistent — kept as-is for backwards compatibility. -// -// NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) -// The nanovdb::Point build type — exposed with a bare accessor. -// Point grids carry GridType::PointIndex (NOT a GridType::Point; -// that enumerator doesn't exist), so GridTypeEnum is given -// explicitly. Polymorphic dispatch routes PointIndex grids to -// NanoGrid. -// -// NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) -// BuildTs whose nanovdb::SetVoxel is unavailable (bool, the -// quantized Fp4/Fp8/Fp16/FpN types, the index types ValueIndex / -// ValueOnIndex, and ValueMask). nanovdb::BuildTraits::is_special -// is true for all of these. Exposed with a bare accessor only — no -// setVoxel, no NodeInfo. The Python accessor's getValue() returns -// the type given by nanovdb::BuildToValueMap::Type — float for -// Fp4 / Fp8 / Fp16 / FpN, uint64 for ValueIndex / ValueOnIndex, -// bool for ValueMask and for the BuildT=bool (Boolean) grid. -// -// NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) -// Subset that has C++ sampler specializations (used by PyMath -// samplers and PyCreateNanoGrid create*Grid factories). - -#ifndef NANOVDB_PY_FOR_EACH_SCALAR_BUILDT -#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) -#define NANOVDB_PY_LOCAL_DEFINED_SCALAR -#endif -#ifndef NANOVDB_PY_FOR_EACH_VECTOR_BUILDT -#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) -#define NANOVDB_PY_LOCAL_DEFINED_VECTOR -#endif -#ifndef NANOVDB_PY_FOR_EACH_POINT_BUILDT -#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) -#define NANOVDB_PY_LOCAL_DEFINED_POINT -#endif -#ifndef NANOVDB_PY_FOR_EACH_READONLY_BUILDT -#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) -#define NANOVDB_PY_LOCAL_DEFINED_READONLY -#endif -#ifndef NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT -#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) -#define NANOVDB_PY_LOCAL_DEFINED_SAMPLEABLE -#endif - -NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(float, Float, Float) -NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(double, Double, Double) -NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(int16_t, Int16, Int16) -NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(int32_t, Int32, Int32) -NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(int64_t, Int64, Int64) -NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(uint8_t, UInt8, UInt8) -NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(uint32_t, UInt32, UInt32) -// nanovdb::Half is intentionally not bound. The source declares it as -// `class Half{};` (an empty placeholder for IEEE 754 half-precision, see -// NanoVDB.h around line 180) and the C++ ProbeValue chain is -// inconsistent — leaf storage carries Half but the ProbeValue specialization -// expects float, causing a type mismatch during instantiation. When the -// upstream Half implementation lands this can be added in a follow-up. - -NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3f, Vec3f, - "Vec3fReadVectorAccessor", Vec3f) -NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3d, Vec3d, - "Vec3dReadAccessor", Vec3d) -NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec4f, Vec4f, - "Vec4fReadAccessor", Vec4f) -NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec4d, Vec4d, - "Vec4dReadAccessor", Vec4d) -NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3u8, Vec3u8, - "Vec3u8ReadAccessor", Vec3u8) -NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3u16, Vec3u16, - "Vec3u16ReadAccessor", Vec3u16) -NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::math::Rgba8, RGBA8, - "RGBA8ReadAccessor", RGBA8) - -NANOVDB_PY_FOR_EACH_POINT_BUILDT(::nanovdb::Point, Point, PointIndex) - -NANOVDB_PY_FOR_EACH_READONLY_BUILDT(bool, Boolean, Boolean) -NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::Fp4, Fp4, Fp4) -NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::Fp8, Fp8, Fp8) -NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::Fp16, Fp16, Fp16) -NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::FpN, FpN, FpN) -NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::ValueIndex, Index, Index) -NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::ValueOnIndex, OnIndex, OnIndex) -NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::ValueMask, Mask, Mask) - -NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(float, Float) -NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(double, Double) -NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(int32_t, Int32) -NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(::nanovdb::Vec3f, Vec3f) - -#ifdef NANOVDB_PY_LOCAL_DEFINED_SCALAR -#undef NANOVDB_PY_LOCAL_DEFINED_SCALAR -#endif -#ifdef NANOVDB_PY_LOCAL_DEFINED_VECTOR -#undef NANOVDB_PY_LOCAL_DEFINED_VECTOR -#endif -#ifdef NANOVDB_PY_LOCAL_DEFINED_POINT -#undef NANOVDB_PY_LOCAL_DEFINED_POINT -#endif -#ifdef NANOVDB_PY_LOCAL_DEFINED_READONLY -#undef NANOVDB_PY_LOCAL_DEFINED_READONLY -#endif -#ifdef NANOVDB_PY_LOCAL_DEFINED_SAMPLEABLE -#undef NANOVDB_PY_LOCAL_DEFINED_SAMPLEABLE -#endif - -#undef NANOVDB_PY_FOR_EACH_SCALAR_BUILDT -#undef NANOVDB_PY_FOR_EACH_VECTOR_BUILDT -#undef NANOVDB_PY_FOR_EACH_POINT_BUILDT -#undef NANOVDB_PY_FOR_EACH_READONLY_BUILDT -#undef NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT diff --git a/nanovdb/nanovdb/python/CMakeLists.txt b/nanovdb/nanovdb/python/CMakeLists.txt index e7b0bab3ae..7350ce84da 100644 --- a/nanovdb/nanovdb/python/CMakeLists.txt +++ b/nanovdb/nanovdb/python/CMakeLists.txt @@ -4,19 +4,8 @@ option(NANOVDB_BUILD_PYTHON_UNITTESTS [=[ "Include the NanoVDB Python unit test. Requires a python interpreter]=] ${NANOVDB_BUILD_UNITTESTS}) -# Stubs are only consumed from an installed wheel (they ship next to the .so -# in nanovdb/), so default ON only when SKBUILD is driving the build. In an -# in-source dev / OpenVDB-CI build the .pyi is not used and stub generation -# can be skipped — which also dodges the macOS CI's sanitizer-runtime -# DYLD_INSERT_LIBRARIES wrapper, under which Python dlopen'ing the .so -# triggers TSan "Interceptors are not working" and aborts stubgen. -option(NANOVDB_BUILD_PYTHON_STUBS - "Generate .pyi type stubs for the nanovdb Python module (requires nanobind 2.x or newer)." - ${SKBUILD}) - nanobind_add_module(nanovdb_python NB_STATIC NanoVDBModule.cc - PyBuildGrid.cc PyCreateNanoGrid.cc PyGridChecksum.cc PyGridHandle.cc @@ -29,8 +18,6 @@ nanobind_add_module(nanovdb_python NB_STATIC PyPrimitives.cc PySampleFromVoxels.cc PyTools.cc - PyTree.cc - PyVoxelBlockManager.cc cuda/PyDeviceBuffer.cc cuda/PyDeviceGridHandle.cu cuda/PyPointsToGrid.cu @@ -46,47 +33,10 @@ if(SKBUILD) set_target_properties(nanovdb_python PROPERTIES INSTALL_RPATH "$ORIGIN/../../openvdb/lib") install(TARGETS nanovdb_python DESTINATION ${NANOVDB_INSTALL_LIBDIR}) install(FILES __init__.py DESTINATION nanovdb) - # Ship the nanovdb C/C++ headers inside the Python wheel so downstream - # extension authors can compile against the same NanoVDB the wheel was - # built with. nanovdb.get_include() resolves to this directory at runtime. - install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../ - DESTINATION nanovdb/include/nanovdb - FILES_MATCHING - PATTERN "*.h" - PATTERN "*.cuh" - PATTERN "python" EXCLUDE - PATTERN "examples" EXCLUDE - PATTERN "unittest" EXCLUDE - PATTERN "cmd" EXCLUDE - PATTERN "docs" EXCLUDE) else() install(TARGETS nanovdb_python DESTINATION ${VDB_PYTHON_INSTALL_DIRECTORY}) endif() -# .pyi type stubs for IDE / type-checker support. Driven by the CMake helper -# that ships with nanobind 2.x; silently skipped on older nanobind where the -# helper does not exist. -if(NANOVDB_BUILD_PYTHON_STUBS AND COMMAND nanobind_add_stub) - set(NANOVDB_STUB_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}") - nanobind_add_stub(nanovdb_python_stub - MODULE nanovdb - OUTPUT "${NANOVDB_STUB_OUTPUT_DIR}/nanovdb.pyi" - PYTHON_PATH $ - DEPENDS nanovdb_python - MARKER_FILE "${NANOVDB_STUB_OUTPUT_DIR}/py.typed" - INCLUDE_PRIVATE) - if(SKBUILD) - install(FILES - "${NANOVDB_STUB_OUTPUT_DIR}/nanovdb.pyi" - "${NANOVDB_STUB_OUTPUT_DIR}/py.typed" - DESTINATION nanovdb) - endif() -elseif(NANOVDB_BUILD_PYTHON_STUBS) - message(STATUS - "NANOVDB_BUILD_PYTHON_STUBS is ON but nanobind_add_stub is unavailable " - "(nanobind >= 2.0 required). Skipping stub generation.") -endif() - # pytest if(NANOVDB_BUILD_PYTHON_UNITTESTS) @@ -99,19 +49,12 @@ if(NANOVDB_BUILD_PYTHON_UNITTESTS) COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/TestNanoVDB.py -v WORKING_DIRECTORY "${NANOVDB_PYTHON_WORKING_DIR}") - # Smoke-run every example script (they self-skip when optional - # dependencies such as NumPy or OpenVDB are unavailable). - add_test(NAME pytest_nanovdb_examples - COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/TestExamples.py -v - WORKING_DIRECTORY "${NANOVDB_PYTHON_WORKING_DIR}") - set_tests_properties(pytest_nanovdb_examples PROPERTIES TIMEOUT 300) - if(WIN32) set(PYTHONPATH "$ENV{PYTHONPATH};${NANOVDB_PYTHON_WORKING_DIR}") string(REPLACE "\\;" ";" PYTHONPATH "${PYTHONPATH}") string(REPLACE ";" "\\;" PYTHONPATH "${PYTHONPATH}") - set_tests_properties(pytest_nanovdb pytest_nanovdb_examples PROPERTIES ENVIRONMENT "PYTHONPATH=${PYTHONPATH}") + set_tests_properties(pytest_nanovdb PROPERTIES ENVIRONMENT "PYTHONPATH=${PYTHONPATH}") else() - set_tests_properties(pytest_nanovdb pytest_nanovdb_examples PROPERTIES ENVIRONMENT "PYTHONPATH=$ENV{PYTHONPATH}:${NANOVDB_PYTHON_WORKING_DIR}") + set_tests_properties(pytest_nanovdb PROPERTIES ENVIRONMENT "PYTHONPATH=$ENV{PYTHONPATH}:${NANOVDB_PYTHON_WORKING_DIR}") endif() endif() diff --git a/nanovdb/nanovdb/python/NanoVDBModule.cc b/nanovdb/nanovdb/python/NanoVDBModule.cc index 38b69f2db8..c812f2f431 100644 --- a/nanovdb/nanovdb/python/NanoVDBModule.cc +++ b/nanovdb/nanovdb/python/NanoVDBModule.cc @@ -1,7 +1,6 @@ // Copyright Contributors to the OpenVDB Project // SPDX-License-Identifier: Apache-2.0 #include -#include #include #include #include @@ -15,14 +14,11 @@ #include #include "cuda/PyDeviceBuffer.h" -#include "PyBuildGrid.h" #include "PyGridHandle.h" #include "PyHostBuffer.h" #include "PyIO.h" #include "PyMath.h" #include "PyTools.h" -#include "PyTree.h" -#include "PyVoxelBlockManager.h" #include "PyGridChecksum.h" namespace nb = nanobind; @@ -34,32 +30,19 @@ namespace pynanovdb { void defineVersion(nb::module_& m) { nb::class_(m, "Version", "Bit-compacted representation of all three version numbers") - .def(nb::init<>(), - "Construct a default-initialized Version matching the running NanoVDB build.") - .def(nb::init(), "data"_a, - "Construct a Version from a raw bit-packed uint32 value.") - .def(nb::init(), "major"_a, "minor"_a, "patch"_a, - "Construct a Version from explicit major, minor, and patch numbers.") - .def(nb::self == nb::self, "rhs"_a, - "Equality of major, minor and patch numbers.") - .def(nb::self < nb::self, "rhs"_a, - "Lexicographic less-than comparison over (major, minor, patch).") - .def(nb::self <= nb::self, "rhs"_a, - "Lexicographic less-than-or-equal comparison.") - .def(nb::self > nb::self, "rhs"_a, - "Lexicographic greater-than comparison.") - .def(nb::self >= nb::self, "rhs"_a, - "Lexicographic greater-than-or-equal comparison.") - .def("id", &Version::id, - "Return the bit-packed uint32 representation of this version.") - .def("getMajor", &Version::getMajor, - "Major version number.") - .def("getMinor", &Version::getMinor, - "Minor version number.") - .def("getPatch", &Version::getPatch, - "Patch version number.") - .def("age", &Version::age, - "Numeric age of this version relative to the running NanoVDB build.") + .def(nb::init<>()) + .def(nb::init(), "data"_a) + .def(nb::init(), "major"_a, "minor"_a, "patch"_a) + .def(nb::self == nb::self, "rhs"_a) + .def(nb::self < nb::self, "rhs"_a) + .def(nb::self <= nb::self, "rhs"_a) + .def(nb::self > nb::self, "rhs"_a) + .def(nb::self >= nb::self, "rhs"_a) + .def("id", &Version::id) + .def("getMajor", &Version::getMajor) + .def("getMinor", &Version::getMinor) + .def("getPatch", &Version::getPatch) + .def("age", &Version::age) .def("__repr__", [](const Version& version) { char str[strlen()]; toStr(str, version); @@ -69,10 +52,7 @@ void defineVersion(nb::module_& m) void definePointTypes(nb::module_& m) { - nb::enum_(m, "PointType", - "Encoding selector for point attributes attached to a PointGrid. " - "Controls the bit width and frame (world / grid / voxel) used when " - "rasterising points to blind data.") + nb::enum_(m, "PointType") .value("Disable", PointType::Disable) .value("PointID", PointType::PointID) .value("World64", PointType::World64) @@ -89,872 +69,169 @@ void definePointTypes(nb::module_& m) template void defineMask(nb::module_& m, const char* name, const char* doc) { nb::class_>(m, name, doc) - .def_static("memUsage", &Mask::memUsage, - "Byte size of a Mask instance.") - .def_static("bitCount", &Mask::bitCount, - "Total number of bits this mask can store.") - .def_static("wordCount", &Mask::wordCount, - "Number of 64-bit words used to back this mask.") - .def("countOn", nb::overload_cast<>(&Mask::countOn, nb::const_), - "Number of bits currently set in the mask.") - .def("countOn", nb::overload_cast(&Mask::countOn, nb::const_), "i"_a, - "Number of bits currently set in the prefix [0, i).") - .def(nb::init<>(), - "Construct an all-off mask.") - .def(nb::init(), "on"_a, - "Construct a mask with every bit set to on.") - .def(nb::init>(), "other"_a, - "Copy-construct from another Mask.") - .def(nb::self == nb::self, "other"_a, - "Bit-for-bit equality with another Mask.") - .def(nb::self != nb::self, "other"_a, - "Bit-for-bit inequality with another Mask.") - .def("isOn", nb::overload_cast(&Mask::isOn, nb::const_), "n"_a, - "True iff bit n is set.") - .def("isOff", nb::overload_cast(&Mask::isOff, nb::const_), "n"_a, - "True iff bit n is cleared.") - .def("isOn", nb::overload_cast<>(&Mask::isOn, nb::const_), - "True iff every bit is set.") - .def("isOff", nb::overload_cast<>(&Mask::isOff, nb::const_), - "True iff every bit is cleared.") - .def("setOn", nb::overload_cast(&Mask::setOn), "n"_a, - "Set bit n.") - .def("setOff", nb::overload_cast(&Mask::setOff), "n"_a, - "Clear bit n.") - .def("set", nb::overload_cast(&Mask::set), "n"_a, "on"_a, - "Assign bit n to the given on/off state.") - .def("setOn", nb::overload_cast<>(&Mask::setOn), - "Set every bit.") - .def("setOff", nb::overload_cast<>(&Mask::setOff), - "Clear every bit.") - .def("set", nb::overload_cast(&Mask::set), "on"_a, - "Set every bit to the same on/off state.") - .def("toggle", nb::overload_cast<>(&Mask::toggle), - "Flip every bit.") - .def("toggle", nb::overload_cast(&Mask::toggle), "n"_a, - "Flip bit n.") + .def_static("memUsage", &Mask::memUsage) + .def_static("bitCount", &Mask::bitCount) + .def_static("wordCount", &Mask::wordCount) + .def("countOn", nb::overload_cast<>(&Mask::countOn, nb::const_)) + .def("countOn", nb::overload_cast(&Mask::countOn, nb::const_), "i"_a) + .def(nb::init<>()) + .def(nb::init(), "on"_a) + .def(nb::init>(), "other"_a) + .def(nb::self == nb::self, "other"_a) + .def(nb::self != nb::self, "other"_a) + .def("isOn", nb::overload_cast(&Mask::isOn, nb::const_), "n"_a) + .def("isOff", nb::overload_cast(&Mask::isOff, nb::const_), "n"_a) + .def("isOn", nb::overload_cast<>(&Mask::isOn, nb::const_)) + .def("isOff", nb::overload_cast<>(&Mask::isOff, nb::const_)) + .def("setOn", nb::overload_cast(&Mask::setOn), "n"_a) + .def("setOff", nb::overload_cast(&Mask::setOff), "n"_a) + .def("set", nb::overload_cast(&Mask::set), "n"_a, "on"_a) + .def("setOn", nb::overload_cast<>(&Mask::setOn)) + .def("setOff", nb::overload_cast<>(&Mask::setOff)) + .def("set", nb::overload_cast(&Mask::set), "on"_a) + .def("toggle", nb::overload_cast<>(&Mask::toggle)) + .def("toggle", nb::overload_cast(&Mask::toggle), "n"_a) .def( - "__iand__", [](Mask& a, const Mask& b) { return a &= b; }, nb::is_operator(), "other"_a, - "In-place bitwise AND with another Mask.") + "__iand__", [](Mask& a, const Mask& b) { return a &= b; }, nb::is_operator(), "other"_a) .def( - "__ior__", [](Mask& a, const Mask& b) { return a |= b; }, nb::is_operator(), "other"_a, - "In-place bitwise OR with another Mask.") + "__ior__", [](Mask& a, const Mask& b) { return a |= b; }, nb::is_operator(), "other"_a) .def( - "__isub__", [](Mask& a, const Mask& b) { return a -= b; }, nb::is_operator(), "other"_a, - "In-place bitwise difference (clear every bit that is set in other).") + "__isub__", [](Mask& a, const Mask& b) { return a -= b; }, nb::is_operator(), "other"_a) .def( - "__ixor__", [](Mask& a, const Mask& b) { return a ^= b; }, nb::is_operator(), "other"_a, - "In-place bitwise XOR with another Mask.") - .def("findFirstOn", &Mask::template findFirst, - "Index of the first set bit, or bitCount() if every bit is clear.") - .def("findFirstOff", &Mask::template findFirst, - "Index of the first clear bit, or bitCount() if every bit is set.") - .def("findNextOn", &Mask::template findNext, "start"_a, - "Index of the first set bit at or after start.") - .def("findNextOff", &Mask::template findNext, "start"_a, - "Index of the first clear bit at or after start.") - .def("findPrevOn", &Mask::template findPrev, "start"_a, - "Index of the first set bit at or before start.") - .def("findPrevOff", &Mask::template findPrev, "start"_a, - "Index of the first clear bit at or before start."); + "__ixor__", [](Mask& a, const Mask& b) { return a ^= b; }, nb::is_operator(), "other"_a) + .def("findFirstOn", &Mask::template findFirst) + .def("findFirstOff", &Mask::template findFirst) + .def("findNextOn", &Mask::template findNext, "start"_a) + .def("findNextOff", &Mask::template findNext, "start"_a) + .def("findPrevOn", &Mask::template findPrev, "start"_a) + .def("findPrevOff", &Mask::template findPrev, "start"_a); } void defineMap(nb::module_& m) { nb::class_(m, "Map", "Defines an affine transform and its inverse represented as a 3x3 matrix and a vec3 translation") - .def(nb::init<>(), - "Construct an identity Map (uniform unit scale, zero translation).") - .def(nb::init(), "s"_a, "t"_a = Vec3d(0.), - "Construct a Map with uniform scale s and translation t.") - .def("set", nb::overload_cast(&Map::template set), "scale"_a, "translation"_a, "taper"_a = 1., - "Rebuild this Map from a uniform scale, translation and optional frustum taper.") - .def("set", nb::overload_cast(&Map::template set), "scale"_a, "translation"_a, "taper"_a = 1., - "Rebuild this Map from a uniform scale, translation and optional frustum taper.") - .def("applyMap", nb::overload_cast(&Map::template applyMap, nb::const_), "ijk"_a, - "Transform an index-space point to world space using 64-bit math; returns a vector of the same dtype as the input.") - .def("applyMap", nb::overload_cast(&Map::template applyMap, nb::const_), "ijk"_a, - "Transform an index-space point to world space using 64-bit math; returns a vector of the same dtype as the input.") - .def("applyMapF", nb::overload_cast(&Map::template applyMapF, nb::const_), "ijk"_a, - "Transform an index-space point to world space using 32-bit math; returns a vector of the same dtype as the input.") - .def("applyMapF", nb::overload_cast(&Map::template applyMapF, nb::const_), "ijk"_a, - "Transform an index-space point to world space using 32-bit math; returns a vector of the same dtype as the input.") - .def("applyJacobian", nb::overload_cast(&Map::template applyJacobian, nb::const_), "ijk"_a, - "Apply the linear (Jacobian) part of the transform using 64-bit math, ignoring translation.") - .def("applyJacobian", nb::overload_cast(&Map::template applyJacobian, nb::const_), "ijk"_a, - "Apply the linear (Jacobian) part of the transform using 64-bit math, ignoring translation.") - .def("applyJacobianF", nb::overload_cast(&Map::template applyJacobianF, nb::const_), "ijk"_a, - "Apply the linear (Jacobian) part of the transform using 32-bit math, ignoring translation.") - .def("applyJacobianF", nb::overload_cast(&Map::template applyJacobianF, nb::const_), "ijk"_a, - "Apply the linear (Jacobian) part of the transform using 32-bit math, ignoring translation.") - .def("applyInverseMap", nb::overload_cast(&Map::template applyInverseMap, nb::const_), "xyz"_a, - "Transform a world-space point back to index space using 64-bit math; returns a vector of the same dtype as the input.") - .def("applyInverseMap", nb::overload_cast(&Map::template applyInverseMap, nb::const_), "xyz"_a, - "Transform a world-space point back to index space using 64-bit math; returns a vector of the same dtype as the input.") - .def("applyInverseMapF", nb::overload_cast(&Map::template applyInverseMapF, nb::const_), "xyz"_a, - "Transform a world-space point back to index space using 32-bit math; returns a vector of the same dtype as the input.") - .def("applyInverseMapF", nb::overload_cast(&Map::template applyInverseMapF, nb::const_), "xyz"_a, - "Transform a world-space point back to index space using 32-bit math; returns a vector of the same dtype as the input.") - .def("applyInverseJacobian", nb::overload_cast(&Map::template applyInverseJacobian, nb::const_), "xyz"_a, - "Apply the inverse linear (Jacobian) part of the transform using 64-bit math.") - .def("applyInverseJacobian", nb::overload_cast(&Map::template applyInverseJacobian, nb::const_), "xyz"_a, - "Apply the inverse linear (Jacobian) part of the transform using 64-bit math.") - .def("applyInverseJacobianF", nb::overload_cast(&Map::template applyInverseJacobianF, nb::const_), "xyz"_a, - "Apply the inverse linear (Jacobian) part using 32-bit math.") - .def("applyInverseJacobianF", nb::overload_cast(&Map::template applyInverseJacobianF, nb::const_), "xyz"_a, - "Apply the inverse linear (Jacobian) part using 32-bit math.") - .def("applyIJT", nb::overload_cast(&Map::template applyIJT, nb::const_), "xyz"_a, - "Apply the inverse-Jacobian-transpose used for transforming normals.") - .def("applyIJT", nb::overload_cast(&Map::template applyIJT, nb::const_), "xyz"_a, - "Apply the inverse-Jacobian-transpose used for transforming normals.") - .def("applyIJTF", nb::overload_cast(&Map::template applyIJTF, nb::const_), "xyz"_a, - "Apply the inverse-Jacobian-transpose in single precision.") - .def("applyIJTF", nb::overload_cast(&Map::template applyIJTF, nb::const_), "xyz"_a, - "Apply the inverse-Jacobian-transpose in single precision.") - .def("getVoxelSize", &Map::getVoxelSize, - "World-space size of a single voxel implied by this Map."); + .def(nb::init<>()) + .def(nb::init(), "s"_a, "t"_a = Vec3d(0.)) + .def("set", nb::overload_cast(&Map::template set), "scale"_a, "translation"_a, "taper"_a = 1.) + .def("set", nb::overload_cast(&Map::template set), "scale"_a, "translation"_a, "taper"_a = 1.) + .def("applyMap", nb::overload_cast(&Map::template applyMap, nb::const_), "ijk"_a) + .def("applyMap", nb::overload_cast(&Map::template applyMap, nb::const_), "ijk"_a) + .def("applyMapF", nb::overload_cast(&Map::template applyMapF, nb::const_), "ijk"_a) + .def("applyMapF", nb::overload_cast(&Map::template applyMapF, nb::const_), "ijk"_a) + .def("applyJacobian", nb::overload_cast(&Map::template applyJacobian, nb::const_), "ijk"_a) + .def("applyJacobian", nb::overload_cast(&Map::template applyJacobian, nb::const_), "ijk"_a) + .def("applyJacobianF", nb::overload_cast(&Map::template applyJacobianF, nb::const_), "ijk"_a) + .def("applyJacobianF", nb::overload_cast(&Map::template applyJacobianF, nb::const_), "ijk"_a) + .def("applyInverseMap", nb::overload_cast(&Map::template applyInverseMap, nb::const_), "xyz"_a) + .def("applyInverseMap", nb::overload_cast(&Map::template applyInverseMap, nb::const_), "xyz"_a) + .def("applyInverseMapF", nb::overload_cast(&Map::template applyInverseMapF, nb::const_), "xyz"_a) + .def("applyInverseMapF", nb::overload_cast(&Map::template applyInverseMapF, nb::const_), "xyz"_a) + .def("applyInverseJacobian", nb::overload_cast(&Map::template applyInverseJacobian, nb::const_), "xyz"_a) + .def("applyInverseJacobian", nb::overload_cast(&Map::template applyInverseJacobian, nb::const_), "xyz"_a) + .def("applyInverseJacobianF", nb::overload_cast(&Map::template applyInverseJacobianF, nb::const_), "xyz"_a) + .def("applyInverseJacobianF", nb::overload_cast(&Map::template applyInverseJacobianF, nb::const_), "xyz"_a) + .def("applyIJT", nb::overload_cast(&Map::template applyIJT, nb::const_), "xyz"_a) + .def("applyIJT", nb::overload_cast(&Map::template applyIJT, nb::const_), "xyz"_a) + .def("applyIJTF", nb::overload_cast(&Map::template applyIJTF, nb::const_), "xyz"_a) + .def("applyIJTF", nb::overload_cast(&Map::template applyIJTF, nb::const_), "xyz"_a) + .def("getVoxelSize", &Map::getVoxelSize); } -// Forward declaration — body lives below defineGridBlindData() so it can -// reference the enum / class bindings registered there. -static nb::object pyGetBlindData(nb::handle py_grid, uint32_t n); - -// Type-erased Grid base class. nb::class_ is bound to nanovdb::GridData -// (the 672 B POD prefix present at the start of every NanoGrid), -// but the Python name is "Grid" to match the C++ class hierarchy where -// Grid (a.k.a. NanoGrid) is the user-facing type and -// GridData is the implementation-detail POD. -// -// Every BuildT-independent method lives here. The lambdas below read public -// data members on GridData directly because the accessor methods named -// version()/gridType()/isLevelSet()/etc. are defined on Grid, not on -// GridData itself — but they all just forward to a GridData data member, -// so the same value is reachable from the base. -void defineGrid(nb::module_& m) -{ - nb::class_(m, "Grid", - "Type-erased base for every NanoVDB grid. Carries header fields, " - "the affine transform, grid flags and blind-data accessors. Concrete " - "BuildT-typed subclasses (FloatGrid, Vec3fGrid, ...) add tree access " - "and per-voxel queries.") - // Validation and flag mutators (already member functions on GridData). - .def("isValid", &GridData::isValid, - "True iff the grid header looks consistent (magic / version / class tags).") - .def("setMinMaxOn", &GridData::setMinMaxOn, "on"_a = true, - "Toggle the HasMinMax grid flag.") - .def("setBBoxOn", &GridData::setBBoxOn, "on"_a = true, - "Toggle the HasBBox grid flag.") - .def("setLongGridNameOn", &GridData::setLongGridNameOn, "on"_a = true, - "Toggle the HasLongGridName grid flag.") - .def("setAverageOn", &GridData::setAverageOn, "on"_a = true, - "Toggle the HasAverage grid flag.") - .def("setStdDeviationOn", &GridData::setStdDeviationOn, "on"_a = true, - "Toggle the HasStdDeviation grid flag.") - .def("setGridName", &GridData::setGridName, "src"_a, - "Overwrite the grid's short name (truncated to the in-header buffer).") - // Affine transforms (already member functions on GridData). - .def("applyMap", nb::overload_cast(&GridData::template applyMap, nb::const_), "xyz"_a, - "Transform an index-space point to world space using this grid's Map.") - .def("applyMap", nb::overload_cast(&GridData::template applyMap, nb::const_), "xyz"_a, - "Transform an index-space point to world space using this grid's Map.") - .def("applyMapF", nb::overload_cast(&GridData::template applyMapF, nb::const_), "xyz"_a, - "Transform an index-space point to world space in single precision.") - .def("applyMapF", nb::overload_cast(&GridData::template applyMapF, nb::const_), "xyz"_a, - "Transform an index-space point to world space in single precision.") - .def("applyJacobian", nb::overload_cast(&GridData::template applyJacobian, nb::const_), "xyz"_a, - "Apply the linear (Jacobian) part of the transform using 64-bit math, ignoring translation.") - .def("applyJacobian", nb::overload_cast(&GridData::template applyJacobian, nb::const_), "xyz"_a, - "Apply the linear (Jacobian) part of the transform using 64-bit math, ignoring translation.") - .def("applyJacobianF", nb::overload_cast(&GridData::template applyJacobianF, nb::const_), "xyz"_a, - "Apply the linear part in single precision.") - .def("applyJacobianF", nb::overload_cast(&GridData::template applyJacobianF, nb::const_), "xyz"_a, - "Apply the linear part in single precision.") - .def("applyInverseMap", nb::overload_cast(&GridData::template applyInverseMap, nb::const_), "xyz"_a, - "Transform a world-space point back to index space.") - .def("applyInverseMap", nb::overload_cast(&GridData::template applyInverseMap, nb::const_), "xyz"_a, - "Transform a world-space point back to index space.") - .def("applyInverseMapF", nb::overload_cast(&GridData::template applyInverseMapF, nb::const_), "xyz"_a, - "Transform a world-space point back to index space in single precision.") - .def("applyInverseMapF", nb::overload_cast(&GridData::template applyInverseMapF, nb::const_), "xyz"_a, - "Transform a world-space point back to index space in single precision.") - .def("applyInverseJacobian", nb::overload_cast(&GridData::template applyInverseJacobian, nb::const_), "xyz"_a, - "Apply the inverse linear (Jacobian) part of the transform using 64-bit math.") - .def("applyInverseJacobian", nb::overload_cast(&GridData::template applyInverseJacobian, nb::const_), "xyz"_a, - "Apply the inverse linear (Jacobian) part of the transform using 64-bit math.") - .def("applyInverseJacobianF", nb::overload_cast(&GridData::template applyInverseJacobianF, nb::const_), "xyz"_a, - "Apply the inverse linear (Jacobian) part using 32-bit math.") - .def("applyInverseJacobianF", nb::overload_cast(&GridData::template applyInverseJacobianF, nb::const_), "xyz"_a, - "Apply the inverse linear (Jacobian) part using 32-bit math.") - .def("applyIJT", nb::overload_cast(&GridData::template applyIJT, nb::const_), "xyz"_a, - "Apply the inverse-Jacobian-transpose used for transforming normals.") - .def("applyIJT", nb::overload_cast(&GridData::template applyIJT, nb::const_), "xyz"_a, - "Apply the inverse-Jacobian-transpose used for transforming normals.") - .def("applyIJTF", nb::overload_cast(&GridData::template applyIJTF, nb::const_), "xyz"_a, - "Apply the inverse-Jacobian-transpose in single precision.") - .def("applyIJTF", nb::overload_cast(&GridData::template applyIJTF, nb::const_), "xyz"_a, - "Apply the inverse-Jacobian-transpose in single precision.") - // The C++ Grid convenience names for the same transforms - // (worldToIndex / indexToWorld and friends, NanoVDB.h). Bound as - // aliases on the type-erased base so the names most users know - // from OpenVDB / NanoVDB are directly discoverable in Python. - .def("worldToIndex", [](const GridData& g, const Vec3f& xyz) { return g.applyInverseMap(xyz); }, "xyz"_a, - "Transform a world-space point to index space. Alias of applyInverseMap.") - .def("worldToIndex", [](const GridData& g, const Vec3d& xyz) { return g.applyInverseMap(xyz); }, "xyz"_a, - "Transform a world-space point to index space. Alias of applyInverseMap.") - .def("indexToWorld", [](const GridData& g, const Vec3f& xyz) { return g.applyMap(xyz); }, "xyz"_a, - "Transform an index-space point to world space. Alias of applyMap.") - .def("indexToWorld", [](const GridData& g, const Vec3d& xyz) { return g.applyMap(xyz); }, "xyz"_a, - "Transform an index-space point to world space. Alias of applyMap.") - .def("worldToIndexDir", [](const GridData& g, const Vec3f& dir) { return g.applyInverseJacobian(dir); }, "dir"_a, - "Transform a world-space direction to index space. Alias of applyInverseJacobian.") - .def("worldToIndexDir", [](const GridData& g, const Vec3d& dir) { return g.applyInverseJacobian(dir); }, "dir"_a, - "Transform a world-space direction to index space. Alias of applyInverseJacobian.") - .def("indexToWorldDir", [](const GridData& g, const Vec3f& dir) { return g.applyJacobian(dir); }, "dir"_a, - "Transform an index-space direction to world space. Alias of applyJacobian.") - .def("indexToWorldDir", [](const GridData& g, const Vec3d& dir) { return g.applyJacobian(dir); }, "dir"_a, - "Transform an index-space direction to world space. Alias of applyJacobian.") - .def("indexToWorldGrad", [](const GridData& g, const Vec3f& grad) { return g.applyIJT(grad); }, "grad"_a, - "Transform an index-space gradient (normal) to world space. Alias of applyIJT.") - .def("indexToWorldGrad", [](const GridData& g, const Vec3d& grad) { return g.applyIJT(grad); }, "grad"_a, - "Transform an index-space gradient (normal) to world space. Alias of applyIJT.") - .def("worldToIndexF", [](const GridData& g, const Vec3f& xyz) { return g.applyInverseMapF(xyz); }, "xyz"_a, - "worldToIndex in single precision. Alias of applyInverseMapF.") - .def("worldToIndexF", [](const GridData& g, const Vec3d& xyz) { return g.applyInverseMapF(xyz); }, "xyz"_a, - "worldToIndex in single precision. Alias of applyInverseMapF.") - .def("indexToWorldF", [](const GridData& g, const Vec3f& xyz) { return g.applyMapF(xyz); }, "xyz"_a, - "indexToWorld in single precision. Alias of applyMapF.") - .def("indexToWorldF", [](const GridData& g, const Vec3d& xyz) { return g.applyMapF(xyz); }, "xyz"_a, - "indexToWorld in single precision. Alias of applyMapF.") - .def("worldToIndexDirF", [](const GridData& g, const Vec3f& dir) { return g.applyInverseJacobianF(dir); }, "dir"_a, - "worldToIndexDir in single precision. Alias of applyInverseJacobianF.") - .def("worldToIndexDirF", [](const GridData& g, const Vec3d& dir) { return g.applyInverseJacobianF(dir); }, "dir"_a, - "worldToIndexDir in single precision. Alias of applyInverseJacobianF.") - .def("indexToWorldDirF", [](const GridData& g, const Vec3f& dir) { return g.applyJacobianF(dir); }, "dir"_a, - "indexToWorldDir in single precision. Alias of applyJacobianF.") - .def("indexToWorldDirF", [](const GridData& g, const Vec3d& dir) { return g.applyJacobianF(dir); }, "dir"_a, - "indexToWorldDir in single precision. Alias of applyJacobianF.") - .def("indexToWorldGradF", [](const GridData& g, const Vec3f& grad) { return g.applyIJTF(grad); }, "grad"_a, - "indexToWorldGrad in single precision. Alias of applyIJTF.") - .def("indexToWorldGradF", [](const GridData& g, const Vec3d& grad) { return g.applyIJTF(grad); }, "grad"_a, - "indexToWorldGrad in single precision. Alias of applyIJTF.") - // Strings, geometry, layout (already member functions on GridData). - .def("gridName", &GridData::gridName, - "Full grid name as a C string. Reads the long-form name from " - "blind data when the HasLongGridName flag is set, falling " - "back to the in-header buffer otherwise. Use shortGridName() " - "if you specifically want the truncated 255-byte header copy.") - .def("memUsage", &GridData::memUsage, - "Byte size of this grid header.") - .def("worldBBox", &GridData::worldBBox, - "World-space bounding box of active voxels.") - .def("indexBBox", &GridData::indexBBox, - "Index-space bounding box of active voxels.") - .def("isEmpty", &GridData::isEmpty, - "True iff this grid has no active voxels.") - // Lifted from Grid via direct data-member access. - .def("version", [](const GridData& g) { return g.mVersion; }, - "NanoVDB Version stored in this grid's header.") - .def("gridSize", [](const GridData& g) { return g.mGridSize; }, - "Total byte size of this grid (header + tree + blind data).") - .def("gridIndex", [](const GridData& g) { return g.mGridIndex; }, - "Zero-based index of this grid within its parent GridHandle.") - .def("gridCount", [](const GridData& g) { return g.mGridCount; }, - "Number of grids stored alongside this one in the parent GridHandle.") - .def("voxelSize", [](const GridData& g) -> const Vec3d& { return g.mVoxelSize; }, - nb::rv_policy::reference_internal, - "World-space voxel size as a Vec3d.") - .def("map", [](const GridData& g) -> const Map& { return g.mMap; }, - nb::rv_policy::reference_internal, - "Affine index-to-world Map associated with this grid.") - .def("gridType", [](const GridData& g) { return g.mGridType; }, - "GridType enumerator naming the BuildT carried by this grid.") - .def("gridClass", [](const GridData& g) { return g.mGridClass; }, - "GridClass enumerator (LevelSet, FogVolume, ...).") - .def("checksum", [](const GridData& g) { return g.mChecksum; }, - "Checksum stored in the grid header; compare against tools.evalChecksum.") - .def("isLevelSet", [](const GridData& g) { return g.mGridClass == GridClass::LevelSet; }, - "True iff this grid's class is LevelSet.") - .def("isFogVolume", [](const GridData& g) { return g.mGridClass == GridClass::FogVolume; }, - "True iff this grid's class is FogVolume.") - .def("isStaggered", [](const GridData& g) { return g.mGridClass == GridClass::Staggered; }, - "True iff this grid's class is Staggered.") - .def("isPointIndex", - [](const GridData& g) { return g.mGridClass == GridClass::PointIndex; }, - "True iff this grid's class is PointIndex.") - .def("isGridIndex", [](const GridData& g) { return g.mGridClass == GridClass::IndexGrid; }, - "True iff this grid's class is IndexGrid.") - .def("isPointData", [](const GridData& g) { return g.mGridClass == GridClass::PointData; }, - "True iff this grid's class is PointData.") - .def("isMask", [](const GridData& g) { return g.mGridClass == GridClass::Topology; }, - "True iff this grid's class is Topology.") - .def("isUnknown", [](const GridData& g) { return g.mGridClass == GridClass::Unknown; }, - "True iff this grid's class is Unknown.") - .def("hasMinMax", [](const GridData& g) { return g.mFlags.isMaskOn(GridFlags::HasMinMax); }, - "True iff per-node min/max stats are stored in this grid.") - .def("hasBBox", [](const GridData& g) { return g.mFlags.isMaskOn(GridFlags::HasBBox); }, - "True iff per-node bbox stats are stored in this grid.") - .def("hasLongGridName", - [](const GridData& g) { return g.mFlags.isMaskOn(GridFlags::HasLongGridName); }, - "True iff the grid's name was too long to fit in the header buffer.") - .def("hasAverage", - [](const GridData& g) { return g.mFlags.isMaskOn(GridFlags::HasAverage); }, - "True iff per-node average stats are stored in this grid.") - .def("hasStdDeviation", - [](const GridData& g) { return g.mFlags.isMaskOn(GridFlags::HasStdDeviation); }, - "True iff per-node standard-deviation stats are stored in this grid.") - .def("isBreadthFirst", - [](const GridData& g) { return g.mFlags.isMaskOn(GridFlags::IsBreadthFirst); }, - "True iff this grid's nodes are laid out in breadth-first order.") - .def("shortGridName", [](const GridData& g) { return std::string(g.mGridName); }, - "Short in-header copy of the grid name as a Python string.") - // Blind data — exposes the sidecar channels that PointGrid and - // OnIndexGrid use to carry their actual values, colors, normals, IDs, - // etc. blindMetaData(n) returns the descriptor; getBlindData(n) - // returns a zero-copy NumPy view onto the underlying bytes typed by - // mDataType (Float -> float32 ndarray, Vec3f -> (N, 3) float32, etc.; - // unrecognized types fall back to a flat uint8 byte view). - .def("blindDataCount", [](const GridData& g) { return g.mBlindMetadataCount; }, - "Number of blind-data channels attached to this grid.") - .def("blindMetaData", - [](const GridData& g, uint32_t n) -> const GridBlindMetaData* { - return n < g.mBlindMetadataCount ? g.blindMetaData(n) : nullptr; - }, - nb::rv_policy::reference_internal, "n"_a, - "GridBlindMetaData descriptor for the n-th blind-data channel, or None if n is out of range.") - .def("findBlindData", [](const GridData& g, const std::string& name) -> int { - for (uint32_t i = 0; i < g.mBlindMetadataCount; ++i) { - const auto* meta = g.blindMetaData(i); - if (std::strncmp(meta->mName, name.c_str(), GridBlindMetaData::MaxNameSize) == 0) - return static_cast(i); - } - return -1; - }, "name"_a, - "Index of the blind-data channel whose name matches name, or -1 if none.") - .def("findBlindDataForSemantic", [](const GridData& g, GridBlindDataSemantic sem) -> int { - for (uint32_t i = 0; i < g.mBlindMetadataCount; ++i) { - if (g.blindMetaData(i)->mSemantic == sem) - return static_cast(i); - } - return -1; - }, "semantic"_a, - "Index of the blind-data channel whose semantic matches the given enum, or -1 if none.") - .def("getBlindData", &pyGetBlindData, "n"_a, - nb::keep_alive<0, 1>(), - "Return a zero-copy NumPy view of the n-th blind data channel, " - "or None if n is out of range. dtype and shape are derived from " - "the channel's mDataType / mValueCount. The view keeps the grid " - "alive (and therefore the GridHandle that owns the buffer)."); -} - -// BuildT-dependent slice of the typed grid Python class. Inherits the -// type-erased Grid base bound by defineGrid() above — anything that doesn't -// need to know BuildT lives there, not here. -template void defineNanoGrid(nb::module_& m, const char* name) -{ - auto cls = nb::class_, GridData>(m, name, - "BuildT-typed NanoVDB grid. Inherits the type-erased Grid base for " - "header / transform / blind data, and adds the typed tree and " - "accessor API for the underlying value type.") - .def("getAccessor", &NanoGrid::getAccessor, - "Return a DefaultReadAccessor caching the most recently visited path.") - .def("activeVoxelCount", &NanoGrid::activeVoxelCount, - "Total number of active voxels in this grid. See nanovdb::Grid::activeVoxelCount in NanoVDB.h.") - .def("isSequential", [](const NanoGrid& grid) { return grid.isSequential(); }, - "True iff this grid's nodes are laid out sequentially (per-level contiguous).") - .def("tree", - nb::overload_cast<>(&NanoGrid::tree, nb::const_), - nb::rv_policy::reference_internal, - "Return the tree associated with this grid. Lifetime is " - "anchored to the grid (and therefore to the GridHandle)."); - // Grid::valueCount / pointCount are SFINAE-gated in C++ to the - // index and Point BuildTs respectively — mirror that gating here. - if constexpr (BuildTraits::is_index) { - cls.def("valueCount", - [](const NanoGrid& grid) { return grid.valueCount(); }, - "Total number of values indexed by this grid. Sizes the " - "external or blind-data channel arrays that this index " - "grid's per-voxel indices map into."); - } - if constexpr (util::is_same::value) { - cls.def("pointCount", - [](const NanoGrid& grid) { return grid.pointCount(); }, - "Total number of points indexed by this PointGrid."); - } - // Add leaf_values() only for BuildTs whose LeafData carries T mValues[512]. - PyLeafValuesBinder::apply(cls); -} - -void defineGridBlindData(nb::module_& m) -{ - nb::enum_(m, "GridBlindDataClass", - "Coarse classifier for a blind-data channel (index, attribute, " - "channel, ...). Pairs with GridBlindDataSemantic to describe what " - "the channel actually carries.") - .value("Unknown", GridBlindDataClass::Unknown) - .value("IndexArray", GridBlindDataClass::IndexArray) - .value("AttributeArray", GridBlindDataClass::AttributeArray) - .value("GridName", GridBlindDataClass::GridName) - .value("ChannelArray", GridBlindDataClass::ChannelArray) - .value("End", GridBlindDataClass::End) - .export_values(); - - nb::enum_(m, "GridBlindDataSemantic", - "Fine-grained role of a blind-data channel: PointPosition, " - "PointColor, PointNormal, ... Used by find* helpers on Grid.") - .value("Unknown", GridBlindDataSemantic::Unknown) - .value("PointPosition", GridBlindDataSemantic::PointPosition) - .value("PointColor", GridBlindDataSemantic::PointColor) - .value("PointNormal", GridBlindDataSemantic::PointNormal) - .value("PointRadius", GridBlindDataSemantic::PointRadius) - .value("PointVelocity", GridBlindDataSemantic::PointVelocity) - .value("PointId", GridBlindDataSemantic::PointId) - .value("WorldCoords", GridBlindDataSemantic::WorldCoords) - .value("GridCoords", GridBlindDataSemantic::GridCoords) - .value("VoxelCoords", GridBlindDataSemantic::VoxelCoords) - .value("LevelSet", GridBlindDataSemantic::LevelSet) - .value("FogVolume", GridBlindDataSemantic::FogVolume) - .value("Staggered", GridBlindDataSemantic::Staggered) - .value("End", GridBlindDataSemantic::End) - .export_values(); - - nb::class_(m, "GridBlindMetaData", - "Sidecar metadata for one blind-data channel attached to a Grid.") - .def_ro("valueCount", &GridBlindMetaData::mValueCount, - "Number of values stored in this channel.") - .def_ro("valueSize", &GridBlindMetaData::mValueSize, - "Byte size of a single value in this channel.") - .def_ro("semantic", &GridBlindMetaData::mSemantic, - "GridBlindDataSemantic describing what this channel carries.") - .def_ro("dataClass", &GridBlindMetaData::mDataClass, - "GridBlindDataClass coarsely classifying this channel.") - .def_ro("dataType", &GridBlindMetaData::mDataType, - "GridType enumerator giving the dtype of a single value.") - .def("name", [](const GridBlindMetaData& m) { return std::string(m.mName); }, - "Name of this blind-data channel as a Python string.") - .def("isValid", &GridBlindMetaData::isValid, - "True iff this descriptor's class/semantic/type combination looks consistent.") - .def("blindDataSize", &GridBlindMetaData::blindDataSize, - "Total byte size of this channel (valueCount * valueSize, padded)."); -} - -// Resolve a blind-data channel into a zero-copy NumPy view. The dtype and -// shape are derived from the GridBlindMetaData's mDataType / mValueSize. -// For unrecognized types we fall back to a flat uint8 byte view so callers -// can still copy out the raw bytes. Falls back to None on a count mismatch -// between mValueSize and the GridType-implied stride. -static nb::object pyGetBlindData(nb::handle py_grid, uint32_t n) -{ - const auto& grid = nb::cast(py_grid); - if (n >= grid.mBlindMetadataCount) return nb::none(); - const auto* meta = grid.blindMetaData(n); - void* data = const_cast(static_cast( - util::PtrAdd(meta, meta->mDataOffset))); - const size_t count = static_cast(meta->mValueCount); - const uint32_t valueSize = meta->mValueSize; - - // The nb::numpy framework tag matters: the def-site keep_alive<0, 1>() - // needs a weak-referenceable return value, which numpy.ndarray is and - // nanobind's framework-agnostic ndarray wrapper is not. - auto make1D = [&](void* p, size_t n_elems, auto sentinel) -> nb::object { - using T = decltype(sentinel); - size_t shape[1] = {n_elems}; - return nb::cast(nb::ndarray, nb::c_contig, nb::device::cpu>( - static_cast(p), 1, shape, py_grid), - nb::rv_policy::reference); - }; - auto make2D = [&](void* p, size_t n_outer, size_t n_inner, auto sentinel) -> nb::object { - using T = decltype(sentinel); - size_t shape[2] = {n_outer, n_inner}; - return nb::cast(nb::ndarray, nb::c_contig, nb::device::cpu>( - static_cast(p), 2, shape, py_grid), - nb::rv_policy::reference); - }; - // Raw byte view fallback. Used either when the data type is unknown OR - // when the recorded mValueSize doesn't match the stride implied by - // mDataType — in that case constructing a typed ndarray would overrun the - // underlying blind-data region. mValueCount * mValueSize is by definition - // the actual byte extent of the channel, so this is always safe. - auto raw = [&]() -> nb::object { - return make1D(data, count * valueSize, uint8_t{}); - }; - - switch (meta->mDataType) { - case GridType::Float: - return valueSize == sizeof(float) ? make1D(data, count, float{}) : raw(); - case GridType::Double: - return valueSize == sizeof(double) ? make1D(data, count, double{}) : raw(); - case GridType::Int16: - return valueSize == sizeof(int16_t) ? make1D(data, count, int16_t{}) : raw(); - case GridType::Int32: - return valueSize == sizeof(int32_t) ? make1D(data, count, int32_t{}) : raw(); - case GridType::Int64: - return valueSize == sizeof(int64_t) ? make1D(data, count, int64_t{}) : raw(); - case GridType::UInt8: - return valueSize == sizeof(uint8_t) ? make1D(data, count, uint8_t{}) : raw(); - case GridType::UInt32: - return valueSize == sizeof(uint32_t) ? make1D(data, count, uint32_t{}) : raw(); - case GridType::Vec3f: - return valueSize == 3 * sizeof(float) ? make2D(data, count, 3, float{}) : raw(); - case GridType::Vec3d: - return valueSize == 3 * sizeof(double) ? make2D(data, count, 3, double{}) : raw(); - case GridType::Vec4f: - return valueSize == 4 * sizeof(float) ? make2D(data, count, 4, float{}) : raw(); - case GridType::Vec4d: - return valueSize == 4 * sizeof(double) ? make2D(data, count, 4, double{}) : raw(); - case GridType::Vec3u8: - return valueSize == 3 * sizeof(uint8_t) ? make2D(data, count, 3, uint8_t{}) : raw(); - case GridType::Vec3u16: - return valueSize == 3 * sizeof(uint16_t) ? make2D(data, count, 3, uint16_t{}) : raw(); - case GridType::RGBA8: - return valueSize == 4 * sizeof(uint8_t) ? make2D(data, count, 4, uint8_t{}) : raw(); - default: - return raw(); - } -} - -// PointAccessor — exposes the per-voxel point attributes that PointGrid -// carries as blind data. PointIndex grids store uint32 voxel indices; -// PointData grids store Vec3f positions. Constructor asserts the grid is -// the right shape; in Python an exception is the result of a mismatch. -// -// gridPoints() / leafPoints(ijk) / voxelPoints(ijk) all return a zero-copy -// NumPy view onto the underlying blind-data buffer, sliced to just the -// range associated with the call. Lifetime is anchored to the accessor. -template -struct PyPointAccessorTraits; -template<> struct PyPointAccessorTraits { using Scalar = uint32_t; }; -template<> struct PyPointAccessorTraits { using Scalar = float; }; - -template -static nb::object pyPointsToNdarray(nb::handle py_self, - const AttT* begin, - uint64_t count); - -template<> -nb::object pyPointsToNdarray(nb::handle py_self, - const uint32_t* begin, - uint64_t count) -{ - size_t shape[1] = {static_cast(count)}; - // nb::numpy tag required — see the equivalent note in pyGetBlindData. - return nb::cast( - nb::ndarray, nb::c_contig, nb::device::cpu>( - const_cast(begin), 1, shape, py_self), - nb::rv_policy::reference); -} - -template<> -nb::object pyPointsToNdarray(nb::handle py_self, - const Vec3f* begin, - uint64_t count) -{ - size_t shape[2] = {static_cast(count), 3}; - // nb::numpy tag required — see the equivalent note in pyGetBlindData. - return nb::cast( - nb::ndarray, nb::c_contig, nb::device::cpu>( - reinterpret_cast(const_cast(begin)), 2, shape, py_self), - nb::rv_policy::reference); -} - -template void definePointAccessor(nb::module_& m, const char* name) -{ - using PA = PointAccessor; - nb::class_(m, name, - "Per-voxel access to the point attributes carried as blind " - "data on a PointGrid. gridPoints / leafPoints / voxelPoints " - "return zero-copy NumPy views.") - .def(nb::init&>(), "grid"_a, nb::keep_alive<1, 2>(), - "Construct a PointAccessor bound to the given PointGrid.") - .def("__bool__", [](const PA& a) { return bool(a); }, - "True iff this accessor is bound to a valid PointGrid.") - .def("grid", &PA::grid, nb::rv_policy::reference_internal, - "Return the PointGrid this accessor is bound to.") - .def("gridPoints", [](nb::handle py_self) -> nb::object { - auto& acc = nb::cast(py_self); - const AttT* begin = nullptr; - const AttT* end = nullptr; - uint64_t count = acc.gridPoints(begin, end); - if (begin == nullptr || count == 0) return nb::none(); - return pyPointsToNdarray(py_self, begin, count); - }, nb::keep_alive<0, 1>(), - "Return all point attributes in the grid as a single NumPy view. " - "The view keeps this accessor alive.") - .def("leafPoints", [](nb::handle py_self, const Coord& ijk) -> nb::object { - auto& acc = nb::cast(py_self); - const AttT* begin = nullptr; - const AttT* end = nullptr; - uint64_t count = acc.leafPoints(ijk, begin, end); - if (begin == nullptr || count == 0) return nb::none(); - return pyPointsToNdarray(py_self, begin, count); - }, "ijk"_a, nb::keep_alive<0, 1>(), - "Return the point attributes contained within the leaf node " - "covering ijk, or None if no leaf is present. The view keeps " - "this accessor alive.") - .def("voxelPoints", [](nb::handle py_self, const Coord& ijk) -> nb::object { - auto& acc = nb::cast(py_self); - const AttT* begin = nullptr; - const AttT* end = nullptr; - uint64_t count = acc.voxelPoints(ijk, begin, end); - if (begin == nullptr || count == 0) return nb::none(); - return pyPointsToNdarray(py_self, begin, count); - }, "ijk"_a, nb::keep_alive<0, 1>(), - "Return the point attributes at the specific voxel ijk, or None " - "if the voxel is inactive / empty. The view keeps this accessor " - "alive."); -} - -// Typed ChannelAccessor over an Index/OnIndex grid: combines the uint64 -// per-voxel index lookup with a read of the matching blind-data channel, so -// channel values can be queried directly by Coord. The C++ ctor only -// NANOVDB_ASSERTs its preconditions (debug builds), so the binding validates -// them explicitly and raises instead of returning an accessor that would -// dereference a null channel pointer. -template -void defineChannelAccessor(nb::module_& m, const char* name) -{ - using CA = ChannelAccessor; - nb::class_(m, name, - "Accessor that reads one blind-data channel of an Index/OnIndex " - "grid at Coord positions. Build via createChannelAccessor() for " - "automatic channel-dtype dispatch.") - .def( - "__init__", - [](CA* self, const NanoGrid& grid, uint32_t channelID) { - if (grid.gridClass() != GridClass::IndexGrid) - throw nb::value_error( - "ChannelAccessor: grid must have GridClass.IndexGrid."); - if (channelID >= grid.blindDataCount()) - throw nb::index_error( - "ChannelAccessor: channelID is out of range."); - new (self) CA(grid, channelID); - if (!*self) - throw nb::type_error( - "ChannelAccessor: the blind-data channel's dataType " - "does not match this accessor's channel type."); - }, - "grid"_a, "channelID"_a = 0u, nb::keep_alive<1, 2>(), - "Construct an accessor over the given index grid's channelID-th " - "blind-data channel. Raises if the grid is not an IndexGrid, the " - "channel is out of range, or its dtype does not match.") - .def("__bool__", [](const CA& acc) { return bool(acc); }, - "True iff this accessor is bound to a valid channel.") - .def("grid", &CA::grid, nb::rv_policy::reference_internal, - "Return the index grid this accessor is bound to.") - .def("valueCount", [](const CA& acc) { return acc.valueCount(); }, - "Total number of values indexed by the underlying index grid.") - .def( - "setChannel", - [](CA& acc, uint32_t channelID) { - if (channelID >= acc.grid().blindDataCount()) - throw nb::index_error( - "setChannel: channelID is out of range."); - if (acc.setChannel(channelID) == nullptr) - throw nb::type_error( - "setChannel: the blind-data channel's dataType does " - "not match this accessor's channel type."); - }, - "channelID"_a, - "Switch this accessor to another blind-data channel of the same " - "grid. Raises if the channel is out of range or its dtype does " - "not match.") - .def("getIndex", [](const CA& acc, const Coord& ijk) { return acc.getIndex(ijk); }, "ijk"_a, - "Linear offset into the channel array for the voxel at ijk.") - .def("idx", [](const CA& acc, int i, int j, int k) { return acc.idx(i, j, k); }, "i"_a, "j"_a, "k"_a, - "Linear offset into the channel array for the voxel at (i, j, k).") - .def("getValue", [](const CA& acc, const Coord& ijk) -> ChannelT { return acc.getValue(ijk); }, "ijk"_a, - "Channel value mapped to the voxel at ijk.") - .def( - "__call__", [](const CA& acc, const Coord& ijk) -> ChannelT { return acc.getValue(ijk); }, nb::is_operator(), "ijk"_a, - "Channel value mapped to the voxel at ijk.") - .def( - "__call__", [](const CA& acc, int i, int j, int k) -> ChannelT { return acc(i, j, k); }, nb::is_operator(), "i"_a, "j"_a, "k"_a, - "Channel value mapped to the voxel at (i, j, k).") - .def("isActive", [](const CA& acc, const Coord& ijk) { return acc.isActive(ijk); }, "ijk"_a, - "True iff the voxel at ijk is active in the index grid.") - .def( - "probeValue", - [](const CA& acc, const Coord& ijk) { - typename util::remove_const::type v; - bool isOn = acc.probeValue(ijk, v); - return std::make_tuple(v, isOn); - }, - "ijk"_a, - "Return (channel value, isActive) for the voxel at ijk in a " - "single tree traversal."); -} - -// Polymorphic factory: dispatch on the grid's index BuildT and the -// channel's recorded dataType, returning the matching typed -// ChannelAccessor. The def-site keep_alive<0, 1>() anchors the grid. -template -nb::object tryCreateChannelAccessor(nb::handle py_grid, uint32_t channelID) -{ - using GridT = NanoGrid; - if (!nb::isinstance(py_grid)) return nb::object(); - const auto& grid = nb::cast(py_grid); - if (grid.gridClass() != GridClass::IndexGrid) - throw nb::value_error( - "createChannelAccessor: grid must have GridClass.IndexGrid."); - if (channelID >= grid.blindDataCount()) - throw nb::index_error( - "createChannelAccessor: channelID is out of range."); - switch (grid.blindMetaData(channelID).mDataType) { - case GridType::Float: return nb::cast(ChannelAccessor(grid, channelID)); - case GridType::Double: return nb::cast(ChannelAccessor(grid, channelID)); - case GridType::Int32: return nb::cast(ChannelAccessor(grid, channelID)); - case GridType::Vec3f: return nb::cast(ChannelAccessor(grid, channelID)); - default: - throw nb::type_error( - "createChannelAccessor: the channel's dataType has no bound " - "ChannelAccessor (supported: Float, Double, Int32, Vec3f)."); - } -} - -nb::object createChannelAccessorImpl(nb::handle py_grid, uint32_t channelID) +void defineGridData(nb::module_& m) { - if (auto r = tryCreateChannelAccessor(py_grid, channelID); r.is_valid()) return r; - if (auto r = tryCreateChannelAccessor(py_grid, channelID); r.is_valid()) return r; - throw nb::type_error( - "createChannelAccessor: grid must be an IndexGrid or OnIndexGrid."); + nb::class_(m, "GridData") + .def("isValid", &GridData::isValid) + .def("setMinMaxOn", &GridData::setMinMaxOn, "on"_a = true) + .def("setBBoxOn", &GridData::setBBoxOn, "on"_a = true) + .def("setLongGridNameOn", &GridData::setLongGridNameOn, "on"_a = true) + .def("setAverageOn", &GridData::setAverageOn, "on"_a = true) + .def("setStdDeviationOn", &GridData::setStdDeviationOn, "on"_a = true) + .def("setGridName", &GridData::setGridName, "src"_a) + .def("applyMap", nb::overload_cast(&GridData::template applyMap, nb::const_), "xyz"_a) + .def("applyMap", nb::overload_cast(&GridData::template applyMap, nb::const_), "xyz"_a) + .def("applyMapF", nb::overload_cast(&GridData::template applyMapF, nb::const_), "xyz"_a) + .def("applyMapF", nb::overload_cast(&GridData::template applyMapF, nb::const_), "xyz"_a) + .def("applyJacobian", nb::overload_cast(&GridData::template applyJacobian, nb::const_), "xyz"_a) + .def("applyJacobian", nb::overload_cast(&GridData::template applyJacobian, nb::const_), "xyz"_a) + .def("applyJacobianF", nb::overload_cast(&GridData::template applyJacobianF, nb::const_), "xyz"_a) + .def("applyJacobianF", nb::overload_cast(&GridData::template applyJacobianF, nb::const_), "xyz"_a) + .def("applyInverseMap", nb::overload_cast(&GridData::template applyInverseMap, nb::const_), "xyz"_a) + .def("applyInverseMap", nb::overload_cast(&GridData::template applyInverseMap, nb::const_), "xyz"_a) + .def("applyInverseMapF", nb::overload_cast(&GridData::template applyInverseMapF, nb::const_), "xyz"_a) + .def("applyInverseMapF", nb::overload_cast(&GridData::template applyInverseMapF, nb::const_), "xyz"_a) + .def("applyInverseJacobian", nb::overload_cast(&GridData::template applyInverseJacobian, nb::const_), "xyz"_a) + .def("applyInverseJacobian", nb::overload_cast(&GridData::template applyInverseJacobian, nb::const_), "xyz"_a) + .def("applyInverseJacobianF", nb::overload_cast(&GridData::template applyInverseJacobianF, nb::const_), "xyz"_a) + .def("applyInverseJacobianF", nb::overload_cast(&GridData::template applyInverseJacobianF, nb::const_), "xyz"_a) + .def("applyIJT", nb::overload_cast(&GridData::template applyIJT, nb::const_), "xyz"_a) + .def("applyIJT", nb::overload_cast(&GridData::template applyIJT, nb::const_), "xyz"_a) + .def("applyIJTF", nb::overload_cast(&GridData::template applyIJTF, nb::const_), "xyz"_a) + .def("applyIJTF", nb::overload_cast(&GridData::template applyIJTF, nb::const_), "xyz"_a) + .def("gridName", &GridData::gridName) + .def("memUsage", &GridData::memUsage) + .def("worldBBox", &GridData::worldBBox) + .def("indexBBox", &GridData::indexBBox) + .def("isEmpty", &GridData::isEmpty); } -// Type-erased grid introspector. Mirrors nanovdb::GridMetaData (768B) and -// answers "what's in this buffer?" questions without needing to know -// BuildT. Construct from a Grid (which is the Python-side GridData); all -// queries below are flat data-member reads with no tree traversal. -void defineGridMetaData(nb::module_& m) +template void defineGrid(nb::module_& m, const char* name) { - // Constructing GridMetaData calls into nanovdb::GridMetaData::safeCast - // which has a NANOVDB_ASSERT(gridData && gridData->isValid()). nanobind - // already rejects Python None at the type-check level (None can't bind to - // const GridData*), but a Grid wrapping a corrupted / partially-formed - // buffer would still abort debug builds and undefined-behave in release. - // Guard explicitly: validate first, raise nb::value_error on bad input. - nb::class_(m, "GridMetaData", - "Type-erased introspector. Mirrors FileMetaData " - "but reads from an in-memory grid header.") - .def("__init__", - [](GridMetaData* self, const GridData* gd) { - if (gd == nullptr) { - throw nb::value_error("GridMetaData: grid must not be None"); - } - if (!gd->isValid()) { - throw nb::value_error("GridMetaData: grid header is invalid " - "(bad magic, version, or class/type tags)"); - } - new (self) GridMetaData(gd); - }, "grid"_a, - "Construct from a Grid. Raises ValueError if the grid is None or has an invalid header.") - .def_static("safeCast", - [](const GridData* gd) { - // Mirror the spirit of NanoVDB's static safeCast: "is - // it safe to cast this gridData to a GridMetaData?". - // null and invalid grids are by definition not safe; - // return False rather than dereference. - if (gd == nullptr || !gd->isValid()) return false; - return GridMetaData::safeCast(gd); - }, "grid"_a, - "True iff the given grid header is well-formed enough to wrap in a GridMetaData.") - .def("isValid", &GridMetaData::isValid, - "True iff the wrapped header looks consistent.") - .def("gridType", &GridMetaData::gridType, - "GridType enumerator naming the BuildT of the wrapped grid.") - .def("gridClass", &GridMetaData::gridClass, - "GridClass enumerator (LevelSet, FogVolume, ...).") - .def("isLevelSet", &GridMetaData::isLevelSet, - "True iff the wrapped grid's class is LevelSet.") - .def("isFogVolume", &GridMetaData::isFogVolume, - "True iff the wrapped grid's class is FogVolume.") - .def("isStaggered", &GridMetaData::isStaggered, - "True iff the wrapped grid's class is Staggered.") - .def("isPointIndex", &GridMetaData::isPointIndex, - "True iff the wrapped grid's class is PointIndex.") - .def("isGridIndex", &GridMetaData::isGridIndex, - "True iff the wrapped grid's class is IndexGrid.") - .def("isPointData", &GridMetaData::isPointData, - "True iff the wrapped grid's class is PointData.") - .def("isMask", &GridMetaData::isMask, - "True iff the wrapped grid's class is Topology.") - .def("isUnknown", &GridMetaData::isUnknown, - "True iff the wrapped grid's class is Unknown.") - .def("hasMinMax", &GridMetaData::hasMinMax, - "True iff per-node min/max stats are stored in the wrapped grid.") - .def("hasBBox", &GridMetaData::hasBBox, - "True iff per-node bbox stats are stored in the wrapped grid.") - .def("hasLongGridName", &GridMetaData::hasLongGridName, - "True iff the wrapped grid's name was too long for the header buffer.") - .def("hasAverage", &GridMetaData::hasAverage, - "True iff per-node average stats are stored in the wrapped grid.") - .def("hasStdDeviation", &GridMetaData::hasStdDeviation, - "True iff per-node standard-deviation stats are stored in the wrapped grid.") - .def("isBreadthFirst", &GridMetaData::isBreadthFirst, - "True iff the wrapped grid is laid out breadth-first.") - .def("gridSize", &GridMetaData::gridSize, - "Total byte size of the wrapped grid.") - .def("gridIndex", &GridMetaData::gridIndex, - "Index of this grid within its parent GridHandle.") - .def("gridCount", &GridMetaData::gridCount, - "Number of grids in the parent GridHandle.") - .def("shortGridName", [](const GridMetaData& m) { return std::string(m.shortGridName()); }, - "Short in-header copy of the grid name as a Python string.") - .def("map", &GridMetaData::map, nb::rv_policy::reference_internal, - "Affine index-to-world Map of the wrapped grid.") - .def("worldBBox", &GridMetaData::worldBBox, nb::rv_policy::reference_internal, - "World-space bounding box of the wrapped grid's active voxels.") - .def("indexBBox", &GridMetaData::indexBBox, nb::rv_policy::reference_internal, - "Index-space bounding box of the wrapped grid's active voxels.") - .def("voxelSize", &GridMetaData::voxelSize, - "World-space voxel size of the wrapped grid.") - .def("blindDataCount", &GridMetaData::blindDataCount, - "Number of blind-data channels attached to the wrapped grid.") - .def("activeVoxelCount", &GridMetaData::activeVoxelCount, - "Total active voxel count of the wrapped grid.") - .def("activeTileCount", &GridMetaData::activeTileCount, "level"_a, - "Number of active tiles at the given tree level (1=lower, 2=upper, 3=root).") - .def("nodeCount", &GridMetaData::nodeCount, "level"_a, - "Number of nodes at the given tree level (0=leaf, 1=lower, 2=upper).") - .def("checksum", &GridMetaData::checksum, nb::rv_policy::reference_internal, - "Checksum stored in the wrapped grid's header.") - .def("rootTableSize", &GridMetaData::rootTableSize, - "Number of entries in the wrapped grid's root tile table.") - .def("isEmpty", &GridMetaData::isEmpty, - "True iff the wrapped grid has no active voxels.") - .def("version", &GridMetaData::version, - "NanoVDB Version stored in the wrapped grid's header."); + nb::class_, GridData>(m, name) + .def("version", &NanoGrid::version) + .def("memUsage", &NanoGrid::memUsage) + .def("gridSize", &NanoGrid::gridSize) + .def("gridIndex", &NanoGrid::gridIndex) + .def("gridCount", &NanoGrid::gridCount) + .def("getAccessor", &NanoGrid::getAccessor) + .def("voxelSize", &NanoGrid::voxelSize) + .def("map", &NanoGrid::map) + .def("worldBBox", &NanoGrid::worldBBox) + .def("indexBBox", &NanoGrid::indexBBox) + .def("activeVoxelCount", &NanoGrid::activeVoxelCount) + .def("isValid", &NanoGrid::isValid) + .def("gridType", &NanoGrid::gridType) + .def("gridClass", &NanoGrid::gridClass) + .def("isLevelSet", &NanoGrid::isLevelSet) + .def("isFogVolume", &NanoGrid::isFogVolume) + .def("isStaggered", &NanoGrid::isStaggered) + .def("isPointIndex", &NanoGrid::isPointIndex) + .def("isGridIndex", &NanoGrid::isGridIndex) + .def("isPointData", &NanoGrid::isPointData) + .def("isMask", &NanoGrid::isMask) + .def("isUnknown", &NanoGrid::isUnknown) + .def("hasMinMax", &NanoGrid::hasMinMax) + .def("hasBBox", &NanoGrid::hasBBox) + .def("hasLongGridName", &NanoGrid::hasLongGridName) + .def("hasAverage", &NanoGrid::hasAverage) + .def("hasStdDeviation", &NanoGrid::hasStdDeviation) + .def("isBreadthFirst", &NanoGrid::isBreadthFirst) + // .def("isLexicographic", &NanoGrid::isLexicographic) + .def("isSequential", [](const NanoGrid& grid) { return grid.isSequential(); }) + .def("gridName", &NanoGrid::gridName) + .def("shortGridName", &NanoGrid::shortGridName) + .def("checksum", &NanoGrid::checksum) + .def("isEmpty", &NanoGrid::isEmpty); } template nb::class_> defineAccessor(nb::module_& m, const char* name) { - // Use the decoded value type (nanovdb::BuildToValueMap::Type) - // rather than DefaultReadAccessor::ValueType. For ordinary types - // (float/double/Int*/Vec*) the two are identical, but for Half / Fp* the - // accessor decodes to float on read, for ValueIndex/OnIndex it returns - // uint64, and for ValueMask / bool it returns bool. The probeValue out- - // parameter and the Python return type both want the decoded form. - using ValueType = typename nanovdb::BuildToValueMap::Type; + using ValueType = typename DefaultReadAccessor::ValueType; using CoordType = typename DefaultReadAccessor::CoordType; - nb::class_> accessor(m, name, - "Read accessor that caches the most recently visited tree path for " - "fast neighbor lookups. Construct from a typed grid."); - accessor.def(nb::init&>(), "grid"_a, - "Construct an accessor bound to the given grid.") - .def("getValue", nb::overload_cast(&DefaultReadAccessor::getValue, nb::const_), "ijk"_a, - "Return the grid's value at the integer Coord ijk.") - .def("getValue", nb::overload_cast(&DefaultReadAccessor::getValue, nb::const_), "i"_a, "j"_a, "k"_a, - "Return the grid's value at the integer voxel (i, j, k).") + nb::class_> accessor(m, name); + accessor.def(nb::init&>(), "grid"_a) + .def("getValue", nb::overload_cast(&DefaultReadAccessor::getValue, nb::const_), "ijk"_a) + .def("getValue", nb::overload_cast(&DefaultReadAccessor::getValue, nb::const_), "i"_a, "j"_a, "k"_a) .def( - "__call__", [](const DefaultReadAccessor& accessor, const CoordType& ijk) { return accessor.getValue(ijk); }, nb::is_operator(), "ijk"_a, - "Operator form of getValue(ijk).") + "__call__", [](const DefaultReadAccessor& accessor, const CoordType& ijk) { return accessor.getValue(ijk); }, nb::is_operator(), "ijk"_a) .def( "__call__", [](const DefaultReadAccessor& accessor, int i, int j, int k) { return accessor.getValue(i, j, k); }, nb::is_operator(), "i"_a, "j"_a, - "k"_a, - "Operator form of getValue(i, j, k).") - .def("isActive", &DefaultReadAccessor::isActive, "ijk"_a, - "True iff the voxel at ijk is active.") + "k"_a) + .def("isActive", &DefaultReadAccessor::isActive, "ijk"_a) .def( "probeValue", [](const DefaultReadAccessor& accessor, const CoordType& ijk) { @@ -962,8 +239,7 @@ template nb::class_> defineAccessor bool isOn = accessor.probeValue(ijk, v); return std::make_tuple(v, isOn); }, - "ijk"_a, - "Return (value, isActive) for the voxel at ijk in a single tree traversal."); + "ijk"_a); return accessor; } @@ -973,8 +249,7 @@ template void defineScalarAccessor(nb::module_& m, const char* using CoordType = typename DefaultReadAccessor::CoordType; defineAccessor(m, name) - .def("getNodeInfo", &DefaultReadAccessor::getNodeInfo, "ijk"_a, - "Return a NodeInfo describing the deepest tree node covering ijk.") + .def("getNodeInfo", &DefaultReadAccessor::getNodeInfo, "ijk"_a) .def( "setVoxel", [](DefaultReadAccessor& accessor, const CoordType& ijk, const ValueType& v) { @@ -982,8 +257,7 @@ template void defineScalarAccessor(nb::module_& m, const char* accessor.template set(ijk, v); }, "ijk"_a, - "v"_a, - "Set the value at ijk to v and mark the voxel active."); + "v"_a); } template void defineVectorAccessor(nb::module_& m, const char* name) @@ -998,29 +272,19 @@ template void defineVectorAccessor(nb::module_& m, const char* accessor.template set(ijk, v); }, "ijk"_a, - "v"_a, - "Set the vector value at ijk to v and mark the voxel active."); + "v"_a); } template void defineNodeInfo(nb::module_& m, const char* name) { - nb::class_::NodeInfo>(m, name, - "Descriptor of the deepest tree node covering a queried voxel. " - "Returned by ReadAccessor.getNodeInfo().") - .def_ro("level", &GetNodeInfo::NodeInfo::level, - "Tree level of the node (0=leaf, 1=lower, 2=upper, 3=root).") - .def_ro("dim", &GetNodeInfo::NodeInfo::dim, - "Side length of the node's covered region in voxels.") - .def_ro("minimum", &GetNodeInfo::NodeInfo::minimum, - "Minimum active value within this node.") - .def_ro("maximum", &GetNodeInfo::NodeInfo::maximum, - "Maximum active value within this node.") - .def_ro("average", &GetNodeInfo::NodeInfo::average, - "Average of active values within this node.") - .def_ro("stdDevi", &GetNodeInfo::NodeInfo::stdDevi, - "Standard deviation of active values within this node.") - .def_ro("bbox", &GetNodeInfo::NodeInfo::bbox, - "Index-space bounding box of this node's covered region."); + nb::class_::NodeInfo>(m, name) + .def_ro("level", &GetNodeInfo::NodeInfo::level) + .def_ro("dim", &GetNodeInfo::NodeInfo::dim) + .def_ro("minimum", &GetNodeInfo::NodeInfo::minimum) + .def_ro("maximum", &GetNodeInfo::NodeInfo::maximum) + .def_ro("average", &GetNodeInfo::NodeInfo::average) + .def_ro("stdDevi", &GetNodeInfo::NodeInfo::stdDevi) + .def_ro("bbox", &GetNodeInfo::NodeInfo::bbox); } bool isCudaAvailable() @@ -1053,9 +317,7 @@ NB_MODULE(nanovdb, m) m.def("isCudaAvailable", &isCudaAvailable, "Returns whether or not the module was compiled with CUDA support"); m.def("isGpuAvailable", &isGpuAvailable, "Returns whether a CUDA-capable GPU is available at runtime"); - nb::enum_(m, "GridType", - "Enumerator naming every BuildT a NanoVDB grid can carry. Used by " - "handle.gridType(n) and as the dtype field on GridBlindMetaData.") + nb::enum_(m, "GridType") .value("Unknown", GridType::Unknown) .value("Float", GridType::Float) .value("Double", GridType::Double) @@ -1080,19 +342,15 @@ NB_MODULE(nanovdb, m) .value("PointIndex", GridType::PointIndex) .value("Vec3u8", GridType::Vec3u8) .value("Vec3u16", GridType::Vec3u16) - .value("UInt8", GridType::UInt8) .value("End", GridType::End) - .export_values() - .def("__repr__", [](const GridType& gridType) { - char str[strlen()]; - toStr(str, gridType); - return std::string(str); - }); + .export_values(); + // .def("__repr__", [](const GridType& gridType) { + // char str[strlen()]; + // toStr(str, gridType); + // return std::string(str); + // }); - nb::enum_(m, "GridClass", - "Semantic class of a grid (LevelSet, FogVolume, PointIndex, ...). " - "Independent of GridType / BuildT; consumed by tools that special-" - "case sign / point / index grids.") + nb::enum_(m, "GridClass") .value("Unknown", GridClass::Unknown) .value("LevelSet", GridClass::LevelSet) .value("FogVolume", GridClass::FogVolume) @@ -1104,12 +362,12 @@ NB_MODULE(nanovdb, m) .value("IndexGrid", GridClass::IndexGrid) .value("TensorGrid", GridClass::TensorGrid) .value("End", GridClass::End) - .export_values() - .def("__repr__", [](const GridClass& gridClass) { - char str[strlen()]; - toStr(str, gridClass); - return std::string(str); - }); + .export_values(); + // .def("__repr__", [](const GridClass& gridClass) { + // char str[strlen()]; + // toStr(str, gridClass); + // return std::string(str); + // }); defineVersion(m); @@ -1125,98 +383,35 @@ NB_MODULE(nanovdb, m) defineMap(m); - // CheckMode + Checksum must be bound before defineGrid() because - // Grid.checksum() returns Checksum by value. - defineCheckMode(m); - defineChecksum(m); + defineGridData(m); - // GridBlindData enums + GridBlindMetaData class — must be bound before - // defineGrid() because Grid.blindMetaData()/findBlindDataForSemantic() - // use them in their signatures. - defineGridBlindData(m); - defineGrid(m); - defineGridMetaData(m); + defineGrid(m, "FloatGrid"); + defineScalarAccessor(m, "FloatReadAccessor"); + defineNodeInfo(m, "FloatNodeInfo"); - // Tree / node bindings must come BEFORE defineNanoGrid because - // NanoGrid.tree() returns NanoTree (registered here) by const - // reference. Per-BuildT, register Leaf, Lower, Upper, Root, Tree in - // child->parent order so each return type is registered before the - // method binding that returns it. -#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \ - defineNanoLeaf(m, #Suffix "Leaf"); \ - defineNanoLower(m, #Suffix "Lower"); \ - defineNanoUpper(m, #Suffix "Upper"); \ - defineNanoRoot(m, #Suffix "Root"); \ - defineNanoTree(m, #Suffix "Tree"); \ - defineNodeManager(m, #Suffix "NodeManager"); -#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \ - defineNanoLeaf(m, #Suffix "Leaf"); \ - defineNanoLower(m, #Suffix "Lower"); \ - defineNanoUpper(m, #Suffix "Upper"); \ - defineNanoRoot(m, #Suffix "Root"); \ - defineNanoTree(m, #Suffix "Tree"); \ - defineNodeManager(m, #Suffix "NodeManager"); -#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \ - defineNanoLeaf(m, #Suffix "Leaf"); \ - defineNanoLower(m, #Suffix "Lower"); \ - defineNanoUpper(m, #Suffix "Upper"); \ - defineNanoRoot(m, #Suffix "Root"); \ - defineNanoTree(m, #Suffix "Tree"); \ - defineNodeManager(m, #Suffix "NodeManager"); -#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \ - defineNanoLeaf(m, #Suffix "Leaf"); \ - defineNanoLower(m, #Suffix "Lower"); \ - defineNanoUpper(m, #Suffix "Upper"); \ - defineNanoRoot(m, #Suffix "Root"); \ - defineNanoTree(m, #Suffix "Tree"); \ - defineNodeManager(m, #Suffix "NodeManager"); -#include "BuildTypes.def" + defineGrid(m, "DoubleGrid"); + defineScalarAccessor(m, "DoubleReadAccessor"); + defineNodeInfo(m, "DoubleNodeInfo"); - // Now bind the per-BuildT NanoGrid + accessors (tree() return type now - // registered above). -#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \ - defineNanoGrid(m, #Suffix "Grid"); \ - defineScalarAccessor(m, #Suffix "ReadAccessor"); \ - defineNodeInfo(m, #Suffix "NodeInfo"); -#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \ - defineNanoGrid(m, #Suffix "Grid"); \ - defineVectorAccessor(m, AccessorName); -#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \ - defineNanoGrid(m, #Suffix "Grid"); \ - defineAccessor(m, #Suffix "ReadAccessor"); -#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \ - defineNanoGrid(m, #Suffix "Grid"); \ - defineAccessor(m, #Suffix "ReadAccessor"); -#include "BuildTypes.def" + defineGrid(m, "Int32Grid"); + defineScalarAccessor(m, "Int32ReadAccessor"); + defineNodeInfo(m, "Int32NodeInfo"); - // Host-side NodeManagerHandle + module-scope createNodeManager. - defineNodeManagerHandle(m); - defineCreateNodeManager(m); + defineGrid(m, "Vec3fGrid"); + defineVectorAccessor(m, "Vec3fReadVectorAccessor"); - // PointAccessor variants — PointIndex grids carry uint32 indices, - // PointData grids carry Vec3f positions. - definePointAccessor(m, "PointIndexAccessor"); - definePointAccessor(m, "PointDataAccessor"); + defineGrid(m, "RGBA8Grid"); + defineVectorAccessor(m, "RGBA8ReadAccessor"); - // ChannelAccessor — the channel dtypes match the - // source BuildTs accepted by tools.createNanoGridIndex / OnIndex. - defineChannelAccessor(m, "IndexFloatChannelAccessor"); - defineChannelAccessor(m, "IndexDoubleChannelAccessor"); - defineChannelAccessor(m, "IndexInt32ChannelAccessor"); - defineChannelAccessor(m, "IndexVec3fChannelAccessor"); - defineChannelAccessor(m, "OnIndexFloatChannelAccessor"); - defineChannelAccessor(m, "OnIndexDoubleChannelAccessor"); - defineChannelAccessor(m, "OnIndexInt32ChannelAccessor"); - defineChannelAccessor(m, "OnIndexVec3fChannelAccessor"); - m.def("createChannelAccessor", &createChannelAccessorImpl, - "grid"_a, "channelID"_a = 0u, nb::keep_alive<0, 1>(), - "Return a typed ChannelAccessor over the channelID-th blind-data " - "channel of an IndexGrid or OnIndexGrid, dispatching on the " - "channel's recorded dataType. The accessor keeps the grid alive."); + defineGrid(m, "PointGrid"); + defineAccessor(m, "PointReadAccessor"); defineHostBuffer(m); defineHostGridHandle(m); + defineCheckMode(m); + defineChecksum(m); + #ifdef NANOVDB_USE_CUDA defineDeviceBuffer(m); defineDeviceGridHandle(m); @@ -1225,8 +420,6 @@ NB_MODULE(nanovdb, m) nb::module_ toolsModule = m.def_submodule("tools"); toolsModule.doc() = "A submodule that implements tools for NanoVDB grids"; defineToolsModule(toolsModule); - defineVoxelBlockManagerModule(toolsModule); - defineBuildGridModule(toolsModule); nb::module_ ioModule = m.def_submodule("io"); ioModule.doc() = "A submodule that implements I/O functionality for NanoVDB grids"; diff --git a/nanovdb/nanovdb/python/PyBuildGrid.cc b/nanovdb/nanovdb/python/PyBuildGrid.cc deleted file mode 100644 index 90f98457b2..0000000000 --- a/nanovdb/nanovdb/python/PyBuildGrid.cc +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: Apache-2.0 -#include "PyBuildGrid.h" - -#include -#include - -#include -#include -#include -#include - -#include -#include - -namespace nb = nanobind; -using namespace nb::literals; -using namespace nanovdb; - -namespace pynanovdb { - -// Bind nanovdb::tools::build::Grid together with its -// ValueAccessor and Tree::WriteAccessor proxies, plus a -// .toNanoVDB() shortcut that bakes the build grid into a host NanoGrid -// via tools::createNanoGrid. -// -// One instantiation per writable BuildT in BuildTypes.def (scalars + -// vectors). All three classes live under the nanovdb.tools.build submodule; -// the Python class names mirror the existing typed-grid naming so -// nanovdb.tools.build.FloatGrid is the mutable counterpart of the read-only -// nanovdb.FloatGrid. -template -static void defineBuildGrid(nb::module_& m, - const char* gridName, - const char* valueAccName, - const char* writeAccName) -{ - using GridT = tools::build::Grid; - using TreeT = tools::build::Tree; - using AccT = tools::build::ValueAccessor; - using WriteAccT = typename TreeT::WriteAccessor; - using ValueT = typename GridT::ValueType; - - // ----- build::Grid ----- - nb::class_(m, gridName, - "Mutable host-side grid builder for this BuildT. Write voxels via " - "setValue / getAccessor / getWriteAccessor, then call toNanoVDB() " - "to bake a frozen NanoGrid GridHandle.") - .def(nb::init(), - "background"_a, - "name"_a = std::string(""), - "gridClass"_a = GridClass::Unknown, - "Construct an empty mutable build grid. Voxels read as " - "background until written.") - .def("getValue", - [](const GridT& self, const Coord& ijk) -> ValueT { - return self.getValue(ijk); - }, - "ijk"_a, - "Return the value at ijk (background if no leaf covers it).") - .def("setValue", - [](GridT& self, const Coord& ijk, const ValueT& value) { - self.setValue(ijk, value); - }, - "ijk"_a, "value"_a, - "Set the voxel value at ijk and mark the voxel active.") - // build::Grid has no top-level isActive(ijk); the read path is via - // ValueAccessor. Spin up a fresh accessor for the single query so - // Python callers don't have to. - .def("isActive", - [](GridT& self, const Coord& ijk) { - AccT acc = self.getAccessor(); - return acc.isActive(ijk); - }, - "ijk"_a, - "Return True iff ijk is in an active voxel. Equivalent to " - "self.getAccessor().isActive(ijk), but allocates a fresh " - "accessor for each call — for repeated queries use " - "self.getAccessor() and reuse it.") - .def("setValueOn", - [](GridT& self, const Coord& ijk) { - AccT acc = self.getAccessor(); - acc.setValueOn(ijk); - }, - "ijk"_a, - "Mark ijk active without changing the stored value. Equivalent " - "to self.getAccessor().setValueOn(ijk).") - .def("nodeCount", - [](const GridT& self) -> std::array { - return self.nodeCount(); - }, - "Return a 3-tuple (leaf_count, lower_count, upper_count).") - .def("gridType", &GridT::gridType, - "Return the GridType enumerator this BuildT carries.") - .def("gridClass", &GridT::gridClass, - "Return the GridClass assigned at construction time.") - .def("getName", &GridT::getName, - "Return the grid name (as passed at construction).") - .def("setName", &GridT::setName, "name"_a, - "Replace the grid name.") - .def("setTransform", &GridT::setTransform, - "scale"_a = 1.0, - "translation"_a = Vec3d(0.0), - "Set an affine index-to-world map from a uniform scale and " - "translation. Replaces any prior transform.") - .def_prop_ro("background", - [](const GridT& self) -> ValueT { - return self.mRoot.background(); - }, - "The background value supplied at construction.") - .def("getAccessor", &GridT::getAccessor, - nb::keep_alive<0, 1>(), - "Return a ValueAccessor wired to this grid's root. Thread-safe " - "for reads, NOT thread-safe for writes. The accessor borrows " - "from this grid — the grid must outlive it.") - // WriteAccessor's defaulted move constructor would leave its - // internal ValueAccessor's `mRoot&` reference dangling (it points - // into the moved-from WriteAccessor's own mRoot field). Bypass - // the move path entirely by heap-allocating and handing nanobind - // ownership — the C++ object stays at a stable address for its - // whole lifetime. - .def("getWriteAccessor", - [](GridT& self) -> WriteAccT* { - return new WriteAccT(self.mRoot, self.mMutex); - }, - nb::rv_policy::take_ownership, - nb::keep_alive<0, 1>(), - "Return a WriteAccessor for thread-safe writes; the accessor " - "buffers changes into a private root and merges them into the " - "parent grid on destruction (or on an explicit merge() call). " - "Held by nanobind on the heap so the accessor's internal " - "references stay valid.") - // Baking a large grid is the most expensive operation on this - // class; release the GIL so other Python threads can run during - // the conversion (the lambda only touches C++ state). - .def("toNanoVDB", - [](const GridT& self, - tools::StatsMode sMode, - CheckMode cMode, - int verbose) { - return tools::createNanoGrid( - self, sMode, cMode, verbose); - }, - nb::call_guard(), - "sMode"_a = tools::StatsMode::Default, - "cMode"_a = CheckMode::Default, - "verbose"_a = 0, - "Bake this mutable grid into a host NanoGrid and " - "return its GridHandle. The build grid is unchanged. " - "Releases the GIL during conversion."); - - // ----- build::ValueAccessor ----- - // - // Move-only (copy is deleted) — returned by getAccessor(). Caches the - // last leaf / lower / upper node it touched, so repeated access to - // neighboring coordinates is fast. - nb::class_(m, valueAccName, - "Move-only read/write accessor returned by build.Grid.getAccessor(). " - "Caches the most recently visited tree path for fast neighbor access.") - .def("getValue", - [](const AccT& self, const Coord& ijk) -> ValueT { - return self.getValue(ijk); - }, - "ijk"_a, "Return the value at ijk (uses the cache).") - .def("setValue", - [](AccT& self, const Coord& ijk, const ValueT& value) { - self.setValue(ijk, value); - }, - "ijk"_a, "value"_a, - "Set the value at ijk and mark it active (uses the cache).") - .def("setValueOn", - [](AccT& self, const Coord& ijk) { self.setValueOn(ijk); }, - "ijk"_a, - "Mark ijk active without changing the stored value.") - .def("isActive", - [](const AccT& self, const Coord& ijk) { - return self.isActive(ijk); - }, - "ijk"_a, - "Return True iff ijk is in an active voxel.") - .def("isValueOn", - [](const AccT& self, const Coord& ijk) { - return self.isValueOn(ijk); - }, - "ijk"_a, - "Alias for isActive(ijk)."); - - // ----- build::Tree::WriteAccessor ----- - // - // Move-only. Holds its own root node + a reference to the parent root's - // mutex; on destruction (or explicit merge()) it locks the mutex and - // splices its buffered nodes into the parent. Designed for multi-thread - // writes — one WriteAccessor per thread, no shared mutable state. - nb::class_(m, writeAccName, - "Thread-safe write accessor returned by build.Grid.getWriteAccessor(). " - "Buffers writes into a private root and merges them into the parent " - "grid on destruction or explicit merge().") - .def("setValue", - [](WriteAccT& self, const Coord& ijk, const ValueT& value) { - self.setValue(ijk, value); - }, - "ijk"_a, "value"_a, - "Buffer a set into this accessor's private root.") - .def("setValueOn", - [](WriteAccT& self, const Coord& ijk) { self.setValueOn(ijk); }, - "ijk"_a, - "Buffer an active-state set into this accessor's private root.") - .def("merge", &WriteAccT::merge, - "Lock the parent mutex and splice this accessor's buffered " - "nodes into the parent grid. Called automatically when the " - "accessor is destroyed; calling it explicitly is only " - "necessary if you want the changes visible to the parent " - "before the accessor goes out of scope."); -} - -void defineBuildGridModule(nb::module_& toolsModule) -{ - nb::module_ buildModule = toolsModule.def_submodule("build"); - buildModule.doc() = - "Mutable, voxel-by-voxel CPU grid builder mirroring " - "nanovdb::tools::build::*. Construct a typed Grid (e.g. " - "FloatGrid(0.0, 'mygrid')), populate it with setValue / " - "ValueAccessor / WriteAccessor, then call .toNanoVDB() to bake " - "a host NanoGrid handle."; - - // X-macro instantiation over every writable BuildT: scalars (full - // arithmetic) and vectors. Read-only special BuildTs (Boolean, Fp*, - // Index, Mask) and Point are deliberately excluded — they have no - // SetValue specialization and can't be built voxel-by-voxel. -#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \ - defineBuildGrid(buildModule, #Suffix "Grid", \ - #Suffix "ValueAccessor", #Suffix "WriteAccessor"); -#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \ - defineBuildGrid(buildModule, #Suffix "Grid", \ - #Suffix "ValueAccessor", #Suffix "WriteAccessor"); -#include "BuildTypes.def" -} - -} // namespace pynanovdb diff --git a/nanovdb/nanovdb/python/PyBuildGrid.h b/nanovdb/nanovdb/python/PyBuildGrid.h deleted file mode 100644 index 71547887ab..0000000000 --- a/nanovdb/nanovdb/python/PyBuildGrid.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: Apache-2.0 -#ifndef NANOVDB_PYBUILDGRID_HAS_BEEN_INCLUDED -#define NANOVDB_PYBUILDGRID_HAS_BEEN_INCLUDED - -#include - -namespace nb = nanobind; - -namespace pynanovdb { - -/// @brief Register the nanovdb.tools.build submodule and its per-BuildT -/// Grid / ValueAccessor / WriteAccessor classes (one set per writable -/// scalar and vector BuildT in BuildTypes.def). Constructs the submodule -/// as `toolsModule.def_submodule("build")`. -void defineBuildGridModule(nb::module_& toolsModule); - -} // namespace pynanovdb - -#endif diff --git a/nanovdb/nanovdb/python/PyCreateNanoGrid.cc b/nanovdb/nanovdb/python/PyCreateNanoGrid.cc index bbd0b9a9ce..eed931766c 100644 --- a/nanovdb/nanovdb/python/PyCreateNanoGrid.cc +++ b/nanovdb/nanovdb/python/PyCreateNanoGrid.cc @@ -11,9 +11,6 @@ #include #include -#include -#include - namespace nb = nanobind; using namespace nb::literals; using namespace nanovdb; @@ -38,561 +35,20 @@ GridHandle createNanoGridFromFunc(const BuildT& template void defineCreateNanoGrid(nb::module_& m, const char* name) { - m.def(name, &createNanoGridFromFunc, nb::call_guard(), "background"_a, "name"_a, "gridClass"_a, "func"_a, "bbox"_a, - "Construct a NanoGrid by evaluating func(Coord) over every voxel in bbox. " - "Returns a GridHandle owning the freshly-built grid."); + m.def(name, &createNanoGridFromFunc, nb::call_guard(), "background"_a, "name"_a, "gridClass"_a, "func"_a, "bbox"_a); } template void defineOpenToNanoVDB(nb::module_& m) { #ifdef NANOVDB_USE_OPENVDB - m.def("openToNanoVDB", &tools::openToNanoVDB, - "base"_a, - "sMode"_a = tools::StatsMode::Default, - "cMode"_a = CheckMode::Default, - "verbose"_a = 0, - "Convert an OpenVDB base grid to a NanoVDB GridHandle."); + m.def("openToNanoVDB", &tools::openToNanoVDB, "base"_a, "sMode"_a = tools::StatsMode::Default, "cMode"_a = CheckMode::Default, "verbose"_a = 0); #endif } -// ============================================================================ -// Conversion bindings: AbsDiff/RelDiff oracle classes, and the polymorphic -// createNanoGrid free functions for quantized + index destination BuildTs. -// Each accepts source = NanoGrid OR build::Grid. -// ============================================================================ - -namespace { - -// ----- Quantized (Fp4/Fp8/Fp16) ----- -// -// C++ signature: createNanoGrid(srcGrid, -// sMode, cMode, ditherOn, verbose, buffer). -// -// Try SrcBuildT against both NanoGrid and build::Grid -// and return an empty nb::object on no match so the caller can fall through -// to the next SrcBuildT. -// -// GIL is held for the isinstance / cast dispatch (which touches the Python -// object's type and reference graph) but released around the underlying -// tools::createNanoGrid traversal — the source data lives in stable C++ -// storage whose lifetime is anchored by the Python wrapper passed in via -// py_src, so it's safe to read without holding the GIL. -template -nb::object tryQuantizeFpX(nb::handle py_src, - tools::StatsMode sMode, - CheckMode cMode, - bool ditherOn, - int verbose) -{ - using NanoSrcT = NanoGrid; - using BuildSrcT = tools::build::Grid; - if (nb::isinstance(py_src)) { - const auto& src = nb::cast(py_src); - GridHandle handle; - { - nb::gil_scoped_release release; - handle = tools::createNanoGrid( - src, sMode, cMode, ditherOn, verbose); - } - return nb::cast(std::move(handle)); - } - if (nb::isinstance(py_src)) { - const auto& src = nb::cast(py_src); - GridHandle handle; - { - nb::gil_scoped_release release; - handle = tools::createNanoGrid( - src, sMode, cMode, ditherOn, verbose); - } - return nb::cast(std::move(handle)); - } - // Invalid (not None) object signals "SrcBuildT didn't match"; the caller - // tests is_valid() and falls through to the next SrcBuildT. - return nb::object(); -} - -template -nb::object createNanoGridFpX(nb::handle py_src, - tools::StatsMode sMode, - CheckMode cMode, - bool ditherOn, - int verbose, - const char* pyFnName) -{ - // The C++ Fp{4,8,16,N} preProcess static_asserts SrcValueT == float; - // double sources hit a compile-time error, so we accept float only. - if (auto r = tryQuantizeFpX( - py_src, sMode, cMode, ditherOn, verbose); r.is_valid()) return r; - std::string msg(pyFnName); - msg += ": source must be a FloatGrid or nanovdb.tools.build.FloatGrid " - "(Fp4/Fp8/Fp16/FpN require a float source value type)."; - throw nb::type_error(msg.c_str()); -} - -// ----- FpN (variable bit-width) ----- -// -// C++ signature: createNanoGrid(srcGrid, -// sMode, cMode, ditherOn, verbose, oracle, buffer). OracleT is AbsDiff or -// RelDiff; the binding exposes both as separate Python overloads. -template -nb::object tryQuantizeFpN(nb::handle py_src, - tools::StatsMode sMode, - CheckMode cMode, - bool ditherOn, - int verbose, - const OracleT& oracle) -{ - using NanoSrcT = NanoGrid; - using BuildSrcT = tools::build::Grid; - // Same GIL pattern as tryQuantizeFpX: hold the GIL through the - // isinstance / cast dispatch, release it for the conversion. - if (nb::isinstance(py_src)) { - const auto& src = nb::cast(py_src); - GridHandle handle; - { - nb::gil_scoped_release release; - handle = tools::createNanoGrid( - src, sMode, cMode, ditherOn, verbose, oracle); - } - return nb::cast(std::move(handle)); - } - if (nb::isinstance(py_src)) { - const auto& src = nb::cast(py_src); - GridHandle handle; - { - nb::gil_scoped_release release; - handle = tools::createNanoGrid( - src, sMode, cMode, ditherOn, verbose, oracle); - } - return nb::cast(std::move(handle)); - } - // Invalid (not None) object signals "SrcBuildT didn't match"; the caller - // tests is_valid() and falls through to the next SrcBuildT. - return nb::object(); -} - -template -nb::object createNanoGridFpNImpl(nb::handle py_src, - const OracleT& oracle, - tools::StatsMode sMode, - CheckMode cMode, - bool ditherOn, - int verbose) -{ - if (auto r = tryQuantizeFpN( - py_src, sMode, cMode, ditherOn, verbose, oracle); r.is_valid()) return r; - throw nb::type_error( - "createNanoGridFpN: source must be a FloatGrid or " - "nanovdb.tools.build.FloatGrid (FpN requires a float source value type)."); -} - -// ----- Index / OnIndex ----- -// -// C++ signature: createNanoGrid(srcGrid, -// channels, includeStats, includeTiles, verbose, buffer). DstBuildT is -// ValueIndex or ValueOnIndex; the binding exposes both as separate -// named functions. Source set is wider than the quantized variants — -// any arithmetic or vector source can be re-cast as an index grid. -template -nb::object tryIndexify(nb::handle py_src, - uint32_t channels, - bool includeStats, - bool includeTiles, - int verbose) -{ - using NanoSrcT = NanoGrid; - using BuildSrcT = tools::build::Grid; - // Same GIL pattern as tryQuantizeFpX. - if (nb::isinstance(py_src)) { - const auto& src = nb::cast(py_src); - GridHandle handle; - { - nb::gil_scoped_release release; - handle = tools::createNanoGrid( - src, channels, includeStats, includeTiles, verbose); - } - return nb::cast(std::move(handle)); - } - if (nb::isinstance(py_src)) { - const auto& src = nb::cast(py_src); - GridHandle handle; - { - nb::gil_scoped_release release; - handle = tools::createNanoGrid( - src, channels, includeStats, includeTiles, verbose); - } - return nb::cast(std::move(handle)); - } - // Invalid (not None) object signals "SrcBuildT didn't match"; the caller - // tests is_valid() and falls through to the next SrcBuildT. - return nb::object(); -} - -template -nb::object createIndexImpl(nb::handle py_src, - uint32_t channels, - bool includeStats, - bool includeTiles, - int verbose, - const char* pyFnName) -{ - if (auto r = tryIndexify(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r; - if (auto r = tryIndexify(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r; - if (auto r = tryIndexify(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r; - if (auto r = tryIndexify(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r; - std::string msg(pyFnName); - msg += ": source must be a FloatGrid, DoubleGrid, Int32Grid, " - "Vec3fGrid, or the matching nanovdb.tools.build.* mutable grid."; - throw nb::type_error(msg.c_str()); -} - -// ----- tools.CreateNanoGrid: converter class with blind-data authoring ----- -// -// Mirrors nanovdb::tools::CreateNanoGrid. The C++ class is -// templated on the source grid type, so this binding stores the source -// Python object plus the recorded settings and addBlindData() calls, then -// dispatches over the supported SrcBuildTs when getHandle() is called — -// constructing the C++ converter, replaying the recorded state, and baking -// the handle. The destination BuildT is the source BuildT (the C++ -// default); quantized and index destinations remain on the -// createNanoGridFp* / createNanoGridIndex free functions above. Authored -// channels come back zeroed — fill them through the writable NumPy view -// returned by grid.getBlindData(n). - -// Byte size of one element of the given blind-data GridType, or 0 when the -// size cannot be derived (the caller must then pass it explicitly). Matches -// the per-type size table enforced by GridBlindMetaData::isValid(). -uint32_t blindDataTypeSize(GridType dataType) -{ - switch (dataType) { - case GridType::Float: return 4u; - case GridType::Double: return 8u; - case GridType::Int16: return 2u; - case GridType::Int32: return 4u; - case GridType::Int64: return 8u; - case GridType::UInt8: return 1u; - case GridType::UInt32: return 4u; - case GridType::Half: return 2u; - case GridType::RGBA8: return 4u; - case GridType::Fp8: return 1u; - case GridType::Fp16: return 2u; - case GridType::Vec3f: return 12u; - case GridType::Vec3d: return 24u; - case GridType::Vec4f: return 16u; - case GridType::Vec4d: return 32u; - case GridType::Vec3u8: return 3u; - case GridType::Vec3u16: return 6u; - default: return 0u; - } -} - -class PyCreateNanoGrid -{ - struct BlindDataSpec - { - std::string name; - GridBlindDataSemantic semantic; - GridBlindDataClass dataClass; - GridType dataType; - uint64_t count; - uint32_t size; - }; - -public: - explicit PyCreateNanoGrid(nb::object src) - : mSrc(std::move(src)) - { - if (!(matches() || matches() || - matches() || matches())) { - throw nb::type_error( - "CreateNanoGrid: source must be a FloatGrid, DoubleGrid, " - "Int32Grid, Vec3fGrid, or the matching " - "nanovdb.tools.build.* mutable grid."); - } - } - - // Validates eagerly (the C++ ctor only NANOVDB_ASSERTs, which release - // builds skip) so mistakes surface here rather than as an invalid grid. - uint64_t addBlindData(const std::string& name, - uint64_t count, - GridType dataType, - GridBlindDataSemantic semantic, - GridBlindDataClass dataClass, - uint32_t size) - { - if (name.size() >= GridBlindMetaData::MaxNameSize) { - throw nb::value_error( - "addBlindData: name exceeds the 255 character limit."); - } - if (size == 0u) size = blindDataTypeSize(dataType); - if (size == 0u) { - throw nb::value_error( - "addBlindData: the element size cannot be derived from this " - "dataType — pass size explicitly."); - } - const GridBlindMetaData meta(0, count, size, semantic, dataClass, dataType); - if (!meta.isValid()) { - throw nb::value_error( - "addBlindData: invalid combination of dataSemantic, " - "dataClass, dataType, and size."); - } - mBlind.push_back(BlindDataSpec{name, semantic, dataClass, dataType, count, size}); - return static_cast(mBlind.size() - 1); - } - - void setStats(tools::StatsMode mode) { mStats = mode; } - void setChecksum(CheckMode mode) { mChecksum = mode; } - void setVerbose(int mode) { mVerbose = mode; } - void enableDithering(bool on) { mDither = on; } - - nb::object getHandle() const - { - if (auto r = tryGetHandle(); r.is_valid()) return r; - if (auto r = tryGetHandle(); r.is_valid()) return r; - if (auto r = tryGetHandle(); r.is_valid()) return r; - if (auto r = tryGetHandle(); r.is_valid()) return r; - throw nb::type_error("CreateNanoGrid: unsupported source grid type."); - } - -private: - template bool matches() const - { - return nb::isinstance>(mSrc) || - nb::isinstance>(mSrc); - } - - template nb::object tryGetHandle() const - { - using NanoSrcT = NanoGrid; - using BuildSrcT = tools::build::Grid; - if (nb::isinstance(mSrc)) return this->bake(nb::cast(mSrc)); - if (nb::isinstance(mSrc)) return this->bake(nb::cast(mSrc)); - // Invalid (not None) object signals "SrcBuildT didn't match"; the caller - // tests is_valid() and falls through to the next SrcBuildT. - return nb::object(); - } - - // Same GIL pattern as tryQuantizeFpX: the dispatch above runs with the - // GIL held, the traversal runs without it (the source's lifetime is - // anchored by mSrc). - template nb::object bake(const SrcGridT& src) const - { - GridHandle handle; - { - nb::gil_scoped_release release; - tools::CreateNanoGrid converter(src); - converter.setStats(mStats); - converter.setChecksum(mChecksum); - converter.setVerbose(mVerbose); - converter.enableDithering(mDither); - for (const auto& b : mBlind) { - converter.addBlindData(b.name, b.semantic, b.dataClass, b.dataType, - static_cast(b.count), - static_cast(b.size)); - } - handle = converter.getHandle(); - // The C++ converter allocates authored channels without clearing - // them (C++ callers memcpy their payload in). Zero-fill here so - // the NumPy view starts deterministic. The authored channels are - // the first mBlind.size() blind-data entries — any converter- - // added channel (e.g. a long grid name) is appended after them. - if (!mBlind.empty()) { - if (auto* dst = const_cast(handle.gridData())) { - for (size_t i = 0; i < mBlind.size(); ++i) { - const GridBlindMetaData* meta = dst->blindMetaData(uint32_t(i)); - std::memset(const_cast(meta->blindData()), 0, - meta->blindDataSize()); - } - } - } - } - return nb::cast(std::move(handle)); - } - - nb::object mSrc; - std::vector mBlind; - tools::StatsMode mStats = tools::StatsMode::Default; - CheckMode mChecksum = CheckMode::Default; - int mVerbose = 0; - bool mDither = false; -}; - -} // namespace - -void defineCreateNanoGridConversions(nb::module_& toolsModule) -{ - // ------ Oracle classes ------ - nb::class_(toolsModule, "AbsDiff", - "Compression oracle for FpN: accept the approximation when " - "|exact - approx| <= tolerance. A tolerance of -1.0 (the " - "default) means uninitialized; any non-negative value (including " - "0.0) is treated as initialized by the operator bool() check, " - "or the C++ create function can fill it in via init().") - .def(nb::init(), "tolerance"_a = -1.0f, - "Construct an AbsDiff oracle with the given absolute tolerance.") - .def("getTolerance", &tools::AbsDiff::getTolerance, - "Return the current absolute tolerance.") - .def("setTolerance", &tools::AbsDiff::setTolerance, "tolerance"_a, - "Replace the current absolute tolerance.") - .def("__bool__", - [](const tools::AbsDiff& self) { return bool(self); }, - "True iff the tolerance has been initialized (>= 0)."); - - nb::class_(toolsModule, "RelDiff", - "Compression oracle for FpN: accept the approximation when " - "|exact - approx| / max(|exact|, |approx|) <= tolerance.") - .def(nb::init(), "tolerance"_a = -1.0f, - "Construct a RelDiff oracle with the given relative tolerance.") - .def("getTolerance", &tools::RelDiff::getTolerance, - "Return the current relative tolerance.") - .def("setTolerance", &tools::RelDiff::setTolerance, "tolerance"_a, - "Replace the current relative tolerance.") - .def("__bool__", - [](const tools::RelDiff& self) { return bool(self); }, - "True iff the tolerance has been initialized (>= 0)."); - - // ------ Quantized fixed-width: Fp4 / Fp8 / Fp16 ------ - toolsModule.def("createNanoGridFp4", - [](nb::handle src, tools::StatsMode sMode, CheckMode cMode, - bool ditherOn, int verbose) { - return createNanoGridFpX(src, sMode, cMode, ditherOn, verbose, - "createNanoGridFp4"); - }, - "src"_a, "sMode"_a = tools::StatsMode::Default, - "cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0, - "Quantize a NanoGrid or nanovdb.tools.build.FloatGrid into a " - "NanoGrid (4 bits per voxel). ditherOn adds sub-quantum " - "noise to break up banding."); - - toolsModule.def("createNanoGridFp8", - [](nb::handle src, tools::StatsMode sMode, CheckMode cMode, - bool ditherOn, int verbose) { - return createNanoGridFpX(src, sMode, cMode, ditherOn, verbose, - "createNanoGridFp8"); - }, - "src"_a, "sMode"_a = tools::StatsMode::Default, - "cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0, - "Quantize a NanoGrid or nanovdb.tools.build.FloatGrid into a NanoGrid (8 bits " - "per voxel). ditherOn adds sub-quantum noise."); - - toolsModule.def("createNanoGridFp16", - [](nb::handle src, tools::StatsMode sMode, CheckMode cMode, - bool ditherOn, int verbose) { - return createNanoGridFpX(src, sMode, cMode, ditherOn, verbose, - "createNanoGridFp16"); - }, - "src"_a, "sMode"_a = tools::StatsMode::Default, - "cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0, - "Quantize a NanoGrid or nanovdb.tools.build.FloatGrid into a NanoGrid (16 bits " - "per voxel). ditherOn adds sub-quantum noise."); - - // ------ Variable bit-width: FpN with AbsDiff or RelDiff oracle ------ - // - // Two overloads — one per oracle type. Python dispatch picks the - // right one from the oracle argument's type. The createNanoGrid C++ - // template's parameter order is (src, sMode, cMode, ditherOn, verbose, - // oracle, buffer); we reorder for Python so oracle comes second - // (most callers want to specify it explicitly), then mode/dither - // parameters as kwargs with defaults. - toolsModule.def("createNanoGridFpN", - [](nb::handle src, const tools::AbsDiff& oracle, - tools::StatsMode sMode, CheckMode cMode, bool ditherOn, int verbose) { - return createNanoGridFpNImpl(src, oracle, sMode, cMode, ditherOn, verbose); - }, - "src"_a, "oracle"_a = tools::AbsDiff(), - "sMode"_a = tools::StatsMode::Default, - "cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0, - "Quantize a NanoGrid or nanovdb.tools.build.FloatGrid into a NanoGrid (variable " - "bits per voxel; each leaf picks the smallest N that satisfies " - "the oracle's tolerance). Pass an AbsDiff oracle for absolute " - "error bound, or use the RelDiff overload for relative error."); - - toolsModule.def("createNanoGridFpN", - [](nb::handle src, const tools::RelDiff& oracle, - tools::StatsMode sMode, CheckMode cMode, bool ditherOn, int verbose) { - return createNanoGridFpNImpl(src, oracle, sMode, cMode, ditherOn, verbose); - }, - "src"_a, "oracle"_a, - "sMode"_a = tools::StatsMode::Default, - "cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0, - "FpN overload accepting a RelDiff oracle for relative error."); - - // ------ Index / OnIndex ------ - // - // createNanoGridIndex / createNanoGridOnIndex are the canonical names - // for the broad-source-coverage index conversion bindings. A narrower - // createOnIndexGrid factory still lives in PyVoxelBlockManager.cc as - // the test scaffolding entry point used by the VBM unit tests. - toolsModule.def("createNanoGridIndex", - [](nb::handle src, uint32_t channels, bool includeStats, - bool includeTiles, int verbose) { - return createIndexImpl( - src, channels, includeStats, includeTiles, verbose, - "createNanoGridIndex"); - }, - "src"_a, "channels"_a = 0u, "includeStats"_a = true, - "includeTiles"_a = true, "verbose"_a = 0, - "Convert a source grid into a NanoGrid. Every voxel " - "(active or inactive) gets a unique uint64 sequential index, with " - "the original values stored as blind data when channels > 0."); - - toolsModule.def("createNanoGridOnIndex", - [](nb::handle src, uint32_t channels, bool includeStats, - bool includeTiles, int verbose) { - return createIndexImpl( - src, channels, includeStats, includeTiles, verbose, - "createNanoGridOnIndex"); - }, - "src"_a, "channels"_a = 0u, "includeStats"_a = true, - "includeTiles"_a = true, "verbose"_a = 0, - "Convert a source grid into a NanoGrid. Only the " - "active voxels get a sequential index — the canonical input to " - "buildVoxelBlockManager."); - - // ------ CreateNanoGrid converter class (blind-data authoring) ------ - nb::class_(toolsModule, "CreateNanoGrid", - "Reusable converter mirroring nanovdb::tools::CreateNanoGrid. " - "Construct from a NanoGrid or nanovdb.tools.build.* grid (float, " - "double, int32, or Vec3f), optionally declare blind-data channels " - "with addBlindData(), then bake a fresh NanoGrid of the same BuildT " - "with getHandle(). Authored channels come back zero-filled — write " - "their contents through the writable NumPy view returned by " - "grid.getBlindData(n). For quantized (Fp*) or index destinations " - "use the createNanoGridFp* / createNanoGridIndex functions instead.") - .def(nb::init(), "srcGrid"_a, - "Construct a converter reading from srcGrid (a NanoGrid or " - "nanovdb.tools.build.* grid of BuildT float, double, int32, or " - "Vec3f). The converter keeps srcGrid alive.") - .def("addBlindData", &PyCreateNanoGrid::addBlindData, - "name"_a, "count"_a, "dataType"_a = GridType::Float, - "dataSemantic"_a = GridBlindDataSemantic::Unknown, - "dataClass"_a = GridBlindDataClass::AttributeArray, - "size"_a = 0u, - "Declare a blind-data channel of count elements of dataType to " - "be allocated in the destination grid, and return its channel " - "index. size (bytes per element) is derived from dataType when " - "omitted; pass it explicitly for dataTypes without a fixed " - "element size. The C++ signature orders the parameters (name, " - "dataSemantic, dataClass, dataType, count, size) — reordered " - "here so the common case reads addBlindData(name, count).") - .def("setStats", &PyCreateNanoGrid::setStats, "mode"_a, - "Set the StatsMode used when baking the destination grid.") - .def("setChecksum", &PyCreateNanoGrid::setChecksum, "mode"_a, - "Set the CheckMode used when baking the destination grid.") - .def("setVerbose", &PyCreateNanoGrid::setVerbose, "mode"_a = 1, - "Set the verbosity level used when baking the destination grid.") - .def("enableDithering", &PyCreateNanoGrid::enableDithering, "on"_a = true, - "Toggle dithering of the destination grid (only meaningful for " - "quantized BuildTs; kept for parity with the C++ class).") - .def("getHandle", &PyCreateNanoGrid::getHandle, - "Bake and return a GridHandle owning a NanoGrid of the source's " - "BuildT, including any channels declared via addBlindData(). " - "Each call bakes a fresh grid."); -} - -#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \ - template void defineCreateNanoGrid(nb::module_&, const char*); -#include "BuildTypes.def" +template void defineCreateNanoGrid(nb::module_&, const char*); +template void defineCreateNanoGrid(nb::module_&, const char*); +template void defineCreateNanoGrid(nb::module_&, const char*); +template void defineCreateNanoGrid(nb::module_&, const char*); template void defineOpenToNanoVDB(nb::module_&); diff --git a/nanovdb/nanovdb/python/PyCreateNanoGrid.h b/nanovdb/nanovdb/python/PyCreateNanoGrid.h index e7abafbd90..3402ca67b8 100644 --- a/nanovdb/nanovdb/python/PyCreateNanoGrid.h +++ b/nanovdb/nanovdb/python/PyCreateNanoGrid.h @@ -15,21 +15,6 @@ template void defineCreateNanoGrid(nb::module_& m, const char* template void defineOpenToNanoVDB(nb::module_& m); #endif -/// @brief Bind the AbsDiff / RelDiff quantization oracle classes and the -/// polymorphic createNanoGridFp4 / Fp8 / Fp16 / FpN / Index / OnIndex -/// free functions on the nanovdb.tools submodule. Sources accepted -/// include both NanoGrid and tools::build::Grid: -/// the quantized createNanoGridFp* / FpN paths accept float only -/// (C++ Fp{4,8,16,N}::preProcess static-asserts SrcValueT == float); -/// the createNanoGridIndex / OnIndex paths accept float, double, -/// int32_t, and Vec3f sources. Additional source BuildTs can be -/// added by extending the explicit try-each-SrcBuildT chains in -/// createNanoGridFpX / FpNImpl / createIndexImpl. Also binds the -/// tools.CreateNanoGrid converter class, which mirrors the C++ -/// tools::CreateNanoGrid and adds blind-data authoring via -/// addBlindData() (same source BuildT set as the index paths). -void defineCreateNanoGridConversions(nb::module_& toolsModule); - } // namespace pynanovdb #endif diff --git a/nanovdb/nanovdb/python/PyGridChecksum.cc b/nanovdb/nanovdb/python/PyGridChecksum.cc index 113c7a3a18..281fd05c0b 100644 --- a/nanovdb/nanovdb/python/PyGridChecksum.cc +++ b/nanovdb/nanovdb/python/PyGridChecksum.cc @@ -14,10 +14,7 @@ namespace pynanovdb { void defineCheckMode(nb::module_& m) { - nb::enum_(m, "CheckMode", - "Selector controlling how aggressively a grid checksum is computed: " - "Disable skips checksumming, Partial covers only the header, Full " - "covers the whole grid, and Default picks the recommended mode.") + nb::enum_(m, "CheckMode") .value("Disable", CheckMode::Disable) .value("Partial", CheckMode::Partial) .value("Full", CheckMode::Full) @@ -27,65 +24,13 @@ void defineCheckMode(nb::module_& m) void defineChecksum(nb::module_& m) { - nb::class_(m, "Checksum", - "64-bit checksum value stored in a grid header. Produced by " - "tools.evalChecksum and compared against the stored one by " - "tools.validateChecksum.") - .def(nb::self == nb::self, "rhs"_a, - "Equality of two Checksum values.") - .def(nb::self != nb::self, "rhs"_a, - "Inequality of two Checksum values.") - .def("isEmpty", &Checksum::isEmpty, - "True iff no checksum is stored (checksumming was disabled).") - .def("isHalf", &Checksum::isHalf, - "True iff only the header portion (grid + tree + root) is checksummed.") - .def("isFull", &Checksum::isFull, - "True iff both the header portion and all nodes are checksummed.") - .def("mode", &Checksum::mode, - "CheckMode this checksum was computed with (Disable, Partial, or Full)."); + nb::class_(m, "Checksum").def(nb::self == nb::self, "rhs"_a).def(nb::self != nb::self, "rhs"_a); } void defineUpdateChecksum(nb::module_& m) { m.def( - "updateChecksum", [](GridData* gridData, CheckMode mode) { tools::updateChecksum(gridData, mode); }, "gridData"_a, "mode"_a, - "Recompute and store the checksum of gridData using the given CheckMode."); -} - -void defineEvalChecksumModule(nb::module_& toolsModule) -{ - // tools.evalChecksum(grid, mode) — compute a fresh checksum for the - // given grid without writing it back. Mirrors the GridData* overload - // in tools/GridChecksum.h; the polymorphism over BuildT is implicit - // because every NanoGrid is-a GridData in the C++ hierarchy (and - // the Python class binding declares NanoGrid as derived from - // GridData). - toolsModule.def("evalChecksum", - [](const GridData* gridData, CheckMode mode) -> Checksum { - if (gridData == nullptr) { - throw nb::value_error("evalChecksum: grid is None."); - } - return tools::evalChecksum(gridData, mode); - }, - "grid"_a, "mode"_a = CheckMode::Default, - nb::call_guard(), - "Compute and return the Checksum for the given grid using the " - "specified CheckMode. Does not modify the grid."); - - // tools.validateChecksum(grid, mode) — compare the stored checksum - // against a freshly computed one and return a bool. - toolsModule.def("validateChecksum", - [](const GridData* gridData, CheckMode mode) -> bool { - if (gridData == nullptr) { - throw nb::value_error("validateChecksum: grid is None."); - } - return tools::validateChecksum(gridData, mode); - }, - "grid"_a, "mode"_a = CheckMode::Default, - nb::call_guard(), - "Return True iff the grid's stored checksum matches a freshly " - "computed one for the given CheckMode. A grid with no stored " - "checksum (Checksum.isEmpty()) is considered valid."); + "updateChecksum", [](GridData* gridData, CheckMode mode) { tools::updateChecksum(gridData, mode); }, "gridData"_a, "mode"_a); } } // namespace pynanovdb diff --git a/nanovdb/nanovdb/python/PyGridChecksum.h b/nanovdb/nanovdb/python/PyGridChecksum.h index d3f0f2af39..dd988f871c 100644 --- a/nanovdb/nanovdb/python/PyGridChecksum.h +++ b/nanovdb/nanovdb/python/PyGridChecksum.h @@ -13,11 +13,6 @@ void defineCheckMode(nb::module_& m); void defineChecksum(nb::module_& m); void defineUpdateChecksum(nb::module_& m); -/// @brief Bind tools.evalChecksum and tools.validateChecksum on the -/// nanovdb.tools submodule. Both accept any bound NanoGrid via -/// GridData* upcast (handled by nanobind's class hierarchy). -void defineEvalChecksumModule(nb::module_& toolsModule); - } // namespace pynanovdb #endif diff --git a/nanovdb/nanovdb/python/PyGridHandle.cc b/nanovdb/nanovdb/python/PyGridHandle.cc index fdcdc8228d..efd4253337 100644 --- a/nanovdb/nanovdb/python/PyGridHandle.cc +++ b/nanovdb/nanovdb/python/PyGridHandle.cc @@ -22,7 +22,6 @@ void defineHostGridHandle(nb::module_& m) new (&handle) GridHandle(std::move(buffer)); }, "t"_a.noconvert()); - defineGridHandleUtilities(m); } } // namespace pynanovdb diff --git a/nanovdb/nanovdb/python/PyGridHandle.h b/nanovdb/nanovdb/python/PyGridHandle.h index fe377fb842..b102899365 100644 --- a/nanovdb/nanovdb/python/PyGridHandle.h +++ b/nanovdb/nanovdb/python/PyGridHandle.h @@ -7,189 +7,62 @@ #include #include -#include - -#include namespace nb = nanobind; namespace pynanovdb { -// Polymorphic host-side `handle.grid(n)`: dispatch on gridType(n) to the -// matching NanoGrid subclass currently bound in Python. Returns -// None when the underlying BuildT is not bound in this build — see the -// list of bound types in BuildTypes.def. The returned object is parented -// to the handle so the handle is kept alive at least as long as the grid. -template -inline nb::object pyHostGrid(nb::handle py_handle, uint32_t n) -{ - auto& handle = nb::cast&>(py_handle); - if (n >= handle.gridCount()) return nb::none(); - switch (handle.gridType(n)) { -#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \ - case nanovdb::GridType::GridTypeEnum: { \ - auto* grid = handle.template grid(n); \ - return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \ - : nb::none(); \ - } -#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \ - case nanovdb::GridType::GridTypeEnum: { \ - auto* grid = handle.template grid(n); \ - return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \ - : nb::none(); \ - } -#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \ - case nanovdb::GridType::GridTypeEnum: { \ - auto* grid = handle.template grid(n); \ - return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \ - : nb::none(); \ - } -#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \ - case nanovdb::GridType::GridTypeEnum: { \ - auto* grid = handle.template grid(n); \ - return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \ - : nb::none(); \ - } -#include "BuildTypes.def" - default: - return nb::none(); - } -} - -// Free functions splitGrids / mergeGrids exposed at module scope. Templated -// on BufferT so the same definitions work for both the host GridHandle and -// the device GridHandle bindings. -template void defineGridHandleUtilities(nb::module_& m) -{ - using HandleT = nanovdb::GridHandle; - m.def("splitGrids", [](const HandleT& handle) { - auto handles = nanovdb::splitGrids(handle); - nb::list out; - for (auto& h : handles) { - out.append(std::move(h)); - } - return out; - }, nb::arg("handle"), - "Split a multi-grid handle into a list of single-grid handles, " - "each owning a freshly-allocated buffer."); - // mergeGrids: collect non-owning pointers to each handle and forward to - // the borrowing nanovdb::mergeGrids overload. The vector-of-handles - // overload is unusable from a Python list because GridHandle is move-only - // — building the vector would move from (and silently empty) the caller's - // Python objects. - m.def("mergeGrids", [](nb::sequence handles) { - std::vector sources; - sources.reserve(nb::len(handles)); - for (nb::handle item : handles) { - sources.push_back(&nb::cast(item)); - } - return nanovdb::mergeGrids(sources.data(), sources.size()); - }, nb::arg("handles"), - "Combine a list of GridHandles into a single multi-grid GridHandle. " - "Input handles are read by const reference; the new handle owns a " - "freshly-allocated buffer and the inputs are left untouched."); -} - template nb::class_> defineGridHandle(nb::module_& m, const char* name) { - return nb::class_>(m, name, - "Owns a buffer holding one or more serialized NanoVDB grids. " - "Construct via nanovdb.tools.create* factories or nanovdb.io.readGrid(s); " - "access individual grids via handle.grid(n).") - .def(nb::init<>(), - "Construct an empty handle. Use the nanovdb.tools.create* " - "factories or nanovdb.io.readGrid(s) instead in normal use.") - .def("reset", &nanovdb::GridHandle::reset, - "Drop the underlying buffer; the handle becomes empty.") - .def("size", &nanovdb::GridHandle::bufferSize, - "Total byte size of the buffer backing this handle (sum of " - "every grid plus any internal padding).") - .def("isEmpty", &nanovdb::GridHandle::isEmpty, - "True iff the handle owns no buffer.") - .def("empty", &nanovdb::GridHandle::empty, - "Same as isEmpty(). Retained for parity with the C++ " - "GridHandle::empty() member.") + return nb::class_>(m, name) + .def(nb::init<>()) + .def("reset", &nanovdb::GridHandle::reset) + .def("size", &nanovdb::GridHandle::bufferSize) + .def("isEmpty", &nanovdb::GridHandle::isEmpty) + .def("empty", &nanovdb::GridHandle::empty) .def( - "__bool__", - [](const nanovdb::GridHandle& handle) { return !handle.empty(); }, - nb::is_operator(), - "True iff the handle owns a non-empty buffer (`not isEmpty()`).") - .def("copy", - [](const nanovdb::GridHandle& handle) { - return handle.template copy(); - }, - "Return a deep copy of this GridHandle backed by a freshly-allocated buffer.") - .def("grid", &pyHostGrid, nb::arg("n") = 0, - nb::keep_alive<0, 1>(), - "Return the n-th grid as a typed Grid subclass selected by " - "gridType(n), or None if the BuildT is not bound in Python. " - "The returned grid keeps this handle alive.") - .def("isPadded", &nanovdb::GridHandle::isPadded, - "True iff this handle's buffer is aligned past the natural " - "GridData alignment (used by the I/O code path).") - .def("gridCount", &nanovdb::GridHandle::gridCount, - "Number of grids stored in this handle.") - .def( - "gridSize", - [](const nanovdb::GridHandle& handle, uint32_t n) { - // GridHandle::gridSize(n) indexes mMetaData[n] unchecked, so an - // out-of-range n (including any n on an empty handle) is UB. - if (n >= handle.gridCount()) - throw nb::index_error("gridSize: grid index out of range [0, gridCount())."); - return handle.gridSize(n); - }, - nb::arg("n") = 0, - "Byte size of the n-th grid (without padding).") - .def( - "gridType", - [](const nanovdb::GridHandle& handle, uint32_t n) { - // GridHandle::gridType(n) indexes mMetaData[n] unchecked, so an - // out-of-range n (including any n on an empty handle) is UB. - if (n >= handle.gridCount()) - throw nb::index_error("gridType: grid index out of range [0, gridCount())."); - return handle.gridType(n); - }, - nb::arg("n") = 0, - "GridType enumerator of the n-th grid (e.g. GridType.Float). " - "Cheap to query — does not require materializing the grid.") + "__bool__", [](const nanovdb::GridHandle& handle) { handle.empty(); }, nb::is_operator()) + .def("floatGrid", nb::overload_cast(&nanovdb::GridHandle::template grid), nb::arg("n") = 0, nb::rv_policy::reference_internal) + .def("doubleGrid", + nb::overload_cast(&nanovdb::GridHandle::template grid), + nb::arg("n") = 0, + nb::rv_policy::reference_internal) + .def("int32Grid", + nb::overload_cast(&nanovdb::GridHandle::template grid), + nb::arg("n") = 0, + nb::rv_policy::reference_internal) + .def("vec3fGrid", + nb::overload_cast(&nanovdb::GridHandle::template grid), + nb::arg("n") = 0, + nb::rv_policy::reference_internal) + .def("rgba8Grid", + nb::overload_cast(&nanovdb::GridHandle::template grid), + nb::arg("n") = 0, + nb::rv_policy::reference_internal) + .def("isPadded", &nanovdb::GridHandle::isPadded) + .def("gridCount", &nanovdb::GridHandle::gridCount) + .def("gridSize", &nanovdb::GridHandle::gridSize, nb::arg("n") = 0) + .def("gridType", &nanovdb::GridHandle::gridType, nb::arg("n") = 0) .def( "gridData", - [](nanovdb::GridHandle& handle, uint32_t n) { - // gridData(n) returns nullptr for out-of-range n, but gridSize(n) - // below indexes mMetaData[n] unchecked — guard both here. - if (n >= handle.gridCount()) - throw nb::index_error("gridData: grid index out of range [0, gridCount())."); - return nb::bytes(handle.gridData(n), handle.gridSize(n)); - }, + [](nanovdb::GridHandle& handle, uint32_t n) { return nb::bytes(handle.gridData(n), handle.gridSize(n)); }, nb::arg("n") = 0, - nb::rv_policy::reference_internal, - "Raw byte contents of the n-th grid as a Python bytes object. " - "Useful for hashing or for handing off to non-NanoVDB tooling.") - .def("write", nb::overload_cast(&nanovdb::GridHandle::write, nb::const_), - nb::arg("fileName"), - "Write every grid in this handle to the given .nvdb file.") - .def("write", nb::overload_cast(&nanovdb::GridHandle::write, nb::const_), - nb::arg("fileName"), nb::arg("n"), - "Write just the n-th grid in this handle to the given .nvdb file.") + nb::rv_policy::reference_internal) + .def("write", nb::overload_cast(&nanovdb::GridHandle::write, nb::const_), nb::arg("fileName")) + .def("write", nb::overload_cast(&nanovdb::GridHandle::write, nb::const_), nb::arg("fileName"), nb::arg("n")) .def( - "read", [](nanovdb::GridHandle& handle, const std::string& fileName) { handle.read(fileName); }, - nb::arg("fileName"), - "Replace this handle's contents with every grid read from the " - "given .nvdb file.") + "read", [](nanovdb::GridHandle& handle, const std::string& fileName) { handle.read(fileName); }, nb::arg("fileName")) .def( "read", [](nanovdb::GridHandle& handle, const std::string& fileName, uint32_t n) { handle.read(fileName, n); }, nb::arg("fileName"), - nb::arg("n"), - "Replace this handle's contents with the n-th grid read from " - "the given .nvdb file.") + nb::arg("n")) .def( "read", [](nanovdb::GridHandle& handle, const std::string& fileName, const std::string& gridName) { handle.read(fileName, gridName); }, nb::arg("fileName"), - nb::arg("gridName"), - "Replace this handle's contents with the grid of the given " - "name read from the .nvdb file."); + nb::arg("gridName")); + } void defineHostGridHandle(nb::module_& m); diff --git a/nanovdb/nanovdb/python/PyGridStats.cc b/nanovdb/nanovdb/python/PyGridStats.cc index b528bac395..fbd8caec15 100644 --- a/nanovdb/nanovdb/python/PyGridStats.cc +++ b/nanovdb/nanovdb/python/PyGridStats.cc @@ -2,26 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 #include "PyGridStats.h" -#include - -#include #include -#include - namespace nb = nanobind; -using namespace nb::literals; using namespace nanovdb; namespace pynanovdb { void defineStatsMode(nb::module_& m) { - nb::enum_(m, "StatsMode", - "Selector controlling which per-node statistics are computed by " - "tools.updateGridStats: Disable skips stats, BBox refreshes only " - "bounding boxes, MinMax adds min/max, and All adds average and " - "standard deviation as well.") + nb::enum_(m, "StatsMode") .value("Disable", tools::StatsMode::Disable) .value("BBox", tools::StatsMode::BBox) .value("MinMax", tools::StatsMode::MinMax) @@ -30,201 +20,4 @@ void defineStatsMode(nb::module_& m) .value("End", tools::StatsMode::End); } -namespace { - -// ----- Extrema binding (rank 0 and rank 1 share the same surface) ----- -template -static void defineExtrema(nb::module_& m, const char* name) -{ - using ValueT = typename NanoGrid::ValueType; - using ExtremaT = tools::Extrema; - - nb::class_(m, name, - "Running minimum / maximum accumulator over a stream of values. " - "Build via repeated add(v) calls or via tools.getExtrema(grid, bbox).") - .def(nb::init<>(), - "Default-construct an Extrema with min = numeric_limits::max and " - "max = numeric_limits::lowest, so any subsequent .add(v) gives " - "exact min/max.") - .def("min", - [](const ExtremaT& self) -> ValueT { return self.min(); }, - "Return the minimum value observed so far.") - .def("max", - [](const ExtremaT& self) -> ValueT { return self.max(); }, - "Return the maximum value observed so far.") - .def("add", - [](ExtremaT& self, const ValueT& v) { self.add(v); }, - "value"_a, - "Update min/max with a single sample.") - .def("__bool__", - [](const ExtremaT& self) { return bool(self); }, - "True iff the Extrema has accumulated at least one sample " - "(i.e. min <= max).") - .def_static("hasMinMax", &ExtremaT::hasMinMax, - "True for value types where min/max is meaningful " - "(everything except bool).") - .def_static("hasAverage", &ExtremaT::hasAverage, - "Always False — Extrema does not compute averages; " - "use Stats for that.") - .def_static("hasStdDeviation", &ExtremaT::hasStdDeviation, - "Always False — Extrema does not compute standard " - "deviation; use Stats for that.") - .def_static("hasStats", &ExtremaT::hasStats, - "True iff the value type supports the min/max bookkeeping " - "(everything except bool)."); -} - -// ----- Stats binding (inherits Extrema) ----- -template -static void defineStats(nb::module_& m, const char* name) -{ - using ValueT = typename NanoGrid::ValueType; - using BaseT = tools::Extrema; - using StatsT = tools::Stats; - - nb::class_(m, name, - "Running min/max/mean/variance/std accumulator over a stream of " - "values. Extends Extrema with sample-count-weighted moments.") - .def(nb::init<>(), - "Default-construct a Stats accumulator with zero samples.") - .def("add", - [](StatsT& self, const ValueT& v) { self.add(v); }, - "value"_a, - "Add a single sample.") - .def("size", - [](const StatsT& self) -> size_t { return self.size(); }, - "Number of samples accumulated so far.") - .def("avg", - [](const StatsT& self) -> double { return self.avg(); }, - "Arithmetic mean of all samples.") - .def("mean", - [](const StatsT& self) -> double { return self.mean(); }, - "Alias for avg().") - .def("var", - [](const StatsT& self) -> double { return self.var(); }, - "Population variance (Sum(x-mean)^2 / N). Returns 0 if " - "fewer than two samples have been added.") - .def("variance", - [](const StatsT& self) -> double { return self.variance(); }, - "Alias for var().") - .def("std", - [](const StatsT& self) -> double { return self.std(); }, - "Standard deviation = sqrt(var()).") - .def("stdDev", - [](const StatsT& self) -> double { return self.stdDev(); }, - "Alias for std().") - .def_static("hasMinMax", &StatsT::hasMinMax, - "True for value types where min/max is meaningful.") - .def_static("hasAverage", &StatsT::hasAverage, - "True for value types that support mean/variance.") - .def_static("hasStdDeviation", &StatsT::hasStdDeviation, - "True for value types that support standard deviation.") - .def_static("hasStats", &StatsT::hasStats, - "True for value types where full statistics is meaningful."); -} - -// ----- updateGridStats polymorphic dispatch ---------------------------------- -// -// Match the IsNanoGridValid/callNanoGrid pattern: an Op struct with `known` -// (called for every BuildT that's in scope) and `unknown` (fallback). Only -// scalar + vector BuildTs have a meaningful Stats specialization, so the -// other arms raise. -struct UpdateGridStatsOp -{ - template - static void known(GridData* gridData, tools::StatsMode mode) - { - using GridT = NanoGrid; - using ValueT = typename GridT::ValueType; - if constexpr (BuildTraits::is_special && - !util::is_same::value) { - // Special / quantized / index / mask BuildTs don't have an - // arithmetic ValueT, so tools::updateGridStats's MinMax / All - // branches would instantiate Stats / Extrema - // with no meaningful semantics (and may not even compile). - // The Disable and BBox branches use NoopStats, which works - // for any ValueT — drive that directly here so the BBox path - // remains available on special grids (it just recomputes - // node bounding boxes without touching min/max/avg). - if (mode == tools::StatsMode::Disable) { - return; - } else if (mode == tools::StatsMode::BBox) { - tools::GridStats> stats; - stats.update(*static_cast(gridData)); - } else { - throw nb::value_error( - "updateGridStats: this grid's BuildT (special / " - "quantized / index / mask) has no arithmetic value " - "type — only StatsMode.Disable and StatsMode.BBox " - "are supported."); - } - } else { - tools::updateGridStats(static_cast(gridData), mode); - } - } - static void unknown(GridData*, tools::StatsMode) { - throw nb::value_error( - "updateGridStats: unsupported GridType / BuildT combination."); - } -}; - -// ----- getExtrema (per-BuildT factory) --------------------------------------- -template -static tools::Extrema::ValueType> -pyGetExtrema(const NanoGrid& grid, const CoordBBox& bbox) -{ - return tools::getExtrema(grid, bbox); -} - -} // namespace - -void defineGridStatsModule(nb::module_& toolsModule) -{ - // Per-BuildT Extrema + Stats. One pair per scalar/vector BuildT — the - // value types are all distinct so we get N pairs of new Python classes. -#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \ - defineExtrema(toolsModule, #Suffix "Extrema"); \ - defineStats(toolsModule, #Suffix "Stats"); -#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \ - defineExtrema(toolsModule, #Suffix "Extrema"); \ - defineStats(toolsModule, #Suffix "Stats"); -#include "BuildTypes.def" - - // Polymorphic updateGridStats. Accepts any bound NanoGrid (via - // upcast to GridData*) and dispatches on its mGridType. - toolsModule.def("updateGridStats", - [](GridData* gridData, tools::StatsMode mode) { - if (gridData == nullptr) { - throw nb::value_error("updateGridStats: grid is None."); - } - callNanoGrid(gridData, mode); - }, - "grid"_a, "mode"_a = tools::StatsMode::Default, - nb::call_guard(), - "Recompute and write per-node statistics into the given grid in " - "place. Polymorphic over BuildT. Scalar, vector, and Boolean " - "grids accept every StatsMode (Disable / BBox / MinMax / All); " - "Boolean grids use the C++ NoopStats path internally regardless " - "of mode because there's no arithmetic min/max/avg/dev on bool. " - "Other special (quantized / index / mask) grids accept Disable " - "and BBox (the latter recomputes node bounding boxes only); " - "MinMax and All raise ValueError because their value type has " - "no arithmetic semantics."); - - // Per-BuildT getExtrema. We expose one overload per scalar/vector - // BuildT — they each return a Python-side Extrema of the - // matching name. -#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \ - toolsModule.def("getExtrema", &pyGetExtrema, \ - "grid"_a, "bbox"_a, nb::call_guard(), \ - "Return the Extrema of all values in the grid that intersect " \ - "the given bbox."); -#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \ - toolsModule.def("getExtrema", &pyGetExtrema, \ - "grid"_a, "bbox"_a, nb::call_guard(), \ - "Return the Extrema of all values in the grid that intersect " \ - "the given bbox."); -#include "BuildTypes.def" -} - } // namespace pynanovdb diff --git a/nanovdb/nanovdb/python/PyGridStats.h b/nanovdb/nanovdb/python/PyGridStats.h index 07b0c3a0dd..90254bc23b 100644 --- a/nanovdb/nanovdb/python/PyGridStats.h +++ b/nanovdb/nanovdb/python/PyGridStats.h @@ -11,12 +11,6 @@ namespace pynanovdb { void defineStatsMode(nb::module_& m); -/// @brief Register per-BuildT Extrema and Stats classes (one set per -/// scalar / vector BuildT in BuildTypes.def) and the polymorphic -/// tools.updateGridStats / tools.getExtrema helpers under the -/// nanovdb.tools submodule. -void defineGridStatsModule(nb::module_& toolsModule); - } #endif diff --git a/nanovdb/nanovdb/python/PyGridValidator.cc b/nanovdb/nanovdb/python/PyGridValidator.cc index db9a46fc8f..8e4f20df64 100644 --- a/nanovdb/nanovdb/python/PyGridValidator.cc +++ b/nanovdb/nanovdb/python/PyGridValidator.cc @@ -2,21 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 #include "PyGridValidator.h" -#include -#include -#include - #include -#include #include #ifdef NANOVDB_USE_CUDA #include #endif -#include -#include -#include -#include +#include namespace nb = nanobind; using namespace nb::literals; @@ -26,8 +18,7 @@ namespace pynanovdb { template void defineValidateGrids(nb::module_& m) { - m.def("validateGrids", &tools::validateGrids>, - "handle"_a, "mode"_a, "verbose"_a); + m.def("validateGrids", &tools::validateGrids>, "handle"_a, "mode"_a, "verbose"_a); } template void defineValidateGrids(nb::module_&); @@ -35,126 +26,4 @@ template void defineValidateGrids(nb::module_&); template void defineValidateGrids(nb::module_&); #endif -namespace { - -// callNanoGrid op for checkGrid: writes into a fixed-size buffer and returns -// it as a std::pair for nb::make_tuple consumption. -// -// tools::checkGrid writes error messages with util::sprint / util::strcpy, -// neither of which is bounded — they trust the caller's buffer is big -// enough. Size the buffer well above what any current error message -// produces (the longest formatted message in GridValidator.h is on the -// order of ~80 characters once both GridType and GridClass enumerator -// names are stringified) and leave generous headroom for future -// additions. -struct CheckGridOp -{ - static constexpr size_t kErrorBufSize = 4096; - - template - static std::pair known(const GridData* gridData, - CheckMode mode) - { - char buf[kErrorBufSize]; - buf[0] = '\0'; - tools::checkGrid( - static_cast*>(gridData), buf, mode); - const bool ok = (buf[0] == '\0'); - return {ok, std::string(buf)}; - } - static std::pair unknown(const GridData* gridData, - CheckMode /*mode*/) - { - (void)gridData; - return {false, "Unsupported GridType for checkGrid"}; - } -}; - -// callNanoGrid op for isValid — wraps tools::isValid for every -// switched-over BuildT. -struct IsValidOp -{ - template - static bool known(const GridData* gridData, CheckMode mode, bool verbose) - { - return tools::isValid( - static_cast*>(gridData), mode, verbose); - } - static bool unknown(const GridData* gridData, CheckMode /*mode*/, bool verbose) - { - if (verbose && gridData != nullptr) { - char str[16]; - std::cerr << "Validation failed: Unsupported GridType: \"" - << toStr(str, gridData->mGridType) << "\"" - << std::endl; - } - return false; - } -}; - -} // namespace - -void defineGridValidatorModule(nb::module_& toolsModule) -{ - // Single-grid validate. Takes a handle, a grid index, and the - // usual mode + verbose flags. Returns true iff the grid passes all - // tests for the given mode. Bound for both host and device handles - // (when CUDA is enabled), matching validateGrids' coverage — - // tools::validateGrid does host-side dispatch via callNanoGrid on - // the host-resident gridData() pointer that DeviceGridHandle also - // exposes, so the same overload pair is appropriate. - toolsModule.def("validateGrid", - &tools::validateGrid>, - "handle"_a, "gridID"_a, - "mode"_a = CheckMode::Default, "verbose"_a = false, - nb::call_guard(), - "Validate the gridID'th grid in the handle against the given " - "CheckMode. Returns False (without raising) if gridID is out " - "of range or the grid fails any check. CheckMode.Disable is a " - "short-circuit that always returns True without inspecting " - "the grid (even when gridID is out of range), matching the " - "C++ behavior. Complements validateGrids() which checks the " - "whole handle."); -#ifdef NANOVDB_USE_CUDA - toolsModule.def("validateGrid", - &tools::validateGrid>, - "handle"_a, "gridID"_a, - "mode"_a = CheckMode::Default, "verbose"_a = false, - nb::call_guard(), - "Validate the gridID'th grid in the device handle (uses the " - "host-resident copy of the grid metadata for the actual " - "checks). Same semantics as the host-handle overload."); -#endif - - // Polymorphic checkGrid — returns (ok, error_message). Mirrors the C++ - // char-buffer-out signature, but the buffer is hidden inside the - // binding so Python callers get a Python str. - toolsModule.def("checkGrid", - [](const GridData* gridData, CheckMode mode) - -> std::pair { - if (gridData == nullptr) { - return {false, "Grid is None"}; - } - return callNanoGrid(gridData, mode); - }, - "grid"_a, "mode"_a = CheckMode::Full, - nb::call_guard(), - "Run structural validation checks on the grid for the given " - "CheckMode. Returns a (ok, error_message) tuple — error_message " - "is empty when ok is True."); - - // Polymorphic isValid — convenience wrapper. Same as checkGrid + a - // checksum check, returning just the bool. - toolsModule.def("isValid", - [](const GridData* gridData, CheckMode mode, bool verbose) { - if (gridData == nullptr) return false; - return callNanoGrid(gridData, mode, verbose); - }, - "grid"_a, "mode"_a = CheckMode::Default, "verbose"_a = false, - nb::call_guard(), - "Return True iff the grid passes structural validation AND its " - "stored checksum matches a freshly computed one for the given " - "CheckMode."); -} - } // namespace pynanovdb diff --git a/nanovdb/nanovdb/python/PyGridValidator.h b/nanovdb/nanovdb/python/PyGridValidator.h index 6ce2f2db2b..659dede241 100644 --- a/nanovdb/nanovdb/python/PyGridValidator.h +++ b/nanovdb/nanovdb/python/PyGridValidator.h @@ -11,12 +11,6 @@ namespace pynanovdb { template void defineValidateGrids(nb::module_& m); -/// @brief Register tools.validateGrid (single grid in a handle), -/// tools.checkGrid (polymorphic, returns (bool, error_str)) and -/// tools.isValid (polymorphic shortcut) under the nanovdb.tools -/// submodule. Bound once; polymorphic dispatch uses callNanoGrid. -void defineGridValidatorModule(nb::module_& toolsModule); - } // namespace pynanovdb #endif diff --git a/nanovdb/nanovdb/python/PyHostBuffer.cc b/nanovdb/nanovdb/python/PyHostBuffer.cc index 7a929aacf4..e8fdfb3946 100644 --- a/nanovdb/nanovdb/python/PyHostBuffer.cc +++ b/nanovdb/nanovdb/python/PyHostBuffer.cc @@ -11,10 +11,7 @@ namespace pynanovdb { void defineHostBuffer(nb::module_& m) { - nb::class_(m, "HostBuffer", - "Default host-side buffer used to back a GridHandle. Memory is " - "owned by this buffer and freed when the handle (and therefore " - "the buffer) is destroyed."); + nb::class_(m, "HostBuffer"); } } // namespace pynanovdb diff --git a/nanovdb/nanovdb/python/PyIO.cc b/nanovdb/nanovdb/python/PyIO.cc index 4573b93d02..5659bb915b 100644 --- a/nanovdb/nanovdb/python/PyIO.cc +++ b/nanovdb/nanovdb/python/PyIO.cc @@ -21,67 +21,40 @@ namespace { void defineFileGridMetaData(nb::module_& m) { - nb::class_(m, "FileMetaData", - "Per-grid header read from the .nvdb file index. Mirrors the C++ " - "io::FileMetaData layout; subclassed by FileGridMetaData which adds " - "the grid name string.") - .def_ro("gridSize", &io::FileMetaData::gridSize, - "Uncompressed grid size in bytes.") - .def_ro("fileSize", &io::FileMetaData::fileSize, - "On-disk byte size of this grid (post-codec).") - .def_ro("nameKey", &io::FileMetaData::nameKey, - "Hash of the grid name used as a fast lookup key.") - .def_ro("voxelCount", &io::FileMetaData::voxelCount, - "Number of active voxels in this grid.") - .def_ro("gridType", &io::FileMetaData::gridType, - "GridType enumerator naming the BuildT of this grid.") - .def_ro("gridClass", &io::FileMetaData::gridClass, - "GridClass enumerator (LevelSet, FogVolume, ...).") - .def_ro("indexBBox", &io::FileMetaData::indexBBox, - "Axis-aligned bounding box of active voxels in index space.") - .def_ro("worldBBox", &io::FileMetaData::worldBBox, - "Axis-aligned bounding box of active voxels in world space.") - .def_ro("voxelSize", &io::FileMetaData::voxelSize, - "World-space size of a single voxel.") - .def_ro("nameSize", &io::FileMetaData::nameSize, - "Length of the grid name string including the null terminator.") + nb::class_(m, "FileMetaData") + .def_ro("gridSize", &io::FileMetaData::gridSize) + .def_ro("fileSize", &io::FileMetaData::fileSize) + .def_ro("nameKey", &io::FileMetaData::nameKey) + .def_ro("voxelCount", &io::FileMetaData::voxelCount) + .def_ro("gridType", &io::FileMetaData::gridType) + .def_ro("gridClass", &io::FileMetaData::gridClass) + .def_ro("indexBBox", &io::FileMetaData::indexBBox) + .def_ro("worldBBox", &io::FileMetaData::worldBBox) + .def_ro("voxelSize", &io::FileMetaData::voxelSize) + .def_ro("nameSize", &io::FileMetaData::nameSize) .def_prop_ro("nodeCount", [](io::FileMetaData& metaData) { return std::make_tuple(metaData.nodeCount[0], metaData.nodeCount[1], metaData.nodeCount[2], metaData.nodeCount[3]); - }, - "Tuple (leaf, lower, upper, root) of node counts in this grid.") + }) .def_prop_ro("tileCount", - [](io::FileMetaData& metaData) { return std::make_tuple(metaData.tileCount[0], metaData.tileCount[1], metaData.tileCount[2]); }, - "Tuple (lower-tile, upper-tile, root-tile) of active-tile counts.") - .def_ro("codec", &io::FileMetaData::codec, - "Codec used to compress this grid on disk.") - .def_ro("blindDataCount", &io::FileMetaData::blindDataCount, - "Number of blind-data channels attached to this grid.") - .def_ro("version", &io::FileMetaData::version, - "NanoVDB version stored in the file when this grid was written."); - - nb::bind_vector>(m, "FileMetaDataVector", - "List of FileMetaData entries, one per grid in a .nvdb file."); - - nb::class_(m, "FileGridMetaData", - "FileMetaData extended with the grid name. Returned by " - "readGridMetaData() so callers can identify grids by name without " - "materializing them.") - .def_ro("gridName", &io::FileGridMetaData::gridName, - "Grid name as a Python string.") - .def("memUsage", &io::FileGridMetaData::memUsage, - "Byte size of this metadata record in memory."); - - nb::bind_vector>(m, "FileGridMetaDataVector", - "List of FileGridMetaData entries, one per grid in a .nvdb file."); + [](io::FileMetaData& metaData) { return std::make_tuple(metaData.tileCount[0], metaData.tileCount[1], metaData.tileCount[2]); }) + .def_ro("codec", &io::FileMetaData::codec) + .def_ro("blindDataCount", &io::FileMetaData::blindDataCount) + .def_ro("version", &io::FileMetaData::version); + + nb::bind_vector>(m, "FileMetaDataVector"); + + nb::class_(m, "FileGridMetaData") + .def_ro("gridName", &io::FileGridMetaData::gridName) + .def("memUsage", &io::FileGridMetaData::memUsage); + + nb::bind_vector>(m, "FileGridMetaDataVector"); } template void defineReadWriteGrid(nb::module_& m) { - m.def("hasGrid", nb::overload_cast(&io::hasGrid), "fileName"_a, "gridName"_a, - "Return True iff the .nvdb file at fileName contains a grid named gridName."); - m.def("readGridMetaData", nb::overload_cast(&io::readGridMetaData), "fileName"_a, - "Return a FileGridMetaDataVector describing every grid stored in the .nvdb file."); + m.def("hasGrid", nb::overload_cast(&io::hasGrid), "fileName"_a, "gridName"_a); + m.def("readGridMetaData", nb::overload_cast(&io::readGridMetaData), "fileName"_a); } template nb::list readGrids(const std::string& fileName, int verbose, const BufferT& buffer) @@ -118,26 +91,21 @@ void defineHostReadWriteGrid(nb::module_& m) "fileName"_a, "handle"_a, "codec"_a = io::Codec::NONE, - "verbose"_a = 0, - "Write the grids in handle to the .nvdb file at fileName using the given codec."); - m.def("writeGrids", &writeGrids, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0, - "Write every GridHandle in the handles list to the .nvdb file at fileName."); + "verbose"_a = 0); + m.def("writeGrids", &writeGrids, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0); m.def("readGrid", nb::overload_cast(&io::template readGrid), "fileName"_a, "n"_a = 0, "verbose"_a = 0, - "buffer"_a = BufferT(), - "Read the n-th grid from the .nvdb file at fileName into a fresh GridHandle."); + "buffer"_a = BufferT()); m.def("readGrid", nb::overload_cast(&io::template readGrid), "fileName"_a, "gridName"_a, "verbose"_a = 0, - "buffer"_a = BufferT(), - "Read the grid named gridName from the .nvdb file at fileName into a fresh GridHandle."); - m.def("readGrids", &readGrids, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT(), - "Read every grid from the .nvdb file at fileName, returning a list of GridHandles."); + "buffer"_a = BufferT()); + m.def("readGrids", &readGrids, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT()); } #ifdef NANOVDB_USE_CUDA @@ -151,26 +119,21 @@ void defineDeviceReadWriteGrid(nb::module_& m) "fileName"_a, "handle"_a, "codec"_a = io::Codec::NONE, - "verbose"_a = 0, - "Write the grids in a device-backed handle to the .nvdb file at fileName."); - m.def("deviceWriteGrids", &writeGrids, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0, - "Write every device-backed GridHandle in handles to the .nvdb file at fileName."); + "verbose"_a = 0); + m.def("deviceWriteGrids", &writeGrids, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0); m.def("deviceReadGrid", nb::overload_cast(&io::template readGrid), "fileName"_a, "n"_a = 0, "verbose"_a = 0, - "buffer"_a = BufferT(), - "Read the n-th grid from the .nvdb file at fileName into a fresh DeviceGridHandle."); + "buffer"_a = BufferT()); m.def("deviceReadGrid", nb::overload_cast(&io::template readGrid), "fileName"_a, "gridName"_a, "verbose"_a = 0, - "buffer"_a = BufferT(), - "Read the grid named gridName from the .nvdb file at fileName into a fresh DeviceGridHandle."); - m.def("deviceReadGrids", &readGrids, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT(), - "Read every grid from the .nvdb file at fileName into device-backed handles."); + "buffer"_a = BufferT()); + m.def("deviceReadGrids", &readGrids, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT()); } #endif @@ -178,19 +141,16 @@ void defineDeviceReadWriteGrid(nb::module_& m) void defineIOModule(nb::module_& m) { - nb::enum_(m, "Codec", - "Compression codec selector used when writing a .nvdb file. NONE " - "writes raw bytes; ZIP uses zlib; BLOSC uses the blosc codec when " - "compiled in.") + nb::enum_(m, "Codec") .value("NONE", io::Codec::NONE) .value("ZIP", io::Codec::ZIP) .value("BLOSC", io::Codec::BLOSC) - .export_values() - .def("__repr__", [](const io::Codec& codec) { - char str[strlen()]; - toStr(str, codec); - return std::string(str); - }); + .export_values(); + // .def("__repr__", [](const io::Codec& codec) { + // char str[strlen()]; + // toStr(str, codec); + // return std::string(str); + // }); defineFileGridMetaData(m); defineHostReadWriteGrid(m); diff --git a/nanovdb/nanovdb/python/PyMath.cc b/nanovdb/nanovdb/python/PyMath.cc index 2e212f09b9..337865ed8c 100644 --- a/nanovdb/nanovdb/python/PyMath.cc +++ b/nanovdb/nanovdb/python/PyMath.cc @@ -11,6 +11,7 @@ #include #include "PySampleFromVoxels.h" +#include "cuda/PySampleFromVoxels.h" namespace nb = nanobind; using namespace nb::literals; @@ -25,27 +26,18 @@ void defineCoord(nb::module_& m) using ValueType = math::Coord::ValueType; nb::class_(m, "Coord", "Signed (i, j, k) 32-bit integer coordinate class, similar to openvdb::math::Coord") - .def(nb::init<>(), - "Construct (0, 0, 0).") - .def(nb::init(), "n"_a, - "Construct (n, n, n).") - .def(nb::init(), "i"_a, "j"_a, "k"_a, - "Construct (i, j, k).") + .def(nb::init<>()) + .def(nb::init(), "n"_a) + .def(nb::init(), "i"_a, "j"_a, "k"_a) .def_prop_rw( - "x", [](const math::Coord& ijk) { return ijk.x(); }, [](math::Coord& ijk, int32_t i) { ijk.x() = i; }, - "First component of the (i, j, k) triple.") + "x", [](const math::Coord& ijk) { return ijk.x(); }, [](math::Coord& ijk, int32_t i) { ijk.x() = i; }) .def_prop_rw( - "y", [](const math::Coord& ijk) { return ijk.y(); }, [](math::Coord& ijk, int32_t j) { ijk.y() = j; }, - "Second component of the (i, j, k) triple.") + "y", [](const math::Coord& ijk) { return ijk.y(); }, [](math::Coord& ijk, int32_t j) { ijk.y() = j; }) .def_prop_rw( - "z", [](const math::Coord& ijk) { return ijk.z(); }, [](math::Coord& ijk, int32_t k) { ijk.z() = k; }, - "Third component of the (i, j, k) triple.") - .def_static("max", &math::Coord::max, - "Largest representable Coord (INT32_MAX in every component).") - .def_static("min", &math::Coord::min, - "Smallest representable Coord (INT32_MIN in every component).") - .def_static("memUsage", &math::Coord::memUsage, - "Byte size of a Coord instance.") + "z", [](const math::Coord& ijk) { return ijk.z(); }, [](math::Coord& ijk, int32_t k) { ijk.z() = k; }) + .def_static("max", &math::Coord::max) + .def_static("min", &math::Coord::min) + .def_static("memUsage", &math::Coord::memUsage) .def( "__getitem__", [](const math::Coord& ijk, size_t i) { @@ -54,8 +46,7 @@ void defineCoord(nb::module_& m) } return ijk[static_cast(i)]; }, - "i"_a, - "Read the i-th component (0=x, 1=y, 2=z).") + "i"_a) .def( "__setitem__", [](math::Coord& ijk, size_t i, ValueType value) { @@ -65,69 +56,41 @@ void defineCoord(nb::module_& m) ijk[static_cast(i)] = value; }, "i"_a, - "value"_a, - "Write the i-th component (0=x, 1=y, 2=z).") + "value"_a) .def( - "__and__", [](const math::Coord& a, math::Coord::IndexType b) { return a & b; }, nb::is_operator(), "n"_a, - "Component-wise bitwise AND with the scalar n.") + "__and__", [](const math::Coord& a, math::Coord::IndexType b) { return a & b; }, nb::is_operator(), "n"_a) .def( - "__lshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a << b; }, nb::is_operator(), "n"_a, - "Component-wise left shift by n bits.") + "__lshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a << b; }, nb::is_operator(), "n"_a) .def( - "__rshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a >> b; }, nb::is_operator(), "n"_a, - "Component-wise right shift by n bits.") - .def(nb::self < nb::self, "rhs"_a, - "Lexicographic less-than comparison.") - .def(nb::self == nb::self, "rhs"_a, - "Equality of all three components.") - .def(nb::self != nb::self, "rhs"_a, - "Inequality of any one component.") + "__rshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a >> b; }, nb::is_operator(), "n"_a) + .def(nb::self < nb::self, "rhs"_a) + .def(nb::self == nb::self, "rhs"_a) + .def(nb::self != nb::self, "rhs"_a) .def( - "__iand__", [](math::Coord& a, int b) { return a &= b; }, nb::is_operator(), "n"_a, - "In-place component-wise bitwise AND with the scalar n.") + "__iand__", [](math::Coord& a, int b) { return a &= b; }, nb::is_operator(), "n"_a) .def( - "__ilshift__", [](math::Coord& a, uint32_t b) { return a <<= b; }, nb::is_operator(), "n"_a, - "In-place component-wise left shift by n bits.") + "__ilshift__", [](math::Coord& a, uint32_t b) { return a <<= b; }, nb::is_operator(), "n"_a) .def( - "__irshift__", [](math::Coord& a, uint32_t b) { return a >>= b; }, nb::is_operator(), "n"_a, - "In-place component-wise right shift by n bits.") + "__irshift__", [](math::Coord& a, uint32_t b) { return a >>= b; }, nb::is_operator(), "n"_a) .def( - "__iadd__", [](math::Coord& a, int b) { return a += b; }, nb::is_operator(), "n"_a, - "In-place add the scalar n to every component.") - .def(nb::self + nb::self, "rhs"_a, - "Component-wise addition.") - .def(nb::self - nb::self, "rhs"_a, - "Component-wise subtraction.") - .def(-nb::self, - "Negate every component.") - .def(nb::self += nb::self, "rhs"_a, - "In-place component-wise addition.") - .def(nb::self -= nb::self, "rhs"_a, - "In-place component-wise subtraction.") - .def("minComponent", &math::Coord::minComponent, "other"_a, - "Component-wise minimum with other. See nanovdb::math::Coord::minComponent in NanoVDB.h.") - .def("maxComponent", &math::Coord::maxComponent, "other"_a, - "Component-wise maximum with other. See nanovdb::math::Coord::maxComponent in NanoVDB.h.") - .def("offsetBy", nb::overload_cast(&math::Coord::offsetBy, nb::const_), "dx"_a, "dy"_a, "dz"_a, - "Return a new Coord offset by (dx, dy, dz). See nanovdb::math::Coord::offsetBy in NanoVDB.h.") - .def("offsetBy", nb::overload_cast(&math::Coord::offsetBy, nb::const_), "n"_a, - "Return a new Coord offset by n in every component.") - .def_static("lessThan", &math::Coord::lessThan, "a"_a, "b"_a, - "Component-wise a < b returning a Coord of 0 / 1 flags.") - .def_static("Floor", &math::Coord::template Floor>, "xyz"_a, - "Floor each component of a Vec3f to produce an integer Coord.") - .def_static("Floor", &math::Coord::template Floor>, "xyz"_a, - "Floor each component of a Vec3d to produce an integer Coord.") - .def("hash", &math::Coord::template hash<12>, - "Spatial hash of this coordinate suited for hashed root-table lookups.") - .def("octant", &math::Coord::octant, - "Return the 0..7 octant index of this coordinate's sign bits.") - .def("asVec3s", &math::Coord::asVec3s, - "Convert to a Vec3f (float) with no scaling.") - .def("asVec3d", &math::Coord::asVec3d, - "Convert to a Vec3d (double) with no scaling.") - .def("round", &math::Coord::round, - "Component-wise round; for an integer Coord this is the identity.") + "__iadd__", [](math::Coord& a, int b) { return a += b; }, nb::is_operator(), "n"_a) + .def(nb::self + nb::self, "rhs"_a) + .def(nb::self - nb::self, "rhs"_a) + .def(-nb::self) + .def(nb::self += nb::self, "rhs"_a) + .def(nb::self -= nb::self, "rhs"_a) + .def("minComponent", &math::Coord::minComponent, "other"_a) + .def("maxComponent", &math::Coord::maxComponent, "other"_a) + .def("offsetBy", nb::overload_cast(&math::Coord::offsetBy, nb::const_), "dx"_a, "dy"_a, "dz"_a) + .def("offsetBy", nb::overload_cast(&math::Coord::offsetBy, nb::const_), "n"_a) + .def_static("lessThan", &math::Coord::lessThan, "a"_a, "b"_a) + .def_static("Floor", &math::Coord::template Floor>, "xyz"_a) + .def_static("Floor", &math::Coord::template Floor>, "xyz"_a) + .def("hash", &math::Coord::template hash<12>) + .def("octant", &math::Coord::octant) + .def("asVec3s", &math::Coord::asVec3s) + .def("asVec3d", &math::Coord::asVec3d) + .def("round", &math::Coord::round) .def("__repr__", [](const math::Coord& ijk) { std::stringstream ostr; ostr << ijk; @@ -138,20 +101,13 @@ void defineCoord(nb::module_& m) template void defineVec3(nb::module_& m, const char* name, const char* doc) { nb::class_>(m, name, doc) - .def(nb::init<>(), - "Construct a zero-initialized vector.") - .def(nb::init(), "x"_a, - "Construct (x, x, x).") - .def(nb::init(), "x"_a, "y"_a, "z"_a, - "Construct (x, y, z).") - .def(nb::init>(), "v"_a, - "Copy-construct from another Vec3.") - .def(nb::init(), "ijk"_a, - "Construct from an integer Coord, casting each component.") - .def(nb::self == nb::self, "rhs"_a, - "Component-wise equality.") - .def(nb::self != nb::self, "rhs"_a, - "Component-wise inequality.") + .def(nb::init<>()) + .def(nb::init(), "x"_a) + .def(nb::init(), "x"_a, "y"_a, "z"_a) + .def(nb::init>(), "v"_a) + .def(nb::init(), "ijk"_a) + .def(nb::self == nb::self, "rhs"_a) + .def(nb::self != nb::self, "rhs"_a) .def( "__getitem__", [](const math::Vec3& v, size_t i) { @@ -160,8 +116,7 @@ template void defineVec3(nb::module_& m, const char* name, const cha } return v[static_cast(i)]; }, - "i"_a, - "Read the i-th component (0=x, 1=y, 2=z).") + "i"_a) .def( "__setitem__", [](math::Vec3& v, size_t i, T value) { @@ -171,68 +126,38 @@ template void defineVec3(nb::module_& m, const char* name, const cha v[static_cast(i)] = value; }, "i"_a, - "value"_a, - "Write the i-th component (0=x, 1=y, 2=z).") - .def("dot", &math::Vec3::template dot>, "v"_a, - "Dot product with another vector.") - .def("cross", &math::Vec3::template cross>, "v"_a, - "Cross product with another vector.") - .def("lengthSqr", &math::Vec3::lengthSqr, - "Squared Euclidean length (cheaper than length()).") - .def("length", &math::Vec3::length, - "Euclidean length of this vector.") - .def(-nb::self, - "Negate every component.") - .def(nb::self * nb::self, "v"_a, - "Component-wise multiplication.") - .def(nb::self / nb::self, "v"_a, - "Component-wise division.") - .def(nb::self + nb::self, "v"_a, - "Component-wise addition.") - .def(nb::self - nb::self, "v"_a, - "Component-wise subtraction.") - .def(nb::self + math::Coord(), "ijk"_a, - "Add an integer Coord component-wise.") - .def(nb::self - math::Coord(), "ijk"_a, - "Subtract an integer Coord component-wise.") - .def(nb::self * T(), "s"_a, - "Multiply every component by the scalar s.") - .def(nb::self / T(), "s"_a, - "Divide every component by the scalar s.") - .def(nb::self += nb::self, "v"_a, - "In-place component-wise addition.") - .def(nb::self += math::Coord(), "ijk"_a, - "In-place add an integer Coord.") - .def(nb::self -= nb::self, "v"_a, - "In-place component-wise subtraction.") - .def(nb::self -= math::Coord(), "ijk"_a, - "In-place subtract an integer Coord.") - .def(nb::self *= T(), "s"_a, - "In-place scalar multiply.") - .def(nb::self /= T(), "s"_a, - "In-place scalar divide.") - .def("normalize", &math::Vec3::normalize, - "Scale this vector to unit length in place.") - .def("minComponent", &math::Vec3::minComponent, "other"_a, - "Component-wise minimum with other.") - .def("maxComponent", &math::Vec3::maxComponent, "other"_a, - "Component-wise maximum with other.") - .def("min", &math::Vec3::min, - "Smallest single component of this vector.") - .def("max", &math::Vec3::max, - "Largest single component of this vector.") - .def("floor", &math::Vec3::floor, - "Component-wise floor.") - .def("ceil", &math::Vec3::ceil, - "Component-wise ceiling.") - .def("round", &math::Vec3::round, - "Component-wise round.") + "value"_a) + .def("dot", &math::Vec3::template dot>, "v"_a) + .def("cross", &math::Vec3::template cross>, "v"_a) + .def("lengthSqr", &math::Vec3::lengthSqr) + .def("length", &math::Vec3::length) + .def(-nb::self) + .def(nb::self * nb::self, "v"_a) + .def(nb::self / nb::self, "v"_a) + .def(nb::self + nb::self, "v"_a) + .def(nb::self - nb::self, "v"_a) + .def(nb::self + math::Coord(), "ijk"_a) + .def(nb::self - math::Coord(), "ijk"_a) + .def(nb::self * T(), "s"_a) + .def(nb::self / T(), "s"_a) + .def(nb::self += nb::self, "v"_a) + .def(nb::self += math::Coord(), "ijk"_a) + .def(nb::self -= nb::self, "v"_a) + .def(nb::self -= math::Coord(), "ijk"_a) + .def(nb::self *= T(), "s"_a) + .def(nb::self /= T(), "s"_a) + .def("normalize", &math::Vec3::normalize) + .def("minComponent", &math::Vec3::minComponent, "other"_a) + .def("maxComponent", &math::Vec3::maxComponent, "other"_a) + .def("min", &math::Vec3::min) + .def("max", &math::Vec3::max) + .def("floor", &math::Vec3::floor) + .def("ceil", &math::Vec3::ceil) + .def("round", &math::Vec3::round) .def( - "__mul__", [](const T& a, math::Vec3 b) { return a * b; }, nb::is_operator(), "b"_a, - "Right-multiply: scalar * Vec3.") + "__mul__", [](const T& a, math::Vec3 b) { return a * b; }, nb::is_operator(), "b"_a) .def( - "__truediv__", [](const T& a, math::Vec3 b) { return a / b; }, nb::is_operator(), "b"_a, - "Right-divide: scalar / Vec3, component-wise.") + "__truediv__", [](const T& a, math::Vec3 b) { return a / b; }, nb::is_operator(), "b"_a) .def("__repr__", [](const math::Vec3& v) { std::stringstream ostr; ostr << v; @@ -243,18 +168,12 @@ template void defineVec3(nb::module_& m, const char* name, const cha template void defineVec4(nb::module_& m, const char* name, const char* doc) { nb::class_>(m, name, doc) - .def(nb::init<>(), - "Construct a zero-initialized vector.") - .def(nb::init(), "x"_a, - "Construct (x, x, x, x).") - .def(nb::init(), "x"_a, "y"_a, "z"_a, "w"_a, - "Construct (x, y, z, w).") - .def(nb::init>(), "v"_a, - "Copy-construct from another Vec4.") - .def(nb::self == nb::self, "rhs"_a, - "Component-wise equality.") - .def(nb::self != nb::self, "rhs"_a, - "Component-wise inequality.") + .def(nb::init<>()) + .def(nb::init(), "x"_a) + .def(nb::init(), "x"_a, "y"_a, "z"_a, "w"_a) + .def(nb::init>(), "v"_a) + .def(nb::self == nb::self, "rhs"_a) + .def(nb::self != nb::self, "rhs"_a) .def( "__getitem__", [](const math::Vec4& v, size_t i) { @@ -263,8 +182,7 @@ template void defineVec4(nb::module_& m, const char* name, const cha } return v[static_cast(i)]; }, - "i"_a, - "Read the i-th component (0=x, 1=y, 2=z, 3=w).") + "i"_a) .def( "__setitem__", [](math::Vec4& v, size_t i, T value) { @@ -274,48 +192,28 @@ template void defineVec4(nb::module_& m, const char* name, const cha v[static_cast(i)] = value; }, "i"_a, - "value"_a, - "Write the i-th component (0=x, 1=y, 2=z, 3=w).") - .def("dot", &math::Vec4::template dot>, "v"_a, - "Dot product with another vector.") - .def("lengthSqr", &math::Vec4::lengthSqr, - "Squared Euclidean length (cheaper than length()).") - .def("length", &math::Vec4::length, - "Euclidean length of this vector.") - .def(-nb::self, - "Negate every component.") - .def(nb::self * nb::self, "v"_a, - "Component-wise multiplication.") - .def(nb::self / nb::self, "v"_a, - "Component-wise division.") - .def(nb::self + nb::self, "v"_a, - "Component-wise addition.") - .def(nb::self - nb::self, "v"_a, - "Component-wise subtraction.") - .def(nb::self * T(), "s"_a, - "Multiply every component by the scalar s.") - .def(nb::self / T(), "s"_a, - "Divide every component by the scalar s.") - .def(nb::self += nb::self, "v"_a, - "In-place component-wise addition.") - .def(nb::self -= nb::self, "v"_a, - "In-place component-wise subtraction.") - .def(nb::self *= T(), "s"_a, - "In-place scalar multiply.") - .def(nb::self /= T(), "s"_a, - "In-place scalar divide.") - .def("normalize", &math::Vec4::normalize, - "Scale this vector to unit length in place.") - .def("minComponent", &math::Vec4::minComponent, "other"_a, - "Component-wise minimum with other.") - .def("maxComponent", &math::Vec4::maxComponent, "other"_a, - "Component-wise maximum with other.") + "value"_a) + .def("dot", &math::Vec4::template dot>, "v"_a) + .def("lengthSqr", &math::Vec4::lengthSqr) + .def("length", &math::Vec4::length) + .def(-nb::self) + .def(nb::self * nb::self, "v"_a) + .def(nb::self / nb::self, "v"_a) + .def(nb::self + nb::self, "v"_a) + .def(nb::self - nb::self, "v"_a) + .def(nb::self * T(), "s"_a) + .def(nb::self / T(), "s"_a) + .def(nb::self += nb::self, "v"_a) + .def(nb::self -= nb::self, "v"_a) + .def(nb::self *= T(), "s"_a) + .def(nb::self /= T(), "s"_a) + .def("normalize", &math::Vec4::normalize) + .def("minComponent", &math::Vec4::minComponent, "other"_a) + .def("maxComponent", &math::Vec4::maxComponent, "other"_a) .def( - "__mul__", [](const T& a, math::Vec4 b) { return a * b; }, nb::is_operator(), "b"_a, - "Right-multiply: scalar * Vec4.") + "__mul__", [](const T& a, math::Vec4 b) { return a * b; }, nb::is_operator(), "b"_a) .def( - "__truediv__", [](const T& a, math::Vec4 b) { return a / b; }, nb::is_operator(), "b"_a, - "Right-divide: scalar / Vec4, component-wise.") + "__truediv__", [](const T& a, math::Vec4 b) { return a / b; }, nb::is_operator(), "b"_a) .def("__repr__", [](const math::Vec4& v) { std::stringstream ostr; ostr << v; @@ -328,30 +226,18 @@ void defineRgba8(nb::module_& m) using ValueType = math::Rgba8::ValueType; nb::class_(m, "Rgba8", "8-bit red, green, blue, alpha packed into 32 bit unsigned int") - .def(nb::init<>(), - "Construct a fully transparent black Rgba8 (all components 0).") - .def(nb::init(), "other"_a, - "Copy-construct from another Rgba8.") - .def(nb::init(), "r"_a, "g"_a, "b"_a, "a"_a = 255, - "Construct from four 0..255 uint8 channels; a defaults to fully opaque.") - .def(nb::init(), "v"_a, - "Construct a gray Rgba8 with every channel set to v.") - .def(nb::init(), "r"_a, "g"_a, "b"_a, "a"_a = 1.0, - "Construct from four 0..1 floats, clamped and quantized to uint8.") - .def(nb::init(), "rgb"_a, - "Construct from an RGB float triple; alpha defaults to opaque.") - .def(nb::init(), "rgba"_a, - "Construct from an RGBA float quadruple.") - .def(nb::self < nb::self, "rhs"_a, - "Less-than comparison on the packed uint32 representation.") - .def(nb::self == nb::self, "rhs"_a, - "Equality on the packed uint32 representation.") - .def("lengthSqr", &math::Rgba8::lengthSqr, - "Squared length over (r, g, b, a) as integers.") - .def("length", &math::Rgba8::length, - "Euclidean length over (r, g, b, a) as floats.") - .def("asFloat", &math::Rgba8::asFloat, "n"_a, - "Return the n-th channel as a 0..1 float.") + .def(nb::init<>()) + .def(nb::init(), "other"_a) + .def(nb::init(), "r"_a, "g"_a, "b"_a, "a"_a = 255) + .def(nb::init(), "v"_a) + .def(nb::init(), "r"_a, "g"_a, "b"_a, "a"_a = 1.0) + .def(nb::init(), "rgb"_a) + .def(nb::init(), "rgba"_a) + .def(nb::self < nb::self, "rhs"_a) + .def(nb::self == nb::self, "rhs"_a) + .def("lengthSqr", &math::Rgba8::lengthSqr) + .def("length", &math::Rgba8::length) + .def("asFloat", &math::Rgba8::asFloat, "n"_a) .def( "__getitem__", [](const math::Rgba8& rgba, size_t i) { @@ -360,8 +246,7 @@ void defineRgba8(nb::module_& m) } return rgba[static_cast(i)]; }, - "i"_a, - "Read the i-th channel as a uint8 (0=r, 1=g, 2=b, 3=a).") + "i"_a) .def( "__setitem__", [](math::Rgba8& rgba, size_t i, ValueType value) { @@ -371,38 +256,26 @@ void defineRgba8(nb::module_& m) rgba[static_cast(i)] = value; }, "i"_a, - "value"_a, - "Write the i-th channel as a uint8 (0=r, 1=g, 2=b, 3=a).") + "value"_a) .def_prop_rw( - "packed", [](const math::Rgba8& rgba) { return rgba.packed(); }, [](math::Rgba8& rgba, uint32_t packed) { rgba.packed() = packed; }, - "The raw 32-bit packed RGBA representation.") + "packed", [](const math::Rgba8& rgba) { return rgba.packed(); }, [](math::Rgba8& rgba, uint32_t packed) { rgba.packed() = packed; }) .def_prop_rw( - "r", [](const math::Rgba8& rgba) { return rgba.r(); }, [](math::Rgba8& rgba, uint8_t r) { rgba.r() = r; }, - "Red channel as a uint8 0..255.") + "r", [](const math::Rgba8& rgba) { return rgba.r(); }, [](math::Rgba8& rgba, uint8_t r) { rgba.r() = r; }) .def_prop_rw( - "g", [](const math::Rgba8& rgba) { return rgba.g(); }, [](math::Rgba8& rgba, uint8_t g) { rgba.g() = g; }, - "Green channel as a uint8 0..255.") + "g", [](const math::Rgba8& rgba) { return rgba.g(); }, [](math::Rgba8& rgba, uint8_t g) { rgba.g() = g; }) .def_prop_rw( - "b", [](const math::Rgba8& rgba) { return rgba.b(); }, [](math::Rgba8& rgba, uint8_t b) { rgba.b() = b; }, - "Blue channel as a uint8 0..255.") + "b", [](const math::Rgba8& rgba) { return rgba.b(); }, [](math::Rgba8& rgba, uint8_t b) { rgba.b() = b; }) .def_prop_rw( - "a", [](const math::Rgba8& rgba) { return rgba.a(); }, [](math::Rgba8& rgba, uint8_t a) { rgba.a() = a; }, - "Alpha channel as a uint8 0..255.") - .def("asVec3f", [](const math::Rgba8& rgba) { return Vec3f(rgba); }, - "Convert RGB channels to a Vec3f of 0..1 floats (alpha dropped).") - .def("asVec4f", [](const math::Rgba8& rgba) { return Vec4f(rgba); }, - "Convert RGBA channels to a Vec4f of 0..1 floats."); + "a", [](const math::Rgba8& rgba) { return rgba.a(); }, [](math::Rgba8& rgba, uint8_t a) { rgba.a() = a; }) + .def("asVec3f", [](const math::Rgba8& rgba) { return Vec3f(rgba); }) + .def("asVec4f", [](const math::Rgba8& rgba) { return Vec4f(rgba); }); } template void defineBaseBBox(nb::module_& m, const char* name) { - nb::class_>(m, name, - "Axis-aligned bounding-box base class. Stores a min / max corner; " - "concrete subclasses add the open-interval vs closed-interval semantics.") - .def(nb::self == nb::self, "rhs"_a, - "Equality of both min and max corners.") - .def(nb::self != nb::self, "rhs"_a, - "Inequality of either min or max corner.") + nb::class_>(m, name) + .def(nb::self == nb::self, "rhs"_a) + .def(nb::self != nb::self, "rhs"_a) .def( "__getitem__", [](const math::BaseBBox& bbox, size_t i) { @@ -411,8 +284,7 @@ template void defineBaseBBox(nb::module_& m, const char* name) } return bbox[static_cast(i)]; }, - "i"_a, - "Read corner 0 (min) or corner 1 (max).") + "i"_a) .def( "__setitem__", [](math::BaseBBox& bbox, size_t i, const Vec3T& value) { @@ -422,45 +294,29 @@ template void defineBaseBBox(nb::module_& m, const char* name) bbox[static_cast(i)] = value; }, "i"_a, - "value"_a, - "Write corner 0 (min) or corner 1 (max).") + "value"_a) .def_prop_rw( - "min", [](const math::BaseBBox& bbox) { return bbox.min(); }, [](math::BaseBBox& bbox, const Vec3T& min) { bbox.min() = min; }, - "Minimum corner of the bounding box.") + "min", [](const math::BaseBBox& bbox) { return bbox.min(); }, [](math::BaseBBox& bbox, const Vec3T& min) { bbox.min() = min; }) .def_prop_rw( - "max", [](const math::BaseBBox& bbox) { return bbox.max(); }, [](math::BaseBBox& bbox, const Vec3T& max) { bbox.max() = max; }, - "Maximum corner of the bounding box.") - .def("translate", &math::BaseBBox::translate, "xyz"_a, - "Translate this bounding box by xyz in place.") - .def("expand", nb::overload_cast(&math::BaseBBox::expand), "xyz"_a, - "Grow this bounding box to include the point xyz.") - .def("expand", nb::overload_cast&>(&math::BaseBBox::expand), "bbox"_a, - "Grow this bounding box to include another bbox in its entirety.") - .def("intersect", &math::BaseBBox::intersect, "bbox"_a, - "Shrink this bounding box to the intersection with bbox.") - .def("isInside", &math::BaseBBox::isInside, "xyz"_a, - "True iff xyz lies inside this bounding box."); + "max", [](const math::BaseBBox& bbox) { return bbox.max(); }, [](math::BaseBBox& bbox, const Vec3T& max) { bbox.max() = max; }) + .def("translate", &math::BaseBBox::translate, "xyz"_a) + .def("expand", nb::overload_cast(&math::BaseBBox::expand), "xyz"_a) + .def("expand", nb::overload_cast&>(&math::BaseBBox::expand), "bbox"_a) + .def("intersect", &math::BaseBBox::intersect, "bbox"_a) + .def("isInside", &math::BaseBBox::isInside, "xyz"_a); } template void defineBBoxFloatingPoint(nb::module_& m, const char* name, const char* doc) { nb::class_, math::BaseBBox>(m, name, doc) - .def(nb::init<>(), - "Construct an empty bounding box (min > max sentinel).") - .def(nb::init(), "min"_a, "max"_a, - "Construct from explicit min and max corners.") - .def(nb::init(), "min"_a, "max"_a, - "Construct from integer Coord corners, cast to floating-point.") - .def_static("createCube", &math::BBox::createCube, "min"_a, "dim"_a, - "Construct an axis-aligned cube of side dim anchored at min.") - .def(nb::init&>(), "bbox"_a, - "Construct from an integer CoordBBox, cast to floating-point.") - .def("empty", &math::BBox::empty, - "True iff this bbox is empty (any min component > the matching max).") - .def("dim", &math::BBox::dim, - "Return max - min as a Vec3 of side lengths.") - .def("isInside", &math::BBox::isInside, "p"_a, - "True iff p lies inside this bounding box (half-open interval).") + .def(nb::init<>()) + .def(nb::init(), "min"_a, "max"_a) + .def(nb::init(), "min"_a, "max"_a) + .def_static("createCube", &math::BBox::createCube, "min"_a, "dim"_a) + .def(nb::init&>(), "bbox"_a) + .def("empty", &math::BBox::empty) + .def("dim", &math::BBox::dim) + .def("isInside", &math::BBox::isInside, "p"_a) .def("__repr__", [](const math::BBox& b) { std::stringstream ostr; ostr << b; @@ -473,39 +329,24 @@ template void defineBBoxInteger(nb::module_& m, const char* nam using ValueType = typename CoordT::ValueType; nb::class_, math::BaseBBox>(m, name, doc) - .def(nb::init<>(), - "Construct an empty CoordBBox (min > max sentinel).") - .def(nb::init(), "min"_a, "max"_a, - "Construct from explicit min and max Coord corners (inclusive).") + .def(nb::init<>()) + .def(nb::init(), "min"_a, "max"_a) .def( "__iter__", [](const math::BBox& b) { return nb::make_iterator(nb::type>(), "CoordBBoxIterator", b.begin(), b.end()); }, - nb::keep_alive<0, 1>(), - "Iterate over every Coord in this CoordBBox in row-major order.") - .def_static("createCube", nb::overload_cast(&math::BBox::createCube), "min"_a, "dim"_a, - "Construct a cube of side dim voxels anchored at the min Coord.") - .def_static("createCube", nb::overload_cast(&math::BBox::createCube), "min"_a, "max"_a, - "Construct a cube spanning [min, max] in every axis.") - .def("is_divisible", &math::BBox::is_divisible, - "True iff this CoordBBox has more than one voxel in every axis.") - .def("empty", &math::BBox::empty, - "True iff this CoordBBox is empty (any min component > the matching max).") - .def("dim", &math::BBox::dim, - "Return max - min + 1 as a Coord of side lengths (inclusive).") - .def("volume", &math::BBox::volume, - "Total number of voxels enclosed by this CoordBBox.") - .def("isInside", nb::overload_cast(&math::BBox::isInside, nb::const_), "p"_a, - "True iff the integer point p lies inside this CoordBBox.") - .def("isInside", nb::overload_cast&>(&math::BBox::isInside, nb::const_), "b"_a, - "True iff b lies entirely inside this CoordBBox.") - .def("asFloat", &math::BBox::template asReal, - "Convert this CoordBBox to a floating-point BBox of Vec3f.") - .def("asDouble", &math::BBox::template asReal, - "Convert this CoordBBox to a floating-point BBox of Vec3d.") - .def("hasOverlap", &math::BBox::hasOverlap, "b"_a, - "True iff this CoordBBox shares any voxel with b.") - .def("expandBy", &math::BBox::expandBy, "padding"_a, - "Grow this CoordBBox by the given padding in every direction.") + nb::keep_alive<0, 1>()) + .def_static("createCube", nb::overload_cast(&math::BBox::createCube), "min"_a, "dim"_a) + .def_static("createCube", nb::overload_cast(&math::BBox::createCube), "min"_a, "max"_a) + .def("is_divisible", &math::BBox::is_divisible) + .def("empty", &math::BBox::empty) + .def("dim", &math::BBox::dim) + .def("volume", &math::BBox::volume) + .def("isInside", nb::overload_cast(&math::BBox::isInside, nb::const_), "p"_a) + .def("isInside", nb::overload_cast&>(&math::BBox::isInside, nb::const_), "b"_a) + .def("asFloat", &math::BBox::template asReal) + .def("asDouble", &math::BBox::template asReal) + .def("hasOverlap", &math::BBox::hasOverlap, "b"_a) + .def("expandBy", &math::BBox::expandBy, "padding"_a) .def("__repr__", [](const CoordBBox& b) { std::stringstream ostr; ostr << b; @@ -536,12 +377,33 @@ void defineMathModule(nb::module_& m) defineBaseBBox(m, "CoordBaseBBox"); defineBBoxInteger(m, "CoordBBox", "Bounding box for Coord minimum and maximum"); -#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \ - defineNearestNeighborSampler(m, #Suffix "NearestNeighborSampler"); \ - defineTrilinearSampler(m, #Suffix "TrilinearSampler"); \ - defineTriquadraticSampler(m, #Suffix "TriquadraticSampler"); \ - defineTricubicSampler(m, #Suffix "TricubicSampler"); -#include "BuildTypes.def" + defineNearestNeighborSampler(m, "FloatNearestNeighborSampler"); + defineTrilinearSampler(m, "FloatTrilinearSampler"); + defineTriquadraticSampler(m, "FloatTriquadraticSampler"); + defineTricubicSampler(m, "FloatTricubicSampler"); + + defineNearestNeighborSampler(m, "DoubleNearestNeighborSampler"); + defineTrilinearSampler(m, "DoubleTrilinearSampler"); + defineTriquadraticSampler(m, "DoubleTriquadraticSampler"); + defineTricubicSampler(m, "DoubleTricubicSampler"); + + defineNearestNeighborSampler(m, "Int32NearestNeighborSampler"); + defineTrilinearSampler(m, "Int32TrilinearSampler"); + defineTriquadraticSampler(m, "Int32TriquadraticSampler"); + defineTricubicSampler(m, "Int32TricubicSampler"); + + defineNearestNeighborSampler(m, "Vec3fNearestNeighborSampler"); + defineTrilinearSampler(m, "Vec3fTrilinearSampler"); + defineTriquadraticSampler(m, "Vec3fTriquadraticSampler"); + defineTricubicSampler(m, "Vec3fTricubicSampler"); + +#ifdef NANOVDB_USE_CUDA + nb::module_ cudaModule = m.def_submodule("cuda"); + cudaModule.doc() = "A submodule that implements CUDA-accelerated math functions"; + + defineSampleFromVoxels(cudaModule, "sampleFromVoxels"); + defineSampleFromVoxels(cudaModule, "sampleFromVoxels"); +#endif } } // namespace pynanovdb diff --git a/nanovdb/nanovdb/python/PyPrimitives.cc b/nanovdb/nanovdb/python/PyPrimitives.cc index e061435af9..29053d4e68 100644 --- a/nanovdb/nanovdb/python/PyPrimitives.cc +++ b/nanovdb/nanovdb/python/PyPrimitives.cc @@ -32,9 +32,11 @@ GridHandle createLevelSetSphere(GridType gridType, switch (gridType) { case GridType::Float: return createLevelSetSphere(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); case GridType::Double: return createLevelSetSphere(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - default: - throw std::runtime_error( - "createLevelSetSphere: only float and double grid types are supported"); + default: { + std::stringstream ss; + // ss << "Cannot createLevelSetSphere for grid of type \"" << toStr(gridType); + throw std::runtime_error(ss.str() + "\""); + } } } @@ -56,9 +58,11 @@ GridHandle createLevelSetTorus(GridType gridType, return createLevelSetTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); case GridType::Double: return createLevelSetTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - default: - throw std::runtime_error( - "createLevelSetTorus: only float and double grid types are supported"); + default: { + std::stringstream ss; + // ss << "Cannot createLevelSetTorus for grid of type \"" << toStr(gridType); + throw std::runtime_error(ss.str() + "\""); + } } } @@ -77,9 +81,11 @@ GridHandle createFogVolumeSphere(GridType gridType, switch (gridType) { case GridType::Float: return createFogVolumeSphere(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); case GridType::Double: return createFogVolumeSphere(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - default: - throw std::runtime_error( - "createFogVolumeSphere: only float and double grid types are supported"); + default: { + std::stringstream ss; + // ss << "Cannot createFogVolumeSphere for grid of type \"" << toStr(gridType); + throw std::runtime_error(ss.str() + "\""); + } } } @@ -101,218 +107,14 @@ GridHandle createFogVolumeTorus(GridType gridType, return createFogVolumeTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); case GridType::Double: return createFogVolumeTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - default: - throw std::runtime_error( - "createFogVolumeTorus: only float and double grid types are supported"); - } -} - -// ---------- New primitives ---------- -// Same float/double switch pattern as the four existing primitives above. -// The C++ templates also accept Fp4/Fp8/Fp16/FpN; those flavors live on -// the createNanoGrid path with explicit oracle and dither parameters -// rather than being expressed as primitive overloads here. - -template -GridHandle createLevelSetBox(GridType gridType, - double width, - double height, - double depth, - const Vec3d& center, - double voxelSize, - double halfWidth, - const Vec3d& origin, - const std::string& name, - tools::StatsMode sMode, - CheckMode cMode, - const BufferT& buffer) -{ - switch (gridType) { - case GridType::Float: - return tools::createLevelSetBox( - width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - case GridType::Double: - return tools::createLevelSetBox( - width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - default: - throw std::runtime_error( - "createLevelSetBox: only float and double grid types are supported"); - } -} - -template -GridHandle createLevelSetBBox(GridType gridType, - double width, - double height, - double depth, - double thickness, - const Vec3d& center, - double voxelSize, - double halfWidth, - const Vec3d& origin, - const std::string& name, - tools::StatsMode sMode, - CheckMode cMode, - const BufferT& buffer) -{ - switch (gridType) { - case GridType::Float: - return tools::createLevelSetBBox( - width, height, depth, thickness, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - case GridType::Double: - return tools::createLevelSetBBox( - width, height, depth, thickness, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - default: - throw std::runtime_error( - "createLevelSetBBox: only float and double grid types are supported"); - } -} - -template -GridHandle createLevelSetOctahedron(GridType gridType, - double scale, - const Vec3d& center, - double voxelSize, - double halfWidth, - const Vec3d& origin, - const std::string& name, - tools::StatsMode sMode, - CheckMode cMode, - const BufferT& buffer) -{ - switch (gridType) { - case GridType::Float: - return tools::createLevelSetOctahedron( - scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - case GridType::Double: - return tools::createLevelSetOctahedron( - scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - default: - throw std::runtime_error( - "createLevelSetOctahedron: only float and double grid types are supported"); - } -} - -template -GridHandle createFogVolumeBox(GridType gridType, - double width, - double height, - double depth, - const Vec3d& center, - double voxelSize, - double halfWidth, - const Vec3d& origin, - const std::string& name, - tools::StatsMode sMode, - CheckMode cMode, - const BufferT& buffer) -{ - switch (gridType) { - case GridType::Float: - return tools::createFogVolumeBox( - width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - case GridType::Double: - return tools::createFogVolumeBox( - width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - default: - throw std::runtime_error( - "createFogVolumeBox: only float and double grid types are supported"); + default: { + std::stringstream ss; + // ss << "Cannot createFogVolumeTorus for grid of type \"" << toStr(gridType); + throw std::runtime_error(ss.str() + "\""); } -} - -template -GridHandle createFogVolumeOctahedron(GridType gridType, - double scale, - const Vec3d& center, - double voxelSize, - double halfWidth, - const Vec3d& origin, - const std::string& name, - tools::StatsMode sMode, - CheckMode cMode, - const BufferT& buffer) -{ - switch (gridType) { - case GridType::Float: - return tools::createFogVolumeOctahedron( - scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - case GridType::Double: - return tools::createFogVolumeOctahedron( - scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - default: - throw std::runtime_error( - "createFogVolumeOctahedron: only float and double grid types are supported"); } } -// Point primitives. The result is always a PointDataGrid (uint32 storage), -// so unlike the level-set / fog-volume primitives there's no value-type -// dispatch worth exposing — the intermediate level-set's precision is -// not user-controllable in this binding. The C++ template also accepts -// BuildT=double, but that path segfaults during scatter at least with the -// current C++ implementation, so the binding stays on the float-only -// instantiation that's exercised by the C++ unit tests. -template -GridHandle createPointSphere(int pointsPerVoxel, - double radius, - const Vec3d& center, - double voxelSize, - const Vec3d& origin, - const std::string& name, - CheckMode mode, - const BufferT& buffer) -{ - return tools::createPointSphere( - pointsPerVoxel, radius, center, voxelSize, origin, name, mode, buffer); -} - -template -GridHandle createPointTorus(int pointsPerVoxel, - double majorRadius, - double minorRadius, - const Vec3d& center, - double voxelSize, - const Vec3d& origin, - const std::string& name, - CheckMode cMode, - const BufferT& buffer) -{ - return tools::createPointTorus( - pointsPerVoxel, majorRadius, minorRadius, center, voxelSize, origin, name, cMode, buffer); -} - -template -GridHandle createPointBox(int pointsPerVoxel, - double width, - double height, - double depth, - const Vec3d& center, - double voxelSize, - const Vec3d& origin, - const std::string& name, - CheckMode mode, - const BufferT& buffer) -{ - return tools::createPointBox( - pointsPerVoxel, width, height, depth, center, voxelSize, origin, name, mode, buffer); -} - -// createPointScatter takes an existing level set as its source. We bind -// the float source variant — the C++ template also accepts double, but -// the existing primitives and tests use float, and the source grid is -// the runtime-typed nanovdb::NanoGrid, so a single overload keeps -// the Python surface simple. -template -GridHandle createPointScatter(const NanoGrid& srcGrid, - int pointsPerVoxel, - const std::string& name, - CheckMode mode, - const BufferT& buffer) -{ - return tools::createPointScatter( - srcGrid, pointsPerVoxel, name, mode, buffer); -} - } // namespace template void definePrimitives(nb::module_& m) @@ -329,8 +131,7 @@ template void definePrimitives(nb::module_& m) "name"_a = "sphere_ls", "sMode"_a = tools::StatsMode::Default, "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), - "Narrow-band level set of a sphere of the given radius and center."); + "buffer"_a = BufferT()); m.def("createLevelSetTorus", nb::overload_cast void definePrimitives(nb::module_& m) "name"_a = "torus_ls", "sMode"_a = tools::StatsMode::Default, "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), - "Narrow-band level set of a torus with the given major and minor radii."); + "buffer"_a = BufferT()); m.def("createFogVolumeSphere", nb::overload_cast( @@ -369,8 +169,7 @@ template void definePrimitives(nb::module_& m) "name"_a = "sphere_fog", "sMode"_a = tools::StatsMode::Default, "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), - "Sparse fog volume of a sphere of the given radius and center."); + "buffer"_a = BufferT()); m.def("createFogVolumeTorus", nb::overload_cast void definePrimitives(nb::module_& m) "name"_a = "torus_fog", "sMode"_a = tools::StatsMode::Default, "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), - "Sparse fog volume of a torus with the given major and minor radii."); - - // ---------- Level-set / fog-volume box / bbox / octahedron primitives ---- - m.def("createLevelSetBox", &createLevelSetBox, - "gridType"_a = GridType::Float, - "width"_a = 40.0, - "height"_a = 60.0, - "depth"_a = 100.0, - "center"_a = Vec3d(0.0), - "voxelSize"_a = 1.0, - "halfWidth"_a = 3.0, - "origin"_a = Vec3d(0.0), - "name"_a = "box_ls", - "sMode"_a = tools::StatsMode::Default, - "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), - "Narrow-band level set of an axis-aligned box."); - - m.def("createLevelSetBBox", &createLevelSetBBox, - "gridType"_a = GridType::Float, - "width"_a = 40.0, - "height"_a = 60.0, - "depth"_a = 100.0, - "thickness"_a = 10.0, - "center"_a = Vec3d(0.0), - "voxelSize"_a = 1.0, - "halfWidth"_a = 3.0, - "origin"_a = Vec3d(0.0), - "name"_a = "bbox_ls", - "sMode"_a = tools::StatsMode::Default, - "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), - "Narrow-band level set of a hollow box wireframe (BBox = bounding " - "box edges with the given thickness)."); - - m.def("createLevelSetOctahedron", &createLevelSetOctahedron, - "gridType"_a = GridType::Float, - "scale"_a = 100.0, - "center"_a = Vec3d(0.0), - "voxelSize"_a = 1.0, - "halfWidth"_a = 3.0, - "origin"_a = Vec3d(0.0), - // Default name spells the shape correctly even though the - // upstream C++ default still carries the historical - // "octadedron_ls" typo. Callers can override either way. - "name"_a = "octahedron_ls", - "sMode"_a = tools::StatsMode::Default, - "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), - "Narrow-band level set of an octahedron."); - - m.def("createFogVolumeBox", &createFogVolumeBox, - "gridType"_a = GridType::Float, - "width"_a = 40.0, - "height"_a = 60.0, - "depth"_a = 100.0, - "center"_a = Vec3d(0.0), - "voxelSize"_a = 1.0, - "halfWidth"_a = 3.0, - "origin"_a = Vec3d(0.0), - "name"_a = "box_fog", - "sMode"_a = tools::StatsMode::Default, - "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), - "Sparse fog volume of a box (exterior 0/inactive, interior active " - "with values smoothly varying from 0 at the surface to 1 inside)."); - - m.def("createFogVolumeOctahedron", &createFogVolumeOctahedron, - "gridType"_a = GridType::Float, - "scale"_a = 100.0, - "center"_a = Vec3d(0.0), - "voxelSize"_a = 1.0, - "halfWidth"_a = 3.0, - "origin"_a = Vec3d(0.0), - "name"_a = "octahedron_fog", - "sMode"_a = tools::StatsMode::Default, - "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), - "Sparse fog volume of an octahedron."); - - // ---------- Point primitives ---------- - m.def("createPointSphere", &createPointSphere, - "pointsPerVoxel"_a = 1, - "radius"_a = 100.0, - "center"_a = Vec3d(0.0), - "voxelSize"_a = 1.0, - "origin"_a = Vec3d(0.0), - "name"_a = "sphere_points", - "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), - "PointDataGrid of points scattered on the surface of a sphere. " - "The output grid is always a UInt32 PointDataGrid; the " - "intermediate level-set's value type is hard-coded to float."); - - m.def("createPointTorus", &createPointTorus, - "pointsPerVoxel"_a = 1, - "majorRadius"_a = 100.0, - "minorRadius"_a = 50.0, - "center"_a = Vec3d(0.0), - "voxelSize"_a = 1.0, - "origin"_a = Vec3d(0.0), - "name"_a = "torus_points", - "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), - "PointDataGrid of points scattered on the surface of a torus. " - "Always returns a UInt32 PointDataGrid."); - - m.def("createPointBox", &createPointBox, - "pointsPerVoxel"_a = 1, - "width"_a = 40.0, - "height"_a = 60.0, - "depth"_a = 100.0, - "center"_a = Vec3d(0.0), - "voxelSize"_a = 1.0, - "origin"_a = Vec3d(0.0), - "name"_a = "box_points", - "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), - "PointDataGrid of points scattered on the surface of a box. " - "Always returns a UInt32 PointDataGrid."); - - m.def("createPointScatter", &createPointScatter, - "srcGrid"_a, - "pointsPerVoxel"_a = 1, - "name"_a = "point_scatter", - "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), - "Scatter a PointDataGrid into the active voxels of a " - "NanoGrid level set. The source grid must satisfy " - "srcGrid.isLevelSet() and have an active bounding box; " - "non-level-set sources (e.g. fog volumes) raise RuntimeError. " - "Point coordinates are stored as blind data in world space."); + "buffer"_a = BufferT()); } template void definePrimitives(nb::module_&); diff --git a/nanovdb/nanovdb/python/PySampleFromVoxels.cc b/nanovdb/nanovdb/python/PySampleFromVoxels.cc index 23816e776c..82dc9a26d1 100644 --- a/nanovdb/nanovdb/python/PySampleFromVoxels.cc +++ b/nanovdb/nanovdb/python/PySampleFromVoxels.cc @@ -15,53 +15,20 @@ namespace { template void defineSampleFromVoxels(nb::module_& m, const char* name) { - using CoordT = typename TreeT::CoordType; - using ValueT = typename TreeT::ValueType; - using SamplerT = math::SampleFromVoxels; - auto cls = nb::class_(m, name, - "Callable sampler that reconstructs a grid value at an arbitrary " - "index-space position. Build via the matching create*Sampler() factory.") + using CoordT = typename TreeT::CoordType; + nb::class_>(m, name) .def( - "__call__", [](const SamplerT& sampler, const CoordT& ijk) { return sampler(ijk); }, nb::is_operator(), "ijk"_a, - "Sample the grid at integer voxel coordinate ijk.") + "__call__", [](const math::SampleFromVoxels& sampler, const CoordT& ijk) { return sampler(ijk); }, nb::is_operator(), "ijk"_a) .def( - "__call__", [](const SamplerT& sampler, const Vec3f& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a, - "Sample the grid at fractional index-space position xyz.") + "__call__", [](const math::SampleFromVoxels& sampler, const Vec3f& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a) .def( - "__call__", [](const SamplerT& sampler, const Vec3d& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a, - "Sample the grid at fractional index-space position xyz (double)."); - // gradient() exists on the trilinear sampler only, zeroCrossing() on the - // trilinear and triquadratic samplers, and both static_assert a - // floating-point ValueT in C++ — mirror that gating here. - if constexpr (Order == 1 && util::is_floating_point::value) { - cls.def( - "gradient", [](const SamplerT& sampler, const Vec3f& xyz) { return sampler.gradient(xyz); }, "xyz"_a, - "Return the index-space gradient of the trilinear reconstruction " - "at fractional index-space position xyz. Use " - "grid.indexToWorldGrad() to move it to world space.") - .def( - "gradient", [](const SamplerT& sampler, const Vec3d& xyz) { return sampler.gradient(xyz); }, "xyz"_a, - "Return the index-space gradient at fractional index-space " - "position xyz (double)."); - } - if constexpr ((Order == 1 || Order == 2) && util::is_floating_point::value) { - cls.def( - "zeroCrossing", [](const SamplerT& sampler, const Vec3f& xyz) { return sampler.zeroCrossing(xyz); }, "xyz"_a, - "True iff the reconstruction stencil at fractional index-space " - "position xyz straddles the zero iso-surface.") - .def( - "zeroCrossing", [](const SamplerT& sampler, const Vec3d& xyz) { return sampler.zeroCrossing(xyz); }, "xyz"_a, - "True iff the reconstruction stencil at fractional index-space " - "position xyz (double) straddles the zero iso-surface."); - } + "__call__", [](const math::SampleFromVoxels& sampler, const Vec3d& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a); } template void defineCreateSampler(nb::module_& m, const char* name) { m.def( - name, [](const Grid& grid) { return math::createSampler(grid.tree()); }, "grid"_a, - "Build a sampler of the matching order that reads values from the " - "given grid's tree."); + name, [](const Grid& grid) { return math::createSampler(grid.tree()); }, "grid"_a); } } // namespace @@ -98,11 +65,24 @@ template void defineTricubicSampler(nb::module_& m, const char* defineCreateSampler(m, "createTricubicSampler"); } -#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \ - template void defineNearestNeighborSampler(nb::module_&, const char*); \ - template void defineTrilinearSampler(nb::module_&, const char*); \ - template void defineTriquadraticSampler(nb::module_&, const char*); \ - template void defineTricubicSampler(nb::module_&, const char*); -#include "BuildTypes.def" +template void defineNearestNeighborSampler(nb::module_&, const char*); +template void defineTrilinearSampler(nb::module_&, const char*); +template void defineTriquadraticSampler(nb::module_&, const char*); +template void defineTricubicSampler(nb::module_&, const char*); + +template void defineNearestNeighborSampler(nb::module_&, const char*); +template void defineTrilinearSampler(nb::module_&, const char*); +template void defineTriquadraticSampler(nb::module_&, const char*); +template void defineTricubicSampler(nb::module_&, const char*); + +template void defineNearestNeighborSampler(nb::module_&, const char*); +template void defineTrilinearSampler(nb::module_&, const char*); +template void defineTriquadraticSampler(nb::module_&, const char*); +template void defineTricubicSampler(nb::module_&, const char*); + +template void defineNearestNeighborSampler(nb::module_&, const char*); +template void defineTrilinearSampler(nb::module_&, const char*); +template void defineTriquadraticSampler(nb::module_&, const char*); +template void defineTricubicSampler(nb::module_&, const char*); } // namespace pynanovdb diff --git a/nanovdb/nanovdb/python/PyTools.cc b/nanovdb/nanovdb/python/PyTools.cc index 9aa8f93452..4df996d557 100644 --- a/nanovdb/nanovdb/python/PyTools.cc +++ b/nanovdb/nanovdb/python/PyTools.cc @@ -16,7 +16,6 @@ #include "PyNanoToOpenVDB.h" #ifdef NANOVDB_USE_CUDA #include "cuda/PyPointsToGrid.h" -#include "cuda/PySampleFromVoxels.h" #include "cuda/PySignedFloodFill.h" #endif @@ -32,17 +31,12 @@ void defineToolsModule(nb::module_& m) defineStatsMode(m); - defineGridStatsModule(m); - defineGridValidatorModule(m); - defineEvalChecksumModule(m); - - defineCreateNanoGridConversions(m); - definePrimitives(m); -#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \ - defineCreateNanoGrid(m, "create" #Suffix "Grid"); -#include "BuildTypes.def" + defineCreateNanoGrid(m, "createFloatGrid"); + defineCreateNanoGrid(m, "createDoubleGrid"); + defineCreateNanoGrid(m, "createInt32Grid"); + defineCreateNanoGrid(m, "createVec3fGrid"); #ifdef NANOVDB_USE_OPENVDB defineOpenToNanoVDB(m); @@ -61,9 +55,6 @@ void defineToolsModule(nb::module_& m) defineSignedFloodFill(cudaModule, "signedFloodFill"); definePointsToGrid(cudaModule, "pointsToRGBA8Grid"); - - defineSampleFromVoxels(cudaModule, "sampleFromVoxels"); - defineSampleFromVoxels(cudaModule, "sampleFromVoxels"); #endif } diff --git a/nanovdb/nanovdb/python/PyTree.cc b/nanovdb/nanovdb/python/PyTree.cc deleted file mode 100644 index 64eb7533b7..0000000000 --- a/nanovdb/nanovdb/python/PyTree.cc +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: Apache-2.0 -#include "PyTree.h" - -#include - -namespace nb = nanobind; -using namespace nb::literals; -using namespace nanovdb; - -namespace pynanovdb { - -// Polymorphic mgr() that returns the right typed NodeManager based on the -// handle's stored gridType. Dispatch follows the same X-macro pattern as -// pyHostGrid / pyDeviceGrid; unbound BuildTs return None rather than the -// generic getMgr() ptr that would be reinterpreted. -template -static nb::object pyNodeMgr(nb::handle py_self) -{ - using HandleT = NodeManagerHandle; - auto& handle = nb::cast(py_self); - if (!handle.data()) return nb::none(); - // We need to read the stored gridType, but it's private. The public - // mgr() returns NULL for type mismatch, so iterate by BuildT. - // The X-macro produces one case per bound BuildT; first non-null wins. -#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \ - if (auto* m = handle.template mgr()) { \ - return nb::cast(m, nb::rv_policy::reference, py_self); \ - } -#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \ - if (auto* m = handle.template mgr()) { \ - return nb::cast(m, nb::rv_policy::reference, py_self); \ - } -#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \ - if (auto* m = handle.template mgr()) { \ - return nb::cast(m, nb::rv_policy::reference, py_self); \ - } -#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \ - if (auto* m = handle.template mgr()) { \ - return nb::cast(m, nb::rv_policy::reference, py_self); \ - } -#include "BuildTypes.def" - return nb::none(); -} - -void defineNodeManagerHandle(nb::module_& m) -{ - using HandleT = NodeManagerHandle; - nb::class_(m, "NodeManagerHandle", - "Owns the memory backing a NodeManager. Move-only. " - "Obtain via nanovdb.createNodeManager(grid).") - .def("size", - [](const HandleT& h) { return h.size(); }, - "Byte size of the buffer backing this NodeManagerHandle.") - .def( - "__bool__", - [](const HandleT& h) { return h.data() != nullptr; }, - nb::is_operator(), - "True iff this handle owns a non-empty buffer.") - .def("mgr", &pyNodeMgr, - nb::keep_alive<0, 1>(), - "Return the typed NodeManager for the grid this handle was " - "built from, or None if the BuildT is not Python-visible. The " - "returned NodeManager keeps this handle alive."); -} - -// createNodeManager has one template instantiation per BuildT. We expose a -// single polymorphic `createNodeManager(grid)` that picks the right one -// based on the runtime type of `grid` (any nb::class_-bound NanoGrid). -// nb::isinstance is a fast type check that avoids the exception-on-mismatch -// overhead that would come from trying nb::cast and catching cast_error for -// every non-matching BuildT. -template -static nb::object tryCreateNodeManager(nb::handle py_grid) -{ - using GridT = NanoGrid; - if (!nb::isinstance(py_grid)) { - return nb::object(); // sentinel: "not this BuildT, try next" - } - auto& grid = nb::cast(py_grid); - return nb::cast(createNodeManager(grid)); -} - -void defineCreateNodeManager(nb::module_& m) -{ - m.def("createNodeManager", - [](nb::handle py_grid) -> nb::object { - // Try every bound BuildT; first successful cast wins. -#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \ - if (auto obj = tryCreateNodeManager(py_grid); obj.is_valid()) { \ - return obj; \ - } -#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \ - if (auto obj = tryCreateNodeManager(py_grid); obj.is_valid()) { \ - return obj; \ - } -#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \ - if (auto obj = tryCreateNodeManager(py_grid); obj.is_valid()) { \ - return obj; \ - } -#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \ - if (auto obj = tryCreateNodeManager(py_grid); obj.is_valid()) { \ - return obj; \ - } -#include "BuildTypes.def" - throw nb::type_error( - "createNodeManager: argument is not a NanoVDB grid of any " - "bound BuildT"); - }, - "grid"_a, - // The constructed NodeManager stores a raw pointer back to the - // grid; the handle must therefore keep the grid (and transitively - // the GridHandle that owns the grid's buffer) alive. - nb::keep_alive<0, 1>(), - "Build a NodeManager for the given grid, returning a " - "NodeManagerHandle that owns the underlying buffer. The handle's " - "mgr() method returns the typed NodeManager. The handle keeps the " - "source grid alive for as long as it lives."); -} - -} // namespace pynanovdb diff --git a/nanovdb/nanovdb/python/PyTree.h b/nanovdb/nanovdb/python/PyTree.h deleted file mode 100644 index f6029317e2..0000000000 --- a/nanovdb/nanovdb/python/PyTree.h +++ /dev/null @@ -1,486 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: Apache-2.0 -#ifndef NANOVDB_PYTREE_HAS_BEEN_INCLUDED -#define NANOVDB_PYTREE_HAS_BEEN_INCLUDED - -#include -#include -#include - -#include -#include -#include - -#include // std::is_arithmetic_v, std::enable_if - -namespace nb = nanobind; - -namespace pynanovdb { - -// -------------------- NanoLeaf -------------------- -// -// Binds the 8^3 leaf node. Methods that don't depend on whether the leaf -// stores a contiguous T[512] array are bound unconditionally; the -// zero-copy 512-element NumPy values() view is only bound when the leaf -// layout actually carries T mValues[512] (the BuildTraits::is_special -// types use packed / index / mask layouts where a 512-element T view is -// either impossible or misleading). -template void defineNanoLeaf(nb::module_& m, const char* name) -{ - using LeafT = nanovdb::NanoLeaf; - using ValueT = typename LeafT::ValueType; - using CoordT = typename LeafT::CoordType; - - auto cls = nb::class_(m, name, - "Leaf node — 8x8x8 voxels. Inherits stats and bbox from the same " - "leaf-data block bound across BuildTs."); - - cls.def("origin", &LeafT::origin, - "Index-space origin (minimum corner) of this leaf node.") - .def("bbox", &LeafT::bbox, - "Index-space bounding box of this leaf's active voxels.") - .def("hasBBox", &LeafT::hasBBox, - "True iff this leaf carries a cached active bounding box.") - .def_static("dim", &LeafT::dim, - "Side length of a leaf node in voxels (always 8).") - .def_static("voxelCount", &LeafT::voxelCount, - "Total voxel count in a leaf (always 512).") - .def("memUsage", &LeafT::memUsage, - "Byte size of this leaf node.") - .def("isActive", - nb::overload_cast(&LeafT::isActive, nb::const_), - nb::arg("ijk"), - "True iff the voxel at index-space ijk is active.") - .def("isActive", - [](const LeafT& leaf, uint32_t n) { - // Underlying mValueMask.isOn(n) is unchecked; release builds - // skip the C++ NANOVDB_ASSERT and would silently read OOB. - if (n >= LeafT::voxelCount()) { - throw nb::index_error( - "Leaf.isActive(n): n out of range [0, voxelCount)"); - } - return leaf.isActive(n); - }, - nb::arg("n"), - "True iff the n-th voxel (linear index into the 512-element leaf) is active.") - .def("getValue", - [](const LeafT& leaf, uint32_t offset) { - // mValues[offset] is unchecked in C++; guard the Python side. - if (offset >= LeafT::voxelCount()) { - throw nb::index_error( - "Leaf.getValue(offset): offset out of range [0, voxelCount)"); - } - return leaf.getValue(offset); - }, - nb::arg("offset"), - "Return the value at linear offset (0..511) within this leaf.") - .def("getValue", - nb::overload_cast(&LeafT::getValue, nb::const_), - nb::arg("ijk"), - "Return the value at index-space ijk; ijk must lie inside this leaf.") - .def("getFirstValue", &LeafT::getFirstValue, - "Value at the leaf's first voxel (linear offset 0).") - .def("getLastValue", &LeafT::getLastValue, - "Value at the leaf's last voxel (linear offset 511).") - .def("minimum", &LeafT::minimum, - "Minimum active value within this leaf.") - .def("maximum", &LeafT::maximum, - "Maximum active value within this leaf.") - .def("average", &LeafT::average, - "Average of active values within this leaf.") - .def("stdDeviation", &LeafT::stdDeviation, - "Standard deviation of active values within this leaf.") - // NOTE: variance() omitted — NanoVDB.h line 4388 uses unqualified - // Pow2() which fails ADL for non-float ValueTs (ValueIndex / - // ValueMask / etc.). Users can compute it as stdDeviation() ** 2. - .def("flags", &LeafT::flags, - "Raw leaf flag bits.") - .def("valueMask", &LeafT::valueMask, nb::rv_policy::reference_internal, - "Reference to the leaf's 512-bit active-value mask.") - .def("probeValue", - [](const LeafT& leaf, const CoordT& ijk) { - ValueT v; - bool on = leaf.probeValue(ijk, v); - return std::make_tuple(v, on); - }, - nb::arg("ijk"), - "Return (value, isActive) for the voxel at index-space ijk."); - - // Zero-copy 512-element NumPy view of mValues. Only enabled for - // BuildTs whose ValueType is a primitive arithmetic type (float, - // double, int*). For Fp* / Index / Mask / bool / Point the leaf uses - // packed / mask / void layouts. For Vec3f / Vec3d / Vec4f / Vec4d / - // Vec3u8 / Vec3u16 / Rgba8 the leaf carries a struct array that - // nanobind's ndarray can't represent directly — those would need a - // flattened (count, dim) component-typed view which a follow-up can - // add. Users can still walk every leaf via the bound getValue(). - if constexpr (std::is_arithmetic_v - && !nanovdb::BuildTraits::is_special) { - cls.def("values", - [](nb::handle py_self) { - auto& leaf = nb::cast(py_self); - size_t shape[1] = {LeafT::voxelCount()}; - return nb::cast( - nb::ndarray, nb::c_contig, nb::device::cpu>( - static_cast(leaf.data()->mValues), - size_t(1), shape, py_self), - nb::rv_policy::reference); - }, - nb::keep_alive<0, 1>(), - "Return a zero-copy NumPy view of the 512 leaf values. The view " - "keeps the leaf (and transitively the GridHandle that owns the " - "underlying buffer) alive."); - } -} - -// -------------------- NanoUpper / NanoLower -------------------- -// -// Both internal node levels share the same C++ API surface (just different -// LOG2DIM). One helper templated on the concrete InternalNode type. -template -void defineInternalNodeBase(nb::class_& cls) -{ - using ValueT = typename InternalT::ValueType; - using CoordT = typename InternalT::CoordType; - cls.def("origin", &InternalT::origin, - "Index-space origin (minimum corner) of this internal node.") - .def("bbox", &InternalT::bbox, - "Index-space bounding box of this internal node's active voxels.") - .def_static("dim", &InternalT::dim, - "Side length in voxels covered by this internal node.") - .def_static("memUsage", []() { return InternalT::memUsage(); }, - "Byte size of an internal node.") - .def("minimum", &InternalT::minimum, - "Minimum active value within this node's subtree.") - .def("maximum", &InternalT::maximum, - "Maximum active value within this node's subtree.") - .def("average", &InternalT::average, - "Average of active values within this node's subtree.") - .def("stdDeviation", &InternalT::stdDeviation, - "Standard deviation of active values within this node's subtree.") - // variance() omitted for parity with the leaf binding; compute as - // stdDeviation() ** 2 in Python. - .def("valueMask", &InternalT::valueMask, nb::rv_policy::reference_internal, - "Reference to the node's active-tile mask.") - .def("childMask", &InternalT::childMask, nb::rv_policy::reference_internal, - "Reference to the node's child-pointer mask (1 where a child node exists).") - .def("getValue", - nb::overload_cast(&InternalT::getValue, nb::const_), - nb::arg("ijk"), - "Return the value at index-space ijk by descending into the subtree.") - .def("getFirstValue", &InternalT::getFirstValue, - "Value at the first (lowest-indexed) tile in this node.") - .def("getLastValue", &InternalT::getLastValue, - "Value at the last (highest-indexed) tile in this node.") - .def("isActive", - nb::overload_cast(&InternalT::isActive, nb::const_), - nb::arg("ijk"), - "True iff the voxel at index-space ijk is active.") - .def("probeValue", - [](const InternalT& node, const CoordT& ijk) { - ValueT v; - bool on = node.probeValue(ijk, v); - return std::make_tuple(v, on); - }, - nb::arg("ijk"), - "Return (value, isActive) for the voxel at index-space ijk."); -} - -template void defineNanoUpper(nb::module_& m, const char* name) -{ - using UpperT = nanovdb::NanoUpper; - nb::class_ cls(m, name, - "Upper internal node — 32x32x32 (covers a 4096^3 region in index space)."); - defineInternalNodeBase(cls); -} - -template void defineNanoLower(nb::module_& m, const char* name) -{ - using LowerT = nanovdb::NanoLower; - nb::class_ cls(m, name, - "Lower internal node — 16x16x16 (covers a 128^3 region in index space)."); - defineInternalNodeBase(cls); -} - -// -------------------- NanoRoot -------------------- -template void defineNanoRoot(nb::module_& m, const char* name) -{ - using RootT = nanovdb::NanoRoot; - using ValueT = typename RootT::ValueType; - using CoordT = typename RootT::CoordType; - - nb::class_(m, name, "Root node — top of the tree, holds the tile table.") - .def("background", &RootT::background, nb::rv_policy::reference_internal, - "Background value returned for inactive voxels.") - .def("tileCount", &RootT::tileCount, - "Number of active tiles in the root tile table.") - .def("getTableSize", &RootT::getTableSize, - "Total number of entries in the root tile table.") - .def("isEmpty", &RootT::isEmpty, - "True iff the root has no active tiles.") - .def("bbox", &RootT::bbox, nb::rv_policy::reference_internal, - "Index-space bounding box of every active value in this tree.") - .def("minimum", &RootT::minimum, nb::rv_policy::reference_internal, - "Minimum active value in the tree.") - .def("maximum", &RootT::maximum, nb::rv_policy::reference_internal, - "Maximum active value in the tree.") - .def("average", &RootT::average, nb::rv_policy::reference_internal, - "Average of active values in the tree.") - .def("stdDeviation", &RootT::stdDeviation, nb::rv_policy::reference_internal, - "Standard deviation of active values in the tree.") - .def("memUsage", - nb::overload_cast<>(&RootT::memUsage, nb::const_), - "Byte size of the root node and its tile table.") - .def("getValue", - nb::overload_cast(&RootT::getValue, nb::const_), - nb::arg("ijk"), - "Return the value at index-space ijk by walking from the root.") - .def("isActive", - nb::overload_cast(&RootT::isActive, nb::const_), - nb::arg("ijk"), - "True iff the voxel at index-space ijk is active.") - .def("probeValue", - [](const RootT& root, const CoordT& ijk) { - ValueT v; - bool on = root.probeValue(ijk, v); - return std::make_tuple(v, on); - }, - nb::arg("ijk"), - "Return (value, isActive) for the voxel at index-space ijk."); -} - -// -------------------- NanoTree -------------------- -template void defineNanoTree(nb::module_& m, const char* name) -{ - using TreeT = nanovdb::NanoTree; - using ValueT = typename TreeT::ValueType; - using CoordT = typename TreeT::CoordType; - using RootT = typename TreeT::RootType; - using UpperT = typename TreeT::UpperNodeType; - using LowerT = typename TreeT::LowerNodeType; - using LeafT = typename TreeT::LeafNodeType; - - nb::class_(m, name, - "Tree — owns the root and provides bulk metadata queries " - "(node counts, active voxel count, extrema).") - .def("root", - nb::overload_cast<>(&TreeT::root, nb::const_), - nb::rv_policy::reference_internal, - "Root node of this tree. Lifetime is anchored to the tree.") - .def("background", &TreeT::background, nb::rv_policy::reference_internal, - "Background value returned for inactive voxels.") - .def("activeVoxelCount", &TreeT::activeVoxelCount, - "Total number of active voxels in this tree.") - // activeTileCount(level): valid range is 1..3 (lower / upper / root - // tile counts). C++ uses NANOVDB_ASSERT(level > 0 && level <= 3) - // which is a no-op in release builds — so guard explicitly. - .def("activeTileCount", - [](const TreeT& tree, uint32_t level) -> uint32_t { - if (level < 1 || level > 3) { - throw nb::value_error( - "Tree.activeTileCount(level): level must be 1, 2, or 3"); - } - return tree.activeTileCount(level); - }, - nb::arg("level"), - "Number of active tiles at the given tree level (1=lower, 2=upper, 3=root).") - // nodeCount(level): valid range is 0..2 (leaf / lower / upper). - // C++ uses NANOVDB_ASSERT(level < 3), again no-op in release. - // The lambda's `int level` argument disambiguates the call against - // Tree's templated nodeCount() overload at the C++ level, - // so we don't need an overload_cast / static_cast wrapper here. - .def("nodeCount", - [](const TreeT& tree, int level) -> uint32_t { - if (level < 0 || level >= 3) { - throw nb::value_error( - "Tree.nodeCount(level): level must be 0, 1, or 2"); - } - return tree.nodeCount(level); - }, - nb::arg("level"), - "Number of nodes at the given tree level (0=leaf, 1=lower, 2=upper).") - .def("totalNodeCount", &TreeT::totalNodeCount, - "Sum of node counts across every tree level.") - .def_static("memUsage", &TreeT::memUsage, - "Byte size of a single tree structure (header only).") - .def("getValue", - nb::overload_cast(&TreeT::getValue, nb::const_), - nb::arg("ijk"), - "Return the value at index-space ijk by descending from the root.") - .def("isActive", &TreeT::isActive, nb::arg("ijk"), - "True iff the voxel at index-space ijk is active.") - .def("probeValue", - [](const TreeT& tree, const CoordT& ijk) { - ValueT v; - bool on = tree.probeValue(ijk, v); - return std::make_tuple(v, on); - }, - nb::arg("ijk"), - "Return (value, isActive) for the voxel at index-space ijk.") - .def("extrema", - [](const TreeT& tree) { - ValueT mn, mx; - tree.extrema(mn, mx); - return std::make_tuple(mn, mx); - }, - "Return (min, max) of the active values over the whole tree.") - .def("getFirstLeaf", - nb::overload_cast<>(&TreeT::getFirstLeaf, nb::const_), - nb::rv_policy::reference_internal, - "First leaf node in breadth-first order, or None if the tree is empty.") - .def("getFirstLower", - nb::overload_cast<>(&TreeT::getFirstLower, nb::const_), - nb::rv_policy::reference_internal, - "First lower internal node in breadth-first order, or None if none exists.") - .def("getFirstUpper", - nb::overload_cast<>(&TreeT::getFirstUpper, nb::const_), - nb::rv_policy::reference_internal, - "First upper internal node in breadth-first order, or None if none exists."); -} - -// -------------------- NodeManager -------------------- -// -// NodeManager is heap-managed by a NodeManagerHandle (move-only, owns the -// underlying memory). We bind one NodeManager class per BuildT and one -// host-side NodeManagerHandle class. Users get a handle from -// nanovdb.createNodeManager(grid); they then call handle.mgr() to obtain a -// borrowed pointer to the typed NodeManager — its lifetime is anchored to -// the handle via reference_internal. -template void defineNodeManager(nb::module_& m, const char* name) -{ - using NMT = nanovdb::NodeManager; - nb::class_(m, name, - "Sequential breadth-first accessor for the leaf / lower / upper " - "internal nodes of a NanoGrid. Construct via " - "nanovdb.createNodeManager(grid).") - .def("isLinear", - nb::overload_cast<>(&NMT::isLinear, nb::const_), - "True iff this NodeManager is laid out as a linear offset table over a breadth-first grid.") - .def("memUsage", - nb::overload_cast<>(&NMT::memUsage, nb::const_), - "Byte size of this NodeManager.") - .def("nodeCount", - [](const NMT& nm, int level) -> uint64_t { - // Mirror Tree.nodeCount bounds (NodeManager forwards to Tree). - if (level < 0 || level >= 3) { - throw nb::value_error( - "NodeManager.nodeCount(level): level must be 0, 1, or 2"); - } - return nm.nodeCount(level); - }, - nb::arg("level"), - "Number of nodes at the given tree level (0=leaf, 1=lower, 2=upper).") - .def("leafCount", &NMT::leafCount, - "Number of leaf nodes managed by this NodeManager.") - .def("lowerCount", &NMT::lowerCount, - "Number of lower internal nodes managed by this NodeManager.") - .def("upperCount", &NMT::upperCount, - "Number of upper internal nodes managed by this NodeManager.") - // leaf / lower / upper: NANOVDB_ASSERT(i < nodeCount(LEVEL)) in C++ is - // no-op in release, so guard explicitly to convert OOB access into a - // Python IndexError instead of memory corruption. - .def("leaf", - [](const NMT& nm, uint32_t i) -> const nanovdb::NanoLeaf& { - if (i >= nm.leafCount()) { - throw nb::index_error( - "NodeManager.leaf(i): i out of range [0, leafCount)"); - } - return nm.leaf(i); - }, - nb::rv_policy::reference_internal, nb::arg("i"), - "Return the i-th leaf node in breadth-first order.") - .def("lower", - [](const NMT& nm, uint32_t i) -> const nanovdb::NanoLower& { - if (i >= nm.lowerCount()) { - throw nb::index_error( - "NodeManager.lower(i): i out of range [0, lowerCount)"); - } - return nm.lower(i); - }, - nb::rv_policy::reference_internal, nb::arg("i"), - "Return the i-th lower internal node in breadth-first order.") - .def("upper", - [](const NMT& nm, uint32_t i) -> const nanovdb::NanoUpper& { - if (i >= nm.upperCount()) { - throw nb::index_error( - "NodeManager.upper(i): i out of range [0, upperCount)"); - } - return nm.upper(i); - }, - nb::rv_policy::reference_internal, nb::arg("i"), - "Return the i-th upper internal node in breadth-first order."); -} - -void defineNodeManagerHandle(nb::module_& m); -void defineCreateNodeManager(nb::module_& m); - -// -------------------- grid.leaf_values() bulk extractor -------------------- -// -// For non-special BuildTs with breadth-first, fixed-size leaves, the leaf -// values can be reached as a contiguous (N_leaves, 512) array — every leaf -// occupies sizeof(NanoLeaf) bytes and mValues starts at a known offset -// inside each leaf. We bind this on NanoGrid as leaf_values() for -// efficient bulk analytics from Python. -template -struct PyLeafValuesBinder -{ - template static void apply(ClsT&) {} -}; - -template -struct PyLeafValuesBinder::ValueType> - && !nanovdb::BuildTraits::is_special>::type> -{ - template - static void apply(ClsT& cls) - { - using GridT = nanovdb::NanoGrid; - using LeafT = nanovdb::NanoLeaf; - using ValueT = typename LeafT::ValueType; - cls.def("leaf_values", - [](nb::handle py_self) -> nb::object { - auto& grid = nb::cast(py_self); - const auto& tree = grid.tree(); - const uint32_t nLeaves = tree.template nodeCount(); - if (!grid.isBreadthFirst()) { - throw nb::value_error( - "leaf_values() requires a breadth-first grid " - "layout; rebuild via " - "nanovdb.tools.createNanoGrid(...)."); - } - // For an empty grid (no leaves) we still return an ndarray - // — shape (0, 512) — so callers can iterate / np.asarray() - // / shape-test without branching on a None sentinel. - LeafT* first = const_cast(tree.getFirstLeaf()); - size_t shape[2] = {nLeaves, LeafT::voxelCount()}; - int64_t strides[2] = { - static_cast(sizeof(LeafT) / sizeof(ValueT)), - 1 - }; - // first is non-null whenever nLeaves > 0; when nLeaves == 0 - // we pass a dummy non-null aligned pointer (the grid itself) - // so nanobind has something to base the empty array on. - // Nothing will be read since the leading shape is 0. - void* data = (first != nullptr) - ? static_cast(first->data()->mValues) - : static_cast(&grid); - return nb::cast( - nb::ndarray, nb::device::cpu>( - data, size_t(2), shape, py_self, strides), - nb::rv_policy::reference); - }, - nb::keep_alive<0, 1>(), - "Return a zero-copy (N_leaves, 512) NumPy view of every leaf's " - "values, in breadth-first leaf order. Available only for " - "BuildTs whose leaf layout carries T mValues[512] (i.e. not " - "Fp*, Index, Mask, bool, or Point) and only on breadth-first " - "grids. Returns an empty (0, 512) array for grids with no " - "leaves. The view keeps the grid alive."); - } -}; - -} // namespace pynanovdb - -#endif diff --git a/nanovdb/nanovdb/python/PyVoxelBlockManager.cc b/nanovdb/nanovdb/python/PyVoxelBlockManager.cc deleted file mode 100644 index 5e61913050..0000000000 --- a/nanovdb/nanovdb/python/PyVoxelBlockManager.cc +++ /dev/null @@ -1,547 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: Apache-2.0 -#include "PyVoxelBlockManager.h" - -#include -#include - -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace nb = nanobind; -using namespace nb::literals; -using namespace nanovdb; -using nanovdb::tools::VoxelBlockManager; -using nanovdb::tools::VoxelBlockManagerBase; -using nanovdb::tools::VoxelBlockManagerHandle; -using nanovdb::tools::buildVoxelBlockManager; - -namespace pynanovdb { - -// ----------------------- Log2BlockWidth dispatch -------------------------- -// -// Log2BlockWidth is a compile-time template parameter on every VBM helper. -// We expose it to Python as a runtime int and dispatch via a switch that -// instantiates the four useful widths (BlockWidth = 64, 128, 256, 512). -// Larger widths are not bound by default; callers who need them can add a -// new case below. - -template -static auto dispatchLog2BlockWidth(int log2BlockWidth, F&& fn) -{ - switch (log2BlockWidth) { - case 6: return fn(std::integral_constant{}); - case 7: return fn(std::integral_constant{}); - case 8: return fn(std::integral_constant{}); - case 9: return fn(std::integral_constant{}); - default: - throw nb::value_error( - "VoxelBlockManager: log2_block_width must be 6, 7, 8, or 9 " - "(BlockWidth = 64, 128, 256, or 512). Larger widths are not " - "bound in Python by default."); - } -} - -// PyVBMHandle wraps the C++ VoxelBlockManagerHandle and carries the -// log2_block_width the handle was built with. The C++ handle does NOT store -// log2_block_width itself, so without this wrapper the Python binding would -// have to ask the caller every time — which the user can lie about and -// trigger out-of-bounds reads of the metadata buffers. Storing it once at -// build time and consulting it in every accessor closes that hole. -struct PyVBMHandle -{ - VoxelBlockManagerHandle handle; - int log2BlockWidth = 6; - - PyVBMHandle() = default; - PyVBMHandle(VoxelBlockManagerHandle&& h, int lbw) noexcept - : handle(std::move(h)), log2BlockWidth(lbw) {} - - PyVBMHandle(const PyVBMHandle&) = delete; - PyVBMHandle& operator=(const PyVBMHandle&) = delete; - PyVBMHandle(PyVBMHandle&&) = default; - PyVBMHandle& operator=(PyVBMHandle&&) = default; - - uint64_t blockCount() const { return handle.blockCount(); } - uint64_t firstOffset() const { return handle.firstOffset(); } - uint64_t lastOffset() const { return handle.lastOffset(); } - void reset() { handle.reset(); } - int blockWidth() const { return 1 << log2BlockWidth; } - int jumpMapLength() const { return 1 << (log2BlockWidth - 6); } -}; - -// ----------------------- decodeInverseMaps helper ------------------------- -// -// Common implementation used by both the free function and the -// handle.decodeBlock(i) method. Allocates fresh leafIndex (uint32) and -// voxelOffset (uint16) NumPy arrays of length BlockWidth and fills them. -template -static nb::object pyDecodeInverseMapsImpl(const NanoGrid& grid, - uint32_t firstLeafID, - const uint64_t* jumpMap, - uint64_t blockFirstOffset) -{ - constexpr int BlockWidth = 1 << Log2BlockWidth; - constexpr int JumpMapLength = - VoxelBlockManagerBase::JumpMapLength; - - // The C++ decodeInverseMaps iterates leafID = firstLeafID .. - // firstLeafID + nExtraLeaves, where nExtraLeaves is the popcount of - // this block's jumpMap (each set bit marks an additional leaf - // boundary crossed within the block). If the jumpMap is corrupt or - // was built against a different grid, the loop could read past - // tree.getFirstNode<0>(). Pre-compute the upper bound and validate - // it against grid.tree().nodeCount(0) before any allocation. - uint32_t nExtraLeaves = 0; - for (int i = 0; i < JumpMapLength; ++i) - nExtraLeaves += util::countOn(jumpMap[i]); - const uint32_t nLeaves = grid.tree().nodeCount(0); - if (uint64_t(firstLeafID) + uint64_t(nExtraLeaves) >= nLeaves) { - throw nb::value_error( - "decodeInverseMaps: firstLeafID + popcount(jumpMap) would " - "index past grid.tree().nodeCount(0) — the jumpMap is " - "either corrupt or was paired with a different grid."); - } - - // Each call allocates fresh BlockWidth-sized output arrays for the - // leaf-index and voxel-offset results. We use plain new[] (rather than - // a numpy-allocated buffer) because the produced ndarrays are returned - // by reference and Python owns them via the capsule deleters below — - // when the ndarray is destroyed, the capsule's deleter runs delete[]. - // - // The raw pointers live in std::unique_ptr until the matching capsule - // has been constructed; that way if the second allocation, the - // decodeInverseMaps call, or either capsule construction throws, the - // unique_ptr unwinds the half-built state cleanly. After a capsule - // takes ownership we release() so the unique_ptr no longer double-frees. - std::unique_ptr leafIndex(new uint32_t[BlockWidth]); - std::unique_ptr voxelOffset(new uint16_t[BlockWidth]); - - using VBM = VoxelBlockManager; - { - // Release the GIL around the pure-C++ decode kernel — the heavy part, - // and the only part of this helper that touches no Python objects. The - // GIL is re-acquired on scope exit (including during exception unwind) - // before the capsules / ndarrays below are constructed. - nb::gil_scoped_release release; - VBM::template decodeInverseMaps( - &grid, firstLeafID, jumpMap, blockFirstOffset, - leafIndex.get(), voxelOffset.get()); - } - - // nb::capsule wraps the raw pointer + matching delete[] so it can serve - // as the ndarray's owner — the capsule lives as long as the ndarray and - // its destruction runs the deleter. - nb::capsule leafOwner(leafIndex.get(), - [](void* p) noexcept { delete[] static_cast(p); }); - auto* leafRaw = leafIndex.release(); - nb::capsule offsetOwner(voxelOffset.get(), - [](void* p) noexcept { delete[] static_cast(p); }); - auto* offsetRaw = voxelOffset.release(); - - size_t shape[1] = {static_cast(BlockWidth)}; - nb::ndarray, nb::c_contig, nb::device::cpu> - leafArr(leafRaw, size_t(1), shape, leafOwner); - nb::ndarray, nb::c_contig, nb::device::cpu> - offsetArr(offsetRaw, size_t(1), shape, offsetOwner); - return nb::make_tuple( - nb::cast(leafArr, nb::rv_policy::reference), - nb::cast(offsetArr, nb::rv_policy::reference)); -} - -// ------------------- VoxelBlockManagerHandle binding ---------------------- - -static const NanoGrid* castOnIndexGrid(nb::handle py_grid, - const char* fn_name) -{ - if (!nb::isinstance>(py_grid)) { - std::string msg(fn_name); - msg += ": grid must be a NanoVDB grid of build type ValueOnIndex (OnIndexGrid)"; - throw nb::type_error(msg.c_str()); - } - return &nb::cast&>(py_grid); -} - -static void defineHandle(nb::module_& toolsModule) -{ - nb::class_(toolsModule, "VoxelBlockManagerHandle", - "Owns the firstLeafID / jumpMap metadata buffers backing a " - "VoxelBlockManager. Constructed by nanovdb.tools.buildVoxelBlockManager.") - .def(nb::init<>(), - "Construct an empty VoxelBlockManagerHandle with no buffers.") - .def("blockCount", &PyVBMHandle::blockCount, - "Number of voxel blocks managed by this handle.") - .def("firstOffset", &PyVBMHandle::firstOffset, - "Sequential voxel index of the first active voxel covered " - "by this handle (1 by default when the handle covers the " - "full grid).") - .def("lastOffset", &PyVBMHandle::lastOffset, - "Sequential voxel index of the last active voxel covered " - "by this handle.") - .def("reset", &PyVBMHandle::reset, - "Release this handle's buffers and reset it to the empty state.") - .def_prop_ro("log2_block_width", [](const PyVBMHandle& h) { return h.log2BlockWidth; }, - "The log2_block_width this handle was built with. The jumpMap " - "and decodeBlock outputs derive their shapes from this value.") - .def_prop_ro("block_width", &PyVBMHandle::blockWidth, - "BlockWidth = 1 << log2_block_width (64, 128, 256, or 512).") - .def_prop_ro("jump_map_length", &PyVBMHandle::jumpMapLength, - "JumpMapLength = BlockWidth / 64 (1, 2, 4, or 8).") - .def( - "__bool__", - [](const PyVBMHandle& h) { return h.blockCount() > 0; }, - nb::is_operator()) - // Zero-copy view of the (blockCount,) firstLeafID array. - .def("firstLeafID", - [](nb::handle py_self) -> nb::object { - auto& h = nb::cast(py_self); - size_t shape[1] = {static_cast(h.blockCount())}; - // A default-constructed or reset() handle has a null - // hostFirstLeafID(); we still return an empty (0,) ndarray - // so callers don't have to branch on a None sentinel. The - // dummy non-null pointer (the handle itself) keeps nanobind - // happy; nothing is read since the leading shape is 0. - uint32_t* raw = h.handle.hostFirstLeafID(); - void* data = (raw != nullptr) ? static_cast(raw) - : static_cast(&h); - return nb::cast( - nb::ndarray, - nb::c_contig, nb::device::cpu>( - data, size_t(1), shape, py_self), - nb::rv_policy::reference); - }, - nb::keep_alive<0, 1>(), - "Return a zero-copy (blockCount,) uint32 NumPy view of the " - "firstLeafID array. Returns an empty (0,) array on a " - "default-constructed or reset() handle. The view keeps this " - "handle alive.") - // jumpMap is uint64_t[blockCount * JumpMapLength]. JumpMapLength is - // determined by the log2_block_width recorded on the handle, not by - // the caller — that way the returned view always covers exactly the - // allocated buffer, with no risk of OOB reads. - .def("jumpMap", - [](nb::handle py_self) -> nb::object { - auto& h = nb::cast(py_self); - size_t shape[2] = {static_cast(h.blockCount()), - static_cast(h.jumpMapLength())}; - // Same null-buffer guard as firstLeafID(): a - // default-constructed / reset() handle has a null - // hostJumpMap(); return an empty (0, jump_map_length) - // ndarray rather than passing nullptr to nanobind. - uint64_t* raw = h.handle.hostJumpMap(); - void* data = (raw != nullptr) ? static_cast(raw) - : static_cast(&h); - return nb::cast( - nb::ndarray, - nb::c_contig, nb::device::cpu>( - data, size_t(2), shape, py_self), - nb::rv_policy::reference); - }, - nb::keep_alive<0, 1>(), - "Return a zero-copy (blockCount, jump_map_length) uint64 NumPy " - "view of the jumpMap. The shape is determined by the " - "log2_block_width the handle was built with. Returns an empty " - "(0, jump_map_length) array on a default-constructed or reset() " - "handle. The view keeps this handle alive.") - // Decode the inverse maps for a single block of this VBM. The - // log2_block_width is taken from the handle, so the caller cannot - // request a width that doesn't match what was built. - .def("decodeBlock", - [](PyVBMHandle& self, - nb::handle py_grid, - uint64_t block_index) -> nb::object { - const auto* grid = castOnIndexGrid(py_grid, - "VoxelBlockManagerHandle.decodeBlock"); - if (block_index >= self.blockCount()) { - throw nb::index_error( - "VoxelBlockManagerHandle.decodeBlock(block_index): " - "block_index out of range [0, blockCount)."); - } - // Defensive: NanoVDB's buildVoxelBlockManager doesn't always - // initialize firstLeafID for blocks where no leaf starts at - // a block boundary AND no leaf's iteration sweep reaches - // them (e.g. when the source grid is tile-compressed, so - // some sequential offsets correspond to tile values rather - // than leaf voxels). The slot is then uninitialized memory; - // passing it into decodeInverseMaps would lead to an OOB - // read of tree.getFirstNode<0>()[garbage]. Catch the case - // and raise rather than segfault. - const uint32_t firstLeafID = - self.handle.hostFirstLeafID()[block_index]; - const uint32_t nLeaves = grid->tree().nodeCount(0); - if (firstLeafID >= nLeaves) { - throw nb::value_error( - "VoxelBlockManagerHandle.decodeBlock: the VBM's " - "firstLeafID for this block was not initialized by " - "buildVoxelBlockManager (the underlying algorithm " - "doesn't cover blocks that no leaf reaches via its " - "iteration). This typically happens on OnIndex " - "grids built from tile-compressed source grids; " - "until the issue is fixed upstream the workaround " - "is to build the source grid voxel-by-voxel with " - "build::Grid so it stays uncompressed."); - } - return dispatchLog2BlockWidth(self.log2BlockWidth, [&](auto W) { - constexpr int LBW = decltype(W)::value; - constexpr int BlockWidth = 1 << LBW; - constexpr int JumpMapLength = - VoxelBlockManagerBase::JumpMapLength; - const uint64_t blockFirstOffset = - self.firstOffset() + block_index * BlockWidth; - return pyDecodeInverseMapsImpl( - *grid, - firstLeafID, - self.handle.hostJumpMap() + block_index * JumpMapLength, - blockFirstOffset); - }); - }, - "grid"_a, "block_index"_a, - "Decode the inverse maps for the block_index-th block of this " - "VBM. Returns (leaf_index, voxel_offset) uint32 / uint16 NumPy " - "arrays of length BlockWidth = 1< PyVBMHandle { - const auto* grid = castOnIndexGrid(py_grid, "buildVoxelBlockManager"); - // The C++ implementation only NANOVDB_ASSERTs these preconditions, - // which makes them no-ops in release builds. Validate them here - // so Python callers get a clear error instead of UB / abort. - if (!grid->isSequential()) { - throw nb::value_error( - "buildVoxelBlockManager: grid must satisfy " - "grid.isSequential() (fixed-size, breadth-first node " - "layout). NanoVDB grids constructed via " - "tools.createOnIndexGrid satisfy this by default."); - } - return dispatchLog2BlockWidth(log2_block_width, [&](auto W) { - constexpr int LBW = decltype(W)::value; - using Base = VoxelBlockManagerBase; - constexpr uint64_t BlockWidth = Base::BlockWidth; - constexpr uint64_t JumpMapLength = Base::JumpMapLength; - // first_offset must be 1 (mod BlockWidth). The single-arg - // C++ helper would normalize a zero input to 1; we do the - // same here so the in-place builder below sees a valid - // value. Validate the nonzero case ourselves. - if (first_offset != 0 && - ((first_offset - 1) & (BlockWidth - 1)) != 0) { - throw nb::value_error( - "buildVoxelBlockManager: first_offset must satisfy " - "first_offset == 1 (mod BlockWidth). Pass 0 (the " - "default) to let the implementation use 1."); - } - if (first_offset == 0) first_offset = 1; - if (last_offset == 0) last_offset = grid->activeVoxelCount(); - if (last_offset < first_offset) return PyVBMHandle(); - // Capacity must hold at least ceil((last - first + 1) / - // BlockWidth) blocks; otherwise the handle's lastOffset - // would advertise more coverage than blockCount allows - // and decodeBlock would silently truncate. The formula - // below equals the ceil() above when BlockWidth is a - // power of two. - const uint64_t minBlocks = - (last_offset - first_offset + BlockWidth) >> LBW; - if (n_blocks != 0 && n_blocks < minBlocks) { - std::string msg( - "buildVoxelBlockManager: n_blocks must be at " - "least ceil((last_offset - first_offset + 1) / " - "BlockWidth) = "); - msg += std::to_string(minBlocks); - msg += ". Pass 0 (the default) to use the minimum " - "required capacity."; - throw nb::value_error(msg.c_str()); - } - if (n_blocks == 0) n_blocks = minBlocks; - // Allocate the metadata buffers ourselves so we can - // pre-initialize firstLeafID with a sentinel value before - // the in-place builder runs. The C++ allocating overload - // calls HostBuffer::create() which returns uninitialized - // memory; blocks that the algorithm doesn't touch would - // then retain arbitrary values, and our decodeBlock guard - // (firstLeafID >= nLeaves) might miss any garbage value - // that happens to be < nLeaves. By prefilling with nLeaves - // up front, every untouched slot deterministically trips - // the guard. - auto firstLeafIDBuf = HostBuffer::create( - n_blocks * sizeof(uint32_t)); - auto jumpMapBuf = HostBuffer::create( - n_blocks * JumpMapLength * sizeof(uint64_t)); - const uint32_t nLeaves = grid->tree().nodeCount(0); - { - uint32_t* slots = static_cast( - firstLeafIDBuf.data()); - for (uint64_t i = 0; i < n_blocks; ++i) { - slots[i] = nLeaves; - } - } - VoxelBlockManagerHandle handle( - std::move(firstLeafIDBuf), std::move(jumpMapBuf), - n_blocks, first_offset, last_offset); - // In-place builder zeros the jumpMap itself and only - // touches firstLeafID slots it actually visits. Release the - // GIL around it — it's pure C++ (touches no Python objects) - // and may parallelize internally via util::forEach. - { - nb::gil_scoped_release release; - buildVoxelBlockManager(grid, handle); - } - return PyVBMHandle(std::move(handle), LBW); - }); - }, - "grid"_a, - "log2_block_width"_a = 6, - "first_offset"_a = 0, - "last_offset"_a = 0, - "n_blocks"_a = 0, - "Build a host-side VoxelBlockManager from an OnIndexGrid. " - "log2_block_width selects the per-block active-voxel count " - "(6=64, 7=128, 8=256, 9=512). Pass 0 for first_offset / " - "last_offset / n_blocks to use the full grid (first active " - "voxel through grid.activeVoxelCount(), minimum block count). " - "first_offset, if nonzero, must satisfy first_offset == 1 " - "(mod BlockWidth)."); -} - -// --------------------- decodeInverseMaps binding -------------------------- - -static void defineDecode(nb::module_& toolsModule) -{ - toolsModule.def("decodeInverseMaps", - [](nb::handle py_grid, - uint32_t first_leaf_id, - nb::ndarray, - nb::c_contig, nb::device::cpu> jump_map, - uint64_t block_first_offset, - int log2_block_width) -> nb::object { - const auto* grid = castOnIndexGrid(py_grid, "decodeInverseMaps"); - // The C++ helper indexes tree.getFirstNode<0>()[first_leaf_id] - // without a bounds check, so a stray first_leaf_id leads to an - // OOB read. Validate up front. (We also require isSequential(); - // getFirstNode only makes sense on a sequential tree.) - if (!grid->isSequential()) { - throw nb::value_error( - "decodeInverseMaps: grid must satisfy " - "grid.isSequential()."); - } - const uint32_t nLeaves = grid->tree().nodeCount(0); - if (first_leaf_id >= nLeaves) { - throw nb::index_error( - "decodeInverseMaps: first_leaf_id out of range " - "[0, grid.tree().nodeCount(0))."); - } - return dispatchLog2BlockWidth(log2_block_width, [&](auto W) { - constexpr int LBW = decltype(W)::value; - constexpr int JumpMapLength = - VoxelBlockManagerBase::JumpMapLength; - if (jump_map.shape(0) != JumpMapLength) { - std::string msg("decodeInverseMaps: jump_map must have " - "length JumpMapLength = "); - msg += std::to_string(JumpMapLength); - msg += " for log2_block_width="; - msg += std::to_string(LBW); - throw nb::value_error(msg.c_str()); - } - return pyDecodeInverseMapsImpl( - *grid, first_leaf_id, jump_map.data(), - block_first_offset); - }); - }, - "grid"_a, - "first_leaf_id"_a, - "jump_map"_a, - "block_first_offset"_a, - "log2_block_width"_a = 6, - "Decode the inverse maps for a single voxel block of an OnIndexGrid. " - "Returns a (leaf_index, voxel_offset) tuple of fresh NumPy arrays of " - "length BlockWidth = 1< -static nb::object tryCreateOnIndexGrid(nb::handle py_grid, - uint32_t channels, - bool include_stats, - bool include_tiles, - int verbose) -{ - using SrcGridT = NanoGrid; - if (!nb::isinstance(py_grid)) { - return nb::object(); - } - const SrcGridT& src = nb::cast(py_grid); - return nb::cast( - tools::createNanoGrid( - src, channels, include_stats, include_tiles, verbose)); -} - -static void defineCreateOnIndexGrid(nb::module_& toolsModule) -{ - toolsModule.def("createOnIndexGrid", - [](nb::handle py_grid, - uint32_t channels, - bool include_stats, - bool include_tiles, - int verbose) -> nb::object { - // Try every source BuildT we accept. - if (auto r = tryCreateOnIndexGrid( - py_grid, channels, include_stats, include_tiles, verbose); - r.is_valid()) return r; - if (auto r = tryCreateOnIndexGrid( - py_grid, channels, include_stats, include_tiles, verbose); - r.is_valid()) return r; - if (auto r = tryCreateOnIndexGrid( - py_grid, channels, include_stats, include_tiles, verbose); - r.is_valid()) return r; - if (auto r = tryCreateOnIndexGrid( - py_grid, channels, include_stats, include_tiles, verbose); - r.is_valid()) return r; - throw nb::type_error( - "createOnIndexGrid: source grid must be a FloatGrid, " - "DoubleGrid, Int32Grid, or Vec3fGrid (other source BuildTs " - "are not yet bound)."); - }, - "src_grid"_a, - "channels"_a = 0u, - "include_stats"_a = true, - "include_tiles"_a = true, - "verbose"_a = 0, - "Convert a source grid into a NanoGrid " - "(OnIndexGrid). Accepts FloatGrid / DoubleGrid / Int32Grid / " - "Vec3fGrid. This is a narrow helper kept alongside " - "buildVoxelBlockManager; for general index conversion (broader " - "source coverage, blind-data channels) prefer " - "nanovdb.tools.createNanoGridOnIndex."); -} - -void defineVoxelBlockManagerModule(nb::module_& toolsModule) -{ - defineHandle(toolsModule); - defineBuild(toolsModule); - defineDecode(toolsModule); - defineCreateOnIndexGrid(toolsModule); -} - -} // namespace pynanovdb diff --git a/nanovdb/nanovdb/python/PyVoxelBlockManager.h b/nanovdb/nanovdb/python/PyVoxelBlockManager.h deleted file mode 100644 index 6fe29570c6..0000000000 --- a/nanovdb/nanovdb/python/PyVoxelBlockManager.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: Apache-2.0 -#ifndef NANOVDB_PYVOXELBLOCKMANAGER_HAS_BEEN_INCLUDED -#define NANOVDB_PYVOXELBLOCKMANAGER_HAS_BEEN_INCLUDED - -#include - -namespace nb = nanobind; - -namespace pynanovdb { - -/// @brief Bind VoxelBlockManagerHandle, -/// tools.buildVoxelBlockManager, tools.decodeInverseMaps, and the -/// test-scaffold tools.createOnIndexGrid factory under the given -/// Python submodule (expected to be the existing nanovdb.tools). -void defineVoxelBlockManagerModule(nb::module_& toolsModule); - -} // namespace pynanovdb - -#endif diff --git a/nanovdb/nanovdb/python/__init__.py b/nanovdb/nanovdb/python/__init__.py index 9ff10b6c28..4f955e4a02 100644 --- a/nanovdb/nanovdb/python/__init__.py +++ b/nanovdb/nanovdb/python/__init__.py @@ -1,26 +1,9 @@ # Copyright Contributors to the OpenVDB Project # SPDX-License-Identifier: Apache-2.0 -import os import sys - if sys.platform == "win32": - _openvdb_dll_directory = os.path.join( - os.path.dirname(os.path.abspath(__file__)), os.pardir, "openvdb", "lib" - ) - if os.path.isdir(_openvdb_dll_directory): - os.add_dll_directory(_openvdb_dll_directory) - - -def get_include(): - """Return the absolute path to the bundled NanoVDB C/C++ include directory. - - Use this from a downstream Python extension's build system so the extension - compiles against the same NanoVDB headers the installed wheel was built with:: - - import nanovdb - ext_kwargs = dict(include_dirs=[nanovdb.get_include()]) - """ - return os.path.join(os.path.dirname(os.path.abspath(__file__)), "include") - + import os + openvdb_dll_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, 'openvdb', 'lib') + os.add_dll_directory(directory) -from .lib.nanovdb import * # noqa: E402,F401,F403 +from .lib.nanovdb import * diff --git a/nanovdb/nanovdb/python/cuda/PyDeviceBuffer.cc b/nanovdb/nanovdb/python/cuda/PyDeviceBuffer.cc index e024d63939..523e4b5834 100644 --- a/nanovdb/nanovdb/python/cuda/PyDeviceBuffer.cc +++ b/nanovdb/nanovdb/python/cuda/PyDeviceBuffer.cc @@ -13,10 +13,7 @@ namespace pynanovdb { void defineDeviceBuffer(nb::module_& m) { - nb::class_(m, "DeviceBuffer", - "CUDA device-side buffer used to back a DeviceGridHandle. Holds a " - "host mirror and a device pointer; deviceUpload / deviceDownload on " - "the handle move bytes between the two."); + nb::class_(m, "DeviceBuffer"); } } // namespace pynanovdb diff --git a/nanovdb/nanovdb/python/cuda/PyDeviceGridHandle.cu b/nanovdb/nanovdb/python/cuda/PyDeviceGridHandle.cu index 0214474420..983caebd05 100644 --- a/nanovdb/nanovdb/python/cuda/PyDeviceGridHandle.cu +++ b/nanovdb/nanovdb/python/cuda/PyDeviceGridHandle.cu @@ -6,7 +6,6 @@ #include #include -#include namespace nb = nanobind; using namespace nb::literals; @@ -14,48 +13,6 @@ using namespace nanovdb; namespace pynanovdb { -// Device-side polymorphic deviceGrid(n) — same dispatch shape as -// pyHostGrid in PyGridHandle.h, but returns the device pointer. -// gridType(n) is read from the host-side GridData header (the handle keeps -// a host mirror), so this works whether or not the grid has been uploaded. -// Returns None if the device-side grid is null (i.e. no deviceUpload yet) -// or the BuildT is not Python-visible. -static nb::object pyDeviceGrid(nb::handle py_handle, uint32_t n) -{ - using BufferT = nanovdb::cuda::DeviceBuffer; - auto& handle = nb::cast&>(py_handle); - if (n >= handle.gridCount()) return nb::none(); - switch (handle.gridType(n)) { -#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \ - case nanovdb::GridType::GridTypeEnum: { \ - auto* grid = handle.template deviceGrid(n); \ - return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \ - : nb::none(); \ - } -#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \ - case nanovdb::GridType::GridTypeEnum: { \ - auto* grid = handle.template deviceGrid(n); \ - return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \ - : nb::none(); \ - } -#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \ - case nanovdb::GridType::GridTypeEnum: { \ - auto* grid = handle.template deviceGrid(n); \ - return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \ - : nb::none(); \ - } -#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \ - case nanovdb::GridType::GridTypeEnum: { \ - auto* grid = handle.template deviceGrid(n); \ - return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \ - : nb::none(); \ - } -#include "../BuildTypes.def" - default: - return nb::none(); - } -} - void defineDeviceGridHandle(nb::module_& m) { using BufferT = nanovdb::cuda::DeviceBuffer; @@ -70,31 +27,19 @@ void defineDeviceGridHandle(nb::module_& m) new (&handle) GridHandle(std::move(buffer)); }, "cpu_t"_a.noconvert(), - "cuda_t"_a.noconvert(), - "Construct a DeviceGridHandle that wraps an existing pair of " - "host and device uint32 arrays of equal length.") - .def("deviceGrid", &pyDeviceGrid, "n"_a = 0, - nb::keep_alive<0, 1>(), - "Return the n-th device-resident grid as a typed Grid subclass " - "selected by gridType(n), or None if the BuildT is not bound in " - "Python or the device copy has not been uploaded yet. The " - "returned grid keeps this handle alive.") + "cuda_t"_a.noconvert()) + .def("deviceFloatGrid", nb::overload_cast(&GridHandle::template deviceGrid), "n"_a = 0, nb::rv_policy::reference_internal) + .def("deviceDoubleGrid", nb::overload_cast(&GridHandle::template deviceGrid), "n"_a = 0, nb::rv_policy::reference_internal) + .def("deviceInt32Grid", nb::overload_cast(&GridHandle::template deviceGrid), "n"_a = 0, nb::rv_policy::reference_internal) + .def("deviceVec3fGrid", nb::overload_cast(&GridHandle::template deviceGrid), "n"_a = 0, nb::rv_policy::reference_internal) + .def("deviceRGBA8Grid", + nb::overload_cast(&GridHandle::template deviceGrid), + "n"_a = 0, + nb::rv_policy::reference_internal) .def( - "deviceUpload", [](GridHandle& handle, bool sync) { handle.deviceUpload(nullptr, sync); }, "sync"_a = true, - "Copy the host-side buffer to the device. If sync is True the " - "call blocks until the transfer completes.") + "deviceUpload", [](GridHandle& handle, bool sync) { handle.deviceUpload(nullptr, sync); }, "sync"_a = true) .def( - "deviceDownload", [](GridHandle& handle, bool sync) { handle.deviceDownload(nullptr, sync); }, "sync"_a = true, - "Copy the device-side buffer back to the host. If sync is True " - "the call blocks until the transfer completes."); - // NOTE: defineGridHandleUtilities intentionally NOT called for - // DeviceBuffer. Registering nanovdb.splitGrids / nanovdb.mergeGrids as a - // second overload taking a DeviceGridHandle list conflicts with the host - // overload because both signatures take nb::list, and nanobind's - // overload resolution can't disambiguate by element type — it picks one - // and the inner cast fails with std::bad_cast. A properly typed device - // variant (with its own name, or strongly-typed std::vector - // args via nanobind/stl/vector.h) can land later if it's needed. + "deviceDownload", [](GridHandle& handle, bool sync) { handle.deviceDownload(nullptr, sync); }, "sync"_a = true); } } // namespace pynanovdb diff --git a/nanovdb/nanovdb/python/cuda/PyPointsToGrid.cu b/nanovdb/nanovdb/python/cuda/PyPointsToGrid.cu index 7ec30e4df3..933f5570b6 100644 --- a/nanovdb/nanovdb/python/cuda/PyPointsToGrid.cu +++ b/nanovdb/nanovdb/python/cuda/PyPointsToGrid.cu @@ -50,8 +50,7 @@ template void definePointsToGrid(nb::module_& m, const char* na auto handle = converter.getHandle(points, tensor.shape(0)); return handle; }, - "tensor"_a, - "Rasterize the given (N, 3) int32 device tensor of points into a fresh GridHandle."); + "tensor"_a); } template void definePointsToGrid(nb::module_&, const char*); diff --git a/nanovdb/nanovdb/python/examples/README.md b/nanovdb/nanovdb/python/examples/README.md deleted file mode 100644 index 6cf82117ec..0000000000 --- a/nanovdb/nanovdb/python/examples/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# NanoVDB Python Examples - -Runnable `.py` scripts demonstrating the NanoVDB Python bindings. -Each script is self-contained, builds its own input data, and prints -a small summary to stdout. - -Run any example with: - -```bash -python load_inspect.py -``` - -When working from the source tree, the `nanovdb` module needs to be -on `PYTHONPATH` (e.g. via the build output directory): - -```bash -cd /nanovdb/nanovdb/python -PYTHONPATH=. python /path/to/.py -``` - -## Examples - -| Script | What it shows | -| --- | --- | -| [`load_inspect.py`](load_inspect.py) | Polymorphic `handle.grid(n)` access, `GridMetaData` type-erased introspection, mixed-type handles via `mergeGrids`. | -| [`build_grid.py`](build_grid.py) | Voxel-by-voxel construction with `nanovdb.tools.build.FloatGrid`, the cached `ValueAccessor`, the thread-safe `WriteAccessor`, and `.toNanoVDB()` to bake into a host `GridHandle`. | -| [`bulk_leaf_numpy.py`](bulk_leaf_numpy.py) | Zero-copy `(N_leaves, 512)` NumPy view of every leaf's mValues via `grid.leaf_values()`. Includes a global-stats reduction and an in-place mutation that propagates back into the grid. Requires NumPy. | -| [`quantize.py`](quantize.py) | Quantize a `NanoGrid` through `nanovdb.tools.createNanoGridFp{4,8,16,N}`. Shows fixed-width quantization with dithering and variable-width `FpN` with both `AbsDiff` and `RelDiff` oracles. | -| [`validate.py`](validate.py) | `nanovdb.tools.validateGrid` / `validateGrids`, `checkGrid`, `isValid`, and the `evalChecksum` / `validateChecksum` / `updateChecksum` round-trip. | -| [`io_roundtrip.py`](io_roundtrip.py) | `nanovdb.io` write/read round-trip over five primitives: `writeGrids` with codec fallback, `readGridMetaData`, `hasGrid`, `readGrid` by name, `splitGrids`, and zero-copy point positions via `getBlindData`. Port of `ex_write_nanovdb_grids` + `ex_read_nanovdb_sphere_accessor`. | -| [`make_funny_nanovdb.py`](make_funny_nanovdb.py) | Functor-based construction: `tools.createFloatGrid(background, name, gridClass, func, bbox)` evaluates a Python callback at every voxel. Port of `ex_make_funny_nanovdb` on a reduced domain. | -| [`make_typed_grids.py`](make_typed_grids.py) | One solid sphere per value type via `tools.build.{Float,Double,Int16,Int32,Int64,UInt32,Vec3f}Grid`, written to a single file and re-read with polymorphic `handle.grid()` dispatch. Port of `ex_make_typed_grids`. | -| [`raytrace_level_set.py`](raytrace_level_set.py) | CPU sphere-traced level-set render to PGM using `worldToIndex`, the trilinear sampler, `zeroCrossing()`, and normalized `gradient()` shading. Port of `ex_raytrace_level_set` (host path). | -| [`raytrace_fog_volume.py`](raytrace_fog_volume.py) | CPU transmittance ray-march of a fog volume to PGM using a `ReadAccessor` and `Coord.Floor` — the accessor-based sampling idiom. Port of `ex_raytrace_fog_volume` (host path). | -| [`collide_level_set.py`](collide_level_set.py) | Particles colliding with a level set: `worldToIndexF`, `tree.isActive` narrow-band test, accessor distance reads, and `sampler.gradient()` collision normals. Port of `ex_collide_level_set` (host path). | -| [`index_grid_channels.py`](index_grid_channels.py) | `tools.createNanoGridOnIndex(src, channels=1)`, `grid.valueCount()`, coordinate reads through `createChannelAccessor`, and blind-data authoring with `tools.CreateNanoGrid.addBlindData` + the writable `getBlindData` view. Extends the host half of `ex_index_grid_cuda`. Requires NumPy for the authoring section. | -| [`node_manager.py`](node_manager.py) | Linearized node iteration with `createNodeManager`: per-level counts, `leaf(i)` / `lower(i)` access, node origins, masks, and stats. Port of the host half of `ex_nodemanager_cuda`. | -| [`openvdb_interop.py`](openvdb_interop.py) | `tools.openToNanoVDB` / `nanoToOpenVDB` round-trip with accessor comparison on both sides. Self-skips unless built with `NANOVDB_USE_OPENVDB` and `openvdb` is importable. Port of `ex_openvdb_to_nanovdb_accessor`. | - -Scripts that produce files write them to a fresh temporary directory -and print its path, so the source tree stays clean. The whole set is -smoke-tested by [`../test/TestExamples.py`](../test/TestExamples.py) -(ctest name `pytest_nanovdb_examples`) when the module is configured -with `NANOVDB_BUILD_PYTHON_UNITTESTS=ON`. - -For full API signatures and per-argument docstrings, use Python's -`help()` on any symbol — e.g. `help(nanovdb.tools.createNanoGridFpN)`. -The bindings ship `.pyi` type stubs for IDE / type-checker support. diff --git a/nanovdb/nanovdb/python/examples/build_grid.py b/nanovdb/nanovdb/python/examples/build_grid.py deleted file mode 100644 index d4c3f27bed..0000000000 --- a/nanovdb/nanovdb/python/examples/build_grid.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Construct a NanoVDB grid voxel-by-voxel in pure Python. - -nanovdb.tools.build.Grid is the mutable CPU grid builder -that mirrors nanovdb::tools::build::Grid. This example shows the -three construction loops you'll typically reach for (setValue -directly, the cached ValueAccessor, and the thread-safe -WriteAccessor), then bakes each build grid into a host NanoGrid via -.to_nanovdb() and reads it back through the regular polymorphic -handle.grid() API. - -Run with: python build_grid.py -""" -import nanovdb - - -def fill_with_setValue(): - """Simplest path: setValue() directly on the build grid.""" - g = nanovdb.tools.build.FloatGrid( - background=0.0, name="setValue_demo", - gridClass=nanovdb.GridClass.FogVolume) - # Plant five active voxels along the x axis. - for i in range(5): - g.setValue(nanovdb.math.Coord(i, 0, 0), float(i + 1)) - return g - - -def fill_with_accessor(): - """Cached path: getAccessor() avoids re-walking the tree for - each write when consecutive coordinates share a leaf.""" - g = nanovdb.tools.build.FloatGrid(background=0.0, name="accessor_demo") - acc = g.getAccessor() - # The accessor caches the last leaf / lower / upper node, so a - # burst of writes in a 16^3 neighborhood only walks the tree once. - for x in range(8): - for y in range(8): - for z in range(8): - if x + y + z == 7: - acc.setValue(nanovdb.math.Coord(x, y, z), - float(x * 64 + y * 8 + z)) - return g - - -def fill_with_write_accessor(): - """Thread-safe path: getWriteAccessor() buffers writes into a - private root and merges them into the parent on destruction or - on an explicit .merge() call. Useful when fanning out to multiple - threads — one WriteAccessor per thread, no shared mutable state.""" - g = nanovdb.tools.build.FloatGrid(background=0.0, name="write_accessor_demo") - wa = g.getWriteAccessor() - wa.setValue(nanovdb.math.Coord(50, 50, 50), 9.0) - # Before merge, the parent doesn't see the change yet. - assert g.getValue(nanovdb.math.Coord(50, 50, 50)) == 0.0 - wa.merge() - assert g.getValue(nanovdb.math.Coord(50, 50, 50)) == 9.0 - return g - - -def main(): - for builder in (fill_with_setValue, fill_with_accessor, - fill_with_write_accessor): - g = builder() - print(f"=== {g.getName()} ===") - print(f" nodeCount (leaf, lower, upper) = {g.nodeCount()}") - - # Bake into a host NanoGrid. - handle = g.toNanoVDB(sMode=nanovdb.tools.StatsMode.All) - ng = handle.grid() - print(f" baked NanoGrid: type={ng.gridType()}, " - f"active={ng.activeVoxelCount()}, " - f"worldBBox={ng.worldBBox()}") - - # The build grid is left untouched — we can bake again. - handle2 = g.toNanoVDB() - assert handle2.grid().activeVoxelCount() == ng.activeVoxelCount() - - -if __name__ == "__main__": - main() diff --git a/nanovdb/nanovdb/python/examples/bulk_leaf_numpy.py b/nanovdb/nanovdb/python/examples/bulk_leaf_numpy.py deleted file mode 100644 index f967a2ae4a..0000000000 --- a/nanovdb/nanovdb/python/examples/bulk_leaf_numpy.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Bulk per-leaf value access as a zero-copy NumPy array. - -grid.leaf_values() is the highest-bandwidth path from NanoVDB into -NumPy. It returns an (N_leaves, 512) view of every leaf's mValues -without copying — modify it, slice it, feed it into a PyTorch tensor, -hash it for cache lookup, whatever you need. - -Run with: python bulk_leaf_numpy.py -""" -import nanovdb - - -def main(): - try: - import numpy as np - except ImportError: - print("This example requires numpy. Install it with: pip install numpy") - return - - # Build a fog volume sphere with stats so the leaves have meaningful - # min/max attached (just for the printing below — not required by - # leaf_values itself). - handle = nanovdb.tools.createFogVolumeSphere( - radius=20.0, name="bulk_demo") - grid = handle.grid() - print(f"Grid: {grid.gridType()}, active voxels = {grid.activeVoxelCount()}, " - f"leaves = {grid.tree().nodeCount(0)}") - - # leaf_values() is the zero-copy view. Modifying it modifies the grid. - bulk = grid.leaf_values() - # np.asarray adds a NumPy wrapper but doesn't copy. - arr = np.asarray(bulk) - print(f"leaf_values: shape={arr.shape}, dtype={arr.dtype}, " - f"backed by grid memory (no copy).") - - # Global statistics across every voxel in every leaf, computed in C. - # 0.0 voxels (background) are excluded by using a mask. - nonzero = arr[arr != 0.0] - print(f" non-background voxels = {nonzero.size}") - print(f" min = {nonzero.min()}, max = {nonzero.max()}, " - f"mean = {nonzero.mean()}") - - # Per-leaf reductions: each row of `arr` is one leaf's 512 voxels. - per_leaf_max = arr.max(axis=1) - print(f" per-leaf max (first 5): {per_leaf_max[:5]}") - - # Zero-copy means writes propagate. Zero out the first leaf's values - # and read one back through the regular accessor to confirm the grid - # actually changed. (The active *mask* is unchanged — we wrote into - # mValues only — so activeVoxelCount() stays the same.) - arr[0] = 0.0 - leaf = grid.tree().getFirstLeaf() - if leaf is not None: - first_value_after = leaf.getFirstValue() - print(f" zeroed first leaf's values in place: " - f"leaf.getFirstValue() = {first_value_after}, " - f"activeVoxelCount unchanged: {grid.activeVoxelCount()}") - - -if __name__ == "__main__": - main() diff --git a/nanovdb/nanovdb/python/examples/collide_level_set.py b/nanovdb/nanovdb/python/examples/collide_level_set.py deleted file mode 100644 index 3a3bbc29fe..0000000000 --- a/nanovdb/nanovdb/python/examples/collide_level_set.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Collide falling particles against a level-set surface on the CPU. - -Python port of the host path of ex_collide_level_set: particles fall -under gravity; each step transforms the candidate position to index -space with grid.worldToIndexF, tests the narrow band with -tree.isActive, reads the signed distance through a ReadAccessor, and -on penetration reflects the velocity about the surface normal. The C++ -original builds the normal from 6-tap finite differences of the SDF; -here the bound sampler.gradient() provides the same quantity in one -call (index-space units — normalized before use). - -Run with: python collide_level_set.py -""" -import random - -import nanovdb - -NUM_PARTICLES = 500 -NUM_STEPS = 40 -DT = 0.1 -GRAVITY = -9.8 - - -def main(): - handle = nanovdb.tools.createLevelSetSphere(radius=100.0, name="sphere") - grid = handle.grid() - tree = grid.tree() - acc = grid.getAccessor() - sampler = nanovdb.math.createTrilinearSampler(grid) - - # Seed particles above the north pole of the sphere, falling down. - rng = random.Random(42) - particles = [] - for _ in range(NUM_PARTICLES): - p = [rng.uniform(-30.0, 30.0), rng.uniform(115.0, 140.0), - rng.uniform(-30.0, 30.0)] - particles.append((p, [0.0, -20.0, 0.0])) - - total_collisions = 0 - for step in range(NUM_STEPS): - collisions = 0 - for p, v in particles: - v[1] += GRAVITY * DT - next_p = [p[i] + v[i] * DT for i in range(3)] - - ijk = nanovdb.math.Coord.Floor(grid.worldToIndexF( - nanovdb.math.Vec3f(next_p[0], next_p[1], next_p[2]))) - if tree.isActive(ijk): # inside the narrow band? - d = acc.getValue(ijk) - if d <= 0.0: # inside the level set? - ipos = grid.worldToIndexF( - nanovdb.math.Vec3f(next_p[0], next_p[1], next_p[2])) - n = sampler.gradient(ipos) - n.normalize() - # Project the position back to the surface and - # reflect the velocity, as in the C++ example. - for i in range(3): - next_p[i] -= d * n[i] - v_dot_n = sum(v[i] * n[i] for i in range(3)) - for i in range(3): - v[i] -= 2.0 * v_dot_n * n[i] - collisions += 1 - p[:] = next_p - total_collisions += collisions - if collisions: - print(f"step {step:2d}: {collisions} collisions") - - print(f"total collisions over {NUM_STEPS} steps: {total_collisions}") - assert total_collisions > 0 - - # No particle should end up inside the surface. - worst = 0.0 - for p, _ in particles: - ijk = nanovdb.math.Coord.Floor(grid.worldToIndexF( - nanovdb.math.Vec3f(p[0], p[1], p[2]))) - if tree.isActive(ijk): - worst = min(worst, acc.getValue(ijk)) - print(f"deepest final penetration: {worst:.3f} world units") - assert worst > -2.0 * grid.voxelSize()[0] - - -if __name__ == "__main__": - main() diff --git a/nanovdb/nanovdb/python/examples/index_grid_channels.py b/nanovdb/nanovdb/python/examples/index_grid_channels.py deleted file mode 100644 index 91e421d08f..0000000000 --- a/nanovdb/nanovdb/python/examples/index_grid_channels.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Index grids, value channels, and blind-data authoring. - -Part A ports the host half of ex_index_grid_cuda: a float level set is -re-encoded as a NanoGrid whose voxels store sequential -uint64 indices, with the original float values copied into a blind-data -channel — then read back by coordinate through a ChannelAccessor, -which the C++ example only does in a CUDA kernel. Part B authors a -brand-new blind-data channel on a grid with tools.CreateNanoGrid and -fills it in place through the writable getBlindData() NumPy view. - -Run with: python index_grid_channels.py -""" -import nanovdb - - -def index_grid_with_channel(): - src = nanovdb.tools.createLevelSetSphere(radius=50.0, name="sphere") - src_grid = src.grid() - - # channels=1 copies the source values into blind-data channel 0, - # indexed by the per-voxel uint64 indices the OnIndex grid stores. - handle = nanovdb.tools.createNanoGridOnIndex(src_grid, channels=1) - grid = handle.grid() - print(f"OnIndex grid: valueCount={grid.valueCount()}, " - f"source activeVoxelCount={src_grid.activeVoxelCount()}") - assert grid.valueCount() >= src_grid.activeVoxelCount() - - # createChannelAccessor inspects the channel's recorded dataType and - # returns the matching typed accessor (here: OnIndexFloat...). - channel = nanovdb.createChannelAccessor(grid, 0) - print(f"channel accessor: {type(channel).__name__}, " - f"valueCount={channel.valueCount()}") - - src_acc = src_grid.getAccessor() - for ijk in (nanovdb.math.Coord(48, 0, 0), nanovdb.math.Coord(0, 50, 0), - nanovdb.math.Coord(0, 0, 52)): - via_channel = channel.getValue(ijk) - via_source = src_acc.getValue(ijk) - print(f" {ijk}: channel={via_channel:.3f} source={via_source:.3f} " - f"(linear offset {channel.getIndex(ijk)})") - assert via_channel == via_source - - -def author_blind_data(): - try: - import numpy as np - except ImportError: - print("NumPy not found. Skipping the blind-data authoring section.") - return - src = nanovdb.tools.build.FloatGrid(0.0, "authored", - nanovdb.GridClass.Unknown) - for i in range(8): - src.setValue(nanovdb.math.Coord(i, 0, 0), float(i)) - - # Declare a channel at conversion time; it is allocated zero-filled - # in the baked grid and filled afterwards through the writable view. - conv = nanovdb.tools.CreateNanoGrid(src) - ch = conv.addBlindData("temperature", count=64, - dataType=nanovdb.GridType.Float) - handle = conv.getHandle() - grid = handle.grid() - print(f"authored grid has {grid.blindDataCount()} blind-data channel(s)") - - view = grid.getBlindData(ch) - view[:] = np.linspace(273.0, 373.0, num=64, dtype=np.float32) - - # Re-resolve the channel by name and confirm the writes persisted. - n = grid.findBlindData("temperature") - meta = grid.blindMetaData(n) - stored = grid.getBlindData(n) - print(f" {meta.name()!r}: {meta.valueCount} x {meta.dataType}, " - f"range [{stored.min():.1f}, {stored.max():.1f}]") - assert n == ch - assert stored[0] == np.float32(273.0) - - -def main(): - index_grid_with_channel() - print() - author_blind_data() - - -if __name__ == "__main__": - main() diff --git a/nanovdb/nanovdb/python/examples/io_roundtrip.py b/nanovdb/nanovdb/python/examples/io_roundtrip.py deleted file mode 100644 index 3bd77f633a..0000000000 --- a/nanovdb/nanovdb/python/examples/io_roundtrip.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Write a bundle of primitive grids to a .nvdb file and read it back. - -Python port of ex_write_nanovdb_grids, ex_read_nanovdb_sphere, and -ex_read_nanovdb_sphere_accessor, consolidated into one self-contained -round trip: bake five primitives, write them to a single file (with -codec fallback), inspect the file metadata without loading the grids, -re-read one grid by name and all grids at once, and split a merged -handle back apart. The point-sphere section reads the world-space -point positions through the zero-copy getBlindData() NumPy view. - -Run with: python io_roundtrip.py -""" -import os -import tempfile - -import nanovdb - - -def write_primitives(path): - handles = [ - nanovdb.tools.createLevelSetSphere(radius=50.0, name="sphere_ls"), - nanovdb.tools.createLevelSetTorus(majorRadius=50.0, minorRadius=20.0, - name="torus_ls"), - nanovdb.tools.createLevelSetBox(width=40.0, height=60.0, depth=80.0, - name="box_ls"), - nanovdb.tools.createLevelSetBBox(width=40.0, height=60.0, depth=80.0, - thickness=10.0, name="bbox_ls"), - nanovdb.tools.createPointSphere(pointsPerVoxel=2, radius=50.0, - name="sphere_points"), - ] - # BLOSC gives the best compression but is a build-time option; fall - # back to an uncompressed file when this module was built without it. - try: - nanovdb.io.writeGrids(path, handles, codec=nanovdb.io.Codec.BLOSC) - codec = "BLOSC" - except RuntimeError: - nanovdb.io.writeGrids(path, handles, codec=nanovdb.io.Codec.NONE) - codec = "NONE" - print(f"Wrote {len(handles)} grids to {path} (codec={codec})") - - -def inspect_file(path): - # readGridMetaData parses the per-grid file headers without loading - # any voxel data — the cheap way to answer "what's in this file?". - for meta in nanovdb.io.readGridMetaData(path): - print(f" {meta.gridName!r}: type={meta.gridType}, " - f"class={meta.gridClass}, voxels={meta.voxelCount}") - assert nanovdb.io.hasGrid(path, "sphere_ls") - assert not nanovdb.io.hasGrid(path, "no_such_grid") - - -def read_back(path): - # Read a single grid by name, then print the recognizable - # ex_read_nanovdb_sphere_accessor cross-section along the x-axis. - handle = nanovdb.io.readGrid(path, "sphere_ls") - acc = handle.grid().getAccessor() - for i in range(47, 54): - ijk = nanovdb.math.Coord(i, 0, 0) - print(f" sphere_ls({i},0,0) = {acc.getValue(ijk):.2f}") - - # Read every grid, merge them into one multi-grid handle, and split - # that handle back into one handle per grid. - handles = nanovdb.io.readGrids(path) - merged = nanovdb.mergeGrids(handles) - print(f" merged handle holds {merged.gridCount()} grids") - parts = nanovdb.splitGrids(merged) - assert len(parts) == len(handles) - print(f" splitGrids -> {len(parts)} single-grid handles") - return handles - - -def point_positions(handles): - try: - import numpy as np - except ImportError: - print("NumPy not found. Skipping the point-positions section.") - return - # The point sphere stores its world-space positions as a blind-data - # channel; getBlindData() exposes it as a zero-copy (N, 3) view. - points = next(h.grid() for h in handles - if h.grid().gridName() == "sphere_points") - positions = points.getBlindData(0) - radii = np.linalg.norm(positions, axis=1) - print(f" {positions.shape[0]} points, " - f"|p| in [{radii.min():.2f}, {radii.max():.2f}]") - # Points are jittered within their voxel, so allow ~1.5 voxels. - assert np.all(np.abs(radii - 50.0) < 1.5) - - -def main(): - out_dir = tempfile.mkdtemp(prefix="nanovdb_") - path = os.path.join(out_dir, "primitives.nvdb") - write_primitives(path) - inspect_file(path) - handles = read_back(path) - point_positions(handles) - print(f"Output left in {out_dir}") - - -if __name__ == "__main__": - main() diff --git a/nanovdb/nanovdb/python/examples/load_inspect.py b/nanovdb/nanovdb/python/examples/load_inspect.py deleted file mode 100644 index 632f78ec33..0000000000 --- a/nanovdb/nanovdb/python/examples/load_inspect.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Load a NanoVDB grid and inspect it polymorphically. - -handle.grid(i) returns the correct typed Python subclass for whatever -GridType the grid carries, so a single call site can handle a mixed -bundle of grid types. This example builds two grids of different -value types into one handle, then walks the handle inspecting each -grid via the polymorphic accessor plus GridMetaData (the type-erased -introspector that answers "what's in this buffer?" without knowing -BuildT at compile time). - -Run with: python load_inspect.py -""" -import nanovdb - - -def describe_handle(handle): - print(f"Handle contains {handle.gridCount()} grid(s).") - for i in range(handle.gridCount()): - # GridType / gridSize are cheap to query on the handle itself. - gtype = handle.gridType(i) - gsize = handle.gridSize(i) - print(f" [{i}] type={gtype}, size={gsize} bytes") - - # handle.grid(i) returns the matching Grid subclass - # at runtime — no isinstance dispatch needed at the call site. - # The grid name lives on the grid itself, not the handle. - grid = handle.grid(i) - print(f" name={grid.gridName()!r}, " - f"gridClass={grid.gridClass()}") - print(f" activeVoxelCount={grid.activeVoxelCount()}") - print(f" worldBBox={grid.worldBBox()}") - - # GridMetaData is the type-erased introspector — answers - # "what's in this buffer?" without knowing BuildT. - meta = nanovdb.GridMetaData(grid) - print(f" gridSize={meta.gridSize()}, " - f"isLevelSet={meta.isLevelSet()}, " - f"isFogVolume={meta.isFogVolume()}") - - -def main(): - # Build two grids of different types into one handle so we can - # exercise the polymorphic accessor. - h_float = nanovdb.tools.createLevelSetSphere( - radius=10.0, name="sphere_float") - h_double = nanovdb.tools.createLevelSetSphere( - gridType=nanovdb.GridType.Double, radius=10.0, name="sphere_double") - handle = nanovdb.mergeGrids([h_float, h_double]) - - describe_handle(handle) - - print() - print("Polymorphic dispatch from a runtime GridType:") - for i in range(handle.gridCount()): - grid = handle.grid(i) - # Each typed grid carries a getAccessor() that returns the - # appropriate ReadAccessor — float for FloatGrid, - # double for DoubleGrid, etc. - acc = grid.getAccessor() - v = acc.getValue(nanovdb.math.Coord(0, 0, 0)) - print(f" grid[{i}] accessor.getValue(0,0,0) = {v}") - - -if __name__ == "__main__": - main() diff --git a/nanovdb/nanovdb/python/examples/make_funny_nanovdb.py b/nanovdb/nanovdb/python/examples/make_funny_nanovdb.py deleted file mode 100644 index fd9068586e..0000000000 --- a/nanovdb/nanovdb/python/examples/make_funny_nanovdb.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Build a level set from a Python function evaluated at every voxel. - -Python port of ex_make_funny_nanovdb: a trigonometric interference -pattern is CSG-intersected with a sphere and clamped to a narrow band, -using the functor-based nanovdb.tools.createFloatGrid factory. The -callback is one Python call per voxel in the bbox, so this port uses a -65^3 domain (~275k calls, a few seconds) where the C++ original fills -[-500,500]^3 — scale `SIZE` up only if you are willing to wait. - -Run with: python make_funny_nanovdb.py -""" -import math -import os -import tempfile - -import nanovdb - -SIZE = 32 # half-width of the cubic domain, in voxels -BACKGROUND = 5.0 # narrow-band half-width, in world units -FREQ = 0.8 # rescaled from the C++ 0.1 to fit the smaller domain - - -def funny(ijk): - v = 4.0 + 5.0 * (math.cos(ijk.x * FREQ) * math.sin(ijk.y * FREQ) + - math.cos(ijk.y * FREQ) * math.sin(ijk.z * FREQ) + - math.cos(ijk.z * FREQ) * math.sin(ijk.x * FREQ)) - # CSG intersection with a sphere of radius SIZE. - r = math.sqrt(ijk.x ** 2 + ijk.y ** 2 + ijk.z ** 2) - v = max(v, r - SIZE) - # Clamp to the narrow band. - return max(-BACKGROUND, min(BACKGROUND, v)) - - -def main(): - bbox = nanovdb.math.CoordBBox(nanovdb.math.Coord(-SIZE), - nanovdb.math.Coord(SIZE)) - print(f"Evaluating funny() over {bbox} ...") - handle = nanovdb.tools.createFloatGrid( - BACKGROUND, "funny", nanovdb.GridClass.LevelSet, funny, bbox) - - grid = handle.grid() - print(f"activeVoxelCount = {grid.activeVoxelCount()}") - acc = grid.getAccessor() - probe = nanovdb.math.Coord(0, 0, 0) - print(f"value at {probe} = {acc.getValue(probe):.3f}") - assert grid.isLevelSet() - assert grid.activeVoxelCount() > 0 - - out_dir = tempfile.mkdtemp(prefix="nanovdb_") - path = os.path.join(out_dir, "funny.nvdb") - try: - nanovdb.io.writeGrid(path, handle, codec=nanovdb.io.Codec.BLOSC) - except RuntimeError: - nanovdb.io.writeGrid(path, handle, codec=nanovdb.io.Codec.NONE) - print(f"Wrote {path}") - - -if __name__ == "__main__": - main() diff --git a/nanovdb/nanovdb/python/examples/make_typed_grids.py b/nanovdb/nanovdb/python/examples/make_typed_grids.py deleted file mode 100644 index 209364664d..0000000000 --- a/nanovdb/nanovdb/python/examples/make_typed_grids.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Build grids of many value types and read them back polymorphically. - -Python port of ex_make_typed_grids: a small solid sphere is baked once -per value type through the matching nanovdb.tools.build.Grid -mutable builder, all handles are written to a single .nvdb file, and -the file is re-read with handle.grid() returning the correct typed -subclass for each grid at runtime. - -Run with: python make_typed_grids.py -""" -import os -import tempfile - -import nanovdb - -RADIUS = 8 # voxels; ~2.1k active voxels per grid keeps this quick - - -def solid_sphere_coords(): - r2 = RADIUS * RADIUS - for i in range(-RADIUS, RADIUS + 1): - for j in range(-RADIUS, RADIUS + 1): - for k in range(-RADIUS, RADIUS + 1): - if i * i + j * j + k * k < r2: - yield nanovdb.math.Coord(i, j, k) - - -def build_typed_grids(): - # (builder class, grid name, background, voxel value) - specs = [ - (nanovdb.tools.build.FloatGrid, "float_grid", 0.0, 1.0), - (nanovdb.tools.build.DoubleGrid, "double_grid", 0.0, 1.0), - (nanovdb.tools.build.Int16Grid, "int16_grid", 0, 1), - (nanovdb.tools.build.Int32Grid, "int32_grid", 0, 1), - (nanovdb.tools.build.Int64Grid, "int64_grid", 0, 1), - (nanovdb.tools.build.UInt32Grid, "uint32_grid", 0, 1), - (nanovdb.tools.build.Vec3fGrid, "vec3f_grid", - nanovdb.math.Vec3f(0.0), nanovdb.math.Vec3f(1.0, 0.0, 0.0)), - ] - handles = [] - for cls, name, background, value in specs: - grid = cls(background, name, nanovdb.GridClass.Unknown) - for ijk in solid_sphere_coords(): - grid.setValue(ijk, value) - handles.append(grid.toNanoVDB()) - print(f"built {name} ({cls.__name__})") - return handles - - -def main(): - handles = build_typed_grids() - - out_dir = tempfile.mkdtemp(prefix="nanovdb_") - path = os.path.join(out_dir, "custom_types.nvdb") - nanovdb.io.writeGrids(path, handles) - print(f"Wrote {len(handles)} grids to {path}") - - # Re-read and dispatch: handle.grid() returns FloatGrid, Int16Grid, - # Vec3fGrid, ... according to the GridType each grid carries. - probe = nanovdb.math.Coord(0, 0, 0) - for handle in nanovdb.io.readGrids(path): - grid = handle.grid() - value = grid.getAccessor().getValue(probe) - print(f" {grid.gridName():<12} -> {type(grid).__name__:<12} " - f"active={grid.activeVoxelCount()} value(0,0,0)={value}") - assert grid.activeVoxelCount() > 0 - - -if __name__ == "__main__": - main() diff --git a/nanovdb/nanovdb/python/examples/node_manager.py b/nanovdb/nanovdb/python/examples/node_manager.py deleted file mode 100644 index 3ca3feffbe..0000000000 --- a/nanovdb/nanovdb/python/examples/node_manager.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Iterate a grid's nodes linearly through a NodeManager. - -Python port of the host half of ex_nodemanager_cuda: createNodeManager -builds linearized arrays of the tree's leaf, lower, and upper nodes so -they can be visited by index instead of by tree traversal. Each node -exposes its origin, per-node stats, and (on leaves) the raw 512-value -buffer. For bulk NumPy analytics over every leaf at once, see -bulk_leaf_numpy.py's grid.leaf_values() instead. - -Run with: python node_manager.py -""" -import nanovdb - - -def main(): - handle = nanovdb.tools.createLevelSetSphere(radius=50.0, name="sphere") - grid = handle.grid() - tree = grid.tree() - - nmh = nanovdb.createNodeManager(grid) - nm = nmh.mgr() - print(f"NodeManager over {grid.gridName()!r} (linear={nm.isLinear()}):") - print(f" leaves={nm.leafCount()}, lower={nm.lowerCount()}, " - f"upper={nm.upperCount()}") - - # The counts mirror the tree's per-level node counts. - assert nm.leafCount() == tree.nodeCount(0) - assert nm.lowerCount() == tree.nodeCount(1) - assert nm.upperCount() == tree.nodeCount(2) - - # Linear access agrees with tree traversal. - assert nm.leaf(0).origin() == tree.getFirstLeaf().origin() - - # Visit a few leaves by index: origin, activity, and value range. - for i in range(min(5, nm.leafCount())): - leaf = nm.leaf(i) - print(f" leaf[{i}] origin={leaf.origin()} " - f"on={leaf.valueMask().countOn()} " - f"min={leaf.minimum():.3f} max={leaf.maximum():.3f}") - - # Internal nodes are reachable the same way. - lower = nm.lower(0) - print(f" lower[0] origin={lower.origin()} " - f"children={lower.childMask().countOn()}") - - -if __name__ == "__main__": - main() diff --git a/nanovdb/nanovdb/python/examples/openvdb_interop.py b/nanovdb/nanovdb/python/examples/openvdb_interop.py deleted file mode 100644 index 2871026e1a..0000000000 --- a/nanovdb/nanovdb/python/examples/openvdb_interop.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Round-trip a grid between OpenVDB and NanoVDB. - -Python port of ex_openvdb_to_nanovdb and ex_openvdb_to_nanovdb_accessor: -an OpenVDB level-set sphere is converted to NanoVDB with openToNanoVDB, -values are compared through both libraries' accessors, and the NanoVDB -grid is converted back with nanoToOpenVDB. Requires a build with -NANOVDB_USE_OPENVDB and an importable `openvdb` module; the script -skips (exit 0) when either is missing. - -Run with: python openvdb_interop.py -""" -import nanovdb - - -def main(): - if not hasattr(nanovdb.tools, "openToNanoVDB"): - print("nanovdb was built without NANOVDB_USE_OPENVDB. Skipping...") - return - try: - import openvdb - except ImportError: - print("openvdb not found. Skipping...") - return - - sphere = openvdb.createLevelSetSphere(100.0) - sphere.name = "sphere" - handle = nanovdb.tools.openToNanoVDB(sphere) - grid = handle.grid() - print(f"openToNanoVDB: {grid.gridName()!r}, class={grid.gridClass()}, " - f"active={grid.activeVoxelCount()}") - assert grid.gridClass() == nanovdb.GridClass.LevelSet - - # Compare a cross-section through both accessors, as the C++ - # accessor example does. - open_acc = sphere.getAccessor() - nano_acc = grid.getAccessor() - for i in range(97, 104): - open_v = open_acc.getValue((i, 0, 0)) - nano_v = nano_acc.getValue(nanovdb.math.Coord(i, 0, 0)) - print(f" ({i},0,0): openvdb={open_v:.3f} nanovdb={nano_v:.3f}") - assert abs(open_v - nano_v) < 1e-5 - - # And back: NanoVDB -> OpenVDB. - back = nanovdb.tools.nanoToOpenVDB(handle) - print(f"nanoToOpenVDB: {back.name!r}, empty={back.empty()}") - assert back.name == "sphere" - assert not back.empty() - - -if __name__ == "__main__": - main() diff --git a/nanovdb/nanovdb/python/examples/quantize.py b/nanovdb/nanovdb/python/examples/quantize.py deleted file mode 100644 index 83935f3b68..0000000000 --- a/nanovdb/nanovdb/python/examples/quantize.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Quantize a NanoGrid down to a quantized BuildT. - -nanovdb.tools.createNanoGridFp4 / Fp8 / Fp16 / FpN are the quantized -counterparts of the regular grid types. Fp4 / Fp8 / Fp16 use a fixed -bit-width per voxel. FpN picks the bit-width per leaf so each leaf -hits a user-supplied tolerance (the "oracle"). Smaller bit widths -give smaller files at the cost of approximation error. - -Run with: python quantize.py -""" -import nanovdb - - -def gridSize_in_kb(handle): - return handle.gridSize(0) / 1024 - - -def main(): - # Source: a 50-radius sphere fog volume at full float precision. - src_handle = nanovdb.tools.createFogVolumeSphere(radius=50.0) - src_grid = src_handle.grid() - print(f"Source FloatGrid: {gridSize_in_kb(src_handle):.1f} KB, " - f"active voxels = {src_grid.activeVoxelCount()}") - - # Fixed-width quantization. Each subsequent format roughly halves - # the per-voxel storage cost; dithering optional. - for fn_name, label in [ - ("createNanoGridFp16", "Fp16 (16-bit fixed)"), - ("createNanoGridFp8", "Fp8 (8-bit fixed)"), - ("createNanoGridFp4", "Fp4 (4-bit fixed)"), - ]: - h = getattr(nanovdb.tools, fn_name)(src_grid, ditherOn=True) - print(f" {label}: {gridSize_in_kb(h):.1f} KB, " - f"active voxels = {h.grid().activeVoxelCount()}") - - # Variable-width FpN. The oracle picks the per-leaf bit-width to - # meet a tolerance — AbsDiff for absolute error, RelDiff for - # relative error. -1 tolerance means "uninitialized" so we pass - # an explicit value. - abs_oracle = nanovdb.tools.AbsDiff(0.05) # ±0.05 per voxel - h_fpn_abs = nanovdb.tools.createNanoGridFpN(src_grid, abs_oracle) - print(f" FpN (AbsDiff 0.05): {gridSize_in_kb(h_fpn_abs):.1f} KB") - - rel_oracle = nanovdb.tools.RelDiff(0.1) # 10% relative error - h_fpn_rel = nanovdb.tools.createNanoGridFpN(src_grid, rel_oracle) - print(f" FpN (RelDiff 0.10): {gridSize_in_kb(h_fpn_rel):.1f} KB") - - # The output is a regular NanoGrid — read-only, but exposes - # the standard surface (gridType, activeVoxelCount, accessor.getValue - # returning a decoded float, etc). - fpn = h_fpn_abs.grid() - print(f"\nFpN grid type: {fpn.gridType()}, " - f"voxel at (0,0,0) = {fpn.getAccessor().getValue(nanovdb.math.Coord(0, 0, 0))}") - - -if __name__ == "__main__": - main() diff --git a/nanovdb/nanovdb/python/examples/raytrace_fog_volume.py b/nanovdb/nanovdb/python/examples/raytrace_fog_volume.py deleted file mode 100644 index ddb92be4a3..0000000000 --- a/nanovdb/nanovdb/python/examples/raytrace_fog_volume.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Ray-march a fog volume on the CPU and write a PGM image. - -Python port of the host path of ex_raytrace_fog_volume: each ray is -clipped to the grid's index bounding box, then integrated with fixed -steps, accumulating transmittance from the density sampled through a -ReadAccessor at Coord.Floor of the march position — the accessor-based -idiom, in contrast to the sampler-based raytrace_level_set.py. Every -step is one Python-to-C++ call, so the default RES is modest; raise it -for a nicer image. - -Run with: python raytrace_fog_volume.py -""" -import math -import os -import tempfile - -import nanovdb - -RES = 64 # image is RES x RES pixels -FOV_DEG = 45.0 -DT = 1.0 # march step, in voxels -SIGMA = 0.2 # extinction scale applied to the sampled density - - -def clip_to_bbox(eye, direction, bbox): - """Slab-clip a ray against a CoordBBox; returns (t0, t1) or None.""" - t0, t1 = 0.0, math.inf - for axis in range(3): - lo = bbox.min[axis] - eye[axis] - hi = bbox.max[axis] + 1.0 - eye[axis] - d = direction[axis] - if abs(d) < 1e-12: - if lo > 0.0 or hi < 0.0: - return None - continue - near, far = lo / d, hi / d - if near > far: - near, far = far, near - t0, t1 = max(t0, near), min(t1, far) - if t0 > t1: - return None - return t0, t1 - - -def main(): - handle = nanovdb.tools.createFogVolumeSphere(radius=50.0, name="fog") - grid = handle.grid() - bbox = grid.indexBBox() - acc = grid.getAccessor() - - # Perspective camera looking down -z, as in the C++ RayGenOp, - # working directly in index space (the grid transform is uniform). - dim = [bbox.max[i] + 1 - bbox.min[i] for i in range(3)] - center = [bbox.min[i] + 0.5 * dim[i] for i in range(3)] - eye = (center[0], center[1], center[2] + 2.0 * dim[2]) - tan_fov = math.tan(math.radians(FOV_DEG) * 0.5) - - pixels = bytearray(RES * RES) - for y in range(RES): - for x in range(RES): - px = (2.0 * (x + 0.5) / RES - 1.0) * tan_fov - py = (2.0 * (y + 0.5) / RES - 1.0) * tan_fov - norm = math.sqrt(px * px + py * py + 1.0) - direction = (px / norm, py / norm, -1.0 / norm) - - span = clip_to_bbox(eye, direction, bbox) - if span is None: - continue - transmittance = 1.0 - t = span[0] - while t < span[1]: - pos = nanovdb.math.Vec3d(eye[0] + t * direction[0], - eye[1] + t * direction[1], - eye[2] + t * direction[2]) - density = acc.getValue(nanovdb.math.Coord.Floor(pos)) - transmittance *= 1.0 - density * SIGMA * DT - if transmittance < 0.005: - break - t += DT - pixels[y * RES + x] = int(255 * (1.0 - transmittance)) - - out_dir = tempfile.mkdtemp(prefix="nanovdb_") - path = os.path.join(out_dir, "raytrace_fog_volume.pgm") - with open(path, "wb") as f: - f.write(f"P5\n{RES} {RES}\n255\n".encode("ascii")) - f.write(bytes(pixels)) - lit = sum(1 for p in pixels if p > 0) - print(f"Rendered {RES}x{RES} image, {lit} foggy pixels -> {path}") - assert lit > 0 - - -if __name__ == "__main__": - main() diff --git a/nanovdb/nanovdb/python/examples/raytrace_level_set.py b/nanovdb/nanovdb/python/examples/raytrace_level_set.py deleted file mode 100644 index 415787d031..0000000000 --- a/nanovdb/nanovdb/python/examples/raytrace_level_set.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Ray-trace a narrow-band level set on the CPU and write a PGM image. - -Python port of the host path of ex_raytrace_level_set. The C++ example -finds surface hits with math::Ray + HDDA ZeroCrossing, which are not -bound in Python; this port re-expresses the search as sphere tracing — -the clamped SDF value itself bounds the safe step size — using the -trilinear sampler, with sampler.zeroCrossing() confirming the interval -and sampler.gradient() shading the hit. Every sample is one -Python-to-C++ call, so the default RES is modest; raise it for a nicer -image. - -Run with: python raytrace_level_set.py -""" -import math -import os -import tempfile - -import nanovdb - -RES = 64 # image is RES x RES pixels -FOV_DEG = 45.0 -LIGHT = (0.577, 0.577, 0.577) # unit vector toward the light - - -def clip_to_bbox(eye, direction, bbox): - """Slab-clip a ray against a CoordBBox; returns (t0, t1) or None.""" - t0, t1 = 0.0, math.inf - for axis in range(3): - lo = bbox.min[axis] - eye[axis] - hi = bbox.max[axis] + 1.0 - eye[axis] - d = direction[axis] - if abs(d) < 1e-12: - if lo > 0.0 or hi < 0.0: - return None - continue - near, far = lo / d, hi / d - if near > far: - near, far = far, near - t0, t1 = max(t0, near), min(t1, far) - if t0 > t1: - return None - return t0, t1 - - -def trace(sampler, eye, direction, t0, t1, voxel_size): - """Sphere-trace from t0 to t1; returns (hit t, crossing seen) or None.""" - t = t0 - while t < t1: - pos = nanovdb.math.Vec3d(eye[0] + t * direction[0], - eye[1] + t * direction[1], - eye[2] + t * direction[2]) - d = sampler(pos) # world-unit SDF, clamped to the narrow band - if d <= 0.0: - # zeroCrossing() reports whether the reconstruction stencil - # here straddles the iso-surface — the sampler-level analog - # of the HDDA ZeroCrossing test in the C++ example. - return t, sampler.zeroCrossing(pos) - # The clamped SDF bounds the distance to the surface, so it is - # a safe (index-space) step size; never step below half a voxel. - t += max(d / voxel_size, 0.5) - return None - - -def main(): - handle = nanovdb.tools.createLevelSetSphere(radius=100.0, name="sphere") - grid = handle.grid() - bbox = grid.indexBBox() - voxel_size = grid.voxelSize()[0] - sampler = nanovdb.math.createTrilinearSampler(grid) - - # Perspective camera looking down -z, as in the C++ RayGenOp, - # working directly in index space (the grid transform is uniform). - dim = [bbox.max[i] + 1 - bbox.min[i] for i in range(3)] - center = [bbox.min[i] + 0.5 * dim[i] for i in range(3)] - eye = nanovdb.math.Vec3d(center[0], center[1], center[2] + 2.0 * dim[2]) - tan_fov = math.tan(math.radians(FOV_DEG) * 0.5) - - pixels = bytearray(RES * RES) - crossings = 0 - for y in range(RES): - for x in range(RES): - px = (2.0 * (x + 0.5) / RES - 1.0) * tan_fov - py = (2.0 * (y + 0.5) / RES - 1.0) * tan_fov - norm = math.sqrt(px * px + py * py + 1.0) - direction = (px / norm, py / norm, -1.0 / norm) - - span = clip_to_bbox(eye, direction, bbox) - if span is None: - continue - hit = trace(sampler, eye, direction, span[0], span[1], - voxel_size) - if hit is None: - continue - t_hit, crossed = hit - crossings += crossed - pos = nanovdb.math.Vec3d(eye[0] + t_hit * direction[0], - eye[1] + t_hit * direction[1], - eye[2] + t_hit * direction[2]) - # gradient() is in index-space units — normalize before use. - n = sampler.gradient(pos) - n.normalize() - shade = max(0.0, n[0] * LIGHT[0] + n[1] * LIGHT[1] - + n[2] * LIGHT[2]) - pixels[y * RES + x] = int(255 * shade) - - out_dir = tempfile.mkdtemp(prefix="nanovdb_") - path = os.path.join(out_dir, "raytrace_level_set.pgm") - with open(path, "wb") as f: - f.write(f"P5\n{RES} {RES}\n255\n".encode("ascii")) - f.write(bytes(pixels)) - lit = sum(1 for p in pixels if p > 0) - print(f"Rendered {RES}x{RES} image, {lit} lit pixels " - f"({crossings} confirmed zero-crossings) -> {path}") - assert lit > 0 and crossings > 0 - - -if __name__ == "__main__": - main() diff --git a/nanovdb/nanovdb/python/examples/validate.py b/nanovdb/nanovdb/python/examples/validate.py deleted file mode 100644 index 92469c5f4e..0000000000 --- a/nanovdb/nanovdb/python/examples/validate.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Validate a NanoVDB grid and round-trip its checksum. - -nanovdb.tools exposes the grid-quality surface: validateGrid (single -grid), checkGrid -> (ok, error), isValid, plus the checksum helpers -evalChecksum / validateChecksum / updateChecksum. This example walks -through a typical "load and verify" workflow. - -Run with: python validate.py -""" -import nanovdb - - -def main(): - # A well-formed grid: createLevelSetSphere returns a NanoGrid - # with stats and checksum populated to the defaults. - handle = nanovdb.tools.createLevelSetSphere(radius=10.0) - grid = handle.grid() - - # Whole-handle validation. validateGrids returns a single bool. - # The verbose=True flag prints failure details to std::cerr - # (visible from Python via std::cerr -> sys.stderr on most stdlibs). - all_ok = nanovdb.tools.validateGrids( - handle, nanovdb.CheckMode.Default, verbose=False) - print(f"validateGrids(handle, Default) = {all_ok}") - - # Per-grid validation, with a helpful message on failure. - one_ok = nanovdb.tools.validateGrid(handle, 0, nanovdb.CheckMode.Full) - print(f"validateGrid(handle, 0, Full) = {one_ok}") - - # validateGrid returns False (no raise) on out-of-range gridID; - # CheckMode.Disable short-circuits and always returns True. - print(f"validateGrid(handle, 99) = " - f"{nanovdb.tools.validateGrid(handle, 99)} (out of range)") - print(f"validateGrid(handle, 99, Disable) = " - f"{nanovdb.tools.validateGrid(handle, 99, nanovdb.CheckMode.Disable)}" - f" (Disable short-circuit)") - - # tools.checkGrid returns the structural check result plus a - # human-readable error message (empty on success). - ok, msg = nanovdb.tools.checkGrid(grid, nanovdb.CheckMode.Full) - print(f"checkGrid(grid, Full) = ok={ok}, msg={msg!r}") - - # tools.isValid is checkGrid + checksum verification, returning - # one bool. - print(f"isValid(grid, Default) = " - f"{nanovdb.tools.isValid(grid, nanovdb.CheckMode.Default)}") - - # Checksum round-trip. evalChecksum is non-mutating; updateChecksum - # writes back into the grid header. - cs1 = nanovdb.tools.evalChecksum(grid, nanovdb.CheckMode.Full) - nanovdb.tools.updateChecksum(grid, nanovdb.CheckMode.Full) - cs2 = nanovdb.tools.evalChecksum(grid, nanovdb.CheckMode.Full) - print(f"evalChecksum equal after no-op update: {cs1 == cs2}") - print(f"validateChecksum(grid, Full) = " - f"{nanovdb.tools.validateChecksum(grid, nanovdb.CheckMode.Full)}") - - -if __name__ == "__main__": - main() diff --git a/nanovdb/nanovdb/python/test/TestExamples.py b/nanovdb/nanovdb/python/test/TestExamples.py deleted file mode 100644 index abc3584dc5..0000000000 --- a/nanovdb/nanovdb/python/test/TestExamples.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Smoke test: run every example script and require a clean exit. - -The examples are self-contained and degrade gracefully (exit 0 with a -skip message) when an optional dependency such as NumPy or OpenVDB is -missing, so a non-zero exit or an uncaught exception always indicates -a real breakage — typically the bindings drifting under the examples. - -The script list is deliberately explicit rather than a glob so that a -missing listed example fails loudly and unrelated files placed under -examples/ are not executed. -""" - -import os -import subprocess -import sys -import tempfile -import unittest - -EXAMPLES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), - os.pardir, "examples") - -EXAMPLE_SCRIPTS = [ - "build_grid.py", - "bulk_leaf_numpy.py", - "collide_level_set.py", - "index_grid_channels.py", - "io_roundtrip.py", - "load_inspect.py", - "make_funny_nanovdb.py", - "make_typed_grids.py", - "node_manager.py", - "openvdb_interop.py", - "quantize.py", - "raytrace_fog_volume.py", - "raytrace_level_set.py", - "validate.py", -] - - -class TestExamples(unittest.TestCase): - pass - - -def _make_test(script_name): - def test(self): - script = os.path.join(EXAMPLES_DIR, script_name) - self.assertTrue(os.path.isfile(script), - f"example script is missing: {script}") - command = [sys.executable, script] - env = os.environ.copy() - # Preserve output up to a native crash and ask Python to report the - # active frame for fatal signals and Windows exceptions. - env["PYTHONFAULTHANDLER"] = "1" - env["PYTHONUNBUFFERED"] = "1" - if hasattr(os, "add_dll_directory"): - # Match TestNanoVDB.py's in-tree OpenVDB DLL lookup. The child - # starts in a temporary directory, so resolve this while the - # parent is still in the CMake configuration directory. - config = os.path.basename(os.getcwd()) - openvdb_dll_directory = os.path.abspath(os.path.join( - os.getcwd(), os.pardir, os.pardir, os.pardir, os.pardir, - "openvdb", "openvdb", config)) - dll_directories = [openvdb_dll_directory] - cuda_root = env.get("CUDA_PATH") - if cuda_root: - dll_directories.append(os.path.join(cuda_root, "bin")) - env["NANOVDB_TEST_DLL_DIRECTORIES"] = os.pathsep.join( - dll_directories) - # Python imports sitecustomize during startup. Add this test - # directory to the child's module path so sitecustomize.py can - # register only the required OpenVDB and CUDA DLL directories - # before the example imports nanovdb, while still running the - # example as a normal script. - test_directory = os.path.dirname(os.path.abspath(__file__)) - env["PYTHONPATH"] = os.pathsep.join( - (test_directory, env.get("PYTHONPATH", ""))) - # A fresh cwd per run keeps any output files out of the source - # tree (the examples themselves also write to tempdirs). - result = subprocess.run( - command, - cwd=tempfile.mkdtemp(prefix="nanovdb_example_"), - env=env, - capture_output=True, - text=True, - timeout=120, - ) - self.assertEqual( - result.returncode, 0, - f"{script_name} exited with {result.returncode}\n" - f"--- stdout ---\n{result.stdout}\n" - f"--- stderr ---\n{result.stderr}") - - return test - - -for _name in EXAMPLE_SCRIPTS: - _test_name = "test_" + _name.replace(".py", "") - setattr(TestExamples, _test_name, _make_test(_name)) - - -if __name__ == "__main__": - unittest.main() diff --git a/nanovdb/nanovdb/python/test/TestNanoVDB.py b/nanovdb/nanovdb/python/test/TestNanoVDB.py index f8dbaf92db..c204f8f2bc 100644 --- a/nanovdb/nanovdb/python/test/TestNanoVDB.py +++ b/nanovdb/nanovdb/python/test/TestNanoVDB.py @@ -307,7 +307,7 @@ def test_float_grid(self): for i in range(handle.gridCount()): self.assertTrue(handle.gridSize(i) > 0) self.assertEqual(handle.gridType(i), nanovdb.GridType.Float) - grid = handle.grid(i) + grid = handle.floatGrid(i) self.assertIsNotNone(grid) accessor = nanovdb.FloatReadAccessor(grid) coord = nanovdb.math.Coord(0) @@ -325,7 +325,7 @@ def test_checksum(self): for i in range(handle.gridCount()): self.assertTrue(handle.gridSize(i) > 0) self.assertEqual(handle.gridType(i), nanovdb.GridType.Float) - grid = handle.grid(i) + grid = handle.floatGrid(i) self.assertIsNotNone(grid) checksum = grid.checksum() nanovdb.tools.updateChecksum(grid, nanovdb.CheckMode.Default) @@ -337,7 +337,7 @@ class TestGridHandleExchange(unittest.TestCase): def test_list_to_vector(self): handle = nanovdb.tools.createLevelSetTorus(nanovdb.GridType.Double) self.assertEqual(handle.gridCount(), 1) - self.assertIsNotNone(handle.grid()) + self.assertIsNotNone(handle.doubleGrid()) handles = [handle, handle] dstFile = tempfile.NamedTemporaryFile(delete=False) dstFile.close() @@ -353,699 +353,11 @@ def test_list_to_vector(self): for readHandle in readHandles: self.assertEqual(readHandle.gridCount(), 1) self.assertEqual(readHandle.gridType(0), nanovdb.GridType.Double) - self.assertIsInstance(readHandle.grid(), nanovdb.DoubleGrid) + self.assertIsNotNone(readHandle.doubleGrid()) finally: os.unlink(dstFile.name) -class TestPolymorphicGridAccess(unittest.TestCase): - """handle.grid(n) returns the correct typed Grid subclass selected by - gridType(n); the legacy per-type accessors (floatGrid(), etc.) are not - bound.""" - - def test_float_grid(self): - h = nanovdb.tools.createFogVolumeSphere() - self.assertIsInstance(h.grid(), nanovdb.FloatGrid) - self.assertEqual(h.grid().gridType(), nanovdb.GridType.Float) - - def test_double_grid(self): - h = nanovdb.tools.createLevelSetTorus(nanovdb.GridType.Double) - self.assertIsInstance(h.grid(), nanovdb.DoubleGrid) - self.assertEqual(h.grid().gridType(), nanovdb.GridType.Double) - - def test_out_of_range_returns_none(self): - h = nanovdb.tools.createFogVolumeSphere() - self.assertIsNone(h.grid(99)) - - def test_empty_handle_returns_none(self): - self.assertIsNone(nanovdb.GridHandle().grid()) - - def test_typed_accessors_removed(self): - # The legacy per-type accessors were replaced by handle.grid(n). - h = nanovdb.tools.createFogVolumeSphere() - self.assertFalse(hasattr(h, "floatGrid")) - self.assertFalse(hasattr(h, "doubleGrid")) - self.assertFalse(hasattr(h, "int32Grid")) - self.assertFalse(hasattr(h, "vec3fGrid")) - self.assertFalse(hasattr(h, "rgba8Grid")) - - -class TestGridBase(unittest.TestCase): - """Methods that don't depend on BuildT (gridType, gridClass, voxelSize, - isLevelSet/...) resolve via the Grid base class shared by every typed - grid subclass.""" - - def test_grid_base_class_name(self): - # Typed grids inherit from a base class named "Grid" (no more "GridData"). - self.assertTrue(any(b.__name__ == "Grid" for b in nanovdb.FloatGrid.__bases__)) - self.assertFalse(hasattr(nanovdb, "GridData")) - - def test_lifted_methods_accessible_via_inheritance(self): - h = nanovdb.tools.createFogVolumeSphere(name="probe") - g = h.grid() - self.assertEqual(g.gridType(), nanovdb.GridType.Float) - self.assertEqual(g.gridClass(), nanovdb.GridClass.FogVolume) - self.assertTrue(g.isFogVolume()) - self.assertFalse(g.isLevelSet()) - self.assertEqual(g.gridName(), "probe") - self.assertEqual(g.shortGridName(), "probe") - self.assertGreater(g.gridSize(), 0) - self.assertEqual(g.gridCount(), 1) - - -class TestGridMetaData(unittest.TestCase): - """nanovdb.GridMetaData is a type-erased introspector — construct from a - Grid and query gridType/gridClass/voxelSize/etc. without knowing - BuildT.""" - - def test_constructed_from_grid(self): - h = nanovdb.tools.createFogVolumeSphere(name="probe") - m = nanovdb.GridMetaData(h.grid()) - self.assertEqual(m.gridType(), nanovdb.GridType.Float) - self.assertEqual(m.gridClass(), nanovdb.GridClass.FogVolume) - self.assertEqual(m.shortGridName(), "probe") - self.assertTrue(m.isValid()) - self.assertTrue(m.isFogVolume()) - self.assertGreater(m.activeVoxelCount(), 0) - self.assertEqual(m.blindDataCount(), 0) - self.assertTrue(nanovdb.GridMetaData.safeCast(h.grid())) - - -class TestBlindDataEmpty(unittest.TestCase): - """Blind data API (blindDataCount, blindMetaData, findBlindData, - findBlindDataForSemantic, getBlindData) returns sensible None/-1 - sentinels on grids that have no blind data channels.""" - - def test_no_blind_data(self): - h = nanovdb.tools.createFogVolumeSphere() - g = h.grid() - self.assertEqual(g.blindDataCount(), 0) - self.assertIsNone(g.blindMetaData(0)) - self.assertEqual(g.findBlindData("anything"), -1) - self.assertEqual( - g.findBlindDataForSemantic(nanovdb.GridBlindDataSemantic.PointPosition), - -1, - ) - self.assertIsNone(g.getBlindData(0)) - - -class TestSplitMergeCopy(unittest.TestCase): - """splitGrids(h) -> list of single-grid handles, mergeGrids([h1, h2]) - -> combined handle, h.copy() -> deep buffer copy. mergeGrids must not - consume its input handles.""" - - def test_split_and_merge_roundtrip(self): - h1 = nanovdb.tools.createFogVolumeSphere(name="a") - h2 = nanovdb.tools.createLevelSetTorus(nanovdb.GridType.Float, name="b") - merged = nanovdb.mergeGrids([h1, h2]) - self.assertEqual(merged.gridCount(), 2) - split = nanovdb.splitGrids(merged) - self.assertEqual(len(split), 2) - for s in split: - self.assertEqual(s.gridCount(), 1) - - def test_merge_does_not_consume_inputs(self): - # Regression: a previous mergeGrids implementation used - # nb::cast which moved the underlying C++ handle out of - # the Python wrapper, silently emptying h1/h2 after the call. The - # binding now reads each handle by const reference. - h1 = nanovdb.tools.createFogVolumeSphere(name="a") - h2 = nanovdb.tools.createLevelSetTorus(nanovdb.GridType.Float, name="b") - sz1, sz2 = h1.size(), h2.size() - gc1, gc2 = h1.gridCount(), h2.gridCount() - - merged = nanovdb.mergeGrids([h1, h2]) - - self.assertEqual(h1.gridCount(), gc1) - self.assertEqual(h2.gridCount(), gc2) - self.assertEqual(h1.size(), sz1) - self.assertEqual(h2.size(), sz2) - # merged still works - self.assertEqual(merged.gridCount(), 2) - self.assertEqual(merged.grid(0).gridName(), "a") - self.assertEqual(merged.grid(1).gridName(), "b") - - def test_copy_is_deep(self): - src = nanovdb.tools.createFogVolumeSphere(name="orig") - cp = src.copy() - self.assertIsNot(src, cp) - self.assertEqual(cp.gridCount(), src.gridCount()) - self.assertEqual(cp.grid().gridName(), "orig") - - -class TestBuildTRegistrations(unittest.TestCase): - """Every BuildT we bind exposes the right shape — a Grid class, a - ReadAccessor, and (for arithmetic-valued scalars) a NodeInfo. Accessor - surface depends on the type kind: scalar accessors have setVoxel + - getNodeInfo; vector accessors have setVoxel only; read-only accessors - (Boolean, Fp*, Index, OnIndex, Mask) have neither.""" - - SCALARS = ["Int16", "Int64", "UInt8", "UInt32"] - VECTORS = ["Vec3d", "Vec4f", "Vec4d", "Vec3u8", "Vec3u16"] - READONLY = ["Boolean", "Fp4", "Fp8", "Fp16", "FpN", "Index", "OnIndex", "Mask"] - - def test_all_grid_classes_registered(self): - for suffix in self.SCALARS + self.VECTORS + self.READONLY: - cls = getattr(nanovdb, suffix + "Grid", None) - self.assertIsNotNone(cls, f"{suffix}Grid missing") - # All inherit from the type-erased Grid base. - self.assertIn(nanovdb.Grid, cls.__mro__) - - def test_all_accessors_registered(self): - for suffix in self.SCALARS + self.VECTORS + self.READONLY: - acc = getattr(nanovdb, suffix + "ReadAccessor", None) - self.assertIsNotNone(acc, f"{suffix}ReadAccessor missing") - - def test_scalar_accessors_have_setvoxel_and_nodeinfo(self): - for suffix in self.SCALARS: - acc = getattr(nanovdb, suffix + "ReadAccessor") - self.assertTrue(hasattr(acc, "setVoxel"), - f"{suffix}ReadAccessor missing setVoxel") - self.assertTrue(hasattr(acc, "getNodeInfo"), - f"{suffix}ReadAccessor missing getNodeInfo") - self.assertIsNotNone(getattr(nanovdb, suffix + "NodeInfo", None), - f"{suffix}NodeInfo missing") - - def test_vector_accessors_have_setvoxel_no_nodeinfo(self): - # The newer vector accessor names follow the consistent - # ReadAccessor pattern (Vec3dReadAccessor, ...); the - # original Vec3f one is named Vec3fReadVectorAccessor for backwards - # compatibility. - for suffix in self.VECTORS: - acc = getattr(nanovdb, suffix + "ReadAccessor") - self.assertTrue(hasattr(acc, "setVoxel")) - self.assertFalse(hasattr(acc, "getNodeInfo")) - - def test_all_grid_type_enums_reachable(self): - # GridType enum binding must cover every BuildT we register — - # otherwise Python users can't compare against handle.gridType(n). - # Locks in the full set; missing entries (e.g. an unbound enumerator - # for a freshly-added BuildT) get caught here. - for name in ["Float", "Double", "Int16", "Int32", "Int64", "UInt8", - "UInt32", "Boolean", "Half", "RGBA8", "Vec3f", "Vec3d", - "Vec4f", "Vec4d", "Vec3u8", "Vec3u16", "Mask", "Fp4", - "Fp8", "Fp16", "FpN", "Index", "OnIndex", "PointIndex"]: - self.assertTrue(hasattr(nanovdb.GridType, name), - f"nanovdb.GridType.{name} not bound") - - def test_readonly_accessors_have_neither_setvoxel_nor_nodeinfo(self): - # Quantized types decode to float on read but don't bind setVoxel; - # index types return uint64; ValueMask exposes only active-state - # queries. All share a bare ReadAccessor. - for suffix in self.READONLY: - acc = getattr(nanovdb, suffix + "ReadAccessor") - self.assertFalse(hasattr(acc, "setVoxel"), - f"{suffix}ReadAccessor should not have setVoxel") - self.assertFalse(hasattr(acc, "getNodeInfo"), - f"{suffix}ReadAccessor should not have getNodeInfo") - - -class TestTreeNodeWalking(unittest.TestCase): - """Walk a grid's tree from Python: Grid.tree(), Root/Upper/Lower/Leaf - node access and metadata, per-leaf zero-copy values() and bulk - grid.leaf_values() NumPy views, NodeManager + createNodeManager.""" - - @classmethod - def setUpClass(cls): - cls.h = nanovdb.tools.createFogVolumeSphere(name="probe") - cls.g = cls.h.grid() - cls.tree = cls.g.tree() - - def test_grid_tree_basic(self): - self.assertIsInstance(self.tree, nanovdb.FloatTree) - self.assertEqual(self.tree.background(), 3.0) # halfwidth*voxelsize default - self.assertGreater(self.tree.activeVoxelCount(), 0) - self.assertGreaterEqual(self.tree.totalNodeCount(), self.tree.nodeCount(0)) - - def test_extrema(self): - mn, mx = self.tree.extrema() - # FogVolumeSphere produces values in [0, 1]. - self.assertGreaterEqual(mn, 0.0) - self.assertLessEqual(mx, 1.0) - self.assertLessEqual(mn, mx) - - def test_first_leaf_and_node_metadata(self): - leaf = self.tree.getFirstLeaf() - self.assertIsInstance(leaf, nanovdb.FloatLeaf) - self.assertEqual(nanovdb.FloatLeaf.dim(), 8) - self.assertEqual(nanovdb.FloatLeaf.voxelCount(), 512) - # Origin should be aligned to LeafNode dim=8. - for c in (leaf.origin().x, leaf.origin().y, leaf.origin().z): - self.assertEqual(c % 8, 0) - - def test_root_metadata(self): - root = self.tree.root() - self.assertIsInstance(root, nanovdb.FloatRoot) - self.assertGreater(root.tileCount(), 0) - # Root bbox covers ALL active voxels — non-empty for a fog sphere. - bb = root.bbox() - self.assertFalse(bb.empty()) - self.assertEqual(root.background(), self.tree.background()) - - def test_leaf_values_zero_copy(self): - try: - import numpy as np - except ImportError: - self.skipTest("numpy not installed") - leaf = self.tree.getFirstLeaf() - vals = leaf.values() - self.assertEqual(vals.shape, (512,)) - self.assertEqual(vals.dtype, np.float32) - # Mutation through the view writes back into the grid buffer. - original = float(vals[0]) - vals[0] = original + 1.0 - self.assertAlmostEqual(float(leaf.getValue(0)), original + 1.0) - vals[0] = original # restore - - def test_leaf_values_unavailable_for_special_buildts(self): - # ValueIndex / ValueMask / bool / Fp* leaves don't carry T mValues[512], - # so the `values` accessor is not bound for them. - for cls_name in ("BooleanLeaf", "Fp4Leaf", "IndexLeaf", "MaskLeaf"): - leaf_cls = getattr(nanovdb, cls_name) - self.assertFalse(hasattr(leaf_cls, "values"), - f"{cls_name}.values should not be bound") - - def test_bulk_leaf_values(self): - try: - import numpy as np - except ImportError: - self.skipTest("numpy not installed") - bulk = self.g.leaf_values() - self.assertEqual(bulk.shape, (self.tree.nodeCount(0), 512)) - self.assertEqual(bulk.dtype, np.float32) - # First row should match per-leaf values(). - first_via_bulk = np.asarray(bulk[0]) - first_via_leaf = np.asarray(self.tree.getFirstLeaf().values()) - self.assertTrue(np.array_equal(first_via_bulk, first_via_leaf)) - - def test_bulk_leaf_values_empty_grid_returns_empty_array(self): - # A grid with no leaves returns an empty (0, 512) NumPy view rather - # than None, so callers can iterate / shape-test without a sentinel. - try: - import numpy as np - except ImportError: - self.skipTest("numpy not installed") - empty_bbox = nanovdb.math.CoordBBox() # default-constructed = empty - empty_h = nanovdb.tools.createFloatGrid( - 0.0, "empty", nanovdb.GridClass.Unknown, - lambda ijk: 0.0, empty_bbox) - bulk = empty_h.grid().leaf_values() - self.assertEqual(bulk.shape, (0, 512)) - self.assertEqual(bulk.dtype, np.float32) - - def test_node_manager_round_trip(self): - try: - import numpy as np - except ImportError: - self.skipTest("numpy not installed") - handle = nanovdb.createNodeManager(self.g) - self.assertGreater(handle.size(), 0) - self.assertTrue(bool(handle)) - nm = handle.mgr() - self.assertIsInstance(nm, nanovdb.FloatNodeManager) - self.assertTrue(nm.isLinear()) # createNanoGrid produces breadth-first - self.assertEqual(nm.leafCount(), self.tree.nodeCount(0)) - self.assertEqual(nm.lowerCount(), self.tree.nodeCount(1)) - self.assertEqual(nm.upperCount(), self.tree.nodeCount(2)) - # NodeManager.leaf(0) should be the same leaf as tree.getFirstLeaf() - # (breadth-first order). - self.assertEqual(nm.leaf(0).origin(), self.tree.getFirstLeaf().origin()) - self.assertTrue(np.array_equal( - nm.leaf(0).values(), self.tree.getFirstLeaf().values())) - - -class TestBoundsChecks(unittest.TestCase): - """Out-of-range indices on Leaf / Tree / NodeManager raise Python - exceptions rather than falling through into raw memory access. The - underlying C++ uses NANOVDB_ASSERT which is a no-op in release builds, - so the Python layer guards every entry point that takes a level or - index argument. - """ - - @classmethod - def setUpClass(cls): - cls.h = nanovdb.tools.createFogVolumeSphere() - cls.g = cls.h.grid() - cls.tree = cls.g.tree() - cls.leaf = cls.tree.getFirstLeaf() - cls.nm_handle = nanovdb.createNodeManager(cls.g) - cls.nm = cls.nm_handle.mgr() - - def test_leaf_offset_bounds(self): - n = nanovdb.FloatLeaf.voxelCount() - with self.assertRaises(IndexError): - self.leaf.isActive(n) - with self.assertRaises(IndexError): - self.leaf.isActive(n + 1000) - with self.assertRaises(IndexError): - self.leaf.getValue(n) - # In-range still works. - self.assertIsNotNone(self.leaf.getValue(0)) - self.assertIsNotNone(self.leaf.getValue(n - 1)) - - def test_tree_active_tile_count_level(self): - # activeTileCount levels are 1..3 (level 0 is leaves, not tiles). - with self.assertRaises(ValueError): - self.tree.activeTileCount(0) - with self.assertRaises(ValueError): - self.tree.activeTileCount(4) - # In-range still works. - self.assertGreaterEqual(self.tree.activeTileCount(3), 0) - - def test_tree_node_count_level(self): - # nodeCount levels are 0..2 (leaf / lower / upper). - with self.assertRaises(ValueError): - self.tree.nodeCount(-1) - with self.assertRaises(ValueError): - self.tree.nodeCount(3) - self.assertGreater(self.tree.nodeCount(0), 0) - - def test_node_manager_indexed_access(self): - with self.assertRaises(IndexError): - self.nm.leaf(self.nm.leafCount()) - with self.assertRaises(IndexError): - self.nm.lower(self.nm.lowerCount()) - with self.assertRaises(IndexError): - self.nm.upper(self.nm.upperCount()) - with self.assertRaises(ValueError): - self.nm.nodeCount(3) - # In-range still works. - self.assertEqual(self.nm.leaf(0).origin(), self.leaf.origin()) - - -class TestZeroCopyViewLifetimes(unittest.TestCase): - """Returned typed grids, trees, leaves, NodeManagers, and zero-copy - NumPy views must keep their backing buffers alive across the chained - temporary expressions used at the call site (e.g. - `nanovdb.tools.createFogVolumeSphere().grid().tree().getFirstLeaf().values()`). - Without explicit nb::keep_alive linkages the intermediate handle gets - GC'd between expressions and the returned object reads freed memory. - """ - - def _force_gc(self): - import gc - for _ in range(3): - gc.collect() - - def test_handle_grid_temporary(self): - g = nanovdb.tools.createFogVolumeSphere(name="probe").grid() - self._force_gc() - self.assertEqual(g.gridName(), "probe") - - def test_handle_grid_tree_leaf_values_chain(self): - # nb::ndarray requires numpy at runtime, so the - # binding raises TypeError if numpy isn't installed. Skip then. - try: - import numpy # noqa: F401 - except ImportError: - self.skipTest("numpy not installed") - vals = (nanovdb.tools.createFogVolumeSphere() - .grid().tree().getFirstLeaf().values()) - self._force_gc() - # Touching the view shouldn't crash. - self.assertEqual(vals.shape, (512,)) - _ = float(vals[0]) - - def test_grid_leaf_values_temporary(self): - try: - import numpy # noqa: F401 - except ImportError: - self.skipTest("numpy not installed") - bulk = nanovdb.tools.createFogVolumeSphere().grid().leaf_values() - self._force_gc() - self.assertEqual(bulk.shape[1], 512) - _ = float(bulk[0, 0]) - - def test_node_manager_temporary_grid(self): - try: - import numpy # noqa: F401 - except ImportError: - self.skipTest("numpy not installed") - nm = nanovdb.createNodeManager( - nanovdb.tools.createFogVolumeSphere().grid()).mgr() - self._force_gc() - self.assertGreater(nm.leafCount(), 0) - leaf0_vals = nm.leaf(0).values() - self._force_gc() - self.assertEqual(leaf0_vals.shape, (512,)) - - def test_blind_data_temporary(self): - # The grid has no blind data so getBlindData returns None — what we - # care about here is that the temporary chain doesn't segfault. - result = nanovdb.tools.createFogVolumeSphere().grid().getBlindData(0) - self._force_gc() - self.assertIsNone(result) - - -class TestVoxelBlockManager(unittest.TestCase): - """nanovdb.tools.buildVoxelBlockManager + VoxelBlockManagerHandle + - decodeInverseMaps and the createOnIndexGrid test-scaffold factory. - - NOTE: end-to-end decode verification across every block is intentionally - deferred until the Phase 4 build::Grid bindings land. The C++ - buildVoxelBlockManager has an algorithmic gap when the source OnIndex - grid is tile-compressed (blocks not reached by any leaf's iteration - sweep are left with uninitialized firstLeafID). The current host-side - createFloatGrid + createOnIndexGrid path triggers tile compression on - uniform regions, so we only exercise decodeBlock(0) here — that block - is guaranteed to be covered when the grid's firstOffset is 1. The - bindings include a defensive check that raises ValueError if a - user hits the uninitialized-block case rather than crashing. - """ - - def _make_cube_on_index_grid(self): - # 21^3 fully-active cube — matches the C++ unit test's input. - bbox = nanovdb.math.CoordBBox( - nanovdb.math.Coord(125), nanovdb.math.Coord(145)) - float_h = nanovdb.tools.createFloatGrid( - 0.0, "cube", nanovdb.GridClass.Unknown, - lambda ijk: 1.0, bbox) - return nanovdb.tools.createOnIndexGrid( - float_h.grid(), include_stats=False, include_tiles=False) - - def test_create_on_index_grid(self): - h = self._make_cube_on_index_grid() - g = h.grid() - self.assertEqual(g.gridType(), nanovdb.GridType.OnIndex) - self.assertEqual(g.gridClass(), nanovdb.GridClass.IndexGrid) - self.assertGreater(g.activeVoxelCount(), 0) - self.assertTrue(g.isSequential()) - - def test_create_on_index_grid_rejects_unsupported_source(self): - # createOnIndexGrid only accepts {float, double, int32, Vec3f} - # source grids; passing None (or any non-grid object) should raise - # TypeError at the first BuildT-isinstance check. - with self.assertRaises(TypeError): - nanovdb.tools.createOnIndexGrid(None) - - def test_build_voxel_block_manager_handle(self): - h = self._make_cube_on_index_grid() - g = h.grid() - vbm = nanovdb.tools.buildVoxelBlockManager(g, log2_block_width=6) - self.assertGreater(vbm.blockCount(), 0) - self.assertEqual(vbm.firstOffset(), 1) - self.assertEqual(vbm.lastOffset(), g.activeVoxelCount()) - self.assertTrue(bool(vbm)) - - def test_buffers_zero_copy_shape_and_dtype(self): - try: - import numpy as np - except ImportError: - self.skipTest("numpy not installed") - h = self._make_cube_on_index_grid() - # log2_block_width=6 -> JumpMapLength=1. - vbm6 = nanovdb.tools.buildVoxelBlockManager(h.grid(), log2_block_width=6) - self.assertEqual(vbm6.log2_block_width, 6) - self.assertEqual(vbm6.block_width, 64) - self.assertEqual(vbm6.jump_map_length, 1) - fl = vbm6.firstLeafID() - self.assertEqual(fl.shape, (vbm6.blockCount(),)) - self.assertEqual(fl.dtype, np.uint32) - jm = vbm6.jumpMap() - self.assertEqual(jm.shape, (vbm6.blockCount(), 1)) - self.assertEqual(jm.dtype, np.uint64) - # log2_block_width=7 -> JumpMapLength=2 (independent build, separate - # allocation; the jumpMap shape comes from the handle, not the caller). - vbm7 = nanovdb.tools.buildVoxelBlockManager(h.grid(), log2_block_width=7) - self.assertEqual(vbm7.jump_map_length, 2) - self.assertEqual(vbm7.jumpMap().shape, (vbm7.blockCount(), 2)) - - def test_decode_block_zero(self): - try: - import numpy as np - except ImportError: - self.skipTest("numpy not installed") - h = self._make_cube_on_index_grid() - g = h.grid() - vbm = nanovdb.tools.buildVoxelBlockManager(g, log2_block_width=6) - leaf_index, voxel_offset = vbm.decodeBlock(g, 0) - self.assertEqual(leaf_index.shape, (64,)) - self.assertEqual(leaf_index.dtype, np.uint32) - self.assertEqual(voxel_offset.shape, (64,)) - self.assertEqual(voxel_offset.dtype, np.uint16) - # Free function should produce the same result for the same input. - fl = np.asarray(vbm.firstLeafID()) - jm = np.asarray(vbm.jumpMap()) - li_free, vo_free = nanovdb.tools.decodeInverseMaps( - g, int(fl[0]), jm[0], vbm.firstOffset(), log2_block_width=6) - self.assertTrue(np.array_equal(leaf_index, li_free)) - self.assertTrue(np.array_equal(voxel_offset, vo_free)) - - def test_decode_block_out_of_range(self): - h = self._make_cube_on_index_grid() - g = h.grid() - vbm = nanovdb.tools.buildVoxelBlockManager(g) - with self.assertRaises(IndexError): - vbm.decodeBlock(g, vbm.blockCount()) - - def test_log2_block_width_out_of_range(self): - h = self._make_cube_on_index_grid() - g = h.grid() - with self.assertRaises(ValueError): - nanovdb.tools.buildVoxelBlockManager(g, log2_block_width=5) - with self.assertRaises(ValueError): - nanovdb.tools.buildVoxelBlockManager(g, log2_block_width=10) - - def test_build_voxel_block_manager_rejects_misaligned_first_offset(self): - # first_offset must satisfy first_offset == 1 (mod BlockWidth). - # For log2_block_width=6, BlockWidth=64, so 1, 65, 129, ... are valid - # but 2 is not. - h = self._make_cube_on_index_grid() - g = h.grid() - with self.assertRaises(ValueError): - nanovdb.tools.buildVoxelBlockManager( - g, log2_block_width=6, first_offset=2) - # And the wider-block case: log2_block_width=7 -> BlockWidth=128, - # first_offset=65 is valid for width=6 but misaligned for width=7. - with self.assertRaises(ValueError): - nanovdb.tools.buildVoxelBlockManager( - g, log2_block_width=7, first_offset=65) - - def test_decode_inverse_maps_rejects_bad_first_leaf_id(self): - try: - import numpy as np - except ImportError: - self.skipTest("numpy not installed") - h = self._make_cube_on_index_grid() - g = h.grid() - vbm = nanovdb.tools.buildVoxelBlockManager(g, log2_block_width=6) - jm0 = np.asarray(vbm.jumpMap())[0] - n_leaves = g.tree().nodeCount(0) - with self.assertRaises(IndexError): - nanovdb.tools.decodeInverseMaps( - g, n_leaves, jm0, vbm.firstOffset(), log2_block_width=6) - - def test_build_voxel_block_manager_rejects_undersized_n_blocks(self): - # Caller-supplied n_blocks must hold at least - # ceil((last_offset - first_offset + 1) / BlockWidth) blocks; - # smaller values would silently truncate coverage. - h = self._make_cube_on_index_grid() - g = h.grid() - # The cube grid has ~9261 active voxels, so at log2_block_width=6 - # (BlockWidth=64) the minimum is roughly 145 blocks. Passing 1 - # must be rejected. - with self.assertRaises(ValueError): - nanovdb.tools.buildVoxelBlockManager( - g, log2_block_width=6, n_blocks=1) - - def test_build_voxel_block_manager_rejects_non_on_index_grid(self): - # FloatGrid is not an OnIndexGrid. - h_float = nanovdb.tools.createFogVolumeSphere() - with self.assertRaises(TypeError): - nanovdb.tools.buildVoxelBlockManager(h_float.grid()) - - def test_untouched_blocks_trip_sentinel_guard(self): - # Build the cube VBM and probe every block. The Python binding - # prefills firstLeafID with a sentinel (== nLeaves) before calling - # the in-place builder, so any block the upstream algorithm doesn't - # touch deterministically trips the firstLeafID >= nLeaves guard in - # decodeBlock (raising ValueError). The sweep must therefore see - # only two outcomes per block: a successful decode or a sentinel - # ValueError — no segfaults, no IndexError (those would indicate - # block_index out of range, not sentinel), and no silent wrong - # decode. - try: - import numpy as np # noqa: F401 - except ImportError: - self.skipTest("numpy not installed") - h = self._make_cube_on_index_grid() - g = h.grid() - vbm = nanovdb.tools.buildVoxelBlockManager(g, log2_block_width=6) - n_leaves = g.tree().nodeCount(0) - fl = vbm.firstLeafID() - for b in range(vbm.blockCount()): - slot = int(fl[b]) - # Slot must be a real leaf id or the sentinel — never garbage. - self.assertTrue(slot < n_leaves or slot == n_leaves, - f"block {b}: firstLeafID={slot} is neither a real leaf id " - f"(< {n_leaves}) nor the sentinel (== {n_leaves}); " - "uninitialized memory leaked through.") - if slot >= n_leaves: - with self.assertRaises(ValueError): - vbm.decodeBlock(g, b) - else: - # Successful decode path — just confirm the shapes. - li, vo = vbm.decodeBlock(g, b) - self.assertEqual(li.shape, (64,)) - self.assertEqual(vo.shape, (64,)) - - def test_default_constructed_handle_returns_empty_arrays(self): - # A default-constructed VoxelBlockManagerHandle has null backing - # buffers; firstLeafID() and jumpMap() should still return empty - # ndarrays rather than crash on the null pointer. - try: - import numpy as np - except ImportError: - self.skipTest("numpy not installed") - vbm = nanovdb.tools.VoxelBlockManagerHandle() - self.assertEqual(vbm.blockCount(), 0) - self.assertFalse(bool(vbm)) - fl = np.asarray(vbm.firstLeafID()) - self.assertEqual(fl.shape, (0,)) - self.assertEqual(fl.dtype, np.uint32) - jm = np.asarray(vbm.jumpMap()) - # Default-constructed handle uses log2_block_width=6 -> JumpMapLength=1. - self.assertEqual(jm.shape, (0, 1)) - self.assertEqual(jm.dtype, np.uint64) - - def test_reset_handle_returns_empty_arrays(self): - # Same guard but exercising reset() after a real build. - try: - import numpy as np - except ImportError: - self.skipTest("numpy not installed") - h = self._make_cube_on_index_grid() - vbm = nanovdb.tools.buildVoxelBlockManager(h.grid(), log2_block_width=6) - self.assertGreater(vbm.blockCount(), 0) - vbm.reset() - self.assertEqual(vbm.blockCount(), 0) - self.assertEqual(np.asarray(vbm.firstLeafID()).shape, (0,)) - self.assertEqual(np.asarray(vbm.jumpMap()).shape, (0, 1)) - - -class TestGridMetaDataGuards(unittest.TestCase): - """GridMetaData() constructor and safeCast() reject bad input (None, a - Grid wrapping an invalid buffer) with a Python exception or False - rather than asserting / null-dereferencing inside NanoVDB.""" - - def test_init_rejects_none(self): - # nanobind's type system rejects None for const GridData* before our - # validity check runs. Either is acceptable as long as we don't - # crash / abort. - with self.assertRaises((TypeError, ValueError)): - nanovdb.GridMetaData(None) - - def test_safeCast_rejects_none(self): - with self.assertRaises((TypeError, ValueError)): - nanovdb.GridMetaData.safeCast(None) - - def test_valid_grid_still_works(self): - h = nanovdb.tools.createFogVolumeSphere() - m = nanovdb.GridMetaData(h.grid()) - self.assertTrue(m.isValid()) - self.assertTrue(nanovdb.GridMetaData.safeCast(h.grid())) - - class TestReadWriteGrids(unittest.TestCase): def setUp(self): self.gridName = "sphere_ls" @@ -1090,7 +402,7 @@ def test_read_write_grid(self): for i in range(handle.gridCount()): self.assertTrue(handle.gridSize(i) > 0) self.assertEqual(handle.gridType(i), nanovdb.GridType.Float) - grid = handle.grid(i) + grid = handle.floatGrid(i) self.assertIsNotNone(grid) self.assertTrue(grid.activeVoxelCount() > 0) self.assertTrue(grid.isSequential()) @@ -1169,14 +481,14 @@ def test_read_write_grid(self): for i in range(handle.gridCount()): self.assertTrue(handle.gridSize(i) > 0) self.assertEqual(handle.gridType(i), nanovdb.GridType.Float) - grid = handle.grid(i) + grid = handle.floatGrid(i) self.assertIsNotNone(grid) - deviceGrid = handle.deviceGrid(i) + deviceGrid = handle.deviceFloatGrid(i) self.assertIsNone(deviceGrid) handle.deviceUpload() - deviceGrid = handle.deviceGrid(i) + deviceGrid = handle.deviceFloatGrid(i) handle.deviceDownload() - grid = handle.grid(i) + grid = handle.floatGrid(i) self.assertIsNotNone(grid) self.assertIsNotNone(deviceGrid) self.assertTrue(grid.activeVoxelCount() > 0) @@ -1241,12 +553,12 @@ def test_points_to_grid(self): [[1, 2, 3]], dtype=torch.int32, device=torch.device("cuda", 0) ) handle = nanovdb.tools.cuda.pointsToRGBA8Grid(tensor) - deviceGrid = handle.deviceGrid() + deviceGrid = handle.deviceRGBA8Grid() self.assertTrue(deviceGrid) - grid = handle.grid() + grid = handle.rgba8Grid() self.assertFalse(grid) handle.deviceDownload() - grid = handle.grid() + grid = handle.rgba8Grid() self.assertTrue(grid) except ImportError: print("PyTorch not found. Skipping...") @@ -1262,7 +574,7 @@ def test_points_to_grid(self): class TestSignedFloodFill(unittest.TestCase): def test_signed_flood_fill_float(self): handle = nanovdb.tools.cuda.createLevelSetSphere(nanovdb.GridType.Float, 100) - grid = handle.grid() + grid = handle.floatGrid() self.assertIsNotNone(grid) accessor = grid.getAccessor() self.assertFalse(accessor.isActive(nanovdb.math.Coord(103, 0, 0))) @@ -1277,11 +589,11 @@ def test_signed_flood_fill_float(self): self.assertEqual(0.0, accessor(100, 0, 0)) self.assertEqual(1.0, accessor(97, 0, 0)) handle.deviceUpload() - deviceGrid = handle.deviceGrid(0) + deviceGrid = handle.deviceFloatGrid(0) self.assertIsNotNone(deviceGrid) nanovdb.tools.cuda.signedFloodFill(deviceGrid) handle.deviceDownload() - grid = handle.grid() + grid = handle.floatGrid() self.assertIsNotNone(grid) accessor = grid.getAccessor() self.assertEqual(3.0, accessor(103, 0, 0)) @@ -1292,7 +604,7 @@ def test_signed_flood_fill_float(self): def test_signed_flood_fill_double(self): handle = nanovdb.tools.cuda.createLevelSetSphere(nanovdb.GridType.Double, 100) - grid = handle.grid() + grid = handle.doubleGrid() self.assertIsNotNone(grid) accessor = grid.getAccessor() self.assertFalse(accessor.isActive(nanovdb.math.Coord(103, 0, 0))) @@ -1307,11 +619,11 @@ def test_signed_flood_fill_double(self): self.assertEqual(0.0, accessor(100, 0, 0)) self.assertEqual(1.0, accessor(97, 0, 0)) handle.deviceUpload() - deviceGrid = handle.deviceGrid(0) + deviceGrid = handle.deviceDoubleGrid(0) self.assertIsNotNone(deviceGrid) nanovdb.tools.cuda.signedFloodFill(deviceGrid) handle.deviceDownload() - grid = handle.grid() + grid = handle.doubleGrid() self.assertIsNotNone(grid) accessor = grid.getAccessor() self.assertEqual(3.0, accessor(103, 0, 0)) @@ -1344,7 +656,7 @@ def test_sample_from_points_float(self): voxelSize=voxelSize, ) handle.deviceUpload() - grid = handle.deviceGrid() + grid = handle.deviceFloatGrid() self.assertIsNotNone(grid) points = torch.tensor( @@ -1378,7 +690,7 @@ def test_sample_from_points_float(self): dtype=torch.float32, device=torch.device("cuda", 0), ) - nanovdb.tools.cuda.sampleFromVoxels(points, grid, values, gradients) + nanovdb.math.cuda.sampleFromVoxels(points, grid, values, gradients) for i in range(5): self.assertEqual(values[i], expected_values[i]) for i in range(5): @@ -1405,7 +717,7 @@ def test_sample_from_points_double(self): voxelSize=voxelSize, ) handle.deviceUpload() - grid = handle.deviceGrid() + grid = handle.deviceDoubleGrid() self.assertIsNotNone(grid) points = torch.tensor( @@ -1425,7 +737,7 @@ def test_sample_from_points_double(self): expected_values = torch.tensor( [-value, 0.0, -1.0, 1.0, value], dtype=torch.float64 ) - nanovdb.tools.cuda.sampleFromVoxels(points, grid, values) + nanovdb.math.cuda.sampleFromVoxels(points, grid, values) for i in range(5): self.assertEqual(values[i], expected_values[i]) @@ -1448,7 +760,7 @@ def test_float_sampler(self): halfWidth=halfWidth, voxelSize=voxelSize, ) - grid = handle.grid() + grid = handle.floatGrid() xform = grid.map() index_space_pos = xform.applyInverseMap(world_space_pos) sampler = nanovdb.math.createNearestNeighborSampler(grid) @@ -1476,7 +788,7 @@ def test_double_sampler(self): halfWidth=halfWidth, voxelSize=voxelSize, ) - grid = handle.grid() + grid = handle.doubleGrid() xform = grid.map() index_space_pos = xform.applyInverseMap(world_space_pos) sampler = nanovdb.math.createNearestNeighborSampler(grid) @@ -1504,7 +816,7 @@ def test_create_float_nano_grid(self): for i in range(handle.gridCount()): self.assertTrue(handle.gridSize(i) > 0) self.assertEqual(handle.gridType(i), nanovdb.GridType.Float) - grid = handle.grid(i) + grid = handle.floatGrid(i) self.assertIsNotNone(grid) self.assertTrue(grid.activeVoxelCount() > 0) self.assertTrue(grid.isSequential()) @@ -1522,7 +834,7 @@ def test_create_double_nano_grid(self): for i in range(handle.gridCount()): self.assertTrue(handle.gridSize(i) > 0) self.assertEqual(handle.gridType(i), nanovdb.GridType.Double) - grid = handle.grid(i) + grid = handle.doubleGrid(i) self.assertIsNotNone(grid) self.assertTrue(grid.activeVoxelCount() > 0) self.assertTrue(grid.isSequential()) @@ -1540,7 +852,7 @@ def test_create_int_nano_grid(self): for i in range(handle.gridCount()): self.assertTrue(handle.gridSize(i) > 0) self.assertEqual(handle.gridType(i), nanovdb.GridType.Int32) - grid = handle.grid(i) + grid = handle.int32Grid(i) self.assertIsNotNone(grid) self.assertTrue(grid.activeVoxelCount() > 0) self.assertTrue(grid.isSequential()) @@ -1562,7 +874,7 @@ def test_create_vec3f_nano_grid(self): for i in range(handle.gridCount()): self.assertTrue(handle.gridSize(i) > 0) self.assertEqual(handle.gridType(i), nanovdb.GridType.Vec3f) - grid = handle.grid(i) + grid = handle.vec3fGrid(i) self.assertIsNotNone(grid) self.assertTrue(grid.activeVoxelCount() > 0) self.assertTrue(grid.isSequential()) @@ -1570,500 +882,6 @@ def test_create_vec3f_nano_grid(self): self.assertEqual(grid.gridClass(), nanovdb.GridClass.Unknown) -class TestNewPrimitives(unittest.TestCase): - """Phase 5a: the 9 host primitives that didn't ship in Phase 0.""" - - def test_create_level_set_box(self): - h = nanovdb.tools.createLevelSetBox(width=10.0, height=15.0, depth=20.0) - self.assertEqual(h.gridCount(), 1) - self.assertEqual(h.gridType(0), nanovdb.GridType.Float) - self.assertGreater(h.grid().activeVoxelCount(), 0) - self.assertEqual(h.grid().gridClass(), nanovdb.GridClass.LevelSet) - - def test_create_level_set_box_double(self): - h = nanovdb.tools.createLevelSetBox( - gridType=nanovdb.GridType.Double, width=10.0) - self.assertEqual(h.gridType(0), nanovdb.GridType.Double) - - def test_create_level_set_bbox(self): - h = nanovdb.tools.createLevelSetBBox( - width=40.0, height=40.0, depth=40.0, thickness=5.0) - self.assertEqual(h.gridType(0), nanovdb.GridType.Float) - self.assertGreater(h.grid().activeVoxelCount(), 0) - - def test_create_level_set_octahedron(self): - h = nanovdb.tools.createLevelSetOctahedron(scale=20.0) - self.assertEqual(h.gridType(0), nanovdb.GridType.Float) - self.assertGreater(h.grid().activeVoxelCount(), 0) - - def test_create_fog_volume_box(self): - h = nanovdb.tools.createFogVolumeBox(width=10.0) - self.assertEqual(h.gridType(0), nanovdb.GridType.Float) - self.assertEqual(h.grid().gridClass(), nanovdb.GridClass.FogVolume) - self.assertGreater(h.grid().activeVoxelCount(), 0) - - def test_create_fog_volume_octahedron(self): - h = nanovdb.tools.createFogVolumeOctahedron(scale=20.0) - self.assertEqual(h.grid().gridClass(), nanovdb.GridClass.FogVolume) - - def test_create_point_sphere(self): - h = nanovdb.tools.createPointSphere(pointsPerVoxel=2, radius=10.0) - self.assertEqual(h.gridCount(), 1) - # PointGrid stores point counts as UInt32 sequential indices. - self.assertEqual(h.gridType(0), nanovdb.GridType.UInt32) - self.assertEqual(h.grid().gridClass(), nanovdb.GridClass.PointData) - self.assertGreater(h.grid().activeVoxelCount(), 0) - - def test_create_point_torus(self): - h = nanovdb.tools.createPointTorus( - pointsPerVoxel=1, majorRadius=10.0, minorRadius=3.0) - self.assertEqual(h.gridType(0), nanovdb.GridType.UInt32) - self.assertGreater(h.grid().activeVoxelCount(), 0) - - def test_create_point_box(self): - # Box must be large enough to enclose at least one active voxel - # for createPointScatter's internal "ActiveVoxelCount is required" - # precondition to pass. - h = nanovdb.tools.createPointBox( - pointsPerVoxel=1, width=40.0, height=40.0, depth=40.0) - self.assertEqual(h.gridType(0), nanovdb.GridType.UInt32) - self.assertGreater(h.grid().activeVoxelCount(), 0) - - def test_create_point_scatter(self): - # Source level set, then scatter points into it. - sphere = nanovdb.tools.createLevelSetSphere(radius=10.0).grid() - h = nanovdb.tools.createPointScatter(sphere, pointsPerVoxel=2) - self.assertEqual(h.gridType(0), nanovdb.GridType.UInt32) - self.assertGreater(h.grid().activeVoxelCount(), 0) - - def test_create_point_scatter_rejects_non_float_source(self): - # The binding accepts NanoGrid only — other source types - # should raise TypeError at the conversion boundary. - h_double = nanovdb.tools.createLevelSetSphere( - gridType=nanovdb.GridType.Double, radius=10.0) - with self.assertRaises(TypeError): - nanovdb.tools.createPointScatter(h_double.grid()) - - def test_create_point_scatter_rejects_fog_volume(self): - # createPointScatter's C++ implementation requires the source to - # pass srcGrid.isLevelSet(); fog volumes raise RuntimeError. - h_fog = nanovdb.tools.createFogVolumeSphere(radius=10.0) - with self.assertRaises(RuntimeError): - nanovdb.tools.createPointScatter(h_fog.grid()) - - -class TestCreateNanoGridQuantized(unittest.TestCase): - """Phase 5b: tools.createNanoGridFp4 / Fp8 / Fp16 / FpN with AbsDiff/RelDiff.""" - - def _float_sphere(self): - return nanovdb.tools.createLevelSetSphere(radius=10.0).grid() - - def test_quantize_fp4(self): - h = nanovdb.tools.createNanoGridFp4(self._float_sphere()) - self.assertEqual(h.gridType(0), nanovdb.GridType.Fp4) - self.assertGreater(h.grid().activeVoxelCount(), 0) - - def test_quantize_fp8(self): - h = nanovdb.tools.createNanoGridFp8(self._float_sphere()) - self.assertEqual(h.gridType(0), nanovdb.GridType.Fp8) - - def test_quantize_fp16(self): - h = nanovdb.tools.createNanoGridFp16(self._float_sphere()) - self.assertEqual(h.gridType(0), nanovdb.GridType.Fp16) - - def test_quantize_fpn_absdiff(self): - oracle = nanovdb.tools.AbsDiff(0.05) - self.assertAlmostEqual(oracle.getTolerance(), 0.05, places=5) - self.assertTrue(bool(oracle)) - h = nanovdb.tools.createNanoGridFpN(self._float_sphere(), oracle) - self.assertEqual(h.gridType(0), nanovdb.GridType.FpN) - - def test_quantize_fpn_reldiff(self): - oracle = nanovdb.tools.RelDiff(0.1) - self.assertAlmostEqual(oracle.getTolerance(), 0.1, places=5) - h = nanovdb.tools.createNanoGridFpN(self._float_sphere(), oracle) - self.assertEqual(h.gridType(0), nanovdb.GridType.FpN) - - def test_oracle_default_tolerance(self): - # Default-constructed oracle has tolerance == -1, which means - # "uninitialized"; the operator bool() detects that. - a = nanovdb.tools.AbsDiff() - self.assertEqual(a.getTolerance(), -1.0) - self.assertFalse(bool(a)) - a.setTolerance(0.5) - self.assertEqual(a.getTolerance(), 0.5) - self.assertTrue(bool(a)) - - def test_quantize_rejects_double_source(self): - # The C++ Fp{4,8,16,N} preProcess static-asserts SrcValueT == float; - # Python must surface this as a TypeError at the conversion boundary. - h_double = nanovdb.tools.createLevelSetSphere( - gridType=nanovdb.GridType.Double, radius=5.0) - with self.assertRaises(TypeError): - nanovdb.tools.createNanoGridFp16(h_double.grid()) - with self.assertRaises(TypeError): - nanovdb.tools.createNanoGridFpN( - h_double.grid(), nanovdb.tools.AbsDiff(0.05)) - - def test_quantize_from_build_grid(self): - # Phase 5c: build::FloatGrid accepted as quantization source. - bg = nanovdb.tools.build.FloatGrid(0.0) - for i in range(5): - bg.setValue(nanovdb.math.Coord(i, 0, 0), float(i + 1)) - h = nanovdb.tools.createNanoGridFp16(bg) - self.assertEqual(h.gridType(0), nanovdb.GridType.Fp16) - self.assertEqual(h.grid().activeVoxelCount(), 5) - - -class TestCreateNanoGridIndex(unittest.TestCase): - """Phase 5b/5c: tools.createNanoGridIndex / OnIndex with broad source set.""" - - def test_index_from_float_nanogrid(self): - sphere = nanovdb.tools.createLevelSetSphere(radius=10.0).grid() - h = nanovdb.tools.createNanoGridIndex(sphere) - self.assertEqual(h.gridType(0), nanovdb.GridType.Index) - - def test_on_index_from_float_nanogrid(self): - sphere = nanovdb.tools.createLevelSetSphere(radius=10.0).grid() - h = nanovdb.tools.createNanoGridOnIndex(sphere) - self.assertEqual(h.gridType(0), nanovdb.GridType.OnIndex) - - def test_index_from_double_nanogrid(self): - sphere = nanovdb.tools.createLevelSetSphere( - gridType=nanovdb.GridType.Double, radius=10.0).grid() - h = nanovdb.tools.createNanoGridIndex(sphere) - self.assertEqual(h.gridType(0), nanovdb.GridType.Index) - - def test_index_from_int32_build(self): - # Phase 5c source: build::Int32Grid is accepted by the index path. - bg = nanovdb.tools.build.Int32Grid(0) - bg.setValue(nanovdb.math.Coord(0, 0, 0), 42) - bg.setValue(nanovdb.math.Coord(1, 0, 0), -7) - h = nanovdb.tools.createNanoGridOnIndex(bg) - self.assertEqual(h.gridType(0), nanovdb.GridType.OnIndex) - self.assertEqual(h.grid().activeVoxelCount(), 2) - - def test_index_from_vec3f_build(self): - bv = nanovdb.tools.build.Vec3fGrid(nanovdb.math.Vec3f(0.0)) - bv.setValue(nanovdb.math.Coord(0, 0, 0), nanovdb.math.Vec3f(1, 2, 3)) - h = nanovdb.tools.createNanoGridOnIndex(bv) - self.assertEqual(h.gridType(0), nanovdb.GridType.OnIndex) - - def test_index_rejects_none(self): - # The conversion functions accept either a NanoGrid or a - # build::Grid; None matches neither and is rejected at the - # isinstance dispatch. - with self.assertRaises(TypeError): - nanovdb.tools.createNanoGridOnIndex(None) - - def test_index_rejects_unsupported_buildt(self): - # The Phase 5 index conversion accepts float / double / int32 / - # Vec3f sources (NanoGrid or build::Grid). A Vec3d build::Grid - # is a structurally valid grid but a BuildT outside that set — - # the try-each-SrcBuildT chain falls through and raises. - bv = nanovdb.tools.build.Vec3dGrid(nanovdb.math.Vec3d(0.0)) - bv.setValue(nanovdb.math.Coord(0, 0, 0), nanovdb.math.Vec3d(1, 2, 3)) - with self.assertRaises(TypeError): - nanovdb.tools.createNanoGridOnIndex(bv) - - -class TestGridStats(unittest.TestCase): - """nanovdb.tools.Extrema*, Stats*, updateGridStats, getExtrema.""" - - def _five_voxel_float_grid(self): - g = nanovdb.tools.build.FloatGrid(0.0, "stats", nanovdb.GridClass.FogVolume) - for i in range(5): - g.setValue(nanovdb.math.Coord(i, 0, 0), float(i + 1)) - return g.toNanoVDB(sMode=nanovdb.tools.StatsMode.All) - - def test_extrema_default_and_add(self): - ex = nanovdb.tools.FloatExtrema() - self.assertFalse(bool(ex)) - ex.add(2.5) - ex.add(1.0) - ex.add(7.0) - self.assertTrue(bool(ex)) - self.assertEqual(ex.min(), 1.0) - self.assertEqual(ex.max(), 7.0) - # Extrema doesn't compute averages or std deviation. - self.assertTrue(nanovdb.tools.FloatExtrema.hasMinMax()) - self.assertFalse(nanovdb.tools.FloatExtrema.hasAverage()) - self.assertFalse(nanovdb.tools.FloatExtrema.hasStdDeviation()) - - def test_stats_default_and_accumulate(self): - st = nanovdb.tools.FloatStats() - for v in (1.0, 2.0, 3.0, 4.0, 5.0): - st.add(v) - self.assertEqual(st.size(), 5) - self.assertEqual(st.min(), 1.0) - self.assertEqual(st.max(), 5.0) - self.assertAlmostEqual(st.avg(), 3.0) - self.assertAlmostEqual(st.mean(), 3.0) - # Population variance of 1..5 = (((-2)^2 + (-1)^2 + 0 + 1 + 4) / 5) = 2 - self.assertAlmostEqual(st.var(), 2.0) - self.assertAlmostEqual(st.std() ** 2, 2.0) - self.assertTrue(nanovdb.tools.FloatStats.hasAverage()) - self.assertTrue(nanovdb.tools.FloatStats.hasStdDeviation()) - - def test_get_extrema_strictly_inside_active_region(self): - # Pick a bbox strictly inside the root's active bbox so the C++ - # implementation takes the recursive-traversal branch (the - # "bbox contains root.bbox()" branch unconditionally adds the - # background value, which would muddy this assertion). With - # only the three active voxels at (1,0,0)..(3,0,0) sampled, the - # extrema should be exactly their min and max. - h = self._five_voxel_float_grid() - ng = h.grid() - ex = nanovdb.tools.getExtrema( - ng, nanovdb.math.CoordBBox( - nanovdb.math.Coord(1, 0, 0), nanovdb.math.Coord(3, 0, 0))) - self.assertTrue(bool(ex)) - self.assertEqual(ex.min(), 2.0) - self.assertEqual(ex.max(), 4.0) - - def test_update_grid_stats_polymorphic(self): - # Building with StatsMode.Disable leaves stats uncomputed; calling - # tools.updateGridStats on the resulting handle should populate - # them in-place. Asserting "no exception" is the round-trip we - # care about — the actual stats live inside the grid's nodes. - g = nanovdb.tools.build.FloatGrid(0.0) - for i in range(3): - g.setValue(nanovdb.math.Coord(i, 0, 0), float(i + 10)) - h = g.toNanoVDB(sMode=nanovdb.tools.StatsMode.Disable) - ng = h.grid() - nanovdb.tools.updateGridStats(ng, nanovdb.tools.StatsMode.All) - # checkGrid still passes after writing stats. - ok, msg = nanovdb.tools.checkGrid(ng, nanovdb.CheckMode.Full) - self.assertTrue(ok, msg) - - def test_update_grid_stats_on_index_grid(self): - # OnIndexGrid is a special BuildT — MinMax and All raise because - # Stats isn't meaningful, but BBox (NoopStats) is still - # accepted because it only touches node bounding boxes. - bbox = nanovdb.math.CoordBBox( - nanovdb.math.Coord(0), nanovdb.math.Coord(4)) - h_float = nanovdb.tools.createFloatGrid( - 0.0, "src", nanovdb.GridClass.Unknown, lambda ijk: 1.0, bbox) - h_index = nanovdb.tools.createOnIndexGrid(h_float.grid()) - ng = h_index.grid() - with self.assertRaises(ValueError): - nanovdb.tools.updateGridStats(ng, nanovdb.tools.StatsMode.MinMax) - with self.assertRaises(ValueError): - nanovdb.tools.updateGridStats(ng, nanovdb.tools.StatsMode.All) - # BBox mode is a NoopStats path — must succeed. - nanovdb.tools.updateGridStats(ng, nanovdb.tools.StatsMode.BBox) - # And Disable is a true no-op. - nanovdb.tools.updateGridStats(ng, nanovdb.tools.StatsMode.Disable) - - -class TestGridValidate(unittest.TestCase): - """nanovdb.tools.validateGrid, checkGrid, isValid.""" - - def _good_handle(self): - bbox = nanovdb.math.CoordBBox( - nanovdb.math.Coord(0), nanovdb.math.Coord(3)) - return nanovdb.tools.createFloatGrid( - 0.0, "v", nanovdb.GridClass.Unknown, lambda ijk: 1.0, bbox) - - def test_checkGrid_on_valid_grid(self): - h = self._good_handle() - ok, msg = nanovdb.tools.checkGrid(h.grid(), nanovdb.CheckMode.Full) - self.assertTrue(ok) - self.assertEqual(msg, "") - - def test_isValid_on_valid_grid(self): - h = self._good_handle() - self.assertTrue(nanovdb.tools.isValid(h.grid(), nanovdb.CheckMode.Default)) - - def test_validateGrid_on_valid_handle(self): - h = self._good_handle() - self.assertTrue(nanovdb.tools.validateGrid(h, 0)) - # validateGrid with out-of-range gridID returns False, never raises. - self.assertFalse(nanovdb.tools.validateGrid(h, 99)) - - def test_validateGrid_disable_mode_always_true(self): - h = self._good_handle() - self.assertTrue( - nanovdb.tools.validateGrid(h, 99, nanovdb.CheckMode.Disable)) - - @unittest.skipIf( - not nanovdb.isCudaAvailable(), - "nanovdb module was compiled without CUDA support", - ) - @unittest.skipIf( - not nanovdb.isGpuAvailable(), - "No CUDA-capable GPU available at runtime", - ) - def test_validateGrid_on_device_handle(self): - # validateGrid is bound for both host and device handles. The - # device overload routes through the same callNanoGrid dispatch - # against the host-resident copy of the grid metadata. - h = nanovdb.tools.cuda.createLevelSetSphere() - self.assertTrue(nanovdb.tools.validateGrid(h, 0)) - # Out-of-range gridID returns False (without raising); Disable - # mode short-circuits to True even on an out-of-range gridID. - self.assertFalse(nanovdb.tools.validateGrid(h, 99)) - self.assertTrue( - nanovdb.tools.validateGrid(h, 99, nanovdb.CheckMode.Disable)) - - -class TestGridChecksum(unittest.TestCase): - """nanovdb.tools.evalChecksum and validateChecksum.""" - - def _handle(self): - bbox = nanovdb.math.CoordBBox( - nanovdb.math.Coord(0), nanovdb.math.Coord(3)) - return nanovdb.tools.createFloatGrid( - 0.0, "cs", nanovdb.GridClass.Unknown, lambda ijk: 1.0, bbox) - - def test_eval_then_update_then_validate(self): - h = self._handle() - ng = h.grid() - cs1 = nanovdb.tools.evalChecksum(ng, nanovdb.CheckMode.Full) - nanovdb.tools.updateChecksum(ng, nanovdb.CheckMode.Full) - cs2 = nanovdb.tools.evalChecksum(ng, nanovdb.CheckMode.Full) - # Recomputing on an unchanged grid gives the same checksum. - self.assertEqual(cs1, cs2) - self.assertTrue( - nanovdb.tools.validateChecksum(ng, nanovdb.CheckMode.Full)) - - def test_validate_empty_stored_returns_true(self): - # A grid with no stored checksum is considered valid by the C++ - # rule (Checksum.isEmpty() short-circuit). - h = self._handle() - self.assertTrue( - nanovdb.tools.validateChecksum(h.grid(), nanovdb.CheckMode.Default)) - - -class TestBuildGrid(unittest.TestCase): - """nanovdb.tools.build.* — mutable voxel-by-voxel CPU grid builder.""" - - def test_constructor_defaults_and_metadata(self): - g = nanovdb.tools.build.FloatGrid(0.0) - self.assertEqual(g.getName(), "") - self.assertEqual(g.gridClass(), nanovdb.GridClass.Unknown) - self.assertEqual(g.gridType(), nanovdb.GridType.Float) - self.assertEqual(g.background, 0.0) - self.assertEqual(g.nodeCount(), [0, 0, 0]) - g.setName("renamed") - self.assertEqual(g.getName(), "renamed") - - def test_set_get_value_marks_active(self): - g = nanovdb.tools.build.FloatGrid(0.0, "demo") - ijk = nanovdb.math.Coord(1, 2, 3) - self.assertFalse(g.isActive(ijk)) - self.assertEqual(g.getValue(ijk), 0.0) - g.setValue(ijk, 4.5) - self.assertTrue(g.isActive(ijk)) - self.assertEqual(g.getValue(ijk), 4.5) - # An untouched voxel is still background-valued and inactive. - self.assertEqual(g.getValue(nanovdb.math.Coord(10, 0, 0)), 0.0) - self.assertFalse(g.isActive(nanovdb.math.Coord(10, 0, 0))) - - def test_set_value_on_keeps_background_value(self): - g = nanovdb.tools.build.FloatGrid(-1.0, "demo") - ijk = nanovdb.math.Coord(5, 6, 7) - g.setValueOn(ijk) - self.assertTrue(g.isActive(ijk)) - # setValueOn does not change the stored value — still background. - self.assertEqual(g.getValue(ijk), -1.0) - - def test_value_accessor_parity_with_grid(self): - g = nanovdb.tools.build.FloatGrid(0.0) - acc = g.getAccessor() - ijk = nanovdb.math.Coord(100, 200, 300) - acc.setValue(ijk, 7.5) - self.assertEqual(g.getValue(ijk), 7.5) - self.assertEqual(acc.getValue(ijk), 7.5) - self.assertTrue(acc.isActive(ijk)) - # isValueOn is an alias for isActive. - self.assertEqual(acc.isValueOn(ijk), acc.isActive(ijk)) - - def test_write_accessor_explicit_merge(self): - g = nanovdb.tools.build.FloatGrid(0.0) - ijk = nanovdb.math.Coord(50, 50, 50) - wa = g.getWriteAccessor() - wa.setValue(ijk, 9.0) - # Before merge, the parent grid hasn't seen the change yet. - self.assertEqual(g.getValue(ijk), 0.0) - wa.merge() - self.assertEqual(g.getValue(ijk), 9.0) - self.assertTrue(g.isActive(ijk)) - - def test_write_accessor_merges_on_destruction(self): - # When the Python wrapper for a WriteAccessor is collected, the - # C++ destructor runs merge() automatically. Force collection by - # dropping the only reference and running the GC. - import gc - g = nanovdb.tools.build.FloatGrid(0.0) - ijk = nanovdb.math.Coord(60, 60, 60) - wa = g.getWriteAccessor() - wa.setValue(ijk, 3.5) - self.assertEqual(g.getValue(ijk), 0.0) - del wa - gc.collect() - self.assertEqual(g.getValue(ijk), 3.5) - self.assertTrue(g.isActive(ijk)) - - def test_tonanovdb_roundtrip(self): - g = nanovdb.tools.build.FloatGrid(0.0, "trip", nanovdb.GridClass.FogVolume) - g.setValue(nanovdb.math.Coord(0, 0, 0), 1.0) - g.setValue(nanovdb.math.Coord(1, 0, 0), 2.0) - g.setValue(nanovdb.math.Coord(2, 0, 0), 3.0) - h = g.toNanoVDB() - self.assertEqual(h.gridCount(), 1) - ng = h.grid() - self.assertEqual(ng.gridType(), nanovdb.GridType.Float) - self.assertEqual(ng.gridClass(), nanovdb.GridClass.FogVolume) - self.assertEqual(ng.gridName(), "trip") - self.assertEqual(ng.activeVoxelCount(), 3) - - def test_tonanovdb_does_not_consume_source(self): - # Source build::Grid must remain usable after .toNanoVDB(). - g = nanovdb.tools.build.FloatGrid(0.0) - g.setValue(nanovdb.math.Coord(0, 0, 0), 1.0) - _ = g.toNanoVDB() - g.setValue(nanovdb.math.Coord(1, 0, 0), 2.0) - h2 = g.toNanoVDB() - self.assertEqual(h2.grid().activeVoxelCount(), 2) - - def test_int32_build_grid(self): - g = nanovdb.tools.build.Int32Grid(0, "ints", nanovdb.GridClass.Unknown) - g.setValue(nanovdb.math.Coord(0, 0, 0), 42) - g.setValue(nanovdb.math.Coord(1, 1, 1), -7) - self.assertEqual(g.getValue(nanovdb.math.Coord(0, 0, 0)), 42) - self.assertEqual(g.getValue(nanovdb.math.Coord(1, 1, 1)), -7) - h = g.toNanoVDB() - self.assertEqual(h.grid().gridType(), nanovdb.GridType.Int32) - self.assertEqual(h.grid().activeVoxelCount(), 2) - - def test_vec3f_build_grid(self): - g = nanovdb.tools.build.Vec3fGrid( - nanovdb.math.Vec3f(0.0), "v", nanovdb.GridClass.Unknown) - v = nanovdb.math.Vec3f(1.0, 2.0, 3.0) - g.setValue(nanovdb.math.Coord(0, 0, 0), v) - self.assertEqual(g.getValue(nanovdb.math.Coord(0, 0, 0)), v) - h = g.toNanoVDB() - self.assertEqual(h.grid().gridType(), nanovdb.GridType.Vec3f) - - def test_set_transform(self): - g = nanovdb.tools.build.FloatGrid(0.0) - g.setTransform(scale=0.5, translation=nanovdb.math.Vec3d(1.0, 2.0, 3.0)) - g.setValue(nanovdb.math.Coord(0, 0, 0), 1.0) - h = g.toNanoVDB() - ng = h.grid() - vs = ng.voxelSize() - self.assertAlmostEqual(vs[0], 0.5) - self.assertAlmostEqual(vs[1], 0.5) - self.assertAlmostEqual(vs[2], 0.5) - # Index (0,0,0) mapped through (scale=0.5, translation=(1,2,3)) - # lands at world-space (1, 2, 3). - w = ng.map().applyMap(nanovdb.math.Vec3d(0.0, 0.0, 0.0)) - self.assertAlmostEqual(w[0], 1.0) - self.assertAlmostEqual(w[1], 2.0) - self.assertAlmostEqual(w[2], 3.0) - - class TestNanoToOpenVDB(unittest.TestCase): def test_function(self): handle = nanovdb.tools.createLevelSetSphere() @@ -2090,7 +908,7 @@ def test_function(self): for i in range(handle.gridCount()): self.assertTrue(handle.gridSize(i) > 0) self.assertEqual(handle.gridType(i), nanovdb.GridType.Float) - grid = handle.grid(i) + grid = handle.floatGrid(i) self.assertIsNotNone(grid) self.assertTrue(grid.activeVoxelCount() > 0) self.assertTrue(grid.isSequential()) @@ -2101,326 +919,5 @@ def test_function(self): pass -class TestGridTransformAliases(unittest.TestCase): - """worldToIndex / indexToWorld and friends are aliases of the apply* - transform family, mirroring the C++ Grid convenience names.""" - - def setUp(self): - self.handle = nanovdb.tools.createLevelSetSphere( - nanovdb.GridType.Float, radius=10.0, voxelSize=0.5 - ) - self.grid = self.handle.grid() - - def test_world_index_point_aliases(self): - p = nanovdb.math.Vec3d(1.5, -2.0, 3.25) - self.assertEqual(self.grid.worldToIndex(p), self.grid.applyInverseMap(p)) - self.assertEqual(self.grid.indexToWorld(p), self.grid.applyMap(p)) - roundtrip = self.grid.indexToWorld(self.grid.worldToIndex(p)) - for i in range(3): - self.assertAlmostEqual(roundtrip[i], p[i], places=12) - - def test_direction_and_gradient_aliases(self): - d = nanovdb.math.Vec3d(0.25, 1.0, -0.5) - self.assertEqual(self.grid.worldToIndexDir(d), self.grid.applyInverseJacobian(d)) - self.assertEqual(self.grid.indexToWorldDir(d), self.grid.applyJacobian(d)) - self.assertEqual(self.grid.indexToWorldGrad(d), self.grid.applyIJT(d)) - - def test_single_precision_aliases(self): - p = nanovdb.math.Vec3f(1.5, -2.0, 3.25) - self.assertEqual(self.grid.worldToIndexF(p), self.grid.applyInverseMapF(p)) - self.assertEqual(self.grid.indexToWorldF(p), self.grid.applyMapF(p)) - self.assertEqual(self.grid.worldToIndexDirF(p), self.grid.applyInverseJacobianF(p)) - self.assertEqual(self.grid.indexToWorldDirF(p), self.grid.applyJacobianF(p)) - self.assertEqual(self.grid.indexToWorldGradF(p), self.grid.applyIJTF(p)) - - -class TestGridValuePointCount(unittest.TestCase): - """valueCount() on Index/OnIndex grids and pointCount() on PointGrid, - mirroring the SFINAE-gated C++ Grid methods.""" - - def setUp(self): - self.src = nanovdb.tools.createLevelSetSphere( - nanovdb.GridType.Float, radius=5.0, voxelSize=1.0 - ) - - def test_on_index_value_count(self): - handle = nanovdb.tools.createNanoGridOnIndex(self.src.grid()) - grid = handle.grid() - self.assertIsInstance(grid, nanovdb.OnIndexGrid) - self.assertGreaterEqual(grid.valueCount(), self.src.grid().activeVoxelCount()) - - def test_index_value_count(self): - handle = nanovdb.tools.createNanoGridIndex(self.src.grid()) - grid = handle.grid() - self.assertIsInstance(grid, nanovdb.IndexGrid) - self.assertGreaterEqual(grid.valueCount(), self.src.grid().activeVoxelCount()) - - def test_point_count_bound_on_point_grid(self): - # pointCount() lives on NanoGrid (GridType.PointIndex). The - # point primitives bake UInt32 PointData grids, so only the binding's - # presence can be verified host-side without an OpenVDB conversion. - self.assertIn("pointCount", dir(nanovdb.PointGrid)) - self.assertNotIn("valueCount", dir(nanovdb.PointGrid)) - point_data_grid = nanovdb.tools.createPointSphere( - pointsPerVoxel=2, radius=5.0, voxelSize=1.0 - ).grid() - self.assertIsInstance(point_data_grid, nanovdb.UInt32Grid) - self.assertFalse(hasattr(point_data_grid, "pointCount")) - - def test_gated_to_matching_buildts(self): - float_grid = self.src.grid() - self.assertFalse(hasattr(float_grid, "valueCount")) - self.assertFalse(hasattr(float_grid, "pointCount")) - on_index_grid = nanovdb.tools.createNanoGridOnIndex(float_grid).grid() - self.assertFalse(hasattr(on_index_grid, "pointCount")) - - -class TestSamplerGradient(unittest.TestCase): - """gradient() on the trilinear sampler and zeroCrossing() on the - trilinear + triquadratic samplers, for floating-point grids only.""" - - def setUp(self): - self.radius = 10.0 - self.voxelSize = 0.5 - self.handle = nanovdb.tools.createLevelSetSphere( - nanovdb.GridType.Float, radius=self.radius, voxelSize=self.voxelSize - ) - self.grid = self.handle.grid() - # Index-space position on the sphere surface, on the +x axis. - self.surface = self.grid.worldToIndex(nanovdb.math.Vec3d(self.radius, 0.0, 0.0)) - - def test_trilinear_gradient_points_outward(self): - sampler = nanovdb.math.createTrilinearSampler(self.grid) - surface_f = nanovdb.math.Vec3f( - self.surface[0], self.surface[1], self.surface[2] - ) - g = sampler.gradient(surface_f) - # An SDF in world units sampled on an index-space lattice changes by - # ~voxelSize per index step along the outward normal (+x here). The - # tangential components pick up the sphere's curvature across the - # stencil cell, so they are small but not zero. - self.assertAlmostEqual(g[0], self.voxelSize, places=3) - self.assertAlmostEqual(g[1], 0.0, delta=0.05) - self.assertAlmostEqual(g[2], 0.0, delta=0.05) - # The Vec3d overload agrees. - gd = sampler.gradient(nanovdb.math.Vec3d(self.surface)) - for i in range(3): - self.assertAlmostEqual(g[i], gd[i], places=5) - - def test_zero_crossing(self): - # Probe just inside the surface: an exactly-zero stencil corner is - # not a strict sign change, so the on-surface lattice point (20,0,0) - # itself does not count as a crossing. - inside = nanovdb.math.Vec3d(self.surface[0] - 0.5, 0.0, 0.0) - for make in ( - nanovdb.math.createTrilinearSampler, - nanovdb.math.createTriquadraticSampler, - ): - sampler = make(self.grid) - self.assertTrue(abs(sampler(inside)) < self.voxelSize) - self.assertTrue(sampler.zeroCrossing(inside)) - # Deep inside the narrow band there is no crossing. - self.assertFalse(sampler.zeroCrossing(nanovdb.math.Vec3d(0.0, 0.0, 0.0))) - - def test_gated_to_matching_orders_and_buildts(self): - nn = nanovdb.math.createNearestNeighborSampler(self.grid) - self.assertFalse(hasattr(nn, "gradient")) - self.assertFalse(hasattr(nn, "zeroCrossing")) - tq = nanovdb.math.createTriquadraticSampler(self.grid) - self.assertFalse(hasattr(tq, "gradient")) - tc = nanovdb.math.createTricubicSampler(self.grid) - self.assertFalse(hasattr(tc, "gradient")) - self.assertFalse(hasattr(tc, "zeroCrossing")) - bbox = nanovdb.math.CoordBBox(nanovdb.math.Coord(0), nanovdb.math.Coord(7)) - int_grid = nanovdb.tools.createInt32Grid( - 0, "ints", nanovdb.GridClass.Unknown, lambda ijk: 1, bbox - ).grid() - int_sampler = nanovdb.math.createTrilinearSampler(int_grid) - self.assertFalse(hasattr(int_sampler, "gradient")) - self.assertFalse(hasattr(int_sampler, "zeroCrossing")) - - -class TestChecksumMethods(unittest.TestCase): - def test_mode_queries(self): - handle = nanovdb.tools.createLevelSetSphere( - nanovdb.GridType.Float, radius=5.0, voxelSize=1.0 - ) - grid = handle.grid() - stored = grid.checksum() - self.assertFalse(stored.isEmpty()) - self.assertNotEqual(stored.mode(), nanovdb.CheckMode.Disable) - self.assertEqual(stored.isFull(), stored.mode() == nanovdb.CheckMode.Full) - self.assertEqual(stored.isHalf(), stored.mode() == nanovdb.CheckMode.Partial) - disabled = nanovdb.tools.evalChecksum(grid, nanovdb.CheckMode.Disable) - self.assertTrue(disabled.isEmpty()) - self.assertEqual(disabled.mode(), nanovdb.CheckMode.Disable) - - -class TestCreateNanoGridClass(unittest.TestCase): - """tools.CreateNanoGrid converter: bake with authored blind-data - channels, filled through the writable getBlindData() NumPy view.""" - - def _build_source(self): - g = nanovdb.tools.build.FloatGrid(0.0, "blind_src", nanovdb.GridClass.Unknown) - for i in range(8): - g.setValue(nanovdb.math.Coord(i, 0, 0), float(i + 1)) - return g - - def test_bake_without_blind_data(self): - src = self._build_source() - handle = nanovdb.tools.CreateNanoGrid(src).getHandle() - grid = handle.grid() - self.assertIsInstance(grid, nanovdb.FloatGrid) - acc = grid.getAccessor() - for i in range(8): - self.assertEqual(acc.getValue(nanovdb.math.Coord(i, 0, 0)), float(i + 1)) - self.assertEqual(grid.blindDataCount(), 0) - - def test_author_float_channel(self): - import numpy as np - - conv = nanovdb.tools.CreateNanoGrid(self._build_source()) - channel = conv.addBlindData("uv", count=100) - self.assertEqual(channel, 0) - handle = conv.getHandle() - grid = handle.grid() - self.assertEqual(grid.blindDataCount(), 1) - n = grid.findBlindData("uv") - self.assertEqual(n, 0) - meta = grid.blindMetaData(n) - self.assertEqual(meta.valueCount, 100) - self.assertEqual(meta.valueSize, 4) - self.assertEqual(meta.dataType, nanovdb.GridType.Float) - self.assertTrue(meta.isValid()) - view = grid.getBlindData(n) - self.assertEqual(view.shape, (100,)) - self.assertTrue(np.all(view == 0.0)) - view[:] = np.arange(100, dtype=np.float32) - again = grid.getBlindData(n) - self.assertTrue(np.array_equal(again, np.arange(100, dtype=np.float32))) - - def test_author_vec3f_channel_with_semantic(self): - conv = nanovdb.tools.CreateNanoGrid(self._build_source()) - conv.addBlindData( - "N", - count=10, - dataType=nanovdb.GridType.Vec3f, - dataSemantic=nanovdb.GridBlindDataSemantic.PointNormal, - ) - grid = conv.getHandle().grid() - n = grid.findBlindDataForSemantic(nanovdb.GridBlindDataSemantic.PointNormal) - self.assertEqual(n, 0) - self.assertEqual(grid.blindMetaData(n).valueSize, 12) - self.assertEqual(grid.getBlindData(n).shape, (10, 3)) - - def test_multiple_channels_from_nanogrid_source(self): - src = nanovdb.tools.createLevelSetSphere( - nanovdb.GridType.Float, radius=5.0, voxelSize=1.0 - ) - conv = nanovdb.tools.CreateNanoGrid(src.grid()) - self.assertEqual(conv.addBlindData("a", count=4), 0) - self.assertEqual( - conv.addBlindData("b", count=4, dataType=nanovdb.GridType.Int32), 1 - ) - grid = conv.getHandle().grid() - self.assertEqual(grid.blindDataCount(), 2) - self.assertEqual(grid.findBlindData("a"), 0) - self.assertEqual(grid.findBlindData("b"), 1) - # The baked grid still carries the source's values. - self.assertEqual(grid.activeVoxelCount(), src.grid().activeVoxelCount()) - - def test_rejects_invalid_specs(self): - conv = nanovdb.tools.CreateNanoGrid(self._build_source()) - with self.assertRaises(ValueError): - conv.addBlindData("x" * 300, count=1) - with self.assertRaises(ValueError): - conv.addBlindData( - "bad", count=1, dataClass=nanovdb.GridBlindDataClass.GridName - ) - with self.assertRaises(ValueError): - conv.addBlindData("opaque", count=1, dataType=nanovdb.GridType.Unknown) - # Unknown dataType is allowed when the element size is explicit. - conv.addBlindData( - "opaque", count=16, dataType=nanovdb.GridType.Unknown, size=1 - ) - self.assertEqual(conv.getHandle().grid().blindDataCount(), 1) - - def test_rejects_unsupported_source(self): - src = nanovdb.tools.createLevelSetSphere( - nanovdb.GridType.Float, radius=5.0, voxelSize=1.0 - ) - on_index = nanovdb.tools.createNanoGridOnIndex(src.grid()).grid() - with self.assertRaises(TypeError): - nanovdb.tools.CreateNanoGrid(on_index) - with self.assertRaises(TypeError): - nanovdb.tools.CreateNanoGrid(None) - - -class TestChannelAccessor(unittest.TestCase): - """ChannelAccessor reads an Index/OnIndex grid's blind-data channel by - Coord; createChannelAccessor dispatches on the channel's dataType.""" - - def setUp(self): - self.src = nanovdb.tools.createLevelSetSphere( - nanovdb.GridType.Float, radius=5.0, voxelSize=1.0 - ) - self.surface = nanovdb.math.Coord(5, 0, 0) - - def test_factory_reads_channel_values(self): - handle = nanovdb.tools.createNanoGridIndex(self.src.grid(), channels=1) - grid = handle.grid() - acc = nanovdb.createChannelAccessor(grid, 0) - self.assertIsInstance(acc, nanovdb.IndexFloatChannelAccessor) - self.assertTrue(bool(acc)) - self.assertEqual(acc.valueCount(), grid.valueCount()) - src_acc = self.src.grid().getAccessor() - for ijk in (self.surface, nanovdb.math.Coord(0, 5, 0), nanovdb.math.Coord(0, 0, 5)): - self.assertEqual(acc.getValue(ijk), src_acc.getValue(ijk)) - self.assertEqual(acc(ijk), src_acc(ijk)) - self.assertEqual( - acc(self.surface.x, self.surface.y, self.surface.z), - src_acc(self.surface), - ) - self.assertTrue(acc.isActive(self.surface)) - value, is_on = acc.probeValue(self.surface) - self.assertEqual(value, src_acc.getValue(self.surface)) - self.assertTrue(is_on) - self.assertGreater(acc.getIndex(self.surface), 0) - self.assertEqual( - acc.getIndex(self.surface), - acc.idx(self.surface.x, self.surface.y, self.surface.z), - ) - - def test_on_index_factory(self): - handle = nanovdb.tools.createNanoGridOnIndex(self.src.grid(), channels=1) - acc = nanovdb.createChannelAccessor(handle.grid()) - self.assertIsInstance(acc, nanovdb.OnIndexFloatChannelAccessor) - src_acc = self.src.grid().getAccessor() - self.assertEqual(acc.getValue(self.surface), src_acc.getValue(self.surface)) - - def test_direct_constructor_and_set_channel(self): - handle = nanovdb.tools.createNanoGridIndex(self.src.grid(), channels=2) - grid = handle.grid() - acc = nanovdb.IndexFloatChannelAccessor(grid, 1) - self.assertTrue(bool(acc)) - acc.setChannel(0) - self.assertTrue(bool(acc)) - with self.assertRaises(IndexError): - acc.setChannel(2) - - def test_errors(self): - handle = nanovdb.tools.createNanoGridIndex(self.src.grid(), channels=1) - grid = handle.grid() - with self.assertRaises(IndexError): - nanovdb.createChannelAccessor(grid, 1) - with self.assertRaises(TypeError): - nanovdb.IndexDoubleChannelAccessor(grid, 0) - with self.assertRaises(TypeError): - nanovdb.createChannelAccessor(self.src.grid(), 0) - bare = nanovdb.tools.createNanoGridIndex(self.src.grid(), channels=0) - with self.assertRaises(IndexError): - nanovdb.createChannelAccessor(bare.grid(), 0) - - if __name__ == "__main__": unittest.main() diff --git a/nanovdb/nanovdb/python/test/sitecustomize.py b/nanovdb/nanovdb/python/test/sitecustomize.py deleted file mode 100644 index e282fd4fca..0000000000 --- a/nanovdb/nanovdb/python/test/sitecustomize.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: Apache-2.0 -"""Configure dependent-DLL lookup for in-tree Windows Python tests.""" - -import os - - -# Keep these handles alive for the lifetime of the child Python process. -_dll_directory_handles = [] - -if hasattr(os, "add_dll_directory"): - for _path in os.environ.get( - "NANOVDB_TEST_DLL_DIRECTORIES", "").split(os.pathsep): - if os.path.isdir(_path): - try: - _dll_directory_handles.append(os.add_dll_directory(_path)) - except OSError: - pass