warp-lang 1.1.0__py3-none-manylinux2014_aarch64.whl → 1.2.0__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 +418 -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 +514 -77
  211. {warp_lang-1.1.0.dist-info → warp_lang-1.2.0.dist-info}/METADATA +57 -30
  212. warp_lang-1.2.0.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.0.dist-info}/LICENSE.md +0 -0
  217. {warp_lang-1.1.0.dist-info → warp_lang-1.2.0.dist-info}/WHEEL +0 -0
  218. {warp_lang-1.1.0.dist-info → warp_lang-1.2.0.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:
@@ -1676,7 +1651,7 @@ class array(Array):
1676
1651
  # Performance note: try first, ask questions later
1677
1652
  device = warp.context.runtime.get_device(device)
1678
1653
  except:
1679
- warp.context.assert_initialized()
1654
+ warp.context.init()
1680
1655
  raise
1681
1656
 
1682
1657
  if device.is_cuda:
@@ -1805,7 +1780,7 @@ class array(Array):
1805
1780
  # Performance note: try first, ask questions later
1806
1781
  device = warp.context.runtime.get_device(device)
1807
1782
  except:
1808
- warp.context.assert_initialized()
1783
+ warp.context.init()
1809
1784
  raise
1810
1785
 
1811
1786
  if device.is_cpu and not copy and not pinned:
@@ -1831,7 +1806,7 @@ class array(Array):
1831
1806
  # Performance note: try first, ask questions later
1832
1807
  device = warp.context.runtime.get_device(device)
1833
1808
  except:
1834
- warp.context.assert_initialized()
1809
+ warp.context.init()
1835
1810
  raise
1836
1811
 
1837
1812
  check_array_shape(shape)
@@ -1878,7 +1853,7 @@ class array(Array):
1878
1853
  # Performance note: try first, ask questions later
1879
1854
  device = warp.context.runtime.get_device(device)
1880
1855
  except:
1881
- warp.context.assert_initialized()
1856
+ warp.context.init()
1882
1857
  raise
1883
1858
 
1884
1859
  check_array_shape(shape)
@@ -3022,11 +2997,12 @@ class Volume:
3022
2997
  #: Enum value to specify trilinear interpolation during sampling
3023
2998
  LINEAR = constant(1)
3024
2999
 
3025
- def __init__(self, data: array):
3000
+ def __init__(self, data: array, copy: bool = True):
3026
3001
  """Class representing a sparse grid.
3027
3002
 
3028
3003
  Args:
3029
3004
  data (:class:`warp.array`): Array of bytes representing the volume in NanoVDB format
3005
+ copy (bool): Whether the incoming data will be copied or aliased
3030
3006
  """
3031
3007
 
3032
3008
  self.id = 0
@@ -3036,16 +3012,16 @@ class Volume:
3036
3012
 
3037
3013
  if data is None:
3038
3014
  return
3039
-
3040
- if data.device is None:
3041
- raise RuntimeError("Invalid device")
3042
3015
  self.device = data.device
3043
3016
 
3017
+ owner = False
3044
3018
  if self.device.is_cpu:
3045
- self.id = self.runtime.core.volume_create_host(ctypes.cast(data.ptr, ctypes.c_void_p), data.size)
3019
+ self.id = self.runtime.core.volume_create_host(
3020
+ ctypes.cast(data.ptr, ctypes.c_void_p), data.size, copy, owner
3021
+ )
3046
3022
  else:
3047
3023
  self.id = self.runtime.core.volume_create_device(
3048
- self.device.context, ctypes.cast(data.ptr, ctypes.c_void_p), data.size
3024
+ self.device.context, ctypes.cast(data.ptr, ctypes.c_void_p), data.size, copy, owner
3049
3025
  )
3050
3026
 
3051
3027
  if self.id == 0:
@@ -3066,32 +3042,90 @@ class Volume:
3066
3042
  """Returns the raw memory buffer of the Volume as an array"""
3067
3043
  buf = ctypes.c_void_p(0)
3068
3044
  size = ctypes.c_uint64(0)
3045
+ self.runtime.core.volume_get_buffer_info(self.id, ctypes.byref(buf), ctypes.byref(size))
3046
+ return array(ptr=buf.value, dtype=uint8, shape=size.value, device=self.device, owner=False)
3047
+
3048
+ def get_tile_count(self) -> int:
3049
+ """Returns the number of tiles (NanoVDB leaf nodes) of the volume"""
3050
+
3051
+ voxel_count, tile_count = (
3052
+ ctypes.c_uint64(0),
3053
+ ctypes.c_uint32(0),
3054
+ )
3055
+ self.runtime.core.volume_get_tile_and_voxel_count(self.id, ctypes.byref(tile_count), ctypes.byref(voxel_count))
3056
+ return tile_count.value
3057
+
3058
+ def get_tiles(self, out: Optional[array] = None) -> array:
3059
+ """Returns the integer coordinates of all allocated tiles for this volume.
3060
+
3061
+ Args:
3062
+ out (:class:`warp.array`, optional): If provided, use the `out` array to store the tile coordinates, otherwise
3063
+ a new array will be allocated. `out` must be a contiguous array of ``tile_count`` ``vec3i`` or ``tile_count x 3`` ``int32``
3064
+ on the same device as this volume.
3065
+ """
3066
+
3067
+ if self.id == 0:
3068
+ raise RuntimeError("Invalid Volume")
3069
+
3070
+ tile_count = self.get_tile_count()
3071
+ if out is None:
3072
+ out = warp.empty(dtype=int32, shape=(tile_count, 3), device=self.device)
3073
+ elif out.device != self.device or out.shape[0] < tile_count:
3074
+ raise RuntimeError(f"'out' array must an array with at least {tile_count} rows on device {self.device}")
3075
+ elif not _is_contiguous_vec_like_array(out, vec_length=3, scalar_types=(int32,)):
3076
+ raise RuntimeError(
3077
+ "'out' must be a contiguous 1D array with type vec3i or a 2D array of type int32 with shape (N, 3) "
3078
+ )
3079
+
3069
3080
  if self.device.is_cpu:
3070
- self.runtime.core.volume_get_buffer_info_host(self.id, ctypes.byref(buf), ctypes.byref(size))
3081
+ self.runtime.core.volume_get_tiles_host(self.id, out.ptr)
3071
3082
  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)
3083
+ self.runtime.core.volume_get_tiles_device(self.id, out.ptr)
3084
+
3085
+ return out
3086
+
3087
+ def get_voxel_count(self) -> int:
3088
+ """Returns the total number of allocated voxels for this volume"""
3089
+
3090
+ voxel_count, tile_count = (
3091
+ ctypes.c_uint64(0),
3092
+ ctypes.c_uint32(0),
3093
+ )
3094
+ self.runtime.core.volume_get_tile_and_voxel_count(self.id, ctypes.byref(tile_count), ctypes.byref(voxel_count))
3095
+ return voxel_count.value
3096
+
3097
+ def get_voxels(self, out: Optional[array] = None) -> array:
3098
+ """Returns the integer coordinates of all allocated voxels for this volume.
3099
+
3100
+ Args:
3101
+ out (:class:`warp.array`, optional): If provided, use the `out` array to store the voxel coordinates, otherwise
3102
+ a new array will be allocated. `out` must be a contiguous array of ``voxel_count`` ``vec3i`` or ``voxel_count x 3`` ``int32``
3103
+ on the same device as this volume.
3104
+ """
3074
3105
 
3075
- def get_tiles(self) -> array:
3076
3106
  if self.id == 0:
3077
3107
  raise RuntimeError("Invalid Volume")
3078
3108
 
3079
- buf = ctypes.c_void_p(0)
3080
- size = ctypes.c_uint64(0)
3109
+ voxel_count = self.get_voxel_count()
3110
+ if out is None:
3111
+ out = warp.empty(dtype=int32, shape=(voxel_count, 3), device=self.device)
3112
+ elif out.device != self.device or out.shape[0] < voxel_count:
3113
+ raise RuntimeError(f"'out' array must an array with at least {voxel_count} rows on device {self.device}")
3114
+ elif not _is_contiguous_vec_like_array(out, vec_length=3, scalar_types=(int32,)):
3115
+ raise RuntimeError(
3116
+ "'out' must be a contiguous 1D array with type vec3i or a 2D array of type int32 with shape (N, 3) "
3117
+ )
3118
+
3081
3119
  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
3120
+ self.runtime.core.volume_get_voxels_host(self.id, out.ptr)
3084
3121
  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)
3122
+ self.runtime.core.volume_get_voxels_device(self.id, out.ptr)
3091
3123
 
3092
- return array(ptr=buf.value, dtype=int32, shape=(num_tiles, 3), device=self.device, deleter=deleter)
3124
+ return out
3093
3125
 
3094
3126
  def get_voxel_size(self) -> Tuple[float, float, float]:
3127
+ """Voxel size, i.e, world coordinates of voxel's diagonal vector"""
3128
+
3095
3129
  if self.id == 0:
3096
3130
  raise RuntimeError("Invalid Volume")
3097
3131
 
@@ -3099,9 +3133,181 @@ class Volume:
3099
3133
  self.runtime.core.volume_get_voxel_size(self.id, ctypes.byref(dx), ctypes.byref(dy), ctypes.byref(dz))
3100
3134
  return (dx.value, dy.value, dz.value)
3101
3135
 
3136
+ class GridInfo(NamedTuple):
3137
+ """Grid metadata"""
3138
+
3139
+ name: str
3140
+ """Grid name"""
3141
+ size_in_bytes: int
3142
+ """Size of this grid's data, in bytes"""
3143
+
3144
+ grid_index: int
3145
+ """Index of this grid in the data buffer"""
3146
+ grid_count: int
3147
+ """Total number of grids in the data buffer"""
3148
+ type_str: str
3149
+ """String describing the type of the grid values"""
3150
+
3151
+ translation: vec3f
3152
+ """Index-to-world translation"""
3153
+ transform_matrix: mat33f
3154
+ """Linear part of the index-to-world transform"""
3155
+
3156
+ def get_grid_info(self) -> Volume.GridInfo:
3157
+ """Returns the metadata associated with this Volume"""
3158
+
3159
+ grid_index = ctypes.c_uint32(0)
3160
+ grid_count = ctypes.c_uint32(0)
3161
+ grid_size = ctypes.c_uint64(0)
3162
+ translation_buffer = (ctypes.c_float * 3)()
3163
+ transform_buffer = (ctypes.c_float * 9)()
3164
+ type_str_buffer = (ctypes.c_char * 16)()
3165
+
3166
+ name = self.runtime.core.volume_get_grid_info(
3167
+ self.id,
3168
+ ctypes.byref(grid_size),
3169
+ ctypes.byref(grid_index),
3170
+ ctypes.byref(grid_count),
3171
+ translation_buffer,
3172
+ transform_buffer,
3173
+ type_str_buffer,
3174
+ )
3175
+
3176
+ if name is None:
3177
+ raise RuntimeError("Invalid volume")
3178
+
3179
+ return Volume.GridInfo(
3180
+ name.decode("ascii"),
3181
+ grid_size.value,
3182
+ grid_index.value,
3183
+ grid_count.value,
3184
+ type_str_buffer.value.decode("ascii"),
3185
+ vec3f.from_buffer_copy(translation_buffer),
3186
+ mat33f.from_buffer_copy(transform_buffer),
3187
+ )
3188
+
3189
+ _nvdb_type_to_dtype = {
3190
+ "float": float32,
3191
+ "double": float64,
3192
+ "int16": int16,
3193
+ "int32": int32,
3194
+ "int64": int64,
3195
+ "Vec3f": vec3f,
3196
+ "Vec3d": vec3d,
3197
+ "Half": float16,
3198
+ "uint32": uint32,
3199
+ "bool": bool,
3200
+ "Vec4f": vec4f,
3201
+ "Vec4d": vec4d,
3202
+ "Vec3u8": vec3ub,
3203
+ "Vec3u16": vec3us,
3204
+ "uint8": uint8,
3205
+ }
3206
+
3207
+ @property
3208
+ def dtype(self) -> type:
3209
+ """Type of the Volume's values as a Warp type.
3210
+
3211
+ If the grid does not contain values (e.g. index grids) or if the NanoVDB type is not
3212
+ representable as a Warp type, returns ``None``.
3213
+ """
3214
+ return Volume._nvdb_type_to_dtype.get(self.get_grid_info().type_str, None)
3215
+
3216
+ _nvdb_index_types = ("Index", "OnIndex", "IndexMask", "OnIndexMask")
3217
+
3218
+ @property
3219
+ def is_index(self) -> bool:
3220
+ """Whether this Volume contains an index grid, that is, a type of grid that does
3221
+ not explicitly store values but associates each voxel to linearized index.
3222
+ """
3223
+
3224
+ return self.get_grid_info().type_str in Volume._nvdb_index_types
3225
+
3226
+ def get_feature_array_count(self) -> int:
3227
+ """Returns the number of supplemental data arrays stored alongside the grid"""
3228
+
3229
+ return self.runtime.core.volume_get_blind_data_count(self.id)
3230
+
3231
+ class FeatureArrayInfo(NamedTuple):
3232
+ """Metadata for a supplemental data array"""
3233
+
3234
+ name: str
3235
+ """Name of the data array"""
3236
+ ptr: int
3237
+ """Memory address of the start of the array"""
3238
+
3239
+ value_size: int
3240
+ """Size in bytes of the array values"""
3241
+ value_count: int
3242
+ """Number of values in the array"""
3243
+ type_str: str
3244
+ """String describing the type of the array values"""
3245
+
3246
+ def get_feature_array_info(self, feature_index: int) -> Volume.FeatureArrayInfo:
3247
+ """Returns the metadata associated to the feature array at `feature_index`"""
3248
+
3249
+ buf = ctypes.c_void_p(0)
3250
+ value_count = ctypes.c_uint64(0)
3251
+ value_size = ctypes.c_uint32(0)
3252
+ type_str_buffer = (ctypes.c_char * 16)()
3253
+
3254
+ name = self.runtime.core.volume_get_blind_data_info(
3255
+ self.id,
3256
+ feature_index,
3257
+ ctypes.byref(buf),
3258
+ ctypes.byref(value_count),
3259
+ ctypes.byref(value_size),
3260
+ type_str_buffer,
3261
+ )
3262
+
3263
+ if buf.value is None:
3264
+ raise RuntimeError("Invalid feature array")
3265
+
3266
+ return Volume.FeatureArrayInfo(
3267
+ name.decode("ascii"),
3268
+ buf.value,
3269
+ value_size.value,
3270
+ value_count.value,
3271
+ type_str_buffer.value.decode("ascii"),
3272
+ )
3273
+
3274
+ def feature_array(self, feature_index: int, dtype=None) -> array:
3275
+ """Returns one the the grid's feature data arrays as a Warp array
3276
+
3277
+ Args:
3278
+ feature_index: Index of the supplemental data array in the grid
3279
+ dtype: Type for the returned Warp array. If not provided, will be deduced from the array metadata.
3280
+ """
3281
+
3282
+ info = self.get_feature_array_info(feature_index)
3283
+
3284
+ if dtype is None:
3285
+ try:
3286
+ dtype = Volume._nvdb_type_to_dtype[info.type_str]
3287
+ except KeyError:
3288
+ # Unknown type, default to byte array
3289
+ dtype = uint8
3290
+
3291
+ value_count = info.value_count
3292
+ value_size = info.value_size
3293
+
3294
+ if type_size_in_bytes(dtype) == 1:
3295
+ # allow requesting a byte array from any type
3296
+ value_count *= value_size
3297
+ value_size = 1
3298
+ elif value_size == 1 and (value_count % type_size_in_bytes(dtype)) == 0:
3299
+ # allow converting a byte array to any type
3300
+ value_size = type_size_in_bytes(dtype)
3301
+ value_count = value_count // value_size
3302
+
3303
+ if type_size_in_bytes(dtype) != value_size:
3304
+ raise RuntimeError(f"Cannot cast feature data of size {value_size} to array dtype {type_repr(dtype)}")
3305
+
3306
+ return array(ptr=info.ptr, dtype=dtype, shape=value_count, device=self.device, owner=False)
3307
+
3102
3308
  @classmethod
3103
3309
  def load_from_nvdb(cls, file_or_buffer, device=None) -> Volume:
3104
- """Creates a Volume object from a NanoVDB file or in-memory buffer.
3310
+ """Creates a Volume object from a serialized NanoVDB file or in-memory buffer.
3105
3311
 
3106
3312
  Returns:
3107
3313
 
@@ -3113,28 +3319,117 @@ class Volume:
3113
3319
  data = file_or_buffer
3114
3320
 
3115
3321
  magic, version, grid_count, codec = struct.unpack("<QIHH", data[0:16])
3116
- if magic != 0x304244566F6E614E:
3322
+ if magic not in (0x304244566F6E614E, 0x324244566F6E614E): # NanoVDB0 or NanoVDB2 in hex, little-endian
3117
3323
  raise RuntimeError("NanoVDB signature not found")
3118
3324
  if version >> 21 != 32: # checking major version
3119
3325
  raise RuntimeError("Unsupported NanoVDB version")
3120
- if grid_count != 1:
3121
- raise RuntimeError("Only NVDBs with exactly one grid are supported")
3122
3326
 
3123
- grid_data_offset = 192 + struct.unpack("<I", data[152:156])[0]
3327
+ # Skip over segment metadata, store total payload size
3328
+ grid_data_offset = 16 # sizeof(FileHeader)
3329
+ tot_file_size = 0
3330
+ for _ in range(grid_count):
3331
+ grid_file_size = struct.unpack("<Q", data[grid_data_offset + 8 : grid_data_offset + 16])[0]
3332
+ tot_file_size += grid_file_size
3333
+
3334
+ grid_name_size = struct.unpack("<I", data[grid_data_offset + 136 : grid_data_offset + 140])[0]
3335
+ grid_data_offset += 176 + grid_name_size # sizeof(FileMetadata) + grid name
3336
+
3337
+ file_end = grid_data_offset + tot_file_size
3338
+
3124
3339
  if codec == 0: # no compression
3125
- grid_data = data[grid_data_offset:]
3340
+ grid_data = data[grid_data_offset:file_end]
3126
3341
  elif codec == 1: # zip compression
3127
- grid_data = zlib.decompress(data[grid_data_offset + 8 :])
3342
+ grid_data = bytearray()
3343
+ while grid_data_offset < file_end:
3344
+ chunk_size = struct.unpack("<Q", data[grid_data_offset : grid_data_offset + 8])[0]
3345
+ grid_data += zlib.decompress(data[grid_data_offset + 8 :])
3346
+ grid_data_offset += 8 + chunk_size
3347
+
3348
+ elif codec == 2: # blosc compression
3349
+ try:
3350
+ import blosc
3351
+ except ImportError as err:
3352
+ raise RuntimeError(
3353
+ f"NanoVDB buffer is compressed using blosc, but Python module could not be imported: {err}"
3354
+ ) from err
3355
+
3356
+ grid_data = bytearray()
3357
+ while grid_data_offset < file_end:
3358
+ chunk_size = struct.unpack("<Q", data[grid_data_offset : grid_data_offset + 8])[0]
3359
+ grid_data += blosc.decompress(data[grid_data_offset + 8 :])
3360
+ grid_data_offset += 8 + chunk_size
3128
3361
  else:
3129
3362
  raise RuntimeError(f"Unsupported codec code: {codec}")
3130
3363
 
3131
3364
  magic = struct.unpack("<Q", grid_data[0:8])[0]
3132
- if magic != 0x304244566F6E614E:
3365
+ if magic not in (0x304244566F6E614E, 0x314244566F6E614E): # NanoVDB0 or NanoVDB1 in hex, little-endian
3133
3366
  raise RuntimeError("NanoVDB signature not found on grid!")
3134
3367
 
3135
3368
  data_array = array(np.frombuffer(grid_data, dtype=np.byte), device=device)
3136
3369
  return cls(data_array)
3137
3370
 
3371
+ @classmethod
3372
+ def load_from_address(cls, grid_ptr: int, buffer_size: int = 0, device=None) -> Volume:
3373
+ """
3374
+ Creates a new :class:`Volume` aliasing an in-memory grid buffer.
3375
+
3376
+ In contrast to :meth:`load_from_nvdb` which should be used to load serialized NanoVDB grids,
3377
+ here the buffer must be uncompressed and must not contain file header information.
3378
+ If the passed address does not contain a NanoVDB grid, the behavior of this function is undefined.
3379
+
3380
+ Args:
3381
+ grid_ptr: Integer address of the start of the grid buffer
3382
+ 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`.
3383
+ device: Device of the buffer, and of the returned Volume. If not provided, the current Warp device is assumed.
3384
+
3385
+ Returns the newly created Volume.
3386
+ """
3387
+
3388
+ if not grid_ptr:
3389
+ raise (RuntimeError, "Invalid grid buffer pointer")
3390
+
3391
+ # Check that a Volume has not already been created for this address
3392
+ # (to allow this we would need to ref-count the volume descriptor)
3393
+ existing_buf = ctypes.c_void_p(0)
3394
+ existing_size = ctypes.c_uint64(0)
3395
+ warp.context.runtime.core.volume_get_buffer_info(
3396
+ grid_ptr, ctypes.byref(existing_buf), ctypes.byref(existing_size)
3397
+ )
3398
+
3399
+ if existing_buf.value is not None:
3400
+ raise RuntimeError(
3401
+ "A warp Volume has already been created for this grid, aliasing it more than once is not possible."
3402
+ )
3403
+
3404
+ data_array = array(ptr=grid_ptr, dtype=uint8, shape=buffer_size, owner=False, device=device)
3405
+
3406
+ return cls(data_array, copy=False)
3407
+
3408
+ def load_next_grid(self) -> Volume:
3409
+ """
3410
+ Tries to create a new warp Volume for the next grid that is linked to by this Volume.
3411
+
3412
+ The existence of a next grid is deduced from the `grid_index` and `grid_count` metadata
3413
+ as well as the size of this Volume's in-memory buffer.
3414
+
3415
+ Returns the newly created Volume, or None if there is no next grid.
3416
+ """
3417
+
3418
+ grid = self.get_grid_info()
3419
+
3420
+ array = self.array()
3421
+
3422
+ if grid.grid_index + 1 >= grid.grid_count or array.capacity <= grid.size_in_bytes:
3423
+ return None
3424
+
3425
+ next_volume = Volume.load_from_address(
3426
+ array.ptr + grid.size_in_bytes, buffer_size=array.capacity - grid.size_in_bytes, device=self.device
3427
+ )
3428
+ # makes the new Volume keep a reference to the current grid, as we're aliasing its buffer
3429
+ next_volume._previous_grid = self
3430
+
3431
+ return next_volume
3432
+
3138
3433
  @classmethod
3139
3434
  def load_from_numpy(
3140
3435
  cls, ndarray: np.array, min_world=(0.0, 0.0, 0.0), voxel_size=1.0, bg_value=0.0, device=None
@@ -3286,11 +3581,11 @@ class Volume:
3286
3581
 
3287
3582
  Args:
3288
3583
  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.
3584
+ 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,
3585
+ 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
3586
  Repeated points per tile are allowed and will be efficiently deduplicated.
3292
3587
  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
3588
+ 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
3589
  translation (array-like): Translation between the index and world spaces.
3295
3590
  device (Devicelike): The CUDA device to create the volume on, e.g.: "cuda" or "cuda:0".
3296
3591
 
@@ -3301,19 +3596,28 @@ class Volume:
3301
3596
  raise RuntimeError(f"Voxel size must be positive! Got {voxel_size}")
3302
3597
  if not device.is_cuda:
3303
3598
  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!")
3599
+ if not _is_contiguous_vec_like_array(tile_points, vec_length=3, scalar_types=(float32, int32)):
3600
+ raise RuntimeError(
3601
+ "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."
3602
+ )
3310
3603
  if not tile_points.device.is_cuda:
3311
- tile_points = array(tile_points, dtype=tile_points.dtype, device=device)
3604
+ tile_points = tile_points.to(device)
3312
3605
 
3313
3606
  volume = cls(data=None)
3314
3607
  volume.device = device
3315
- in_world_space = tile_points.dtype == vec3
3316
- if hasattr(bg_value, "__len__"):
3608
+ in_world_space = type_scalar_type(tile_points.dtype) == float32
3609
+ if bg_value is None:
3610
+ volume.id = volume.runtime.core.volume_index_from_tiles_device(
3611
+ volume.device.context,
3612
+ ctypes.c_void_p(tile_points.ptr),
3613
+ tile_points.shape[0],
3614
+ voxel_size,
3615
+ translation[0],
3616
+ translation[1],
3617
+ translation[2],
3618
+ in_world_space,
3619
+ )
3620
+ elif hasattr(bg_value, "__len__"):
3317
3621
  volume.id = volume.runtime.core.volume_v_from_tiles_device(
3318
3622
  volume.device.context,
3319
3623
  ctypes.c_void_p(tile_points.ptr),
@@ -3357,6 +3661,73 @@ class Volume:
3357
3661
 
3358
3662
  return volume
3359
3663
 
3664
+ @classmethod
3665
+ def allocate_by_voxels(
3666
+ cls, voxel_points: array, voxel_size: float, translation=(0.0, 0.0, 0.0), device=None
3667
+ ) -> Volume:
3668
+ """Allocate a new Volume with active voxel for each point voxel_points.
3669
+
3670
+ This function creates an *index* Volume, a special kind of volume that does not any store any
3671
+ explicit payload but encodes a linearized index for each active voxel, allowing to lookup and
3672
+ sample data from arbitrary external arrays.
3673
+
3674
+ This function is only supported for CUDA devices.
3675
+
3676
+ Args:
3677
+ voxel_points (:class:`warp.array`): Array of positions that define the voxels to be allocated.
3678
+ 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,
3679
+ 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.
3680
+ Repeated points per tile are allowed and will be efficiently deduplicated.
3681
+ voxel_size (float): Voxel size of the new volume.
3682
+ translation (array-like): Translation between the index and world spaces.
3683
+ device (Devicelike): The CUDA device to create the volume on, e.g.: "cuda" or "cuda:0".
3684
+
3685
+ """
3686
+ device = warp.get_device(device)
3687
+
3688
+ if voxel_size <= 0.0:
3689
+ raise RuntimeError(f"Voxel size must be positive! Got {voxel_size}")
3690
+ if not device.is_cuda:
3691
+ raise RuntimeError("Only CUDA devices are supported for allocate_by_tiles")
3692
+ if not (is_array(voxel_points) and voxel_points.is_contiguous):
3693
+ raise RuntimeError("tile_points must be a contiguous array")
3694
+ if not _is_contiguous_vec_like_array(voxel_points, vec_length=3, scalar_types=(float32, int32)):
3695
+ raise RuntimeError(
3696
+ "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."
3697
+ )
3698
+ if not voxel_points.device.is_cuda:
3699
+ voxel_points = voxel_points.to(device)
3700
+
3701
+ volume = cls(data=None)
3702
+ volume.device = device
3703
+ in_world_space = type_scalar_type(voxel_points.dtype) == float32
3704
+
3705
+ volume.id = volume.runtime.core.volume_from_active_voxels_device(
3706
+ volume.device.context,
3707
+ ctypes.c_void_p(voxel_points.ptr),
3708
+ voxel_points.shape[0],
3709
+ voxel_size,
3710
+ translation[0],
3711
+ translation[1],
3712
+ translation[2],
3713
+ in_world_space,
3714
+ )
3715
+
3716
+ if volume.id == 0:
3717
+ raise RuntimeError("Failed to create volume")
3718
+
3719
+ return volume
3720
+
3721
+
3722
+ def _is_contiguous_vec_like_array(array, vec_length: int, scalar_types: Tuple[type]) -> bool:
3723
+ if not (is_array(array) and array.is_contiguous):
3724
+ return False
3725
+ if type_scalar_type(array.dtype) not in scalar_types:
3726
+ return False
3727
+ return (array.ndim == 1 and type_length(array.dtype) == vec_length) or (
3728
+ array.ndim == 2 and array.shape[1] == vec_length and type_length(array.dtype) == 1
3729
+ )
3730
+
3360
3731
 
3361
3732
  # definition just for kernel type (cannot be a parameter), see mesh.h
3362
3733
  # NOTE: its layout must match the corresponding struct defined in C.
@@ -4185,6 +4556,36 @@ class HashGrid:
4185
4556
 
4186
4557
  class MarchingCubes:
4187
4558
  def __init__(self, nx: int, ny: int, nz: int, max_verts: int, max_tris: int, device=None):
4559
+ """CUDA-based Marching Cubes algorithm to extract a 2D surface mesh from a 3D volume.
4560
+
4561
+ Attributes:
4562
+ id: Unique identifier for this object.
4563
+ verts (:class:`warp.array`): Array of vertex positions of type :class:`warp.vec3f`
4564
+ for the output surface mesh.
4565
+ This is populated after running :func:`surface`.
4566
+ indices (:class:`warp.array`): Array containing indices of type :class:`warp.int32`
4567
+ defining triangles for the output surface mesh.
4568
+ This is populated after running :func:`surface`.
4569
+
4570
+ Each set of three consecutive integers in the array represents a single triangle,
4571
+ in which each integer is an index referring to a vertex in the :attr:`verts` array.
4572
+
4573
+ Args:
4574
+ nx: Number of cubes in the x-direction.
4575
+ ny: Number of cubes in the y-direction.
4576
+ nz: Number of cubes in the z-direction.
4577
+ max_verts: Maximum expected number of vertices (used for array preallocation).
4578
+ max_tris: Maximum expected number of triangles (used for array preallocation).
4579
+ device (Devicelike): CUDA device on which to run marching cubes and allocate memory.
4580
+
4581
+ Raises:
4582
+ RuntimeError: ``device`` not a CUDA device.
4583
+
4584
+ .. note::
4585
+ The shape of the marching cubes should match the shape of the scalar field being surfaced.
4586
+
4587
+ """
4588
+
4188
4589
  self.id = 0
4189
4590
 
4190
4591
  self.runtime = warp.context.runtime
@@ -4210,7 +4611,7 @@ class MarchingCubes:
4210
4611
  from warp.context import zeros
4211
4612
 
4212
4613
  self.verts = zeros(max_verts, dtype=vec3, device=self.device)
4213
- self.indices = zeros(max_tris * 3, dtype=int, device=self.device)
4614
+ self.indices = zeros(max_tris * 3, dtype=warp.int32, device=self.device)
4214
4615
 
4215
4616
  # alloc surfacer
4216
4617
  self.id = ctypes.c_uint64(self.alloc(self.device.context))
@@ -4224,7 +4625,19 @@ class MarchingCubes:
4224
4625
  # destroy surfacer
4225
4626
  self.free(self.id)
4226
4627
 
4227
- def resize(self, nx: int, ny: int, nz: int, max_verts: int, max_tris: int):
4628
+ def resize(self, nx: int, ny: int, nz: int, max_verts: int, max_tris: int) -> None:
4629
+ """Update the expected input and maximum output sizes for the marching cubes calculation.
4630
+
4631
+ This function has no immediate effect on the underlying buffers.
4632
+ The new values take effect on the next :func:`surface` call.
4633
+
4634
+ Args:
4635
+ nx: Number of cubes in the x-direction.
4636
+ ny: Number of cubes in the y-direction.
4637
+ nz: Number of cubes in the z-direction.
4638
+ max_verts: Maximum expected number of vertices (used for array preallocation).
4639
+ max_tris: Maximum expected number of triangles (used for array preallocation).
4640
+ """
4228
4641
  # actual allocations will be resized on next call to surface()
4229
4642
  self.nx = nx
4230
4643
  self.ny = ny
@@ -4232,13 +4645,37 @@ class MarchingCubes:
4232
4645
  self.max_verts = max_verts
4233
4646
  self.max_tris = max_tris
4234
4647
 
4235
- def surface(self, field: array(dtype=float), threshold: float):
4648
+ def surface(self, field: array(dtype=float, ndim=3), threshold: float) -> None:
4649
+ """Compute a 2D surface mesh of a given isosurface from a 3D scalar field.
4650
+
4651
+ The triangles and vertices defining the output mesh are written to the
4652
+ :attr:`indices` and :attr:`verts` arrays.
4653
+
4654
+ Args:
4655
+ field: Scalar field from which to generate a mesh.
4656
+ threshold: Target isosurface value.
4657
+
4658
+ Raises:
4659
+ ValueError: ``field`` is not a 3D array.
4660
+ ValueError: Marching cubes shape does not match the shape of ``field``.
4661
+ RuntimeError: :attr:`max_verts` and/or :attr:`max_tris` might be too small to hold the surface mesh.
4662
+ """
4663
+
4236
4664
  # 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
4665
  num_verts = ctypes.c_int(0)
4238
4666
  num_tris = ctypes.c_int(0)
4239
4667
 
4240
4668
  self.runtime.core.marching_cubes_surface_device.restype = ctypes.c_int
4241
4669
 
4670
+ # For now we require that input field shape matches nx, ny, nz
4671
+ if field.ndim != 3:
4672
+ raise ValueError(f"Input field must be a three-dimensional array (got {field.ndim}).")
4673
+ if field.shape[0] != self.nx or field.shape[1] != self.ny or field.shape[2] != self.nz:
4674
+ raise ValueError(
4675
+ f"Marching cubes shape ({self.nx}, {self.ny}, {self.nz}) does not match the "
4676
+ f"input array shape {field.shape}."
4677
+ )
4678
+
4242
4679
  error = self.runtime.core.marching_cubes_surface_device(
4243
4680
  self.id,
4244
4681
  ctypes.cast(field.ptr, ctypes.c_void_p),
@@ -4362,7 +4799,7 @@ def infer_argument_types(args, template_types, arg_names=None):
4362
4799
  arg_types.append(arg_type(dtype=arg.dtype, ndim=arg.ndim))
4363
4800
  elif arg_type in warp.types.scalar_and_bool_types:
4364
4801
  arg_types.append(arg_type)
4365
- elif arg_type in (int, float):
4802
+ elif arg_type in (int, float, builtins.bool):
4366
4803
  # canonicalize type
4367
4804
  arg_types.append(warp.types.type_to_warp(arg_type))
4368
4805
  elif hasattr(arg_type, "_wp_scalar_type_"):