warp-lang 1.8.1__py3-none-macosx_10_13_universal2.whl → 1.9.1__py3-none-macosx_10_13_universal2.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 (141) hide show
  1. warp/__init__.py +282 -103
  2. warp/__init__.pyi +1904 -114
  3. warp/bin/libwarp-clang.dylib +0 -0
  4. warp/bin/libwarp.dylib +0 -0
  5. warp/build.py +93 -30
  6. warp/build_dll.py +331 -101
  7. warp/builtins.py +1244 -160
  8. warp/codegen.py +317 -206
  9. warp/config.py +1 -1
  10. warp/context.py +1465 -789
  11. warp/examples/core/example_marching_cubes.py +1 -0
  12. warp/examples/core/example_render_opengl.py +100 -3
  13. warp/examples/fem/example_apic_fluid.py +98 -52
  14. warp/examples/fem/example_convection_diffusion_dg.py +25 -4
  15. warp/examples/fem/example_diffusion_mgpu.py +8 -3
  16. warp/examples/fem/utils.py +68 -22
  17. warp/examples/interop/example_jax_kernel.py +2 -1
  18. warp/fabric.py +1 -1
  19. warp/fem/cache.py +27 -19
  20. warp/fem/domain.py +2 -2
  21. warp/fem/field/nodal_field.py +2 -2
  22. warp/fem/field/virtual.py +264 -166
  23. warp/fem/geometry/geometry.py +5 -5
  24. warp/fem/integrate.py +129 -51
  25. warp/fem/space/restriction.py +4 -0
  26. warp/fem/space/shape/tet_shape_function.py +3 -10
  27. warp/jax_experimental/custom_call.py +25 -2
  28. warp/jax_experimental/ffi.py +22 -1
  29. warp/jax_experimental/xla_ffi.py +16 -7
  30. warp/marching_cubes.py +708 -0
  31. warp/native/array.h +99 -4
  32. warp/native/builtin.h +86 -9
  33. warp/native/bvh.cpp +64 -28
  34. warp/native/bvh.cu +58 -58
  35. warp/native/bvh.h +2 -2
  36. warp/native/clang/clang.cpp +7 -7
  37. warp/native/coloring.cpp +8 -2
  38. warp/native/crt.cpp +2 -2
  39. warp/native/crt.h +3 -5
  40. warp/native/cuda_util.cpp +41 -10
  41. warp/native/cuda_util.h +10 -4
  42. warp/native/exports.h +1842 -1908
  43. warp/native/fabric.h +2 -1
  44. warp/native/hashgrid.cpp +37 -37
  45. warp/native/hashgrid.cu +2 -2
  46. warp/native/initializer_array.h +1 -1
  47. warp/native/intersect.h +2 -2
  48. warp/native/mat.h +1910 -116
  49. warp/native/mathdx.cpp +43 -43
  50. warp/native/mesh.cpp +24 -24
  51. warp/native/mesh.cu +26 -26
  52. warp/native/mesh.h +4 -2
  53. warp/native/nanovdb/GridHandle.h +179 -12
  54. warp/native/nanovdb/HostBuffer.h +8 -7
  55. warp/native/nanovdb/NanoVDB.h +517 -895
  56. warp/native/nanovdb/NodeManager.h +323 -0
  57. warp/native/nanovdb/PNanoVDB.h +2 -2
  58. warp/native/quat.h +331 -14
  59. warp/native/range.h +7 -1
  60. warp/native/reduce.cpp +10 -10
  61. warp/native/reduce.cu +13 -14
  62. warp/native/runlength_encode.cpp +2 -2
  63. warp/native/runlength_encode.cu +5 -5
  64. warp/native/scan.cpp +3 -3
  65. warp/native/scan.cu +4 -4
  66. warp/native/sort.cpp +10 -10
  67. warp/native/sort.cu +40 -31
  68. warp/native/sort.h +2 -0
  69. warp/native/sparse.cpp +8 -8
  70. warp/native/sparse.cu +13 -13
  71. warp/native/spatial.h +366 -17
  72. warp/native/temp_buffer.h +2 -2
  73. warp/native/tile.h +471 -82
  74. warp/native/vec.h +328 -14
  75. warp/native/volume.cpp +54 -54
  76. warp/native/volume.cu +1 -1
  77. warp/native/volume.h +2 -1
  78. warp/native/volume_builder.cu +30 -37
  79. warp/native/warp.cpp +150 -149
  80. warp/native/warp.cu +377 -216
  81. warp/native/warp.h +227 -226
  82. warp/optim/linear.py +736 -271
  83. warp/render/imgui_manager.py +289 -0
  84. warp/render/render_opengl.py +99 -18
  85. warp/render/render_usd.py +1 -0
  86. warp/sim/graph_coloring.py +2 -2
  87. warp/sparse.py +558 -175
  88. warp/tests/aux_test_module_aot.py +7 -0
  89. warp/tests/cuda/test_async.py +3 -3
  90. warp/tests/cuda/test_conditional_captures.py +101 -0
  91. warp/tests/geometry/test_hash_grid.py +38 -0
  92. warp/tests/geometry/test_marching_cubes.py +233 -12
  93. warp/tests/interop/test_jax.py +608 -28
  94. warp/tests/sim/test_coloring.py +6 -6
  95. warp/tests/test_array.py +58 -5
  96. warp/tests/test_codegen.py +4 -3
  97. warp/tests/test_context.py +8 -15
  98. warp/tests/test_enum.py +136 -0
  99. warp/tests/test_examples.py +2 -2
  100. warp/tests/test_fem.py +49 -6
  101. warp/tests/test_fixedarray.py +229 -0
  102. warp/tests/test_func.py +18 -15
  103. warp/tests/test_future_annotations.py +7 -5
  104. warp/tests/test_linear_solvers.py +30 -0
  105. warp/tests/test_map.py +15 -1
  106. warp/tests/test_mat.py +1518 -378
  107. warp/tests/test_mat_assign_copy.py +178 -0
  108. warp/tests/test_mat_constructors.py +574 -0
  109. warp/tests/test_module_aot.py +287 -0
  110. warp/tests/test_print.py +69 -0
  111. warp/tests/test_quat.py +140 -34
  112. warp/tests/test_quat_assign_copy.py +145 -0
  113. warp/tests/test_reload.py +2 -1
  114. warp/tests/test_sparse.py +71 -0
  115. warp/tests/test_spatial.py +140 -34
  116. warp/tests/test_spatial_assign_copy.py +160 -0
  117. warp/tests/test_struct.py +43 -3
  118. warp/tests/test_tuple.py +96 -0
  119. warp/tests/test_types.py +61 -20
  120. warp/tests/test_vec.py +179 -34
  121. warp/tests/test_vec_assign_copy.py +143 -0
  122. warp/tests/tile/test_tile.py +245 -18
  123. warp/tests/tile/test_tile_cholesky.py +605 -0
  124. warp/tests/tile/test_tile_load.py +169 -0
  125. warp/tests/tile/test_tile_mathdx.py +2 -558
  126. warp/tests/tile/test_tile_matmul.py +1 -1
  127. warp/tests/tile/test_tile_mlp.py +1 -1
  128. warp/tests/tile/test_tile_shared_memory.py +5 -5
  129. warp/tests/unittest_suites.py +6 -0
  130. warp/tests/walkthrough_debug.py +1 -1
  131. warp/thirdparty/unittest_parallel.py +108 -9
  132. warp/types.py +571 -267
  133. warp/utils.py +68 -86
  134. {warp_lang-1.8.1.dist-info → warp_lang-1.9.1.dist-info}/METADATA +29 -69
  135. {warp_lang-1.8.1.dist-info → warp_lang-1.9.1.dist-info}/RECORD +138 -128
  136. warp/native/marching.cpp +0 -19
  137. warp/native/marching.cu +0 -514
  138. warp/native/marching.h +0 -19
  139. {warp_lang-1.8.1.dist-info → warp_lang-1.9.1.dist-info}/WHEEL +0 -0
  140. {warp_lang-1.8.1.dist-info → warp_lang-1.9.1.dist-info}/licenses/LICENSE.md +0 -0
  141. {warp_lang-1.8.1.dist-info → warp_lang-1.9.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,7 @@
1
+ import warp as wp
2
+
3
+
4
+ @wp.kernel
5
+ def add_kernel(a: wp.array(dtype=wp.int32), b: wp.array(dtype=wp.int32), res: wp.array(dtype=wp.int32)):
6
+ i = wp.tid()
7
+ res[i] = a[i] + b[i]
@@ -426,13 +426,13 @@ def copy_template(test, src_ctor, dst_ctor, src_device, dst_device, n, params: C
426
426
  if expected_error_type is not None:
427
427
  # disable error output from Warp if we expect an exception
428
428
  try:
429
- saved_error_output_enabled = wp.context.runtime.core.is_error_output_enabled()
430
- wp.context.runtime.core.set_error_output_enabled(False)
429
+ saved_error_output_enabled = wp.context.runtime.core.wp_is_error_output_enabled()
430
+ wp.context.runtime.core.wp_set_error_output_enabled(False)
431
431
  with test.assertRaisesRegex(expected_error_type, expected_error_regex):
432
432
  with Capturable(use_graph=params.use_graph, stream=stream):
433
433
  wp.copy(dst, src, stream=stream_arg)
434
434
  finally:
435
- wp.context.runtime.core.set_error_output_enabled(saved_error_output_enabled)
435
+ wp.context.runtime.core.wp_set_error_output_enabled(saved_error_output_enabled)
436
436
  wp.synchronize()
437
437
 
438
438
  # print(f"SUCCESSFUL ERROR PREDICTION: {expected_error_regex}")
@@ -979,6 +979,94 @@ def test_graph_debug_dot_print(test, device):
979
979
  os.remove(dot_file)
980
980
 
981
981
 
982
+ # ================================================================================================================
983
+ # test exceptions
984
+ # ================================================================================================================
985
+
986
+
987
+ # body graphs with allocations are not supported
988
+ def body_with_alloc():
989
+ wp.zeros(10)
990
+
991
+
992
+ @unittest.skipUnless(check_conditional_graph_support(), "Conditional graph nodes not supported")
993
+ def test_error_alloc_if(test, device):
994
+ with wp.ScopedDevice(device):
995
+ cond = wp.ones(1, dtype=wp.int32)
996
+ with test.assertRaisesRegex(
997
+ RuntimeError, r"Conditional body graph contains an unsupported operation \(memory allocation\)"
998
+ ):
999
+ with wp.ScopedCapture():
1000
+ wp.capture_if(condition=cond, on_true=body_with_alloc)
1001
+
1002
+
1003
+ @unittest.skipUnless(check_conditional_graph_support(), "Conditional graph nodes not supported")
1004
+ def test_error_alloc_else(test, device):
1005
+ with wp.ScopedDevice(device):
1006
+ cond = wp.ones(1, dtype=wp.int32)
1007
+ with test.assertRaisesRegex(
1008
+ RuntimeError, r"Conditional body graph contains an unsupported operation \(memory allocation\)"
1009
+ ):
1010
+ with wp.ScopedCapture():
1011
+ wp.capture_if(condition=cond, on_false=body_with_alloc)
1012
+
1013
+
1014
+ @unittest.skipUnless(check_conditional_graph_support(), "Conditional graph nodes not supported")
1015
+ def test_error_alloc_while(test, device):
1016
+ with wp.ScopedDevice(device):
1017
+ cond = wp.ones(1, dtype=wp.int32)
1018
+ with test.assertRaisesRegex(
1019
+ RuntimeError, r"Conditional body graph contains an unsupported operation \(memory allocation\)"
1020
+ ):
1021
+ with wp.ScopedCapture():
1022
+ wp.capture_while(condition=cond, while_body=body_with_alloc)
1023
+
1024
+
1025
+ @unittest.skipUnless(check_conditional_graph_support(), "Conditional graph nodes not supported")
1026
+ def test_error_alloc_if_subgraph(test, device):
1027
+ with wp.ScopedDevice(device):
1028
+ # capture body subgraph
1029
+ with wp.ScopedCapture() as body_capture:
1030
+ body_with_alloc()
1031
+
1032
+ cond = wp.ones(1, dtype=wp.int32)
1033
+ with test.assertRaisesRegex(
1034
+ RuntimeError, r"Child graph contains an unsupported operation \(memory allocation\)"
1035
+ ):
1036
+ with wp.ScopedCapture():
1037
+ wp.capture_if(condition=cond, on_true=body_capture.graph)
1038
+
1039
+
1040
+ @unittest.skipUnless(check_conditional_graph_support(), "Conditional graph nodes not supported")
1041
+ def test_error_alloc_else_subgraph(test, device):
1042
+ with wp.ScopedDevice(device):
1043
+ # capture body subgraph
1044
+ with wp.ScopedCapture() as body_capture:
1045
+ body_with_alloc()
1046
+
1047
+ cond = wp.ones(1, dtype=wp.int32)
1048
+ with test.assertRaisesRegex(
1049
+ RuntimeError, r"Child graph contains an unsupported operation \(memory allocation\)"
1050
+ ):
1051
+ with wp.ScopedCapture():
1052
+ wp.capture_if(condition=cond, on_false=body_capture.graph)
1053
+
1054
+
1055
+ @unittest.skipUnless(check_conditional_graph_support(), "Conditional graph nodes not supported")
1056
+ def test_error_alloc_while_subgraph(test, device):
1057
+ with wp.ScopedDevice(device):
1058
+ # capture body subgraph
1059
+ with wp.ScopedCapture() as body_capture:
1060
+ body_with_alloc()
1061
+
1062
+ cond = wp.ones(1, dtype=wp.int32)
1063
+ with test.assertRaisesRegex(
1064
+ RuntimeError, r"Child graph contains an unsupported operation \(memory allocation\)"
1065
+ ):
1066
+ with wp.ScopedCapture():
1067
+ wp.capture_while(condition=cond, while_body=body_capture.graph)
1068
+
1069
+
982
1070
  devices = get_test_devices()
983
1071
  cuda_devices = get_cuda_test_devices()
984
1072
 
@@ -1040,6 +1128,19 @@ add_function_test(
1040
1128
  TestConditionalCaptures, "test_graph_debug_dot_print", test_graph_debug_dot_print, devices=cuda_devices
1041
1129
  )
1042
1130
 
1131
+ add_function_test(TestConditionalCaptures, "test_error_alloc_if", test_error_alloc_if, devices=cuda_devices)
1132
+ add_function_test(TestConditionalCaptures, "test_error_alloc_else", test_error_alloc_else, devices=cuda_devices)
1133
+ add_function_test(TestConditionalCaptures, "test_error_alloc_while", test_error_alloc_while, devices=cuda_devices)
1134
+ add_function_test(
1135
+ TestConditionalCaptures, "test_error_alloc_if_subgraph", test_error_alloc_if_subgraph, devices=cuda_devices
1136
+ )
1137
+ add_function_test(
1138
+ TestConditionalCaptures, "test_error_alloc_else_subgraph", test_error_alloc_else_subgraph, devices=cuda_devices
1139
+ )
1140
+ add_function_test(
1141
+ TestConditionalCaptures, "test_error_alloc_while_subgraph", test_error_alloc_while_subgraph, devices=cuda_devices
1142
+ )
1143
+
1043
1144
 
1044
1145
  if __name__ == "__main__":
1045
1146
  wp.clear_kernel_cache()
@@ -191,7 +191,44 @@ def test_hashgrid_inputs(test, device):
191
191
  assert_array_equal(counts_ndim, counts_ref)
192
192
 
193
193
 
194
+ def test_hashgrid_multiple_streams(test, device):
195
+ with wp.ScopedDevice(device):
196
+ points = particle_grid(16, 32, 16, (0.0, 0.3, 0.0), cell_radius * 0.25, 0.1)
197
+ points_ref = wp.array(points, dtype=wp.vec3)
198
+ counts_ref = wp.zeros(len(points), dtype=int)
199
+
200
+ grid_dim = 64
201
+ grid_ref = wp.HashGrid(grid_dim, grid_dim, grid_dim)
202
+ grid_ref.build(points_ref, cell_radius)
203
+
204
+ # get reference counts
205
+ wp.launch(kernel=count_neighbors, dim=len(points), inputs=[grid_ref.id, query_radius, points_ref, counts_ref])
206
+
207
+ # create multiple streams
208
+ num_streams = 10
209
+ streams = [wp.Stream(device=device) for _ in range(num_streams)]
210
+ counts_per_stream = [wp.zeros(len(points), dtype=int) for _ in range(num_streams)]
211
+
212
+ # test whether HashGrid and radix sort work with multiple streams without race conditions
213
+ for i in range(num_streams):
214
+ with wp.ScopedStream(streams[i]):
215
+ grid = wp.HashGrid(grid_dim, grid_dim, grid_dim)
216
+ grid.build(points_ref, cell_radius)
217
+
218
+ # get counts for this stream
219
+ wp.launch(
220
+ kernel=count_neighbors,
221
+ dim=len(points),
222
+ inputs=[grid.id, query_radius, points_ref, counts_per_stream[i]],
223
+ )
224
+
225
+ # run this loop after all streams are scheduled to ensure asynchronous behaviour above
226
+ for i in range(num_streams):
227
+ assert_array_equal(counts_per_stream[i], counts_ref)
228
+
229
+
194
230
  devices = get_test_devices()
231
+ cuda_devices = get_cuda_test_devices()
195
232
 
196
233
 
197
234
  class TestHashGrid(unittest.TestCase):
@@ -214,6 +251,7 @@ class TestHashGrid(unittest.TestCase):
214
251
 
215
252
  add_function_test(TestHashGrid, "test_hashgrid_query", test_hashgrid_query, devices=devices)
216
253
  add_function_test(TestHashGrid, "test_hashgrid_inputs", test_hashgrid_inputs, devices=devices)
254
+ add_function_test(TestHashGrid, "test_hashgrid_multiple_streams", test_hashgrid_multiple_streams, devices=cuda_devices)
217
255
 
218
256
 
219
257
  if __name__ == "__main__":
@@ -22,7 +22,9 @@ from warp.tests.unittest_utils import *
22
22
 
23
23
 
24
24
  @wp.kernel
25
- def make_field(field: wp.array3d(dtype=float), center: wp.vec3, radius: float):
25
+ def make_field_sphere_sdf(field: wp.array3d(dtype=float), center: wp.vec3, radius: float):
26
+ """Make a sphere SDF for nodes on the integer domain with node coordinates 0,1,2,3,..."""
27
+
26
28
  i, j, k = wp.tid()
27
29
 
28
30
  p = wp.vec3(float(i), float(j), float(k))
@@ -32,31 +34,242 @@ def make_field(field: wp.array3d(dtype=float), center: wp.vec3, radius: float):
32
34
  field[i, j, k] = d
33
35
 
34
36
 
35
- def test_marching_cubes(test, device):
36
- dim = 64
37
- max_verts = 10**6
38
- max_tris = 10**6
37
+ @wp.kernel
38
+ def make_field_sphere_sdf_unit_domain(
39
+ field: wp.array3d(dtype=float), center: wp.vec3, radius: wp.array(dtype=wp.float32)
40
+ ):
41
+ """Makes a sphere SDF for nodes on the unit domain [-1, 1]^3."""
42
+ i, j, k = wp.tid()
39
43
 
40
- field = wp.zeros(shape=(dim, dim, dim), dtype=float, device=device)
44
+ nx, ny, nz = field.shape[0], field.shape[1], field.shape[2]
45
+
46
+ p = wp.vec3(
47
+ 2.0 * wp.float32(i) / (wp.float32(nx) - 1.0) - 1.0,
48
+ 2.0 * wp.float32(j) / (wp.float32(ny) - 1.0) - 1.0,
49
+ 2.0 * wp.float32(k) / (wp.float32(nz) - 1.0) - 1.0,
50
+ )
51
+
52
+ d = wp.length(p - center) - radius[0]
53
+
54
+ field[i, j, k] = d
55
+
56
+
57
+ @wp.kernel
58
+ def compute_surface_area(
59
+ verts: wp.array(dtype=wp.vec3), faces: wp.array(dtype=wp.int32), out_area: wp.array(dtype=wp.float32)
60
+ ):
61
+ tid = wp.tid()
62
+ vi = faces[3 * tid + 0]
63
+ vj = faces[3 * tid + 1]
64
+ vk = faces[3 * tid + 2]
65
+
66
+ p0 = verts[vi]
67
+ p1 = verts[vj]
68
+ p2 = verts[vk]
69
+
70
+ # Heron's formula for triangle area
71
+ a = wp.length(p1 - p0)
72
+ b = wp.length(p2 - p0)
73
+ c = wp.length(p2 - p1)
74
+ s = (a + b + c) / 2.0
75
+ area = wp.sqrt(s * (s - a) * (s - b) * (s - c))
41
76
 
42
- iso = wp.MarchingCubes(nx=dim, ny=dim, nz=dim, max_verts=max_verts, max_tris=max_tris, device=device)
77
+ wp.atomic_add(out_area, 0, area)
43
78
 
44
- radius = dim / 4.0
45
79
 
46
- wp.launch(make_field, dim=field.shape, inputs=[field, wp.vec3(dim / 2, dim / 2, dim / 2), radius], device=device)
80
+ def validate_marching_cubes_output(test, verts_np, faces_np, check_nonempty=True):
81
+ # check that the face array seems valid
82
+ if check_nonempty:
83
+ test.assertGreater(faces_np.shape[0], 0) # at least one face
84
+ test.assertEqual(faces_np.shape[1], 3) # all faces triangular
85
+ test.assertTrue((faces_np >= 0).all()) # all face inds nonnegative
86
+ test.assertTrue((faces_np < verts_np.shape[0]).all()) # all face inds are in-bounds on the vertex array
87
+ test.assertTrue((faces_np[:, 0] != faces_np[:, 1]).all()) # all faces have unique vertices
88
+ test.assertTrue((faces_np[:, 0] != faces_np[:, 2]).all()) # all faces have unique vertices
89
+ test.assertTrue((faces_np[:, 1] != faces_np[:, 2]).all()) # all faces have unique vertices
90
+ test.assertTrue(
91
+ (np.unique(faces_np.flatten()) == np.arange(verts_np.shape[0])).all()
92
+ ) # all vertices are used in at least one face
93
+
94
+ # check that the vertex array seems valid
95
+ if check_nonempty:
96
+ test.assertGreater(verts_np.shape[0], 0) # at least one vertex
97
+ test.assertEqual(verts_np.shape[1], 3) # all vertices are 3D
98
+ test.assertTrue(np.isfinite(verts_np).all()) # all vertices are finite
99
+
100
+
101
+ def test_marching_cubes(test, device):
102
+ """Basic test of typical usage."""
103
+ node_dim = 64
104
+ cell_dim = node_dim - 1
105
+ field = wp.zeros(shape=(node_dim, node_dim, node_dim), dtype=float, device=device)
106
+ bounds_low = (0.0, 0.0, 0.0)
107
+ bounds_high = (float(cell_dim), float(cell_dim), float(cell_dim))
108
+
109
+ iso = wp.MarchingCubes(
110
+ nx=node_dim,
111
+ ny=node_dim,
112
+ nz=node_dim,
113
+ device=device,
114
+ domain_bounds_lower_corner=bounds_low,
115
+ domain_bounds_upper_corner=bounds_high,
116
+ )
117
+
118
+ radius = node_dim / 4.0
119
+
120
+ wp.launch(
121
+ make_field_sphere_sdf,
122
+ dim=field.shape,
123
+ inputs=[field, wp.vec3(node_dim / 2, node_dim / 2, node_dim / 2), radius],
124
+ device=device,
125
+ )
47
126
 
48
127
  iso.surface(field=field, threshold=0.0)
128
+ verts_np = iso.verts.numpy()
129
+ faces_np = iso.indices.numpy().reshape(-1, 3)
130
+ test.assertEqual(
131
+ iso.indices.dtype, wp.int32
132
+ ) # make sure we are following Warp convention of using a flat array of indices
133
+ validate_marching_cubes_output(test, verts_np, faces_np)
49
134
 
50
135
  # check that all returned vertices lie on the surface of the sphere
51
- length = np.linalg.norm(iso.verts.numpy() - np.array([dim / 2, dim / 2, dim / 2]), axis=1)
136
+ length = np.linalg.norm(verts_np - np.array([node_dim / 2, node_dim / 2, node_dim / 2]), axis=1)
52
137
  error = np.abs(length - radius)
138
+ test.assertTrue(np.max(error) < 1.0)
139
+
140
+ iso.resize(nx=node_dim * 2, ny=node_dim * 2, nz=node_dim * 2) # smoke test for deprecated function
141
+
142
+
143
+ def test_marching_cubes_functional(test, device):
144
+ """Ensure the single-function interface works as expected."""
145
+ node_dim = 64
146
+ cell_dim = node_dim - 1
147
+ field = wp.zeros(shape=(node_dim, node_dim, node_dim), dtype=float, device=device)
148
+ bounds_low = (0.0, 0.0, 0.0)
149
+ bounds_high = (float(cell_dim), float(cell_dim), float(cell_dim))
150
+
151
+ radius = node_dim / 4.0
152
+ wp.launch(
153
+ make_field_sphere_sdf,
154
+ dim=field.shape,
155
+ inputs=[field, wp.vec3(node_dim / 2, node_dim / 2, node_dim / 2), radius],
156
+ device=device,
157
+ )
158
+
159
+ # call via the functional interface
160
+ verts, faces = wp.MarchingCubes.extract_surface_marching_cubes(
161
+ field, threshold=0.0, domain_bounds_lower_corner=bounds_low, domain_bounds_upper_corner=bounds_high
162
+ )
163
+
164
+ verts_np = verts.numpy()
165
+ faces_np = faces.numpy().reshape(-1, 3)
166
+ validate_marching_cubes_output(test, verts_np, faces_np)
53
167
 
168
+ # check that all returned vertices lie on the surface of the sphere
169
+ length = np.linalg.norm(verts_np - np.array([node_dim / 2, node_dim / 2, node_dim / 2]), axis=1)
170
+ error = np.abs(length - radius)
54
171
  test.assertTrue(np.max(error) < 1.0)
55
172
 
56
- iso.resize(nx=dim * 2, ny=dim * 2, nz=dim * 2, max_verts=max_verts, max_tris=max_tris)
173
+
174
+ def test_marching_cubes_nonuniform(test, device):
175
+ """Test the logic for when the dimensions of the grid are not uniform."""
176
+
177
+ dimX = 64
178
+ dimY = 48
179
+ dimZ = 72
180
+ field = wp.zeros(shape=(dimX, dimY, dimZ), dtype=float, device=device)
181
+
182
+ bounds_low = wp.vec3(0.0, 0.0, 0.0)
183
+ bounds_high = wp.vec3(float(dimX), float(dimY), float(dimZ))
184
+
185
+ iso = wp.MarchingCubes(
186
+ nx=dimX,
187
+ ny=dimY,
188
+ nz=dimZ,
189
+ device=device,
190
+ domain_bounds_lower_corner=bounds_low,
191
+ domain_bounds_upper_corner=bounds_high,
192
+ )
193
+
194
+ radius = dimX / 4.0
195
+ wp.launch(
196
+ make_field_sphere_sdf,
197
+ dim=field.shape,
198
+ inputs=[field, wp.vec3(dimX / 2, dimY / 2, dimZ / 2), radius],
199
+ device=device,
200
+ )
201
+
202
+ iso.surface(field=field, threshold=0.0)
203
+ verts_np = iso.verts.numpy()
204
+ faces_np = iso.indices.numpy().reshape(-1, 3)
205
+ validate_marching_cubes_output(test, verts_np, faces_np)
206
+
207
+
208
+ def test_marching_cubes_empty_output(test, device):
209
+ """Make sure we handle the empty-output case correctly."""
210
+
211
+ dim = 64
212
+ field = wp.zeros(shape=(dim, dim, dim), dtype=float, device=device)
213
+
214
+ iso = wp.MarchingCubes(nx=dim, ny=dim, nz=dim, device=device)
215
+
216
+ wp.launch(make_field_sphere_sdf, dim=field.shape, inputs=[field, wp.vec3(0.5, 0.5, 0.5), 0.25], device=device)
217
+
218
+ iso.surface(field=field, threshold=1000.0) # set threshold to a large value so that no vertices are generated
219
+ verts_np = iso.verts.numpy()
220
+ faces_np = iso.indices.numpy().reshape(-1, 3)
221
+ validate_marching_cubes_output(test, verts_np, faces_np, check_nonempty=False)
222
+
223
+ test.assertEqual(faces_np.shape[0], 0) # no faces
224
+ test.assertEqual(verts_np.shape[0], 0) # no vertices
225
+
226
+
227
+ def test_marching_cubes_differentiable(test, device):
228
+ """Check that marching cubes has reasonable gradients.
229
+
230
+ This test constructs an SDF of sphere, extracts a surface, computes its
231
+ surface area, and then differentiates the surface area with respect to
232
+ the sphere's radius.
233
+ """
234
+ node_dim = 64
235
+ cell_dim = node_dim - 1
236
+ bounds_low = wp.vec3(-1.0, -1.0, -1.0)
237
+ bounds_high = wp.vec3(1.0, 1.0, 1.0)
238
+
239
+ radius = 0.5
240
+ radius_wp = wp.full((1,), value=0.5, dtype=wp.float32, device=device, requires_grad=True)
241
+
242
+ with wp.Tape() as tape:
243
+ field = wp.zeros(shape=(node_dim, node_dim, node_dim), dtype=float, device=device, requires_grad=True)
244
+ wp.launch(
245
+ make_field_sphere_sdf_unit_domain,
246
+ dim=field.shape,
247
+ inputs=[field, wp.vec3(0.0, 0.0, 0.0), radius_wp],
248
+ device=device,
249
+ )
250
+
251
+ # call via the functional interface
252
+ verts, faces = wp.MarchingCubes.extract_surface_marching_cubes(
253
+ field, threshold=0.0, domain_bounds_lower_corner=bounds_low, domain_bounds_upper_corner=bounds_high
254
+ )
255
+
256
+ # compute surface area
257
+ area = wp.zeros(shape=(1,), dtype=float, device=device, requires_grad=True)
258
+ wp.launch(compute_surface_area, dim=faces.shape[0] // 3, inputs=[verts, faces, area], device=device)
259
+
260
+ # confirm surface area is correct vs. the analytical ground truth
261
+ area_np = area.numpy()[0]
262
+ test.assertTrue(np.abs(area_np - 4.0 * np.pi * radius * radius) < 1e-2)
263
+
264
+ # compute the gradient of the surface area with respect to the radius
265
+ tape.backward(area)
266
+
267
+ # confirm the gradient is correct vs. the analytical ground truth
268
+ grad_np = radius_wp.grad.numpy()[0]
269
+ test.assertTrue(np.abs(grad_np - 8.0 * np.pi * radius) < 1e-2)
57
270
 
58
271
 
59
- devices = get_selected_cuda_test_devices()
272
+ devices = get_test_devices()
60
273
 
61
274
 
62
275
  class TestMarchingCubes(unittest.TestCase):
@@ -67,6 +280,14 @@ class TestMarchingCubes(unittest.TestCase):
67
280
 
68
281
 
69
282
  add_function_test(TestMarchingCubes, "test_marching_cubes", test_marching_cubes, devices=devices)
283
+ add_function_test(TestMarchingCubes, "test_marching_cubes_functional", test_marching_cubes_functional, devices=devices)
284
+ add_function_test(TestMarchingCubes, "test_marching_cubes_nonuniform", test_marching_cubes_nonuniform, devices=devices)
285
+ add_function_test(
286
+ TestMarchingCubes, "test_marching_cubes_empty_output", test_marching_cubes_empty_output, devices=devices
287
+ )
288
+ add_function_test(
289
+ TestMarchingCubes, "test_marching_cubes_differentiable", test_marching_cubes_differentiable, devices=devices
290
+ )
70
291
 
71
292
 
72
293
  if __name__ == "__main__":