warp-lang 1.1.0__py3-none-manylinux2014_aarch64.whl → 1.2.1__py3-none-manylinux2014_aarch64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of warp-lang might be problematic. Click here for more details.

Files changed (218) hide show
  1. warp/bin/warp-clang.so +0 -0
  2. warp/bin/warp.so +0 -0
  3. warp/build.py +10 -37
  4. warp/build_dll.py +2 -2
  5. warp/builtins.py +274 -6
  6. warp/codegen.py +51 -4
  7. warp/config.py +2 -2
  8. warp/constants.py +4 -0
  9. warp/context.py +422 -203
  10. warp/examples/benchmarks/benchmark_api.py +0 -2
  11. warp/examples/benchmarks/benchmark_cloth_warp.py +0 -1
  12. warp/examples/benchmarks/benchmark_launches.py +0 -2
  13. warp/examples/core/example_dem.py +0 -2
  14. warp/examples/core/example_fluid.py +0 -2
  15. warp/examples/core/example_graph_capture.py +0 -2
  16. warp/examples/core/example_marching_cubes.py +0 -2
  17. warp/examples/core/example_mesh.py +0 -2
  18. warp/examples/core/example_mesh_intersect.py +0 -2
  19. warp/examples/core/example_nvdb.py +0 -2
  20. warp/examples/core/example_raycast.py +0 -2
  21. warp/examples/core/example_raymarch.py +0 -2
  22. warp/examples/core/example_render_opengl.py +0 -2
  23. warp/examples/core/example_sph.py +0 -2
  24. warp/examples/core/example_torch.py +0 -3
  25. warp/examples/core/example_wave.py +0 -2
  26. warp/examples/fem/example_apic_fluid.py +140 -115
  27. warp/examples/fem/example_burgers.py +262 -0
  28. warp/examples/fem/example_convection_diffusion.py +0 -2
  29. warp/examples/fem/example_convection_diffusion_dg.py +0 -2
  30. warp/examples/fem/example_deformed_geometry.py +0 -2
  31. warp/examples/fem/example_diffusion.py +0 -2
  32. warp/examples/fem/example_diffusion_3d.py +5 -4
  33. warp/examples/fem/example_diffusion_mgpu.py +0 -2
  34. warp/examples/fem/example_mixed_elasticity.py +0 -2
  35. warp/examples/fem/example_navier_stokes.py +0 -2
  36. warp/examples/fem/example_stokes.py +0 -2
  37. warp/examples/fem/example_stokes_transfer.py +0 -2
  38. warp/examples/optim/example_bounce.py +0 -2
  39. warp/examples/optim/example_cloth_throw.py +0 -2
  40. warp/examples/optim/example_diffray.py +0 -2
  41. warp/examples/optim/example_drone.py +0 -2
  42. warp/examples/optim/example_inverse_kinematics.py +0 -2
  43. warp/examples/optim/example_inverse_kinematics_torch.py +0 -2
  44. warp/examples/optim/example_spring_cage.py +0 -2
  45. warp/examples/optim/example_trajectory.py +0 -2
  46. warp/examples/optim/example_walker.py +0 -2
  47. warp/examples/sim/example_cartpole.py +0 -2
  48. warp/examples/sim/example_cloth.py +0 -2
  49. warp/examples/sim/example_granular.py +0 -2
  50. warp/examples/sim/example_granular_collision_sdf.py +0 -2
  51. warp/examples/sim/example_jacobian_ik.py +0 -2
  52. warp/examples/sim/example_particle_chain.py +0 -2
  53. warp/examples/sim/example_quadruped.py +0 -2
  54. warp/examples/sim/example_rigid_chain.py +0 -2
  55. warp/examples/sim/example_rigid_contact.py +0 -2
  56. warp/examples/sim/example_rigid_force.py +0 -2
  57. warp/examples/sim/example_rigid_gyroscopic.py +0 -2
  58. warp/examples/sim/example_rigid_soft_contact.py +0 -2
  59. warp/examples/sim/example_soft_body.py +0 -2
  60. warp/fem/__init__.py +1 -0
  61. warp/fem/cache.py +3 -1
  62. warp/fem/geometry/__init__.py +1 -0
  63. warp/fem/geometry/element.py +4 -0
  64. warp/fem/geometry/grid_3d.py +0 -4
  65. warp/fem/geometry/nanogrid.py +455 -0
  66. warp/fem/integrate.py +63 -9
  67. warp/fem/space/__init__.py +43 -158
  68. warp/fem/space/basis_space.py +34 -0
  69. warp/fem/space/collocated_function_space.py +1 -1
  70. warp/fem/space/grid_2d_function_space.py +13 -132
  71. warp/fem/space/grid_3d_function_space.py +16 -154
  72. warp/fem/space/hexmesh_function_space.py +37 -134
  73. warp/fem/space/nanogrid_function_space.py +202 -0
  74. warp/fem/space/quadmesh_2d_function_space.py +12 -119
  75. warp/fem/space/restriction.py +4 -1
  76. warp/fem/space/shape/__init__.py +77 -0
  77. warp/fem/space/shape/cube_shape_function.py +5 -15
  78. warp/fem/space/tetmesh_function_space.py +6 -76
  79. warp/fem/space/trimesh_2d_function_space.py +6 -76
  80. warp/native/array.h +12 -3
  81. warp/native/builtin.h +48 -5
  82. warp/native/bvh.cpp +14 -10
  83. warp/native/bvh.cu +23 -15
  84. warp/native/bvh.h +1 -0
  85. warp/native/clang/clang.cpp +2 -1
  86. warp/native/crt.cpp +11 -1
  87. warp/native/crt.h +18 -1
  88. warp/native/exports.h +187 -0
  89. warp/native/mat.h +47 -0
  90. warp/native/mesh.cpp +1 -1
  91. warp/native/mesh.cu +1 -2
  92. warp/native/nanovdb/GridHandle.h +366 -0
  93. warp/native/nanovdb/HostBuffer.h +590 -0
  94. warp/native/nanovdb/NanoVDB.h +3999 -2157
  95. warp/native/nanovdb/PNanoVDB.h +936 -99
  96. warp/native/quat.h +28 -1
  97. warp/native/rand.h +5 -1
  98. warp/native/vec.h +45 -1
  99. warp/native/volume.cpp +335 -103
  100. warp/native/volume.cu +39 -13
  101. warp/native/volume.h +725 -303
  102. warp/native/volume_builder.cu +381 -360
  103. warp/native/volume_builder.h +16 -1
  104. warp/native/volume_impl.h +61 -0
  105. warp/native/warp.cu +8 -2
  106. warp/native/warp.h +15 -7
  107. warp/render/render_opengl.py +191 -52
  108. warp/sim/integrator_featherstone.py +10 -3
  109. warp/sim/integrator_xpbd.py +16 -22
  110. warp/sparse.py +89 -27
  111. warp/stubs.py +83 -0
  112. warp/tests/assets/test_index_grid.nvdb +0 -0
  113. warp/tests/aux_test_dependent.py +0 -2
  114. warp/tests/aux_test_grad_customs.py +0 -2
  115. warp/tests/aux_test_reference.py +0 -2
  116. warp/tests/aux_test_reference_reference.py +0 -2
  117. warp/tests/aux_test_square.py +0 -2
  118. warp/tests/disabled_kinematics.py +0 -2
  119. warp/tests/test_adam.py +0 -2
  120. warp/tests/test_arithmetic.py +0 -36
  121. warp/tests/test_array.py +9 -11
  122. warp/tests/test_array_reduce.py +0 -2
  123. warp/tests/test_async.py +0 -2
  124. warp/tests/test_atomic.py +0 -2
  125. warp/tests/test_bool.py +58 -50
  126. warp/tests/test_builtins_resolution.py +0 -2
  127. warp/tests/test_bvh.py +0 -2
  128. warp/tests/test_closest_point_edge_edge.py +0 -1
  129. warp/tests/test_codegen.py +0 -4
  130. warp/tests/test_compile_consts.py +130 -10
  131. warp/tests/test_conditional.py +0 -2
  132. warp/tests/test_copy.py +0 -2
  133. warp/tests/test_ctypes.py +6 -8
  134. warp/tests/test_dense.py +0 -2
  135. warp/tests/test_devices.py +0 -2
  136. warp/tests/test_dlpack.py +9 -11
  137. warp/tests/test_examples.py +42 -39
  138. warp/tests/test_fabricarray.py +0 -3
  139. warp/tests/test_fast_math.py +0 -2
  140. warp/tests/test_fem.py +75 -54
  141. warp/tests/test_fp16.py +0 -2
  142. warp/tests/test_func.py +0 -2
  143. warp/tests/test_generics.py +27 -2
  144. warp/tests/test_grad.py +147 -8
  145. warp/tests/test_grad_customs.py +0 -2
  146. warp/tests/test_hash_grid.py +1 -3
  147. warp/tests/test_import.py +0 -2
  148. warp/tests/test_indexedarray.py +0 -2
  149. warp/tests/test_intersect.py +0 -2
  150. warp/tests/test_jax.py +0 -2
  151. warp/tests/test_large.py +11 -9
  152. warp/tests/test_launch.py +0 -2
  153. warp/tests/test_lerp.py +10 -54
  154. warp/tests/test_linear_solvers.py +3 -5
  155. warp/tests/test_lvalue.py +0 -2
  156. warp/tests/test_marching_cubes.py +0 -2
  157. warp/tests/test_mat.py +0 -2
  158. warp/tests/test_mat_lite.py +0 -2
  159. warp/tests/test_mat_scalar_ops.py +0 -2
  160. warp/tests/test_math.py +0 -2
  161. warp/tests/test_matmul.py +35 -37
  162. warp/tests/test_matmul_lite.py +29 -31
  163. warp/tests/test_mempool.py +0 -2
  164. warp/tests/test_mesh.py +0 -3
  165. warp/tests/test_mesh_query_aabb.py +0 -2
  166. warp/tests/test_mesh_query_point.py +0 -2
  167. warp/tests/test_mesh_query_ray.py +0 -2
  168. warp/tests/test_mlp.py +0 -2
  169. warp/tests/test_model.py +0 -2
  170. warp/tests/test_module_hashing.py +111 -0
  171. warp/tests/test_modules_lite.py +0 -3
  172. warp/tests/test_multigpu.py +0 -2
  173. warp/tests/test_noise.py +0 -4
  174. warp/tests/test_operators.py +0 -2
  175. warp/tests/test_options.py +0 -2
  176. warp/tests/test_peer.py +0 -2
  177. warp/tests/test_pinned.py +0 -2
  178. warp/tests/test_print.py +0 -2
  179. warp/tests/test_quat.py +0 -2
  180. warp/tests/test_rand.py +41 -5
  181. warp/tests/test_reload.py +0 -10
  182. warp/tests/test_rounding.py +0 -2
  183. warp/tests/test_runlength_encode.py +0 -2
  184. warp/tests/test_sim_grad.py +0 -2
  185. warp/tests/test_sim_kinematics.py +0 -2
  186. warp/tests/test_smoothstep.py +0 -2
  187. warp/tests/test_snippet.py +0 -2
  188. warp/tests/test_sparse.py +0 -2
  189. warp/tests/test_spatial.py +0 -2
  190. warp/tests/test_special_values.py +362 -0
  191. warp/tests/test_streams.py +0 -2
  192. warp/tests/test_struct.py +0 -2
  193. warp/tests/test_tape.py +0 -2
  194. warp/tests/test_torch.py +0 -2
  195. warp/tests/test_transient_module.py +0 -2
  196. warp/tests/test_types.py +0 -2
  197. warp/tests/test_utils.py +0 -2
  198. warp/tests/test_vec.py +0 -2
  199. warp/tests/test_vec_lite.py +0 -2
  200. warp/tests/test_vec_scalar_ops.py +0 -2
  201. warp/tests/test_verify_fp.py +0 -2
  202. warp/tests/test_volume.py +237 -13
  203. warp/tests/test_volume_write.py +86 -3
  204. warp/tests/unittest_serial.py +10 -9
  205. warp/tests/unittest_suites.py +6 -2
  206. warp/tests/unittest_utils.py +2 -171
  207. warp/tests/unused_test_misc.py +0 -2
  208. warp/tests/walkthrough_debug.py +1 -1
  209. warp/thirdparty/unittest_parallel.py +37 -40
  210. warp/types.py +526 -85
  211. {warp_lang-1.1.0.dist-info → warp_lang-1.2.1.dist-info}/METADATA +61 -31
  212. warp_lang-1.2.1.dist-info/RECORD +359 -0
  213. warp/examples/fem/example_convection_diffusion_dg0.py +0 -204
  214. warp/native/nanovdb/PNanoVDBWrite.h +0 -295
  215. warp_lang-1.1.0.dist-info/RECORD +0 -352
  216. {warp_lang-1.1.0.dist-info → warp_lang-1.2.1.dist-info}/LICENSE.md +0 -0
  217. {warp_lang-1.1.0.dist-info → warp_lang-1.2.1.dist-info}/WHEEL +0 -0
  218. {warp_lang-1.1.0.dist-info → warp_lang-1.2.1.dist-info}/top_level.txt +0 -0
warp/types.py CHANGED
@@ -9,11 +9,10 @@ from __future__ import annotations
9
9
 
10
10
  import builtins
11
11
  import ctypes
12
- import hashlib
13
12
  import inspect
14
13
  import struct
15
14
  import zlib
16
- from typing import Any, Callable, Generic, List, Tuple, TypeVar, Union
15
+ from typing import Any, Callable, Generic, List, NamedTuple, Optional, Tuple, TypeVar, Union
17
16
 
18
17
  import numpy as np
19
18
 
@@ -51,10 +50,6 @@ class Array(Generic[DType]):
51
50
  pass
52
51
 
53
52
 
54
- # shared hash for all constants
55
- _constant_hash = hashlib.sha256()
56
-
57
-
58
53
  def constant(x):
59
54
  """Function to declare compile-time constants accessible from Warp kernels
60
55
 
@@ -62,27 +57,7 @@ def constant(x):
62
57
  x: Compile-time constant value, can be any of the built-in math types.
63
58
  """
64
59
 
65
- global _constant_hash
66
-
67
- # hash the constant value
68
- if isinstance(x, builtins.bool):
69
- # This needs to come before the check for `int` since all boolean
70
- # values are also instances of `int`.
71
- _constant_hash.update(struct.pack("?", x))
72
- elif isinstance(x, int):
73
- _constant_hash.update(struct.pack("<q", x))
74
- elif isinstance(x, float):
75
- _constant_hash.update(struct.pack("<d", x))
76
- elif isinstance(x, float16):
77
- # float16 is a special case
78
- p = ctypes.pointer(ctypes.c_float(x.value))
79
- _constant_hash.update(p.contents)
80
- elif isinstance(x, tuple(scalar_types)):
81
- p = ctypes.pointer(x._type_(x.value))
82
- _constant_hash.update(p.contents)
83
- elif isinstance(x, ctypes.Array):
84
- _constant_hash.update(bytes(x))
85
- else:
60
+ if not isinstance(x, (builtins.bool, int, float, tuple(scalar_and_bool_types), ctypes.Array)):
86
61
  raise RuntimeError(f"Invalid constant type: {type(x)}")
87
62
 
88
63
  return x
@@ -503,7 +478,7 @@ class bool:
503
478
  def __init__(self, x=False):
504
479
  self.value = x
505
480
 
506
- def __bool__(self) -> bool:
481
+ def __bool__(self) -> builtins.bool:
507
482
  return self.value != 0
508
483
 
509
484
  def __float__(self) -> float:
@@ -1675,9 +1650,10 @@ class array(Array):
1675
1650
  try:
1676
1651
  # Performance note: try first, ask questions later
1677
1652
  device = warp.context.runtime.get_device(device)
1678
- except:
1679
- warp.context.assert_initialized()
1680
- raise
1653
+ except Exception:
1654
+ # Fallback to using the public API for retrieving the device,
1655
+ # which takes take of initializing Warp if needed.
1656
+ device = warp.context.get_device(device)
1681
1657
 
1682
1658
  if device.is_cuda:
1683
1659
  desc = data.__cuda_array_interface__
@@ -1804,9 +1780,10 @@ class array(Array):
1804
1780
  try:
1805
1781
  # Performance note: try first, ask questions later
1806
1782
  device = warp.context.runtime.get_device(device)
1807
- except:
1808
- warp.context.assert_initialized()
1809
- raise
1783
+ except Exception:
1784
+ # Fallback to using the public API for retrieving the device,
1785
+ # which takes take of initializing Warp if needed.
1786
+ device = warp.context.get_device(device)
1810
1787
 
1811
1788
  if device.is_cpu and not copy and not pinned:
1812
1789
  # reference numpy memory directly
@@ -1830,9 +1807,10 @@ class array(Array):
1830
1807
  try:
1831
1808
  # Performance note: try first, ask questions later
1832
1809
  device = warp.context.runtime.get_device(device)
1833
- except:
1834
- warp.context.assert_initialized()
1835
- raise
1810
+ except Exception:
1811
+ # Fallback to using the public API for retrieving the device,
1812
+ # which takes take of initializing Warp if needed.
1813
+ device = warp.context.get_device(device)
1836
1814
 
1837
1815
  check_array_shape(shape)
1838
1816
  ndim = len(shape)
@@ -1877,9 +1855,10 @@ class array(Array):
1877
1855
  try:
1878
1856
  # Performance note: try first, ask questions later
1879
1857
  device = warp.context.runtime.get_device(device)
1880
- except:
1881
- warp.context.assert_initialized()
1882
- raise
1858
+ except Exception:
1859
+ # Fallback to using the public API for retrieving the device,
1860
+ # which takes take of initializing Warp if needed.
1861
+ device = warp.context.get_device(device)
1883
1862
 
1884
1863
  check_array_shape(shape)
1885
1864
  ndim = len(shape)
@@ -3022,11 +3001,12 @@ class Volume:
3022
3001
  #: Enum value to specify trilinear interpolation during sampling
3023
3002
  LINEAR = constant(1)
3024
3003
 
3025
- def __init__(self, data: array):
3004
+ def __init__(self, data: array, copy: bool = True):
3026
3005
  """Class representing a sparse grid.
3027
3006
 
3028
3007
  Args:
3029
3008
  data (:class:`warp.array`): Array of bytes representing the volume in NanoVDB format
3009
+ copy (bool): Whether the incoming data will be copied or aliased
3030
3010
  """
3031
3011
 
3032
3012
  self.id = 0
@@ -3036,16 +3016,16 @@ class Volume:
3036
3016
 
3037
3017
  if data is None:
3038
3018
  return
3039
-
3040
- if data.device is None:
3041
- raise RuntimeError("Invalid device")
3042
3019
  self.device = data.device
3043
3020
 
3021
+ owner = False
3044
3022
  if self.device.is_cpu:
3045
- self.id = self.runtime.core.volume_create_host(ctypes.cast(data.ptr, ctypes.c_void_p), data.size)
3023
+ self.id = self.runtime.core.volume_create_host(
3024
+ ctypes.cast(data.ptr, ctypes.c_void_p), data.size, copy, owner
3025
+ )
3046
3026
  else:
3047
3027
  self.id = self.runtime.core.volume_create_device(
3048
- self.device.context, ctypes.cast(data.ptr, ctypes.c_void_p), data.size
3028
+ self.device.context, ctypes.cast(data.ptr, ctypes.c_void_p), data.size, copy, owner
3049
3029
  )
3050
3030
 
3051
3031
  if self.id == 0:
@@ -3066,32 +3046,90 @@ class Volume:
3066
3046
  """Returns the raw memory buffer of the Volume as an array"""
3067
3047
  buf = ctypes.c_void_p(0)
3068
3048
  size = ctypes.c_uint64(0)
3049
+ self.runtime.core.volume_get_buffer_info(self.id, ctypes.byref(buf), ctypes.byref(size))
3050
+ return array(ptr=buf.value, dtype=uint8, shape=size.value, device=self.device, owner=False)
3051
+
3052
+ def get_tile_count(self) -> int:
3053
+ """Returns the number of tiles (NanoVDB leaf nodes) of the volume"""
3054
+
3055
+ voxel_count, tile_count = (
3056
+ ctypes.c_uint64(0),
3057
+ ctypes.c_uint32(0),
3058
+ )
3059
+ self.runtime.core.volume_get_tile_and_voxel_count(self.id, ctypes.byref(tile_count), ctypes.byref(voxel_count))
3060
+ return tile_count.value
3061
+
3062
+ def get_tiles(self, out: Optional[array] = None) -> array:
3063
+ """Returns the integer coordinates of all allocated tiles for this volume.
3064
+
3065
+ Args:
3066
+ out (:class:`warp.array`, optional): If provided, use the `out` array to store the tile coordinates, otherwise
3067
+ a new array will be allocated. `out` must be a contiguous array of ``tile_count`` ``vec3i`` or ``tile_count x 3`` ``int32``
3068
+ on the same device as this volume.
3069
+ """
3070
+
3071
+ if self.id == 0:
3072
+ raise RuntimeError("Invalid Volume")
3073
+
3074
+ tile_count = self.get_tile_count()
3075
+ if out is None:
3076
+ out = warp.empty(dtype=int32, shape=(tile_count, 3), device=self.device)
3077
+ elif out.device != self.device or out.shape[0] < tile_count:
3078
+ raise RuntimeError(f"'out' array must an array with at least {tile_count} rows on device {self.device}")
3079
+ elif not _is_contiguous_vec_like_array(out, vec_length=3, scalar_types=(int32,)):
3080
+ raise RuntimeError(
3081
+ "'out' must be a contiguous 1D array with type vec3i or a 2D array of type int32 with shape (N, 3) "
3082
+ )
3083
+
3069
3084
  if self.device.is_cpu:
3070
- self.runtime.core.volume_get_buffer_info_host(self.id, ctypes.byref(buf), ctypes.byref(size))
3085
+ self.runtime.core.volume_get_tiles_host(self.id, out.ptr)
3071
3086
  else:
3072
- self.runtime.core.volume_get_buffer_info_device(self.id, ctypes.byref(buf), ctypes.byref(size))
3073
- return array(ptr=buf.value, dtype=uint8, shape=size.value, device=self.device)
3087
+ self.runtime.core.volume_get_tiles_device(self.id, out.ptr)
3088
+
3089
+ return out
3090
+
3091
+ def get_voxel_count(self) -> int:
3092
+ """Returns the total number of allocated voxels for this volume"""
3093
+
3094
+ voxel_count, tile_count = (
3095
+ ctypes.c_uint64(0),
3096
+ ctypes.c_uint32(0),
3097
+ )
3098
+ self.runtime.core.volume_get_tile_and_voxel_count(self.id, ctypes.byref(tile_count), ctypes.byref(voxel_count))
3099
+ return voxel_count.value
3100
+
3101
+ def get_voxels(self, out: Optional[array] = None) -> array:
3102
+ """Returns the integer coordinates of all allocated voxels for this volume.
3103
+
3104
+ Args:
3105
+ out (:class:`warp.array`, optional): If provided, use the `out` array to store the voxel coordinates, otherwise
3106
+ a new array will be allocated. `out` must be a contiguous array of ``voxel_count`` ``vec3i`` or ``voxel_count x 3`` ``int32``
3107
+ on the same device as this volume.
3108
+ """
3074
3109
 
3075
- def get_tiles(self) -> array:
3076
3110
  if self.id == 0:
3077
3111
  raise RuntimeError("Invalid Volume")
3078
3112
 
3079
- buf = ctypes.c_void_p(0)
3080
- size = ctypes.c_uint64(0)
3113
+ voxel_count = self.get_voxel_count()
3114
+ if out is None:
3115
+ out = warp.empty(dtype=int32, shape=(voxel_count, 3), device=self.device)
3116
+ elif out.device != self.device or out.shape[0] < voxel_count:
3117
+ raise RuntimeError(f"'out' array must an array with at least {voxel_count} rows on device {self.device}")
3118
+ elif not _is_contiguous_vec_like_array(out, vec_length=3, scalar_types=(int32,)):
3119
+ raise RuntimeError(
3120
+ "'out' must be a contiguous 1D array with type vec3i or a 2D array of type int32 with shape (N, 3) "
3121
+ )
3122
+
3081
3123
  if self.device.is_cpu:
3082
- self.runtime.core.volume_get_tiles_host(self.id, ctypes.byref(buf), ctypes.byref(size))
3083
- deleter = self.device.default_allocator.deleter
3124
+ self.runtime.core.volume_get_voxels_host(self.id, out.ptr)
3084
3125
  else:
3085
- self.runtime.core.volume_get_tiles_device(self.id, ctypes.byref(buf), ctypes.byref(size))
3086
- if self.device.is_mempool_supported:
3087
- deleter = self.device.mempool_allocator.deleter
3088
- else:
3089
- deleter = self.device.default_allocator.deleter
3090
- num_tiles = size.value // (3 * 4)
3126
+ self.runtime.core.volume_get_voxels_device(self.id, out.ptr)
3091
3127
 
3092
- return array(ptr=buf.value, dtype=int32, shape=(num_tiles, 3), device=self.device, deleter=deleter)
3128
+ return out
3093
3129
 
3094
3130
  def get_voxel_size(self) -> Tuple[float, float, float]:
3131
+ """Voxel size, i.e, world coordinates of voxel's diagonal vector"""
3132
+
3095
3133
  if self.id == 0:
3096
3134
  raise RuntimeError("Invalid Volume")
3097
3135
 
@@ -3099,9 +3137,181 @@ class Volume:
3099
3137
  self.runtime.core.volume_get_voxel_size(self.id, ctypes.byref(dx), ctypes.byref(dy), ctypes.byref(dz))
3100
3138
  return (dx.value, dy.value, dz.value)
3101
3139
 
3140
+ class GridInfo(NamedTuple):
3141
+ """Grid metadata"""
3142
+
3143
+ name: str
3144
+ """Grid name"""
3145
+ size_in_bytes: int
3146
+ """Size of this grid's data, in bytes"""
3147
+
3148
+ grid_index: int
3149
+ """Index of this grid in the data buffer"""
3150
+ grid_count: int
3151
+ """Total number of grids in the data buffer"""
3152
+ type_str: str
3153
+ """String describing the type of the grid values"""
3154
+
3155
+ translation: vec3f
3156
+ """Index-to-world translation"""
3157
+ transform_matrix: mat33f
3158
+ """Linear part of the index-to-world transform"""
3159
+
3160
+ def get_grid_info(self) -> Volume.GridInfo:
3161
+ """Returns the metadata associated with this Volume"""
3162
+
3163
+ grid_index = ctypes.c_uint32(0)
3164
+ grid_count = ctypes.c_uint32(0)
3165
+ grid_size = ctypes.c_uint64(0)
3166
+ translation_buffer = (ctypes.c_float * 3)()
3167
+ transform_buffer = (ctypes.c_float * 9)()
3168
+ type_str_buffer = (ctypes.c_char * 16)()
3169
+
3170
+ name = self.runtime.core.volume_get_grid_info(
3171
+ self.id,
3172
+ ctypes.byref(grid_size),
3173
+ ctypes.byref(grid_index),
3174
+ ctypes.byref(grid_count),
3175
+ translation_buffer,
3176
+ transform_buffer,
3177
+ type_str_buffer,
3178
+ )
3179
+
3180
+ if name is None:
3181
+ raise RuntimeError("Invalid volume")
3182
+
3183
+ return Volume.GridInfo(
3184
+ name.decode("ascii"),
3185
+ grid_size.value,
3186
+ grid_index.value,
3187
+ grid_count.value,
3188
+ type_str_buffer.value.decode("ascii"),
3189
+ vec3f.from_buffer_copy(translation_buffer),
3190
+ mat33f.from_buffer_copy(transform_buffer),
3191
+ )
3192
+
3193
+ _nvdb_type_to_dtype = {
3194
+ "float": float32,
3195
+ "double": float64,
3196
+ "int16": int16,
3197
+ "int32": int32,
3198
+ "int64": int64,
3199
+ "Vec3f": vec3f,
3200
+ "Vec3d": vec3d,
3201
+ "Half": float16,
3202
+ "uint32": uint32,
3203
+ "bool": bool,
3204
+ "Vec4f": vec4f,
3205
+ "Vec4d": vec4d,
3206
+ "Vec3u8": vec3ub,
3207
+ "Vec3u16": vec3us,
3208
+ "uint8": uint8,
3209
+ }
3210
+
3211
+ @property
3212
+ def dtype(self) -> type:
3213
+ """Type of the Volume's values as a Warp type.
3214
+
3215
+ If the grid does not contain values (e.g. index grids) or if the NanoVDB type is not
3216
+ representable as a Warp type, returns ``None``.
3217
+ """
3218
+ return Volume._nvdb_type_to_dtype.get(self.get_grid_info().type_str, None)
3219
+
3220
+ _nvdb_index_types = ("Index", "OnIndex", "IndexMask", "OnIndexMask")
3221
+
3222
+ @property
3223
+ def is_index(self) -> bool:
3224
+ """Whether this Volume contains an index grid, that is, a type of grid that does
3225
+ not explicitly store values but associates each voxel to linearized index.
3226
+ """
3227
+
3228
+ return self.get_grid_info().type_str in Volume._nvdb_index_types
3229
+
3230
+ def get_feature_array_count(self) -> int:
3231
+ """Returns the number of supplemental data arrays stored alongside the grid"""
3232
+
3233
+ return self.runtime.core.volume_get_blind_data_count(self.id)
3234
+
3235
+ class FeatureArrayInfo(NamedTuple):
3236
+ """Metadata for a supplemental data array"""
3237
+
3238
+ name: str
3239
+ """Name of the data array"""
3240
+ ptr: int
3241
+ """Memory address of the start of the array"""
3242
+
3243
+ value_size: int
3244
+ """Size in bytes of the array values"""
3245
+ value_count: int
3246
+ """Number of values in the array"""
3247
+ type_str: str
3248
+ """String describing the type of the array values"""
3249
+
3250
+ def get_feature_array_info(self, feature_index: int) -> Volume.FeatureArrayInfo:
3251
+ """Returns the metadata associated to the feature array at `feature_index`"""
3252
+
3253
+ buf = ctypes.c_void_p(0)
3254
+ value_count = ctypes.c_uint64(0)
3255
+ value_size = ctypes.c_uint32(0)
3256
+ type_str_buffer = (ctypes.c_char * 16)()
3257
+
3258
+ name = self.runtime.core.volume_get_blind_data_info(
3259
+ self.id,
3260
+ feature_index,
3261
+ ctypes.byref(buf),
3262
+ ctypes.byref(value_count),
3263
+ ctypes.byref(value_size),
3264
+ type_str_buffer,
3265
+ )
3266
+
3267
+ if buf.value is None:
3268
+ raise RuntimeError("Invalid feature array")
3269
+
3270
+ return Volume.FeatureArrayInfo(
3271
+ name.decode("ascii"),
3272
+ buf.value,
3273
+ value_size.value,
3274
+ value_count.value,
3275
+ type_str_buffer.value.decode("ascii"),
3276
+ )
3277
+
3278
+ def feature_array(self, feature_index: int, dtype=None) -> array:
3279
+ """Returns one the the grid's feature data arrays as a Warp array
3280
+
3281
+ Args:
3282
+ feature_index: Index of the supplemental data array in the grid
3283
+ dtype: Type for the returned Warp array. If not provided, will be deduced from the array metadata.
3284
+ """
3285
+
3286
+ info = self.get_feature_array_info(feature_index)
3287
+
3288
+ if dtype is None:
3289
+ try:
3290
+ dtype = Volume._nvdb_type_to_dtype[info.type_str]
3291
+ except KeyError:
3292
+ # Unknown type, default to byte array
3293
+ dtype = uint8
3294
+
3295
+ value_count = info.value_count
3296
+ value_size = info.value_size
3297
+
3298
+ if type_size_in_bytes(dtype) == 1:
3299
+ # allow requesting a byte array from any type
3300
+ value_count *= value_size
3301
+ value_size = 1
3302
+ elif value_size == 1 and (value_count % type_size_in_bytes(dtype)) == 0:
3303
+ # allow converting a byte array to any type
3304
+ value_size = type_size_in_bytes(dtype)
3305
+ value_count = value_count // value_size
3306
+
3307
+ if type_size_in_bytes(dtype) != value_size:
3308
+ raise RuntimeError(f"Cannot cast feature data of size {value_size} to array dtype {type_repr(dtype)}")
3309
+
3310
+ return array(ptr=info.ptr, dtype=dtype, shape=value_count, device=self.device, owner=False)
3311
+
3102
3312
  @classmethod
3103
3313
  def load_from_nvdb(cls, file_or_buffer, device=None) -> Volume:
3104
- """Creates a Volume object from a NanoVDB file or in-memory buffer.
3314
+ """Creates a Volume object from a serialized NanoVDB file or in-memory buffer.
3105
3315
 
3106
3316
  Returns:
3107
3317
 
@@ -3113,28 +3323,117 @@ class Volume:
3113
3323
  data = file_or_buffer
3114
3324
 
3115
3325
  magic, version, grid_count, codec = struct.unpack("<QIHH", data[0:16])
3116
- if magic != 0x304244566F6E614E:
3326
+ if magic not in (0x304244566F6E614E, 0x324244566F6E614E): # NanoVDB0 or NanoVDB2 in hex, little-endian
3117
3327
  raise RuntimeError("NanoVDB signature not found")
3118
3328
  if version >> 21 != 32: # checking major version
3119
3329
  raise RuntimeError("Unsupported NanoVDB version")
3120
- if grid_count != 1:
3121
- raise RuntimeError("Only NVDBs with exactly one grid are supported")
3122
3330
 
3123
- grid_data_offset = 192 + struct.unpack("<I", data[152:156])[0]
3331
+ # Skip over segment metadata, store total payload size
3332
+ grid_data_offset = 16 # sizeof(FileHeader)
3333
+ tot_file_size = 0
3334
+ for _ in range(grid_count):
3335
+ grid_file_size = struct.unpack("<Q", data[grid_data_offset + 8 : grid_data_offset + 16])[0]
3336
+ tot_file_size += grid_file_size
3337
+
3338
+ grid_name_size = struct.unpack("<I", data[grid_data_offset + 136 : grid_data_offset + 140])[0]
3339
+ grid_data_offset += 176 + grid_name_size # sizeof(FileMetadata) + grid name
3340
+
3341
+ file_end = grid_data_offset + tot_file_size
3342
+
3124
3343
  if codec == 0: # no compression
3125
- grid_data = data[grid_data_offset:]
3344
+ grid_data = data[grid_data_offset:file_end]
3126
3345
  elif codec == 1: # zip compression
3127
- grid_data = zlib.decompress(data[grid_data_offset + 8 :])
3346
+ grid_data = bytearray()
3347
+ while grid_data_offset < file_end:
3348
+ chunk_size = struct.unpack("<Q", data[grid_data_offset : grid_data_offset + 8])[0]
3349
+ grid_data += zlib.decompress(data[grid_data_offset + 8 :])
3350
+ grid_data_offset += 8 + chunk_size
3351
+
3352
+ elif codec == 2: # blosc compression
3353
+ try:
3354
+ import blosc
3355
+ except ImportError as err:
3356
+ raise RuntimeError(
3357
+ f"NanoVDB buffer is compressed using blosc, but Python module could not be imported: {err}"
3358
+ ) from err
3359
+
3360
+ grid_data = bytearray()
3361
+ while grid_data_offset < file_end:
3362
+ chunk_size = struct.unpack("<Q", data[grid_data_offset : grid_data_offset + 8])[0]
3363
+ grid_data += blosc.decompress(data[grid_data_offset + 8 :])
3364
+ grid_data_offset += 8 + chunk_size
3128
3365
  else:
3129
3366
  raise RuntimeError(f"Unsupported codec code: {codec}")
3130
3367
 
3131
3368
  magic = struct.unpack("<Q", grid_data[0:8])[0]
3132
- if magic != 0x304244566F6E614E:
3369
+ if magic not in (0x304244566F6E614E, 0x314244566F6E614E): # NanoVDB0 or NanoVDB1 in hex, little-endian
3133
3370
  raise RuntimeError("NanoVDB signature not found on grid!")
3134
3371
 
3135
3372
  data_array = array(np.frombuffer(grid_data, dtype=np.byte), device=device)
3136
3373
  return cls(data_array)
3137
3374
 
3375
+ @classmethod
3376
+ def load_from_address(cls, grid_ptr: int, buffer_size: int = 0, device=None) -> Volume:
3377
+ """
3378
+ Creates a new :class:`Volume` aliasing an in-memory grid buffer.
3379
+
3380
+ In contrast to :meth:`load_from_nvdb` which should be used to load serialized NanoVDB grids,
3381
+ here the buffer must be uncompressed and must not contain file header information.
3382
+ If the passed address does not contain a NanoVDB grid, the behavior of this function is undefined.
3383
+
3384
+ Args:
3385
+ grid_ptr: Integer address of the start of the grid buffer
3386
+ buffer_size: Size of the buffer, in bytes. If not provided, the size will be assumed to be that of the single grid starting at `grid_ptr`.
3387
+ device: Device of the buffer, and of the returned Volume. If not provided, the current Warp device is assumed.
3388
+
3389
+ Returns the newly created Volume.
3390
+ """
3391
+
3392
+ if not grid_ptr:
3393
+ raise (RuntimeError, "Invalid grid buffer pointer")
3394
+
3395
+ # Check that a Volume has not already been created for this address
3396
+ # (to allow this we would need to ref-count the volume descriptor)
3397
+ existing_buf = ctypes.c_void_p(0)
3398
+ existing_size = ctypes.c_uint64(0)
3399
+ warp.context.runtime.core.volume_get_buffer_info(
3400
+ grid_ptr, ctypes.byref(existing_buf), ctypes.byref(existing_size)
3401
+ )
3402
+
3403
+ if existing_buf.value is not None:
3404
+ raise RuntimeError(
3405
+ "A warp Volume has already been created for this grid, aliasing it more than once is not possible."
3406
+ )
3407
+
3408
+ data_array = array(ptr=grid_ptr, dtype=uint8, shape=buffer_size, owner=False, device=device)
3409
+
3410
+ return cls(data_array, copy=False)
3411
+
3412
+ def load_next_grid(self) -> Volume:
3413
+ """
3414
+ Tries to create a new warp Volume for the next grid that is linked to by this Volume.
3415
+
3416
+ The existence of a next grid is deduced from the `grid_index` and `grid_count` metadata
3417
+ as well as the size of this Volume's in-memory buffer.
3418
+
3419
+ Returns the newly created Volume, or None if there is no next grid.
3420
+ """
3421
+
3422
+ grid = self.get_grid_info()
3423
+
3424
+ array = self.array()
3425
+
3426
+ if grid.grid_index + 1 >= grid.grid_count or array.capacity <= grid.size_in_bytes:
3427
+ return None
3428
+
3429
+ next_volume = Volume.load_from_address(
3430
+ array.ptr + grid.size_in_bytes, buffer_size=array.capacity - grid.size_in_bytes, device=self.device
3431
+ )
3432
+ # makes the new Volume keep a reference to the current grid, as we're aliasing its buffer
3433
+ next_volume._previous_grid = self
3434
+
3435
+ return next_volume
3436
+
3138
3437
  @classmethod
3139
3438
  def load_from_numpy(
3140
3439
  cls, ndarray: np.array, min_world=(0.0, 0.0, 0.0), voxel_size=1.0, bg_value=0.0, device=None
@@ -3286,11 +3585,11 @@ class Volume:
3286
3585
 
3287
3586
  Args:
3288
3587
  tile_points (:class:`warp.array`): Array of positions that define the tiles to be allocated.
3289
- The array can be a 2D, N-by-3 array of :class:`warp.int32` values, indicating index space positions,
3290
- or can be a 1D array of :class:`warp.vec3` values, indicating world space positions.
3588
+ The array may use an integer scalar type (2D N-by-3 array of :class:`warp.int32` or 1D array of `warp.vec3i` values), indicating index space positions,
3589
+ or a floating point scalar type (2D N-by-3 array of :class:`warp.float32` or 1D array of `warp.vec3f` values), indicating world space positions.
3291
3590
  Repeated points per tile are allowed and will be efficiently deduplicated.
3292
3591
  voxel_size (float): Voxel size of the new volume.
3293
- bg_value (float or array-like): Value of unallocated voxels of the volume, also defines the volume's type, a :class:`warp.vec3` volume is created if this is `array-like`, otherwise a float volume is created
3592
+ bg_value (array-like, float, int or None): Value of unallocated voxels of the volume, also defines the volume's type. A :class:`warp.vec3` volume is created if this is `array-like`, an index volume will be created if `bg_value` is ``None``.
3294
3593
  translation (array-like): Translation between the index and world spaces.
3295
3594
  device (Devicelike): The CUDA device to create the volume on, e.g.: "cuda" or "cuda:0".
3296
3595
 
@@ -3301,19 +3600,28 @@ class Volume:
3301
3600
  raise RuntimeError(f"Voxel size must be positive! Got {voxel_size}")
3302
3601
  if not device.is_cuda:
3303
3602
  raise RuntimeError("Only CUDA devices are supported for allocate_by_tiles")
3304
- if not (
3305
- isinstance(tile_points, array)
3306
- and (tile_points.dtype == int32 and tile_points.ndim == 2)
3307
- or (tile_points.dtype == vec3 and tile_points.ndim == 1)
3308
- ):
3309
- raise RuntimeError("Expected an warp array of vec3s or of n-by-3 int32s as tile_points!")
3603
+ if not _is_contiguous_vec_like_array(tile_points, vec_length=3, scalar_types=(float32, int32)):
3604
+ raise RuntimeError(
3605
+ "tile_points must be contiguous and either a 1D warp array of vec3f or vec3i or a 2D n-by-3 array of int32 or float32."
3606
+ )
3310
3607
  if not tile_points.device.is_cuda:
3311
- tile_points = array(tile_points, dtype=tile_points.dtype, device=device)
3608
+ tile_points = tile_points.to(device)
3312
3609
 
3313
3610
  volume = cls(data=None)
3314
3611
  volume.device = device
3315
- in_world_space = tile_points.dtype == vec3
3316
- if hasattr(bg_value, "__len__"):
3612
+ in_world_space = type_scalar_type(tile_points.dtype) == float32
3613
+ if bg_value is None:
3614
+ volume.id = volume.runtime.core.volume_index_from_tiles_device(
3615
+ volume.device.context,
3616
+ ctypes.c_void_p(tile_points.ptr),
3617
+ tile_points.shape[0],
3618
+ voxel_size,
3619
+ translation[0],
3620
+ translation[1],
3621
+ translation[2],
3622
+ in_world_space,
3623
+ )
3624
+ elif hasattr(bg_value, "__len__"):
3317
3625
  volume.id = volume.runtime.core.volume_v_from_tiles_device(
3318
3626
  volume.device.context,
3319
3627
  ctypes.c_void_p(tile_points.ptr),
@@ -3357,6 +3665,73 @@ class Volume:
3357
3665
 
3358
3666
  return volume
3359
3667
 
3668
+ @classmethod
3669
+ def allocate_by_voxels(
3670
+ cls, voxel_points: array, voxel_size: float, translation=(0.0, 0.0, 0.0), device=None
3671
+ ) -> Volume:
3672
+ """Allocate a new Volume with active voxel for each point voxel_points.
3673
+
3674
+ This function creates an *index* Volume, a special kind of volume that does not any store any
3675
+ explicit payload but encodes a linearized index for each active voxel, allowing to lookup and
3676
+ sample data from arbitrary external arrays.
3677
+
3678
+ This function is only supported for CUDA devices.
3679
+
3680
+ Args:
3681
+ voxel_points (:class:`warp.array`): Array of positions that define the voxels to be allocated.
3682
+ The array may use an integer scalar type (2D N-by-3 array of :class:`warp.int32` or 1D array of `warp.vec3i` values), indicating index space positions,
3683
+ or a floating point scalar type (2D N-by-3 array of :class:`warp.float32` or 1D array of `warp.vec3f` values), indicating world space positions.
3684
+ Repeated points per tile are allowed and will be efficiently deduplicated.
3685
+ voxel_size (float): Voxel size of the new volume.
3686
+ translation (array-like): Translation between the index and world spaces.
3687
+ device (Devicelike): The CUDA device to create the volume on, e.g.: "cuda" or "cuda:0".
3688
+
3689
+ """
3690
+ device = warp.get_device(device)
3691
+
3692
+ if voxel_size <= 0.0:
3693
+ raise RuntimeError(f"Voxel size must be positive! Got {voxel_size}")
3694
+ if not device.is_cuda:
3695
+ raise RuntimeError("Only CUDA devices are supported for allocate_by_tiles")
3696
+ if not (is_array(voxel_points) and voxel_points.is_contiguous):
3697
+ raise RuntimeError("tile_points must be a contiguous array")
3698
+ if not _is_contiguous_vec_like_array(voxel_points, vec_length=3, scalar_types=(float32, int32)):
3699
+ raise RuntimeError(
3700
+ "voxel_points must be contiguous and either a 1D warp array of vec3f or vec3i or a 2D n-by-3 array of int32 or float32."
3701
+ )
3702
+ if not voxel_points.device.is_cuda:
3703
+ voxel_points = voxel_points.to(device)
3704
+
3705
+ volume = cls(data=None)
3706
+ volume.device = device
3707
+ in_world_space = type_scalar_type(voxel_points.dtype) == float32
3708
+
3709
+ volume.id = volume.runtime.core.volume_from_active_voxels_device(
3710
+ volume.device.context,
3711
+ ctypes.c_void_p(voxel_points.ptr),
3712
+ voxel_points.shape[0],
3713
+ voxel_size,
3714
+ translation[0],
3715
+ translation[1],
3716
+ translation[2],
3717
+ in_world_space,
3718
+ )
3719
+
3720
+ if volume.id == 0:
3721
+ raise RuntimeError("Failed to create volume")
3722
+
3723
+ return volume
3724
+
3725
+
3726
+ def _is_contiguous_vec_like_array(array, vec_length: int, scalar_types: Tuple[type]) -> bool:
3727
+ if not (is_array(array) and array.is_contiguous):
3728
+ return False
3729
+ if type_scalar_type(array.dtype) not in scalar_types:
3730
+ return False
3731
+ return (array.ndim == 1 and type_length(array.dtype) == vec_length) or (
3732
+ array.ndim == 2 and array.shape[1] == vec_length and type_length(array.dtype) == 1
3733
+ )
3734
+
3360
3735
 
3361
3736
  # definition just for kernel type (cannot be a parameter), see mesh.h
3362
3737
  # NOTE: its layout must match the corresponding struct defined in C.
@@ -4185,6 +4560,36 @@ class HashGrid:
4185
4560
 
4186
4561
  class MarchingCubes:
4187
4562
  def __init__(self, nx: int, ny: int, nz: int, max_verts: int, max_tris: int, device=None):
4563
+ """CUDA-based Marching Cubes algorithm to extract a 2D surface mesh from a 3D volume.
4564
+
4565
+ Attributes:
4566
+ id: Unique identifier for this object.
4567
+ verts (:class:`warp.array`): Array of vertex positions of type :class:`warp.vec3f`
4568
+ for the output surface mesh.
4569
+ This is populated after running :func:`surface`.
4570
+ indices (:class:`warp.array`): Array containing indices of type :class:`warp.int32`
4571
+ defining triangles for the output surface mesh.
4572
+ This is populated after running :func:`surface`.
4573
+
4574
+ Each set of three consecutive integers in the array represents a single triangle,
4575
+ in which each integer is an index referring to a vertex in the :attr:`verts` array.
4576
+
4577
+ Args:
4578
+ nx: Number of cubes in the x-direction.
4579
+ ny: Number of cubes in the y-direction.
4580
+ nz: Number of cubes in the z-direction.
4581
+ max_verts: Maximum expected number of vertices (used for array preallocation).
4582
+ max_tris: Maximum expected number of triangles (used for array preallocation).
4583
+ device (Devicelike): CUDA device on which to run marching cubes and allocate memory.
4584
+
4585
+ Raises:
4586
+ RuntimeError: ``device`` not a CUDA device.
4587
+
4588
+ .. note::
4589
+ The shape of the marching cubes should match the shape of the scalar field being surfaced.
4590
+
4591
+ """
4592
+
4188
4593
  self.id = 0
4189
4594
 
4190
4595
  self.runtime = warp.context.runtime
@@ -4210,7 +4615,7 @@ class MarchingCubes:
4210
4615
  from warp.context import zeros
4211
4616
 
4212
4617
  self.verts = zeros(max_verts, dtype=vec3, device=self.device)
4213
- self.indices = zeros(max_tris * 3, dtype=int, device=self.device)
4618
+ self.indices = zeros(max_tris * 3, dtype=warp.int32, device=self.device)
4214
4619
 
4215
4620
  # alloc surfacer
4216
4621
  self.id = ctypes.c_uint64(self.alloc(self.device.context))
@@ -4224,7 +4629,19 @@ class MarchingCubes:
4224
4629
  # destroy surfacer
4225
4630
  self.free(self.id)
4226
4631
 
4227
- def resize(self, nx: int, ny: int, nz: int, max_verts: int, max_tris: int):
4632
+ def resize(self, nx: int, ny: int, nz: int, max_verts: int, max_tris: int) -> None:
4633
+ """Update the expected input and maximum output sizes for the marching cubes calculation.
4634
+
4635
+ This function has no immediate effect on the underlying buffers.
4636
+ The new values take effect on the next :func:`surface` call.
4637
+
4638
+ Args:
4639
+ nx: Number of cubes in the x-direction.
4640
+ ny: Number of cubes in the y-direction.
4641
+ nz: Number of cubes in the z-direction.
4642
+ max_verts: Maximum expected number of vertices (used for array preallocation).
4643
+ max_tris: Maximum expected number of triangles (used for array preallocation).
4644
+ """
4228
4645
  # actual allocations will be resized on next call to surface()
4229
4646
  self.nx = nx
4230
4647
  self.ny = ny
@@ -4232,13 +4649,37 @@ class MarchingCubes:
4232
4649
  self.max_verts = max_verts
4233
4650
  self.max_tris = max_tris
4234
4651
 
4235
- def surface(self, field: array(dtype=float), threshold: float):
4652
+ def surface(self, field: array(dtype=float, ndim=3), threshold: float) -> None:
4653
+ """Compute a 2D surface mesh of a given isosurface from a 3D scalar field.
4654
+
4655
+ The triangles and vertices defining the output mesh are written to the
4656
+ :attr:`indices` and :attr:`verts` arrays.
4657
+
4658
+ Args:
4659
+ field: Scalar field from which to generate a mesh.
4660
+ threshold: Target isosurface value.
4661
+
4662
+ Raises:
4663
+ ValueError: ``field`` is not a 3D array.
4664
+ ValueError: Marching cubes shape does not match the shape of ``field``.
4665
+ RuntimeError: :attr:`max_verts` and/or :attr:`max_tris` might be too small to hold the surface mesh.
4666
+ """
4667
+
4236
4668
  # WP_API int marching_cubes_surface_host(const float* field, int nx, int ny, int nz, float threshold, wp::vec3* verts, int* triangles, int max_verts, int max_tris, int* out_num_verts, int* out_num_tris);
4237
4669
  num_verts = ctypes.c_int(0)
4238
4670
  num_tris = ctypes.c_int(0)
4239
4671
 
4240
4672
  self.runtime.core.marching_cubes_surface_device.restype = ctypes.c_int
4241
4673
 
4674
+ # For now we require that input field shape matches nx, ny, nz
4675
+ if field.ndim != 3:
4676
+ raise ValueError(f"Input field must be a three-dimensional array (got {field.ndim}).")
4677
+ if field.shape[0] != self.nx or field.shape[1] != self.ny or field.shape[2] != self.nz:
4678
+ raise ValueError(
4679
+ f"Marching cubes shape ({self.nx}, {self.ny}, {self.nz}) does not match the "
4680
+ f"input array shape {field.shape}."
4681
+ )
4682
+
4242
4683
  error = self.runtime.core.marching_cubes_surface_device(
4243
4684
  self.id,
4244
4685
  ctypes.cast(field.ptr, ctypes.c_void_p),
@@ -4362,7 +4803,7 @@ def infer_argument_types(args, template_types, arg_names=None):
4362
4803
  arg_types.append(arg_type(dtype=arg.dtype, ndim=arg.ndim))
4363
4804
  elif arg_type in warp.types.scalar_and_bool_types:
4364
4805
  arg_types.append(arg_type)
4365
- elif arg_type in (int, float):
4806
+ elif arg_type in (int, float, builtins.bool):
4366
4807
  # canonicalize type
4367
4808
  arg_types.append(warp.types.type_to_warp(arg_type))
4368
4809
  elif hasattr(arg_type, "_wp_scalar_type_"):