warp-lang 1.3.1__py3-none-win_amd64.whl → 1.3.3__py3-none-win_amd64.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.

warp/tests/test_bvh.py CHANGED
@@ -45,19 +45,19 @@ def aabb_overlap(a_lower, a_upper, b_lower, b_upper):
45
45
  return 1
46
46
 
47
47
 
48
- def intersect_ray_aabb(start, dir, lower, upper):
49
- l1 = (lower[0] - start[0]) * dir[0]
50
- l2 = (upper[0] - start[0]) * dir[0]
48
+ def intersect_ray_aabb(start, rcp_dir, lower, upper):
49
+ l1 = (lower[0] - start[0]) * rcp_dir[0]
50
+ l2 = (upper[0] - start[0]) * rcp_dir[0]
51
51
  lmin = min(l1, l2)
52
52
  lmax = max(l1, l2)
53
53
 
54
- l1 = (lower[1] - start[1]) * dir[1]
55
- l2 = (upper[1] - start[1]) * dir[1]
54
+ l1 = (lower[1] - start[1]) * rcp_dir[1]
55
+ l2 = (upper[1] - start[1]) * rcp_dir[1]
56
56
  lmin = max(min(l1, l2), lmin)
57
57
  lmax = min(max(l1, l2), lmax)
58
58
 
59
- l1 = (lower[2] - start[2]) * dir[2]
60
- l2 = (upper[2] - start[2]) * dir[2]
59
+ l1 = (lower[2] - start[2]) * rcp_dir[2]
60
+ l2 = (upper[2] - start[2]) * rcp_dir[2]
61
61
  lmin = max(min(l1, l2), lmin)
62
62
  lmax = min(max(l1, l2), lmax)
63
63
 
@@ -108,7 +108,7 @@ def test_bvh(test, type, device):
108
108
  if type == "AABB":
109
109
  host_intersected = aabb_overlap(lower, upper, query_lower, query_upper)
110
110
  else:
111
- host_intersected = intersect_ray_aabb(query_start, query_dir, lower, upper)
111
+ host_intersected = intersect_ray_aabb(query_start, 1.0 / query_dir, lower, upper)
112
112
 
113
113
  test.assertEqual(host_intersected, device_intersected[i])
114
114
 
@@ -129,6 +129,30 @@ def test_bvh_query_ray(test, device):
129
129
  test_bvh(test, "ray", device)
130
130
 
131
131
 
132
+ def test_gh_288(test, device):
133
+ num_bounds = 1
134
+ lowers = ((0.5, -1.0, -1.0),) * num_bounds
135
+ uppers = ((1.0, 1.0, 1.0),) * num_bounds
136
+
137
+ device_lowers = wp.array(lowers, dtype=wp.vec3f, device=device)
138
+ device_uppers = wp.array(uppers, dtype=wp.vec3f, device=device)
139
+
140
+ bvh = wp.Bvh(device_lowers, device_uppers)
141
+
142
+ bounds_intersected = wp.zeros(shape=num_bounds, dtype=int, device=device)
143
+
144
+ for x in (0.0, 0.75):
145
+ query_start = wp.vec3(x, 0.0, 0.0)
146
+ query_dir = wp.vec3(1.0, 0.0, 0.0)
147
+
148
+ wp.launch(
149
+ kernel=bvh_query_ray, dim=1, inputs=[bvh.id, query_start, query_dir, bounds_intersected], device=device
150
+ )
151
+
152
+ device_intersected = bounds_intersected.numpy()
153
+ test.assertEqual(device_intersected.sum(), num_bounds)
154
+
155
+
132
156
  devices = get_test_devices()
133
157
 
134
158
 
@@ -161,6 +185,7 @@ class TestBvh(unittest.TestCase):
161
185
 
162
186
  add_function_test(TestBvh, "test_bvh_aabb", test_bvh_query_aabb, devices=devices)
163
187
  add_function_test(TestBvh, "test_bvh_ray", test_bvh_query_ray, devices=devices)
188
+ add_function_test(TestBvh, "test_gh_288", test_gh_288, devices=devices)
164
189
 
165
190
  if __name__ == "__main__":
166
191
  wp.clear_kernel_cache()
@@ -18,6 +18,8 @@ UNIT_VEC = wp.constant(wp.vec3(SQRT3_OVER_3, SQRT3_OVER_3, SQRT3_OVER_3))
18
18
  ONE_FP16 = wp.constant(wp.float16(1.0))
19
19
  TEST_BOOL = wp.constant(True)
20
20
 
21
+ SHADOWED_GLOBAL = wp.constant(17)
22
+
21
23
 
22
24
  class Foobar:
23
25
  ONE = wp.constant(1)
@@ -68,6 +70,18 @@ def test_closure_capture(test, device):
68
70
  wp.launch(two_closure, dim=(1), inputs=[2], device=device)
69
71
 
70
72
 
73
+ def test_closure_precedence(test, device):
74
+ """Verifies that closure constants take precedence over globals"""
75
+
76
+ SHADOWED_GLOBAL = wp.constant(42)
77
+
78
+ @wp.kernel
79
+ def closure_kernel():
80
+ wp.expect_eq(SHADOWED_GLOBAL, 42)
81
+
82
+ wp.launch(closure_kernel, dim=1, device=device)
83
+
84
+
71
85
  def test_hash_global_capture(test, device):
72
86
  """Verifies that global variables are included in the module hash"""
73
87
 
@@ -206,6 +220,7 @@ add_kernel_test(TestConstants, test_int, dim=1, inputs=[a], devices=devices)
206
220
  add_kernel_test(TestConstants, test_float, dim=1, inputs=[x], devices=devices)
207
221
 
208
222
  add_function_test(TestConstants, "test_closure_capture", test_closure_capture, devices=devices)
223
+ add_function_test(TestConstants, "test_closure_precedence", test_closure_precedence, devices=devices)
209
224
  add_function_test(TestConstants, "test_hash_global_capture", test_hash_global_capture, devices=devices)
210
225
  add_function_test(TestConstants, "test_hash_redefine_kernel", test_hash_redefine_kernel, devices=devices)
211
226
  add_function_test(TestConstants, "test_hash_redefine_constant_only", test_hash_redefine_constant_only, devices=devices)
@@ -250,7 +250,11 @@ add_example_test(
250
250
  test_options_cpu={"num_frames": 10},
251
251
  )
252
252
  add_example_test(
253
- TestOptimExamples, name="optim.example_cloth_throw", devices=test_devices, test_options_cpu={"train_iters": 3}
253
+ TestOptimExamples,
254
+ name="optim.example_cloth_throw",
255
+ devices=test_devices,
256
+ test_options={"test_timeout": 600},
257
+ test_options_cpu={"train_iters": 3},
254
258
  )
255
259
  add_example_test(
256
260
  TestOptimExamples,
@@ -390,6 +394,7 @@ add_example_test(
390
394
  name="fem.example_mixed_elasticity",
391
395
  devices=test_devices,
392
396
  test_options={"nonconforming_stresses": True, "mesh": "quad", "headless": True},
397
+ test_options_cpu={"test_timeout": 600},
393
398
  )
394
399
  add_example_test(
395
400
  TestFemExamples, name="fem.example_stokes_transfer", devices=test_devices, test_options={"headless": True}
warp/tests/test_fem.py CHANGED
@@ -1252,6 +1252,8 @@ def test_particle_quadratures(test, device):
1252
1252
  geo = fem.Grid2D(res=wp.vec2i(2))
1253
1253
 
1254
1254
  domain = fem.Cells(geo)
1255
+
1256
+ # Explicit quadrature
1255
1257
  points, weights = domain.reference_element().instantiate_quadrature(order=4, family=fem.Polynomial.GAUSS_LEGENDRE)
1256
1258
  points_per_cell = len(points)
1257
1259
 
@@ -1266,9 +1268,16 @@ def test_particle_quadratures(test, device):
1266
1268
  test.assertEqual(explicit_quadrature.max_points_per_element(), points_per_cell)
1267
1269
  test.assertEqual(explicit_quadrature.total_point_count(), points_per_cell * geo.cell_count())
1268
1270
 
1271
+ # test integration accuracy
1269
1272
  val = fem.integrate(_bicubic, quadrature=explicit_quadrature)
1270
1273
  test.assertAlmostEqual(val, 1.0 / 16, places=5)
1271
1274
 
1275
+ # test indexing validity
1276
+ arr = wp.empty(explicit_quadrature.total_point_count(), dtype=float)
1277
+ fem.interpolate(_piecewise_constant, dest=arr, quadrature=explicit_quadrature)
1278
+ assert_np_equal(arr.numpy(), np.arange(geo.cell_count()).repeat(points_per_cell))
1279
+
1280
+ # PIC quadrature
1272
1281
  element_indices = wp.array([3, 3, 2], dtype=int, device=device)
1273
1282
  element_coords = wp.array(
1274
1283
  [
@@ -1286,6 +1295,7 @@ def test_particle_quadratures(test, device):
1286
1295
  test.assertEqual(pic_quadrature.total_point_count(), 3)
1287
1296
  test.assertEqual(pic_quadrature.active_cell_count(), 2)
1288
1297
 
1298
+ # Test integration accuracy
1289
1299
  val = fem.integrate(_piecewise_constant, quadrature=pic_quadrature)
1290
1300
  test.assertAlmostEqual(val, 1.25, places=5)
1291
1301
 
@@ -1305,6 +1315,46 @@ def test_particle_quadratures(test, device):
1305
1315
  assert_np_equal(measures.grad.numpy(), np.full(3, 4.0)) # == 1.0 / cell_area
1306
1316
 
1307
1317
 
1318
+ @fem.integrand
1319
+ def _value_at_node(s: fem.Sample, f: fem.Field, values: wp.array(dtype=float)):
1320
+ node_index = fem.operator.node_partition_index(f, s.qp_index)
1321
+ return values[node_index]
1322
+
1323
+
1324
+ def test_nodal_quadrature(test, device):
1325
+ geo = fem.Grid2D(res=wp.vec2i(2))
1326
+
1327
+ domain = fem.Cells(geo)
1328
+
1329
+ space = fem.make_polynomial_space(geo, degree=2, discontinuous=True, family=fem.Polynomial.GAUSS_LEGENDRE)
1330
+ nodal_quadrature = fem.NodalQuadrature(domain, space)
1331
+
1332
+ test.assertEqual(nodal_quadrature.max_points_per_element(), 9)
1333
+ test.assertEqual(nodal_quadrature.total_point_count(), 9 * geo.cell_count())
1334
+
1335
+ val = fem.integrate(_bicubic, quadrature=nodal_quadrature)
1336
+ test.assertAlmostEqual(val, 1.0 / 16, places=5)
1337
+
1338
+ # test accessing data associated to a given node
1339
+
1340
+ piecewise_constant_space = fem.make_polynomial_space(geo, degree=0)
1341
+ geo_partition = fem.LinearGeometryPartition(geo, 3, 4)
1342
+ space_partition = fem.make_space_partition(piecewise_constant_space, geo_partition)
1343
+ field = fem.make_discrete_field(piecewise_constant_space, space_partition=space_partition)
1344
+
1345
+ partition_domain = fem.Cells(geo_partition)
1346
+ partition_nodal_quadrature = fem.NodalQuadrature(partition_domain, piecewise_constant_space)
1347
+
1348
+ partition_node_values = wp.array([5.0], dtype=float)
1349
+ val = fem.integrate(
1350
+ _value_at_node,
1351
+ quadrature=partition_nodal_quadrature,
1352
+ fields={"f": field},
1353
+ values={"values": partition_node_values},
1354
+ )
1355
+ test.assertAlmostEqual(val, 5.0 / geo.cell_count(), places=5)
1356
+
1357
+
1308
1358
  @wp.func
1309
1359
  def aniso_bicubic_fn(x: wp.vec2, scale: wp.vec2):
1310
1360
  return wp.pow(x[0] * scale[0], 3.0) * wp.pow(x[1] * scale[1], 3.0)
@@ -1485,6 +1535,7 @@ add_function_test(TestFem, "test_deformed_geometry", test_deformed_geometry, dev
1485
1535
  add_function_test(TestFem, "test_dof_mapper", test_dof_mapper)
1486
1536
  add_function_test(TestFem, "test_point_basis", test_point_basis)
1487
1537
  add_function_test(TestFem, "test_particle_quadratures", test_particle_quadratures)
1538
+ add_function_test(TestFem, "test_nodal_quadrature", test_nodal_quadrature)
1488
1539
  add_function_test(TestFem, "test_implicit_fields", test_implicit_fields)
1489
1540
  add_kernel_test(TestFem, test_qr_eigenvalues, dim=1, devices=devices)
1490
1541
  add_kernel_test(TestFem, test_qr_inverse, dim=100, devices=devices)
@@ -93,7 +93,8 @@ def test_gradcheck_3d(test, device):
93
93
  inputs=[a_3d, b_3d, c_3d],
94
94
  outputs=[out1_3d, out2_3d],
95
95
  max_inputs_per_var=4,
96
- input_output_mask=[("a", "out1"), ("b", "out2")],
96
+ # use integer indices instead of variable names
97
+ input_output_mask=[(0, 0), (1, 1)],
97
98
  eps=1e-4,
98
99
  )
99
100
 
warp/tests/test_model.py CHANGED
@@ -102,6 +102,61 @@ class TestModel(unittest.TestCase):
102
102
  assert_np_equal(np.array(builder1.edge_rest_angle), np.array(builder2.edge_rest_angle), tol=1.0e-4)
103
103
  assert_np_equal(np.array(builder1.edge_bending_properties), np.array(builder2.edge_bending_properties))
104
104
 
105
+ def test_collapse_fixed_joints(self):
106
+ def add_three_cubes(builder: ModelBuilder, parent_body=-1):
107
+ unit_cube = {"hx": 0.5, "hy": 0.5, "hz": 0.5, "density": 1.0}
108
+ b0 = builder.add_body()
109
+ builder.add_shape_box(body=b0, **unit_cube)
110
+ builder.add_joint_fixed(parent=parent_body, child=b0, parent_xform=wp.transform(wp.vec3(1.0, 0.0, 0.0)))
111
+ b1 = builder.add_body()
112
+ builder.add_shape_box(body=b1, **unit_cube)
113
+ builder.add_joint_fixed(parent=parent_body, child=b1, parent_xform=wp.transform(wp.vec3(0.0, 1.0, 0.0)))
114
+ b2 = builder.add_body()
115
+ builder.add_shape_box(body=b2, **unit_cube)
116
+ builder.add_joint_fixed(parent=parent_body, child=b2, parent_xform=wp.transform(wp.vec3(0.0, 0.0, 1.0)))
117
+ return b2
118
+
119
+ builder = ModelBuilder()
120
+ # only fixed joints
121
+ builder.add_articulation()
122
+ add_three_cubes(builder)
123
+ assert builder.joint_count == 3
124
+ assert builder.body_count == 3
125
+
126
+ # fixed joints followed by a non-fixed joint
127
+ builder.add_articulation()
128
+ last_body = add_three_cubes(builder)
129
+ assert builder.joint_count == 6
130
+ assert builder.body_count == 6
131
+ assert builder.articulation_count == 2
132
+ b3 = builder.add_body()
133
+ builder.add_shape_box(body=b3, hx=0.5, hy=0.5, hz=0.5, density=1.0, pos=wp.vec3(1.0, 2.0, 3.0))
134
+ builder.add_joint_revolute(parent=last_body, child=b3, axis=wp.vec3(0.0, 1.0, 0.0))
135
+
136
+ # a non-fixed joint followed by fixed joints
137
+ builder.add_articulation()
138
+ b4 = builder.add_body()
139
+ builder.add_shape_box(body=b4, hx=0.5, hy=0.5, hz=0.5, density=1.0)
140
+ builder.add_joint_free(parent=-1, child=b4, parent_xform=wp.transform(wp.vec3(0.0, -1.0, 0.0)))
141
+ assert builder.joint_count == 8
142
+ assert builder.body_count == 8
143
+ assert builder.articulation_count == 3
144
+ add_three_cubes(builder, parent_body=b4)
145
+
146
+ builder.collapse_fixed_joints()
147
+
148
+ assert builder.joint_count == 2
149
+ assert builder.articulation_count == 2
150
+ assert builder.articulation_start == [0, 1]
151
+ assert builder.joint_type == [wp.sim.JOINT_REVOLUTE, wp.sim.JOINT_FREE]
152
+ assert builder.shape_count == 11
153
+ assert builder.shape_body == [-1, -1, -1, -1, -1, -1, 0, 1, 1, 1, 1]
154
+ assert builder.body_count == 2
155
+ assert builder.body_com[0] == wp.vec3(1.0, 2.0, 3.0)
156
+ assert builder.body_com[1] == wp.vec3(0.25, 0.25, 0.25)
157
+ assert builder.body_mass == [1.0, 4.0]
158
+ assert builder.body_inv_mass == [1.0, 0.25]
159
+
105
160
 
106
161
  if __name__ == "__main__":
107
162
  wp.clear_kernel_cache()
@@ -0,0 +1,143 @@
1
+ # Copyright (c) 2024 NVIDIA CORPORATION. All rights reserved.
2
+ # NVIDIA CORPORATION and its licensors retain all intellectual property
3
+ # and proprietary rights in and to this software, related documentation
4
+ # and any modifications thereto. Any use, reproduction, disclosure or
5
+ # distribution of this software and related documentation without an express
6
+ # license agreement from NVIDIA CORPORATION is strictly prohibited.
7
+
8
+ import unittest
9
+
10
+ from warp.sim.collide import triangle_closest_point_barycentric
11
+ from warp.tests.unittest_utils import *
12
+
13
+
14
+ # a-b is the edge where the closest point is located at
15
+ @wp.func
16
+ def check_edge_feasible_region(p: wp.vec3, a: wp.vec3, b: wp.vec3, c: wp.vec3, eps: float):
17
+ ap = p - a
18
+ bp = p - b
19
+ ab = b - a
20
+
21
+ if wp.dot(ap, ab) < -eps:
22
+ return False
23
+
24
+ if wp.dot(bp, ab) > eps:
25
+ return False
26
+
27
+ ab_sqr_norm = wp.dot(ab, ab)
28
+ if ab_sqr_norm < eps:
29
+ return False
30
+
31
+ t = wp.dot(ab, c - a) / ab_sqr_norm
32
+
33
+ perpendicular_foot = a + t * ab
34
+
35
+ if wp.dot(c - perpendicular_foot, p - perpendicular_foot) > eps:
36
+ return False
37
+
38
+ return True
39
+
40
+
41
+ # closest point is a
42
+ @wp.func
43
+ def check_vertex_feasible_region(p: wp.vec3, a: wp.vec3, b: wp.vec3, c: wp.vec3, eps: float):
44
+ ap = p - a
45
+ ba = a - b
46
+ ca = a - c
47
+
48
+ if wp.dot(ap, ba) < -eps:
49
+ return False
50
+
51
+ if wp.dot(p, ca) < -eps:
52
+ return False
53
+
54
+ return True
55
+
56
+
57
+ @wp.func
58
+ def return_true():
59
+ return True
60
+
61
+
62
+ def test_point_triangle_closest_point(test, device):
63
+ passed = wp.array([True], dtype=wp.bool, device=device)
64
+
65
+ @wp.kernel
66
+ def test_point_triangle_closest_point_kernel(tri: wp.array(dtype=wp.vec3), passed: wp.array(dtype=wp.bool)):
67
+ state = wp.uint32(wp.rand_init(wp.int32(123), wp.int32(0)))
68
+ eps = 1e-5
69
+
70
+ for _i in range(1000):
71
+ l = wp.float32(0.0)
72
+ while l < eps:
73
+ p = wp.vec3(wp.randn(state), wp.randn(state), wp.randn(state))
74
+ l = wp.length(p)
75
+
76
+ # project to a sphere with r=2
77
+ p = 2.0 * p / l
78
+
79
+ bary = triangle_closest_point_barycentric(tri[0], tri[1], tri[2], p)
80
+
81
+ for dim in range(3):
82
+ v1_index = (dim + 1) % 3
83
+ v2_index = (dim + 2) % 3
84
+ v1 = tri[v1_index]
85
+ v2 = tri[v2_index]
86
+ v3 = tri[dim]
87
+
88
+ # on edge
89
+ if bary[dim] == 0.0 and bary[v1_index] != 0.0 and bary[v2_index] != 0.0:
90
+ if not check_edge_feasible_region(p, v1, v2, v3, eps):
91
+ passed[0] = False
92
+ return
93
+
94
+ # p-closest_p must be perpendicular to v1-v2
95
+ closest_p = a * bary[0] + b * bary[1] + c * bary[2]
96
+ e = v1 - v2
97
+ err = wp.dot(e, closest_p - p)
98
+ if wp.abs(err) > eps:
99
+ passed[0] = False
100
+ return
101
+
102
+ if bary[v1_index] == 0.0 and bary[v2_index] == 0.0:
103
+ if not check_vertex_feasible_region(p, v3, v1, v2, eps):
104
+ passed[0] = False
105
+ return
106
+
107
+ if bary[dim] != 0.0 and bary[v1_index] != 0.0 and bary[v2_index] != 0.0:
108
+ closest_p = a * bary[0] + b * bary[1] + c * bary[2]
109
+ e1 = v1 - v2
110
+ e2 = v1 - v3
111
+ if wp.abs(wp.dot(e1, closest_p - p)) > eps or wp.abs(wp.dot(e2, closest_p - p)) > eps:
112
+ passed[0] = False
113
+ return
114
+
115
+ a = wp.vec3(1.0, 0.0, 0.0)
116
+ b = wp.vec3(0.0, 0.0, 0.0)
117
+ c = wp.vec3(0.0, 1.0, 0.0)
118
+
119
+ tri = wp.array([a, b, c], dtype=wp.vec3, device=device)
120
+ wp.launch(test_point_triangle_closest_point_kernel, dim=1, inputs=[tri, passed], device=device)
121
+ passed = passed.numpy()
122
+
123
+ test.assertTrue(passed.all())
124
+
125
+
126
+ devices = get_test_devices()
127
+
128
+
129
+ class TestTriangleClosestPoint(unittest.TestCase):
130
+ pass
131
+
132
+
133
+ add_function_test(
134
+ TestTriangleClosestPoint,
135
+ "test_point_triangle_closest_point",
136
+ test_point_triangle_closest_point,
137
+ devices=devices,
138
+ check_output=True,
139
+ )
140
+
141
+ if __name__ == "__main__":
142
+ wp.clear_kernel_cache()
143
+ unittest.main(verbosity=2)
warp/tests/test_reload.py CHANGED
@@ -189,7 +189,32 @@ def test_reload_references(test, device):
189
189
  test_dependent.run(expect=4.0, device=device) # 2 * 2 = 4
190
190
 
191
191
 
192
+ def test_graph_launch_after_module_reload(test, device):
193
+ @wp.kernel
194
+ def foo(a: wp.array(dtype=int)):
195
+ a[0] = 42
196
+
197
+ with wp.ScopedDevice(device):
198
+ a = wp.zeros(1, dtype=int)
199
+
200
+ # preload module before graph capture
201
+ wp.load_module(device=device)
202
+
203
+ # capture a launch
204
+ with wp.ScopedCapture(force_module_load=False) as capture:
205
+ wp.launch(foo, dim=1, inputs=[a])
206
+
207
+ # unload the module
208
+ foo.module.unload()
209
+
210
+ # launch previously captured graph
211
+ wp.capture_launch(capture.graph)
212
+
213
+ test.assertEqual(a.numpy()[0], 42)
214
+
215
+
192
216
  devices = get_test_devices()
217
+ cuda_devices = get_cuda_test_devices()
193
218
 
194
219
 
195
220
  class TestReload(unittest.TestCase):
@@ -200,6 +225,9 @@ add_function_test(TestReload, "test_redefine", test_redefine, devices=devices)
200
225
  add_function_test(TestReload, "test_reload", test_reload, devices=devices)
201
226
  add_function_test(TestReload, "test_reload_class", test_reload_class, devices=devices)
202
227
  add_function_test(TestReload, "test_reload_references", test_reload_references, devices=devices)
228
+ add_function_test(
229
+ TestReload, "test_graph_launch_after_module_reload", test_graph_launch_after_module_reload, devices=cuda_devices
230
+ )
203
231
 
204
232
 
205
233
  if __name__ == "__main__":
warp/tests/test_struct.py CHANGED
@@ -589,6 +589,52 @@ def test_dependent_module_import(c: DependentModuleImport_C):
589
589
  wp.tid() # nop, we're just testing codegen
590
590
 
591
591
 
592
+ def test_struct_array_content_hash(test, device):
593
+ # Ensure that the memory address of the struct does not affect the content hash
594
+
595
+ @wp.struct
596
+ class ContentHashStruct:
597
+ i: int
598
+
599
+ @wp.kernel
600
+ def dummy_kernel(a: wp.array(dtype=ContentHashStruct)):
601
+ i = wp.tid()
602
+
603
+ module_hash_0 = wp.get_module(dummy_kernel.__module__).hash_module()
604
+
605
+ # Redefine ContentHashStruct to have the same members as before but a new memory address
606
+ @wp.struct
607
+ class ContentHashStruct:
608
+ i: int
609
+
610
+ @wp.kernel
611
+ def dummy_kernel(a: wp.array(dtype=ContentHashStruct)):
612
+ i = wp.tid()
613
+
614
+ module_hash_1 = wp.get_module(dummy_kernel.__module__).hash_module(recompute_content_hash=True)
615
+
616
+ test.assertEqual(
617
+ module_hash_1,
618
+ module_hash_0,
619
+ "Module hash should be unchanged when ContentHashStruct is redefined but unchanged.",
620
+ )
621
+
622
+ # Redefine ContentHashStruct to have different members. This time we should get a new hash.
623
+ @wp.struct
624
+ class ContentHashStruct:
625
+ i: float
626
+
627
+ @wp.kernel
628
+ def dummy_kernel(a: wp.array(dtype=ContentHashStruct)):
629
+ i = wp.tid()
630
+
631
+ module_hash_2 = wp.get_module(dummy_kernel.__module__).hash_module(recompute_content_hash=True)
632
+
633
+ test.assertNotEqual(
634
+ module_hash_2, module_hash_0, "Module hash should be different when ContentHashStruct redefined and changed."
635
+ )
636
+
637
+
592
638
  devices = get_test_devices()
593
639
 
594
640
 
@@ -636,36 +682,6 @@ add_kernel_test(
636
682
  devices=devices,
637
683
  )
638
684
 
639
- add_kernel_test(
640
- TestStruct,
641
- name="test_struct_mutate_attributes",
642
- kernel=test_struct_mutate_attributes_kernel,
643
- dim=1,
644
- inputs=[],
645
- devices=devices,
646
- )
647
- add_kernel_test(
648
- TestStruct,
649
- kernel=test_uninitialized,
650
- name="test_uninitialized",
651
- dim=1,
652
- inputs=[Uninitialized()],
653
- devices=devices,
654
- )
655
- add_kernel_test(TestStruct, kernel=test_return, name="test_return", dim=1, inputs=[], devices=devices)
656
- add_function_test(TestStruct, "test_nested_struct", test_nested_struct, devices=devices)
657
- add_function_test(TestStruct, "test_nested_array_struct", test_nested_array_struct, devices=devices)
658
- add_function_test(TestStruct, "test_nested_empty_struct", test_nested_empty_struct, devices=devices)
659
- add_function_test(TestStruct, "test_struct_math_conversions", test_struct_math_conversions, devices=devices)
660
- add_kernel_test(
661
- TestStruct,
662
- name="test_struct_default_attributes",
663
- kernel=test_struct_default_attributes_kernel,
664
- dim=1,
665
- inputs=[],
666
- devices=devices,
667
- )
668
-
669
685
  add_kernel_test(
670
686
  TestStruct,
671
687
  name="test_struct_mutate_attributes",
@@ -702,6 +718,8 @@ add_kernel_test(
702
718
  devices=devices,
703
719
  )
704
720
 
721
+ add_function_test(TestStruct, "test_struct_array_content_hash", test_struct_array_content_hash, devices=None)
722
+
705
723
 
706
724
  if __name__ == "__main__":
707
725
  wp.clear_kernel_cache()
warp/tests/test_volume.py CHANGED
@@ -843,6 +843,33 @@ def test_volume_from_numpy(test, device):
843
843
  test.assertIsNone(sphere_vdb_array.deleter)
844
844
 
845
845
 
846
+ def test_volume_from_numpy_3d(test, device):
847
+ # Volume.allocate_from_tiles() is only available with CUDA
848
+ mins = np.array([-3.0, -3.0, -3.0])
849
+ voxel_size = 0.2
850
+ maxs = np.array([3.0, 3.0, 3.0])
851
+ nums = np.ceil((maxs - mins) / (voxel_size)).astype(dtype=int)
852
+ centers = np.array([[-1.0, -1.0, -1.0], [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]])
853
+ rad = 2.5
854
+ sphere_sdf_np = np.zeros(tuple(nums) + (3,))
855
+ for x in range(nums[0]):
856
+ for y in range(nums[1]):
857
+ for z in range(nums[2]):
858
+ for k in range(3):
859
+ pos = mins + voxel_size * np.array([x, y, z])
860
+ dis = np.linalg.norm(pos - centers[k])
861
+ sphere_sdf_np[x, y, z, k] = dis - rad
862
+ sphere_vdb = wp.Volume.load_from_numpy(
863
+ sphere_sdf_np, mins, voxel_size, (rad + 3.0 * voxel_size,) * 3, device=device
864
+ )
865
+
866
+ test.assertNotEqual(sphere_vdb.id, 0)
867
+
868
+ sphere_vdb_array = sphere_vdb.array()
869
+ test.assertEqual(sphere_vdb_array.dtype, wp.uint8)
870
+ test.assertIsNone(sphere_vdb_array.deleter)
871
+
872
+
846
873
  def test_volume_aniso_transform(test, device):
847
874
  # XY-rotation + z scale
848
875
  transform = [
@@ -894,6 +921,9 @@ add_function_test(TestVolume, "test_volume_introspection", test_volume_introspec
894
921
  add_function_test(
895
922
  TestVolume, "test_volume_from_numpy", test_volume_from_numpy, devices=get_selected_cuda_test_devices()
896
923
  )
924
+ add_function_test(
925
+ TestVolume, "test_volume_from_numpy_3d", test_volume_from_numpy_3d, devices=get_selected_cuda_test_devices()
926
+ )
897
927
  add_function_test(
898
928
  TestVolume, "test_volume_aniso_transform", test_volume_aniso_transform, devices=get_selected_cuda_test_devices()
899
929
  )
warp/types.py CHANGED
@@ -1576,7 +1576,7 @@ class array(Array):
1576
1576
 
1577
1577
  Args:
1578
1578
  data (Union[list, tuple, ndarray]): An object to construct the array from, can be a Tuple, List, or generally any type convertible to an np.array
1579
- dtype (Union): One of the built-in types, e.g.: :class:`warp.mat33`, if dtype is Any and data an ndarray then it will be inferred from the array data type
1579
+ dtype (Union): One of the available `data types <#data-types>`_, such as :class:`warp.float32`, :class:`warp.mat33`, or a custom `struct <#structs>`_. If dtype is ``Any`` and data is an ndarray, then it will be inferred from the array data type
1580
1580
  shape (tuple): Dimensions of the array
1581
1581
  strides (tuple): Number of bytes in each dimension between successive elements of the array
1582
1582
  length (int): Number of elements of the data type (deprecated, users should use `shape` argument)
@@ -1601,6 +1601,9 @@ class array(Array):
1601
1601
  self._array_interface = None
1602
1602
  self.is_transposed = False
1603
1603
 
1604
+ # reference to other array
1605
+ self._ref = None
1606
+
1604
1607
  # canonicalize dtype
1605
1608
  if dtype == int:
1606
1609
  dtype = int32
@@ -1614,7 +1617,9 @@ class array(Array):
1614
1617
  if isinstance(shape, int):
1615
1618
  shape = (shape,)
1616
1619
  else:
1617
- shape = tuple(shape)
1620
+ # The type of shape's elements are eventually passed onto capacity which is used to allocate memory. We
1621
+ # explicitly enforce that shape is a tuple of (64-bit) ints to ensure that capacity is 64-bit.
1622
+ shape = tuple(int(x) for x in shape)
1618
1623
  if len(shape) > ARRAY_MAX_DIMS:
1619
1624
  raise RuntimeError(
1620
1625
  f"Failed to create array with shape {shape}, the maximum number of dimensions is {ARRAY_MAX_DIMS}"
@@ -1650,9 +1655,6 @@ class array(Array):
1650
1655
  if requires_grad:
1651
1656
  self._alloc_grad()
1652
1657
 
1653
- # reference to other array
1654
- self._ref = None
1655
-
1656
1658
  def _init_from_data(self, data, dtype, shape, device, copy, pinned):
1657
1659
  if not hasattr(data, "__len__"):
1658
1660
  raise RuntimeError(f"Data must be a sequence or array, got scalar {data}")
@@ -2989,7 +2991,7 @@ class Mesh:
2989
2991
 
2990
2992
  Args:
2991
2993
  points (:class:`warp.array`): Array of vertex positions of type :class:`warp.vec3`
2992
- indices (:class:`warp.array`): Array of triangle indices of type :class:`warp.int32`, should be a 1d array with shape (num_tris, 3)
2994
+ indices (:class:`warp.array`): Array of triangle indices of type :class:`warp.int32`, should be a 1d array with shape (num_tris * 3)
2993
2995
  velocities (:class:`warp.array`): Array of vertex velocities of type :class:`warp.vec3` (optional)
2994
2996
  support_winding_number (bool): If true the mesh will build additional datastructures to support `wp.mesh_query_point_sign_winding_number()` queries
2995
2997
  """
@@ -3527,8 +3529,9 @@ class Volume:
3527
3529
  )
3528
3530
  if hasattr(bg_value, "__len__"):
3529
3531
  # vec3, assuming the numpy array is 4D
3530
- padded_array = np.array((target_shape[0], target_shape[1], target_shape[2], 3), dtype=np.single)
3531
- padded_array[:, :, :, :] = np.array(bg_value)
3532
+ padded_array = np.full(
3533
+ shape=(target_shape[0], target_shape[1], target_shape[2], 3), fill_value=bg_value, dtype=np.single
3534
+ )
3532
3535
  padded_array[0 : ndarray.shape[0], 0 : ndarray.shape[1], 0 : ndarray.shape[2], :] = ndarray
3533
3536
  else:
3534
3537
  padded_amount = (