warp-lang 1.4.1__py3-none-win_amd64.whl → 1.5.0__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.

Files changed (164) hide show
  1. warp/__init__.py +4 -0
  2. warp/autograd.py +43 -8
  3. warp/bin/warp-clang.dll +0 -0
  4. warp/bin/warp.dll +0 -0
  5. warp/build.py +21 -2
  6. warp/build_dll.py +23 -6
  7. warp/builtins.py +1920 -111
  8. warp/codegen.py +186 -62
  9. warp/config.py +2 -2
  10. warp/context.py +322 -73
  11. warp/examples/assets/pixel.jpg +0 -0
  12. warp/examples/benchmarks/benchmark_cloth_paddle.py +86 -0
  13. warp/examples/benchmarks/benchmark_gemm.py +121 -0
  14. warp/examples/benchmarks/benchmark_interop_paddle.py +158 -0
  15. warp/examples/benchmarks/benchmark_tile.py +179 -0
  16. warp/examples/core/example_dem.py +2 -1
  17. warp/examples/core/example_mesh_intersect.py +3 -3
  18. warp/examples/fem/example_adaptive_grid.py +37 -10
  19. warp/examples/fem/example_apic_fluid.py +3 -2
  20. warp/examples/fem/example_convection_diffusion_dg.py +4 -5
  21. warp/examples/fem/example_deformed_geometry.py +1 -1
  22. warp/examples/fem/example_diffusion_3d.py +47 -4
  23. warp/examples/fem/example_distortion_energy.py +220 -0
  24. warp/examples/fem/example_magnetostatics.py +127 -85
  25. warp/examples/fem/example_nonconforming_contact.py +5 -5
  26. warp/examples/fem/example_stokes.py +3 -1
  27. warp/examples/fem/example_streamlines.py +12 -19
  28. warp/examples/fem/utils.py +38 -15
  29. warp/examples/optim/example_walker.py +2 -2
  30. warp/examples/sim/example_cloth.py +2 -25
  31. warp/examples/sim/example_jacobian_ik.py +6 -2
  32. warp/examples/sim/example_quadruped.py +2 -1
  33. warp/examples/tile/example_tile_convolution.py +58 -0
  34. warp/examples/tile/example_tile_fft.py +47 -0
  35. warp/examples/tile/example_tile_filtering.py +105 -0
  36. warp/examples/tile/example_tile_matmul.py +79 -0
  37. warp/examples/tile/example_tile_mlp.py +375 -0
  38. warp/fem/__init__.py +8 -0
  39. warp/fem/cache.py +16 -12
  40. warp/fem/dirichlet.py +1 -1
  41. warp/fem/domain.py +44 -1
  42. warp/fem/field/__init__.py +1 -2
  43. warp/fem/field/field.py +31 -19
  44. warp/fem/field/nodal_field.py +101 -49
  45. warp/fem/field/virtual.py +794 -0
  46. warp/fem/geometry/__init__.py +2 -2
  47. warp/fem/geometry/deformed_geometry.py +3 -105
  48. warp/fem/geometry/element.py +13 -0
  49. warp/fem/geometry/geometry.py +165 -5
  50. warp/fem/geometry/grid_2d.py +3 -6
  51. warp/fem/geometry/grid_3d.py +31 -28
  52. warp/fem/geometry/hexmesh.py +3 -46
  53. warp/fem/geometry/nanogrid.py +3 -2
  54. warp/fem/geometry/{quadmesh_2d.py → quadmesh.py} +280 -159
  55. warp/fem/geometry/tetmesh.py +2 -43
  56. warp/fem/geometry/{trimesh_2d.py → trimesh.py} +354 -186
  57. warp/fem/integrate.py +683 -261
  58. warp/fem/linalg.py +404 -0
  59. warp/fem/operator.py +101 -18
  60. warp/fem/polynomial.py +5 -5
  61. warp/fem/quadrature/quadrature.py +45 -21
  62. warp/fem/space/__init__.py +45 -11
  63. warp/fem/space/basis_function_space.py +451 -0
  64. warp/fem/space/basis_space.py +58 -11
  65. warp/fem/space/function_space.py +146 -5
  66. warp/fem/space/grid_2d_function_space.py +80 -66
  67. warp/fem/space/grid_3d_function_space.py +113 -68
  68. warp/fem/space/hexmesh_function_space.py +96 -108
  69. warp/fem/space/nanogrid_function_space.py +62 -110
  70. warp/fem/space/quadmesh_function_space.py +208 -0
  71. warp/fem/space/shape/__init__.py +45 -7
  72. warp/fem/space/shape/cube_shape_function.py +328 -54
  73. warp/fem/space/shape/shape_function.py +10 -1
  74. warp/fem/space/shape/square_shape_function.py +328 -60
  75. warp/fem/space/shape/tet_shape_function.py +269 -19
  76. warp/fem/space/shape/triangle_shape_function.py +238 -19
  77. warp/fem/space/tetmesh_function_space.py +69 -37
  78. warp/fem/space/topology.py +38 -0
  79. warp/fem/space/trimesh_function_space.py +179 -0
  80. warp/fem/utils.py +6 -331
  81. warp/jax_experimental.py +3 -1
  82. warp/native/array.h +55 -40
  83. warp/native/builtin.h +124 -43
  84. warp/native/bvh.h +4 -0
  85. warp/native/coloring.cpp +600 -0
  86. warp/native/cuda_util.cpp +14 -0
  87. warp/native/cuda_util.h +2 -1
  88. warp/native/fabric.h +8 -0
  89. warp/native/hashgrid.h +4 -0
  90. warp/native/marching.cu +8 -0
  91. warp/native/mat.h +14 -3
  92. warp/native/mathdx.cpp +59 -0
  93. warp/native/mesh.h +4 -0
  94. warp/native/range.h +13 -1
  95. warp/native/reduce.cpp +9 -1
  96. warp/native/reduce.cu +7 -0
  97. warp/native/runlength_encode.cpp +9 -1
  98. warp/native/runlength_encode.cu +7 -1
  99. warp/native/scan.cpp +8 -0
  100. warp/native/scan.cu +8 -0
  101. warp/native/scan.h +8 -1
  102. warp/native/sparse.cpp +8 -0
  103. warp/native/sparse.cu +8 -0
  104. warp/native/temp_buffer.h +7 -0
  105. warp/native/tile.h +1857 -0
  106. warp/native/tile_gemm.h +341 -0
  107. warp/native/tile_reduce.h +210 -0
  108. warp/native/volume_builder.cu +8 -0
  109. warp/native/volume_builder.h +8 -0
  110. warp/native/warp.cpp +10 -2
  111. warp/native/warp.cu +369 -15
  112. warp/native/warp.h +12 -2
  113. warp/optim/adam.py +39 -4
  114. warp/paddle.py +29 -12
  115. warp/render/render_opengl.py +137 -65
  116. warp/sim/graph_coloring.py +292 -0
  117. warp/sim/integrator_euler.py +4 -2
  118. warp/sim/integrator_featherstone.py +115 -44
  119. warp/sim/integrator_vbd.py +6 -0
  120. warp/sim/model.py +90 -17
  121. warp/stubs.py +651 -85
  122. warp/tape.py +12 -7
  123. warp/tests/assets/pixel.npy +0 -0
  124. warp/tests/aux_test_instancing_gc.py +18 -0
  125. warp/tests/test_array.py +207 -48
  126. warp/tests/test_closest_point_edge_edge.py +8 -8
  127. warp/tests/test_codegen.py +120 -1
  128. warp/tests/test_codegen_instancing.py +30 -0
  129. warp/tests/test_collision.py +110 -0
  130. warp/tests/test_coloring.py +241 -0
  131. warp/tests/test_context.py +34 -0
  132. warp/tests/test_examples.py +18 -4
  133. warp/tests/test_fabricarray.py +33 -0
  134. warp/tests/test_fem.py +453 -113
  135. warp/tests/test_func.py +48 -1
  136. warp/tests/test_generics.py +52 -0
  137. warp/tests/test_iter.py +68 -0
  138. warp/tests/test_mat_scalar_ops.py +1 -1
  139. warp/tests/test_mesh_query_point.py +5 -4
  140. warp/tests/test_module_hashing.py +23 -0
  141. warp/tests/test_paddle.py +27 -87
  142. warp/tests/test_print.py +191 -1
  143. warp/tests/test_spatial.py +1 -1
  144. warp/tests/test_tile.py +700 -0
  145. warp/tests/test_tile_mathdx.py +144 -0
  146. warp/tests/test_tile_mlp.py +383 -0
  147. warp/tests/test_tile_reduce.py +374 -0
  148. warp/tests/test_tile_shared_memory.py +190 -0
  149. warp/tests/test_vbd.py +12 -20
  150. warp/tests/test_volume.py +43 -0
  151. warp/tests/unittest_suites.py +23 -2
  152. warp/tests/unittest_utils.py +4 -0
  153. warp/types.py +339 -73
  154. warp/utils.py +22 -1
  155. {warp_lang-1.4.1.dist-info → warp_lang-1.5.0.dist-info}/METADATA +33 -7
  156. {warp_lang-1.4.1.dist-info → warp_lang-1.5.0.dist-info}/RECORD +159 -132
  157. {warp_lang-1.4.1.dist-info → warp_lang-1.5.0.dist-info}/WHEEL +1 -1
  158. warp/fem/field/test.py +0 -180
  159. warp/fem/field/trial.py +0 -183
  160. warp/fem/space/collocated_function_space.py +0 -102
  161. warp/fem/space/quadmesh_2d_function_space.py +0 -261
  162. warp/fem/space/trimesh_2d_function_space.py +0 -153
  163. {warp_lang-1.4.1.dist-info → warp_lang-1.5.0.dist-info}/LICENSE.md +0 -0
  164. {warp_lang-1.4.1.dist-info → warp_lang-1.5.0.dist-info}/top_level.txt +0 -0
warp/tests/test_fem.py CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import math
9
9
  import unittest
10
+ from typing import Any
10
11
 
11
12
  import numpy as np
12
13
 
@@ -14,8 +15,8 @@ import warp as wp
14
15
  import warp.fem as fem
15
16
  from warp.fem import Coords, D, Domain, Field, Sample, curl, div, grad, integrand, normal
16
17
  from warp.fem.cache import dynamic_kernel
17
- from warp.fem.geometry import DeformedGeometry
18
18
  from warp.fem.geometry.closest_point import project_on_tet_at_origin, project_on_tri_at_origin
19
+ from warp.fem.linalg import inverse_qr, spherical_part, symmetric_eigenvalues_qr, symmetric_part
19
20
  from warp.fem.space import shape
20
21
  from warp.fem.types import make_free_sample
21
22
  from warp.fem.utils import (
@@ -23,8 +24,6 @@ from warp.fem.utils import (
23
24
  grid_to_quads,
24
25
  grid_to_tets,
25
26
  grid_to_tris,
26
- inverse_qr,
27
- symmetric_eigenvalues_qr,
28
27
  )
29
28
  from warp.tests.unittest_utils import *
30
29
 
@@ -37,6 +36,17 @@ def linear_form(s: Sample, u: Field):
37
36
  return u(s)
38
37
 
39
38
 
39
+ @integrand
40
+ def scaled_linear_form(s: Sample, u: Field, scale: wp.array(dtype=float)):
41
+ return u(s) * scale[0]
42
+
43
+
44
+ @wp.kernel
45
+ def atomic_sum(v: wp.array(dtype=float), sum: wp.array(dtype=float)):
46
+ i = wp.tid()
47
+ wp.atomic_add(sum, 0, v[i])
48
+
49
+
40
50
  def test_integrate_gradient(test, device):
41
51
  with wp.ScopedDevice(device):
42
52
  # Grid geometry
@@ -51,21 +61,59 @@ def test_integrate_gradient(test, device):
51
61
  u = scalar_space.make_field()
52
62
  u.dof_values = wp.zeros_like(u.dof_values, requires_grad=True)
53
63
 
54
- result = wp.empty(dtype=wp.float64, shape=(1), requires_grad=True)
55
-
64
+ result = wp.empty(dtype=wp.float32, shape=(1), requires_grad=True)
56
65
  tape = wp.Tape()
57
66
 
58
67
  # forward pass
59
68
  with tape:
60
69
  fem.integrate(linear_form, quadrature=quadrature, fields={"u": u}, output=result)
61
-
62
70
  tape.backward(result)
63
71
 
64
72
  test_field = fem.make_test(space=scalar_space, domain=domain)
65
- rhs = fem.integrate(linear_form, quadrature=quadrature, fields={"u": test_field})
66
73
 
67
- err = np.linalg.norm(rhs.numpy() - u.dof_values.grad.numpy())
68
- test.assertLess(err, 1.0e-8)
74
+ u_adj = wp.empty_like(u.dof_values, requires_grad=True)
75
+ scale = wp.ones(1, requires_grad=True)
76
+ loss = wp.zeros(1, requires_grad=True)
77
+
78
+ tape2 = wp.Tape()
79
+ with tape2:
80
+ fem.integrate(
81
+ scaled_linear_form,
82
+ quadrature=quadrature,
83
+ fields={"u": test_field},
84
+ values={"scale": scale},
85
+ assembly="generic",
86
+ output=u_adj,
87
+ )
88
+ wp.launch(atomic_sum, dim=u_adj.shape, inputs=[u_adj, loss])
89
+
90
+ # gradient of scalar integral w.r.t dofs should be equal to linear form vector
91
+ assert_np_equal(u_adj.numpy(), u.dof_values.grad.numpy(), tol=1.0e-8)
92
+ test.assertAlmostEqual(loss.numpy()[0], 1.0, places=4)
93
+
94
+ # Check gradient of linear form vec w.r.t value params
95
+ tape.zero()
96
+ tape2.backward(loss=loss)
97
+
98
+ test.assertAlmostEqual(loss.numpy()[0], scale.grad.numpy()[0], places=4)
99
+ tape2.zero()
100
+ test.assertEqual(scale.grad.numpy()[0], 0.0)
101
+
102
+ # Same, with dispatched assembly
103
+ tape2.reset()
104
+ loss.zero_()
105
+ with tape2:
106
+ fem.integrate(
107
+ scaled_linear_form,
108
+ quadrature=quadrature,
109
+ fields={"u": test_field},
110
+ values={"scale": scale},
111
+ assembly="dispatch",
112
+ output=u_adj,
113
+ )
114
+ wp.launch(atomic_sum, dim=u_adj.shape, inputs=[u_adj, loss])
115
+ tape2.backward(loss=loss)
116
+ test.assertAlmostEqual(loss.numpy()[0], scale.grad.numpy()[0], places=4)
69
117
 
70
118
 
71
119
  @fem.integrand
@@ -121,7 +169,7 @@ def test_interpolate_gradient(test, device):
121
169
  vector_field.dof_values.grad.assign([1.0, 0.0])
122
170
  tape.backward()
123
171
 
124
- assert_np_equal(scalar_field.dof_values.grad.numpy(), np.array([0.0, 0.0, 0.0, 0.0, 0.0, -0.5, 0.0, 0.5, 0.0]))
172
+ assert_np_equal(scalar_field.dof_values.grad.numpy(), np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5, 0.0, 0.5]))
125
173
  assert_np_equal(
126
174
  geo.positions.grad.numpy(),
127
175
  np.array(
@@ -143,7 +191,7 @@ def test_interpolate_gradient(test, device):
143
191
  vector_field.dof_values.grad.assign([0.0, 1.0])
144
192
  tape.backward()
145
193
 
146
- assert_np_equal(scalar_field.dof_values.grad.numpy(), np.array([0.0, 0.0, 0.0, 0.0, -0.5, 0.0, 0.5, 0.0, 0.0]))
194
+ assert_np_equal(scalar_field.dof_values.grad.numpy(), np.array([0.0, 0.0, 0.0, 0.0, 0.0, -0.5, 0.0, 0.5, 0.0]))
147
195
  assert_np_equal(
148
196
  geo.positions.grad.numpy(),
149
197
  np.array(
@@ -345,13 +393,13 @@ def test_grad_decomposition(test, device):
345
393
  test.assertLess(err, 1.0e-8)
346
394
 
347
395
 
348
- def _gen_trimesh(N):
349
- x = np.linspace(0.0, 1.0, N + 1)
350
- y = np.linspace(0.0, 1.0, N + 1)
396
+ def _gen_trimesh(Nx, Ny):
397
+ x = np.linspace(0.0, 1.0, Nx + 1)
398
+ y = np.linspace(0.0, 1.0, Ny + 1)
351
399
 
352
- positions = np.transpose(np.meshgrid(x, y, indexing="ij")).reshape(-1, 2)
400
+ positions = np.transpose(np.meshgrid(x, y, indexing="ij"), axes=(1, 2, 0)).reshape(-1, 2)
353
401
 
354
- vidx = grid_to_tris(N, N)
402
+ vidx = grid_to_tris(Nx, Ny)
355
403
 
356
404
  return wp.array(positions, dtype=wp.vec2), wp.array(vidx, dtype=int)
357
405
 
@@ -360,21 +408,21 @@ def _gen_quadmesh(N):
360
408
  x = np.linspace(0.0, 1.0, N + 1)
361
409
  y = np.linspace(0.0, 1.0, N + 1)
362
410
 
363
- positions = np.transpose(np.meshgrid(x, y, indexing="ij")).reshape(-1, 2)
411
+ positions = np.transpose(np.meshgrid(x, y, indexing="ij"), axes=(1, 2, 0)).reshape(-1, 2)
364
412
 
365
413
  vidx = grid_to_quads(N, N)
366
414
 
367
415
  return wp.array(positions, dtype=wp.vec2), wp.array(vidx, dtype=int)
368
416
 
369
417
 
370
- def _gen_tetmesh(N):
371
- x = np.linspace(0.0, 1.0, N + 1)
372
- y = np.linspace(0.0, 1.0, N + 1)
373
- z = np.linspace(0.0, 1.0, N + 1)
418
+ def _gen_tetmesh(Nx, Ny, Nz):
419
+ x = np.linspace(0.0, 1.0, Nx + 1)
420
+ y = np.linspace(0.0, 1.0, Ny + 1)
421
+ z = np.linspace(0.0, 1.0, Nz + 1)
374
422
 
375
- positions = np.transpose(np.meshgrid(x, y, z, indexing="ij")).reshape(-1, 3)
423
+ positions = np.transpose(np.meshgrid(x, y, z, indexing="ij"), axes=(1, 2, 3, 0)).reshape(-1, 3)
376
424
 
377
- vidx = grid_to_tets(N, N, N)
425
+ vidx = grid_to_tets(Nx, Ny, Nz)
378
426
 
379
427
  return wp.array(positions, dtype=wp.vec3), wp.array(vidx, dtype=int)
380
428
 
@@ -384,107 +432,98 @@ def _gen_hexmesh(N):
384
432
  y = np.linspace(0.0, 1.0, N + 1)
385
433
  z = np.linspace(0.0, 1.0, N + 1)
386
434
 
387
- positions = np.transpose(np.meshgrid(x, y, z, indexing="ij")).reshape(-1, 3)
435
+ positions = np.transpose(np.meshgrid(x, y, z, indexing="ij"), axes=(1, 2, 3, 0)).reshape(-1, 3)
388
436
 
389
437
  vidx = grid_to_hexes(N, N, N)
390
438
 
391
439
  return wp.array(positions, dtype=wp.vec3), wp.array(vidx, dtype=int)
392
440
 
393
441
 
394
- def _launch_test_geometry_kernel(geo: fem.Geometry, device):
395
- @dynamic_kernel(suffix=geo.name, kernel_options={"enable_backward": False})
396
- def test_geo_cells_kernel(
397
- cell_arg: geo.CellArg,
398
- qps: wp.array(dtype=Coords),
399
- qp_weights: wp.array(dtype=float),
400
- cell_measures: wp.array(dtype=float),
401
- ):
402
- cell_index, q = wp.tid()
403
-
404
- coords = qps[q]
405
- s = make_free_sample(cell_index, coords)
442
+ @fem.integrand(kernel_options={"enable_backward": False})
443
+ def _test_geo_cells(
444
+ s: fem.Sample,
445
+ domain: fem.Domain,
446
+ cell_measures: wp.array(dtype=float),
447
+ ):
448
+ wp.atomic_add(cell_measures, s.element_index, fem.measure(domain, s) * s.qp_weight)
406
449
 
407
- wp.atomic_add(cell_measures, cell_index, geo.cell_measure(cell_arg, s) * qp_weights[q])
408
450
 
409
- REF_MEASURE = geo.reference_side().measure()
451
+ @fem.integrand(kernel_options={"enable_backward": False, "max_unroll": 1})
452
+ def _test_geo_sides(
453
+ s: fem.Sample,
454
+ domain: fem.Domain,
455
+ ref_measure: float,
456
+ side_measures: wp.array(dtype=float),
457
+ ):
458
+ side_index = s.element_index
459
+ coords = s.element_coords
410
460
 
411
- @dynamic_kernel(suffix=geo.name, kernel_options={"enable_backward": False, "max_unroll": 1})
412
- def test_geo_sides_kernel(
413
- side_arg: geo.SideArg,
414
- qps: wp.array(dtype=Coords),
415
- qp_weights: wp.array(dtype=float),
416
- side_measures: wp.array(dtype=float),
417
- ):
418
- side_index, q = wp.tid()
461
+ cells = fem.cells(domain)
419
462
 
420
- coords = qps[q]
421
- s = make_free_sample(side_index, coords)
463
+ inner_s = fem.to_inner_cell(domain, s)
464
+ outer_s = fem.to_outer_cell(domain, s)
422
465
 
423
- cell_arg = geo.side_to_cell_arg(side_arg)
424
- inner_cell_index = geo.side_inner_cell_index(side_arg, side_index)
425
- outer_cell_index = geo.side_outer_cell_index(side_arg, side_index)
426
- inner_cell_coords = geo.side_inner_cell_coords(side_arg, side_index, coords)
427
- outer_cell_coords = geo.side_outer_cell_coords(side_arg, side_index, coords)
466
+ pos_side = domain(s)
467
+ pos_inner = cells(inner_s)
468
+ pos_outer = cells(outer_s)
428
469
 
429
- inner_s = make_free_sample(inner_cell_index, inner_cell_coords)
430
- outer_s = make_free_sample(outer_cell_index, outer_cell_coords)
470
+ for k in range(type(pos_side).length):
471
+ wp.expect_near(pos_side[k], pos_inner[k], 0.0001)
472
+ wp.expect_near(pos_side[k], pos_outer[k], 0.0001)
431
473
 
432
- pos_side = geo.side_position(side_arg, s)
433
- pos_inner = geo.cell_position(cell_arg, inner_s)
434
- pos_outer = geo.cell_position(cell_arg, outer_s)
474
+ inner_side_s = fem.to_cell_side(domain, inner_s, side_index)
475
+ outer_side_s = fem.to_cell_side(domain, outer_s, side_index)
435
476
 
436
- # if wp.length(pos_outer - pos_side) > 0.1:
437
- # wp.print(side_index)
477
+ wp.expect_near(coords, inner_side_s.element_coords, 0.0001)
478
+ wp.expect_near(coords, outer_side_s.element_coords, 0.0001)
438
479
 
439
- for k in range(type(pos_side).length):
440
- wp.expect_near(pos_side[k], pos_inner[k], 0.0001)
441
- wp.expect_near(pos_side[k], pos_outer[k], 0.0001)
480
+ area = fem.measure(domain, s)
481
+ wp.atomic_add(side_measures, side_index, area * s.qp_weight)
442
482
 
443
- inner_side_coords = geo.side_from_cell_coords(side_arg, side_index, inner_cell_index, inner_cell_coords)
444
- outer_side_coords = geo.side_from_cell_coords(side_arg, side_index, outer_cell_index, outer_cell_coords)
483
+ F = fem.deformation_gradient(domain, s)
484
+ F_det = fem.Geometry._element_measure(F)
485
+ wp.expect_near(F_det * ref_measure, area)
445
486
 
446
- wp.expect_near(coords, inner_side_coords, 0.0001)
447
- wp.expect_near(coords, outer_side_coords, 0.0001)
448
487
 
449
- area = geo.side_measure(side_arg, s)
450
- wp.atomic_add(side_measures, side_index, area * qp_weights[q])
488
+ @fem.integrand(kernel_options={"enable_backward": False, "max_unroll": 1})
489
+ def _test_side_normals(
490
+ s: fem.Sample,
491
+ domain: fem.Domain,
492
+ ):
493
+ # test consistency of side normal, measure, and deformation gradient
494
+ F = fem.deformation_gradient(domain, s)
451
495
 
452
- # test consistency of side normal, measure, and deformation gradient
453
- F = geo.side_deformation_gradient(side_arg, s)
454
- F_det = DeformedGeometry._side_measure(F)
455
- wp.expect_near(F_det * REF_MEASURE, area)
496
+ nor = fem.normal(domain, s)
497
+ F_cross = fem.Geometry._element_normal(F)
456
498
 
457
- nor = geo.side_normal(side_arg, s)
458
- F_cross = DeformedGeometry._side_normal(F)
499
+ for k in range(type(nor).length):
500
+ wp.expect_near(F_cross[k], nor[k], 0.0001)
459
501
 
460
- for k in range(type(pos_side).length):
461
- wp.expect_near(F_cross[k], nor[k], 0.0001)
462
502
 
503
+ def _launch_test_geometry_kernel(geo: fem.Geometry, device):
463
504
  cell_measures = wp.zeros(dtype=float, device=device, shape=geo.cell_count())
464
-
465
505
  cell_quadrature = fem.RegularQuadrature(fem.Cells(geo), order=2)
466
- cell_qps = wp.array(cell_quadrature.points, dtype=Coords, device=device)
467
- cell_qp_weights = wp.array(cell_quadrature.weights, dtype=float, device=device)
468
-
469
- wp.launch(
470
- kernel=test_geo_cells_kernel,
471
- dim=(geo.cell_count(), cell_qps.shape[0]),
472
- inputs=[geo.cell_arg_value(device), cell_qps, cell_qp_weights, cell_measures],
473
- device=device,
474
- )
475
506
 
476
507
  side_measures = wp.zeros(dtype=float, device=device, shape=geo.side_count())
477
-
478
508
  side_quadrature = fem.RegularQuadrature(fem.Sides(geo), order=2)
479
- side_qps = wp.array(side_quadrature.points, dtype=Coords, device=device)
480
- side_qp_weights = wp.array(side_quadrature.weights, dtype=float, device=device)
481
509
 
482
- wp.launch(
483
- kernel=test_geo_sides_kernel,
484
- dim=(geo.side_count(), side_qps.shape[0]),
485
- inputs=[geo.side_arg_value(device), side_qps, side_qp_weights, side_measures],
486
- device=device,
487
- )
510
+ with wp.ScopedDevice(device):
511
+ fem.interpolate(
512
+ _test_geo_cells,
513
+ quadrature=cell_quadrature,
514
+ values={"cell_measures": cell_measures},
515
+ )
516
+ fem.interpolate(
517
+ _test_geo_sides,
518
+ quadrature=side_quadrature,
519
+ values={"side_measures": side_measures, "ref_measure": geo.reference_side().measure()},
520
+ )
521
+
522
+ if geo.side_normal is not None:
523
+ fem.interpolate(
524
+ _test_side_normals,
525
+ quadrature=side_quadrature,
526
+ )
488
527
 
489
528
  return side_measures, cell_measures
490
529
 
@@ -509,7 +548,7 @@ def test_triangle_mesh(test, device):
509
548
  N = 3
510
549
 
511
550
  with wp.ScopedDevice(device):
512
- positions, tri_vidx = _gen_trimesh(N)
551
+ positions, tri_vidx = _gen_trimesh(N, N)
513
552
 
514
553
  geo = fem.Trimesh2D(tri_vertex_indices=tri_vidx, positions=positions)
515
554
 
@@ -523,6 +562,24 @@ def test_triangle_mesh(test, device):
523
562
  assert_np_equal(cell_measures.numpy(), np.full(cell_measures.shape, 0.5 / (N**2)), tol=1.0e-4)
524
563
  test.assertAlmostEqual(np.sum(side_measures.numpy()), 2 * (N + 1) + N * math.sqrt(2.0), places=4)
525
564
 
565
+ # 3d
566
+
567
+ positions = positions.numpy()
568
+ positions = np.hstack((positions, np.ones((positions.shape[0], 1))))
569
+ positions = wp.array(positions, device=device, dtype=wp.vec3)
570
+
571
+ geo = fem.Trimesh3D(tri_vertex_indices=tri_vidx, positions=positions)
572
+
573
+ test.assertEqual(geo.cell_count(), 2 * (N) ** 2)
574
+ test.assertEqual(geo.vertex_count(), (N + 1) ** 2)
575
+ test.assertEqual(geo.side_count(), 2 * (N + 1) * N + (N**2))
576
+ test.assertEqual(geo.boundary_side_count(), 4 * N)
577
+
578
+ side_measures, cell_measures = _launch_test_geometry_kernel(geo, device)
579
+
580
+ assert_np_equal(cell_measures.numpy(), np.full(cell_measures.shape, 0.5 / (N**2)), tol=1.0e-4)
581
+ test.assertAlmostEqual(np.sum(side_measures.numpy()), 2 * (N + 1) + N * math.sqrt(2.0), places=4)
582
+
526
583
 
527
584
  def test_quad_mesh(test, device):
528
585
  N = 3
@@ -542,6 +599,24 @@ def test_quad_mesh(test, device):
542
599
  assert_np_equal(side_measures.numpy(), np.full(side_measures.shape, 1.0 / (N)), tol=1.0e-4)
543
600
  assert_np_equal(cell_measures.numpy(), np.full(cell_measures.shape, 1.0 / (N**2)), tol=1.0e-4)
544
601
 
602
+ # 3d
603
+
604
+ positions = positions.numpy()
605
+ positions = np.hstack((positions, np.ones((positions.shape[0], 1))))
606
+ positions = wp.array(positions, device=device, dtype=wp.vec3)
607
+
608
+ geo = fem.Quadmesh3D(quad_vertex_indices=quad_vidx, positions=positions)
609
+
610
+ test.assertEqual(geo.cell_count(), N**2)
611
+ test.assertEqual(geo.vertex_count(), (N + 1) ** 2)
612
+ test.assertEqual(geo.side_count(), 2 * (N + 1) * N)
613
+ test.assertEqual(geo.boundary_side_count(), 4 * N)
614
+
615
+ side_measures, cell_measures = _launch_test_geometry_kernel(geo, device)
616
+
617
+ assert_np_equal(side_measures.numpy(), np.full(side_measures.shape, 1.0 / (N)), tol=1.0e-4)
618
+ assert_np_equal(cell_measures.numpy(), np.full(cell_measures.shape, 1.0 / (N**2)), tol=1.0e-4)
619
+
545
620
 
546
621
  def test_grid_3d(test, device):
547
622
  N = 3
@@ -564,7 +639,7 @@ def test_tet_mesh(test, device):
564
639
  N = 3
565
640
 
566
641
  with wp.ScopedDevice(device):
567
- positions, tet_vidx = _gen_tetmesh(N)
642
+ positions, tet_vidx = _gen_tetmesh(N, N, N)
568
643
 
569
644
  geo = fem.Tetmesh(tet_vertex_indices=tet_vidx, positions=positions)
570
645
 
@@ -692,7 +767,7 @@ def test_deformed_geometry(test, device):
692
767
  N = 3
693
768
 
694
769
  with wp.ScopedDevice(device):
695
- positions, tet_vidx = _gen_tetmesh(N)
770
+ positions, tet_vidx = _gen_tetmesh(N, N, N)
696
771
 
697
772
  geo = fem.Tetmesh(tet_vertex_indices=tet_vidx, positions=positions)
698
773
 
@@ -935,6 +1010,17 @@ def test_dof_mapper(test, device):
935
1010
  test.assertAlmostEqual(frob_norm2, 1.0, places=6)
936
1011
 
937
1012
 
1013
+ @wp.func
1014
+ def _expect_near(a: Any, b: Any, tol: float):
1015
+ wp.expect_near(a, b, tol)
1016
+
1017
+
1018
+ @wp.func
1019
+ def _expect_near(a: wp.vec2, b: wp.vec2, tol: float):
1020
+ for k in range(2):
1021
+ wp.expect_near(a[k], b[k], tol)
1022
+
1023
+
938
1024
  def test_shape_function_weight(test, shape: shape.ShapeFunction, coord_sampler, CENTER_COORDS):
939
1025
  NODE_COUNT = shape.NODES_PER_ELEMENT
940
1026
  weight_fn = shape.make_element_inner_weight()
@@ -974,11 +1060,11 @@ def test_shape_function_weight(test, shape: shape.ShapeFunction, coord_sampler,
974
1060
  coords = coord_sampler(rng_state)
975
1061
 
976
1062
  # sum of node weights anywhere should be 1.0
977
- w_sum = float(0.0)
1063
+ w_sum = type(weight_fn(coords, 0))(0.0)
978
1064
  for n in range(NODE_COUNT):
979
1065
  w_sum += weight_fn(coords, n)
980
1066
 
981
- wp.expect_near(w_sum, 1.0, 0.0001)
1067
+ _expect_near(wp.abs(w_sum), type(w_sum)(1.0), 0.0001)
982
1068
 
983
1069
  n_samples = 100
984
1070
  wp.launch(partition_of_unity_test, dim=n_samples, inputs=[])
@@ -1011,10 +1097,27 @@ def test_shape_function_trace(test, shape: shape.ShapeFunction, CENTER_COORDS):
1011
1097
  wp.launch(trace_node_quadrature_unity_test, dim=1, inputs=[])
1012
1098
 
1013
1099
 
1014
- def test_shape_function_gradient(test, shape: shape.ShapeFunction, coord_sampler, coord_delta_sampler):
1100
+ def test_shape_function_gradient(
1101
+ test,
1102
+ shape: shape.ShapeFunction,
1103
+ coord_sampler,
1104
+ coord_delta_sampler,
1105
+ pure_curl: bool = False,
1106
+ pure_spherical: bool = False,
1107
+ ):
1015
1108
  weight_fn = shape.make_element_inner_weight()
1016
1109
  weight_gradient_fn = shape.make_element_inner_weight_gradient()
1017
1110
 
1111
+ @wp.func
1112
+ def scalar_delta(avg_grad: Any, param_delta: Any):
1113
+ return wp.dot(avg_grad, param_delta)
1114
+
1115
+ @wp.func
1116
+ def vector_delta(avg_grad: Any, param_delta: Any):
1117
+ return avg_grad * param_delta
1118
+
1119
+ grad_delta_fn = scalar_delta if shape.value == shape.Value.Scalar else vector_delta
1120
+
1018
1121
  @dynamic_kernel(suffix=shape.name, kernel_options={"enable_backward": False})
1019
1122
  def finite_difference_test():
1020
1123
  i, n = wp.tid()
@@ -1034,10 +1137,15 @@ def test_shape_function_gradient(test, shape: shape.ShapeFunction, coord_sampler
1034
1137
  # 2nd-order finite-difference test
1035
1138
  # See Schroeder 2019, Practical course on computing derivatives in code
1036
1139
  delta_ref = w_p - w_m
1037
- delta_est = wp.dot(gp + gm, param_delta)
1140
+ delta_est = grad_delta_fn(gp + gm, param_delta)
1141
+ _expect_near(delta_ref, delta_est, 0.0001)
1142
+
1143
+ if wp.static(pure_curl):
1144
+ wp.expect_near(wp.ddot(symmetric_part(gp), symmetric_part(gp)), gp.dtype(0.0))
1038
1145
 
1039
- # wp.printf("%d %f %f \n", n, delta_ref, delta_est)
1040
- wp.expect_near(delta_ref, delta_est, 0.0001)
1146
+ if wp.static(pure_spherical):
1147
+ deviatoric_part = gp - spherical_part(gp)
1148
+ wp.expect_near(wp.ddot(deviatoric_part, deviatoric_part), gp.dtype(0.0))
1041
1149
 
1042
1150
  n_samples = 100
1043
1151
  wp.launch(finite_difference_test, dim=(n_samples, shape.NODES_PER_ELEMENT), inputs=[])
@@ -1102,6 +1210,11 @@ def test_square_shape_functions(test, device):
1102
1210
  test_shape_function_gradient(test, P_c2, square_coord_sampler, square_coord_delta_sampler)
1103
1211
  test_shape_function_gradient(test, P_c3, square_coord_sampler, square_coord_delta_sampler)
1104
1212
 
1213
+ N1_1 = shape.SquareNedelecFirstKindShapeFunctions(degree=1)
1214
+ test_shape_function_gradient(test, N1_1, square_coord_sampler, square_coord_delta_sampler)
1215
+ RT_1 = shape.SquareRaviartThomasShapeFunctions(degree=1)
1216
+ test_shape_function_gradient(test, RT_1, square_coord_sampler, square_coord_delta_sampler)
1217
+
1105
1218
  wp.synchronize()
1106
1219
 
1107
1220
 
@@ -1164,6 +1277,11 @@ def test_cube_shape_functions(test, device):
1164
1277
  test_shape_function_gradient(test, P_c2, cube_coord_sampler, cube_coord_delta_sampler)
1165
1278
  test_shape_function_gradient(test, P_c3, cube_coord_sampler, cube_coord_delta_sampler)
1166
1279
 
1280
+ N1_1 = shape.CubeNedelecFirstKindShapeFunctions(degree=1)
1281
+ test_shape_function_gradient(test, N1_1, cube_coord_sampler, cube_coord_delta_sampler)
1282
+ RT_1 = shape.CubeRaviartThomasShapeFunctions(degree=1)
1283
+ test_shape_function_gradient(test, RT_1, cube_coord_sampler, cube_coord_delta_sampler)
1284
+
1167
1285
  wp.synchronize()
1168
1286
 
1169
1287
 
@@ -1184,9 +1302,9 @@ def test_tri_shape_functions(test, device):
1184
1302
  b = param_delta[1]
1185
1303
  return param_delta, Coords(-a - b, a, b)
1186
1304
 
1187
- P_1 = shape.Triangle2DPolynomialShapeFunctions(degree=1)
1188
- P_2 = shape.Triangle2DPolynomialShapeFunctions(degree=2)
1189
- P_3 = shape.Triangle2DPolynomialShapeFunctions(degree=3)
1305
+ P_1 = shape.TrianglePolynomialShapeFunctions(degree=1)
1306
+ P_2 = shape.TrianglePolynomialShapeFunctions(degree=2)
1307
+ P_3 = shape.TrianglePolynomialShapeFunctions(degree=3)
1190
1308
 
1191
1309
  test_shape_function_weight(test, P_1, tri_coord_sampler, TRI_CENTER_COORDS)
1192
1310
  test_shape_function_weight(test, P_2, tri_coord_sampler, TRI_CENTER_COORDS)
@@ -1198,9 +1316,9 @@ def test_tri_shape_functions(test, device):
1198
1316
  test_shape_function_gradient(test, P_2, tri_coord_sampler, tri_coord_delta_sampler)
1199
1317
  test_shape_function_gradient(test, P_3, tri_coord_sampler, tri_coord_delta_sampler)
1200
1318
 
1201
- P_1d = shape.Triangle2DNonConformingPolynomialShapeFunctions(degree=1)
1202
- P_2d = shape.Triangle2DNonConformingPolynomialShapeFunctions(degree=2)
1203
- P_3d = shape.Triangle2DNonConformingPolynomialShapeFunctions(degree=3)
1319
+ P_1d = shape.TriangleNonConformingPolynomialShapeFunctions(degree=1)
1320
+ P_2d = shape.TriangleNonConformingPolynomialShapeFunctions(degree=2)
1321
+ P_3d = shape.TriangleNonConformingPolynomialShapeFunctions(degree=3)
1204
1322
 
1205
1323
  test_shape_function_weight(test, P_1d, tri_coord_sampler, TRI_CENTER_COORDS)
1206
1324
  test_shape_function_weight(test, P_2d, tri_coord_sampler, TRI_CENTER_COORDS)
@@ -1209,6 +1327,12 @@ def test_tri_shape_functions(test, device):
1209
1327
  test_shape_function_gradient(test, P_2d, tri_coord_sampler, tri_coord_delta_sampler)
1210
1328
  test_shape_function_gradient(test, P_3d, tri_coord_sampler, tri_coord_delta_sampler)
1211
1329
 
1330
+ N1_1 = shape.TriangleNedelecFirstKindShapeFunctions(degree=1)
1331
+ test_shape_function_gradient(test, N1_1, tri_coord_sampler, tri_coord_delta_sampler, pure_curl=True)
1332
+
1333
+ RT_1 = shape.TriangleNedelecFirstKindShapeFunctions(degree=1)
1334
+ test_shape_function_gradient(test, RT_1, tri_coord_sampler, tri_coord_delta_sampler, pure_spherical=True)
1335
+
1212
1336
  wp.synchronize()
1213
1337
 
1214
1338
 
@@ -1250,6 +1374,12 @@ def test_tet_shape_functions(test, device):
1250
1374
  test_shape_function_gradient(test, P_2d, tet_coord_sampler, tet_coord_delta_sampler)
1251
1375
  test_shape_function_gradient(test, P_3d, tet_coord_sampler, tet_coord_delta_sampler)
1252
1376
 
1377
+ N1_1 = shape.TetrahedronNedelecFirstKindShapeFunctions(degree=1)
1378
+ test_shape_function_gradient(test, N1_1, tet_coord_sampler, tet_coord_delta_sampler, pure_curl=True)
1379
+
1380
+ RT_1 = shape.TetrahedronRaviartThomasShapeFunctions(degree=1)
1381
+ test_shape_function_gradient(test, RT_1, tet_coord_sampler, tet_coord_delta_sampler, pure_spherical=True)
1382
+
1253
1383
  wp.synchronize()
1254
1384
 
1255
1385
 
@@ -1508,6 +1638,186 @@ def test_implicit_fields(test, device):
1508
1638
  assert_np_equal(discrete_field2.dof_values.numpy(), np.array([2.0] + [5.0] * 3))
1509
1639
 
1510
1640
 
1641
+ @fem.integrand
1642
+ def _expect_pure_curl(s: fem.Sample, field: fem.Field):
1643
+ sym_grad = fem.D(field, s)
1644
+ wp.expect_near(wp.ddot(sym_grad, sym_grad), 0.0)
1645
+ return 0.0
1646
+
1647
+
1648
+ @fem.integrand
1649
+ def _expect_pure_spherical(s: fem.Sample, field: fem.Field):
1650
+ grad = fem.grad(field, s)
1651
+ deviatoric_part = grad - spherical_part(grad)
1652
+ wp.expect_near(wp.ddot(deviatoric_part, deviatoric_part), 0.0)
1653
+ return 0.0
1654
+
1655
+
1656
+ @fem.integrand
1657
+ def _expect_normal_continuity(s: fem.Sample, domain: fem.Domain, field: fem.Field):
1658
+ nor = fem.normal(domain, s)
1659
+ wp.expect_near(wp.dot(fem.inner(field, s), nor), wp.dot(fem.outer(field, s), nor), 0.0001)
1660
+ return 0.0
1661
+
1662
+
1663
+ @fem.integrand
1664
+ def _expect_tangential_continuity(s: fem.Sample, domain: fem.Domain, field: fem.Field):
1665
+ nor = fem.normal(domain, s)
1666
+ in_s = fem.inner(field, s)
1667
+ out_s = fem.outer(field, s)
1668
+ in_t = in_s - wp.dot(in_s, nor) * nor
1669
+ out_t = out_s - wp.dot(out_s, nor) * nor
1670
+
1671
+ _expect_near(in_t, out_t, 0.0001)
1672
+ return 0.0
1673
+
1674
+
1675
+ def test_vector_spaces(test, device):
1676
+ # Test covariant / contravariant mappings
1677
+
1678
+ with wp.ScopedDevice(device):
1679
+ positions, hex_vidx = _gen_quadmesh(3)
1680
+
1681
+ geo = fem.Quadmesh2D(quad_vertex_indices=hex_vidx, positions=positions)
1682
+
1683
+ curl_space = fem.make_polynomial_space(geo, element_basis=fem.ElementBasis.NEDELEC_FIRST_KIND)
1684
+ curl_test = fem.make_test(curl_space)
1685
+
1686
+ curl_field = curl_space.make_field()
1687
+ curl_field.dof_values = wp.array(np.linspace(0.0, 1.0, curl_space.node_count()), dtype=float)
1688
+
1689
+ fem.interpolate(
1690
+ _expect_tangential_continuity,
1691
+ quadrature=fem.RegularQuadrature(fem.Sides(geo), order=2),
1692
+ fields={"field": curl_field.trace()},
1693
+ )
1694
+
1695
+ div_space = fem.make_polynomial_space(geo, element_basis=fem.ElementBasis.RAVIART_THOMAS)
1696
+ div_test = fem.make_test(div_space)
1697
+
1698
+ div_field = div_space.make_field()
1699
+ div_field.dof_values = wp.array(np.linspace(0.0, 1.0, div_space.node_count()), dtype=float)
1700
+
1701
+ fem.interpolate(
1702
+ _expect_normal_continuity,
1703
+ quadrature=fem.RegularQuadrature(fem.Sides(geo), order=2),
1704
+ fields={"field": div_field.trace()},
1705
+ )
1706
+
1707
+ with wp.ScopedDevice(device):
1708
+ positions, hex_vidx = _gen_hexmesh(3)
1709
+
1710
+ geo = fem.Hexmesh(hex_vertex_indices=hex_vidx, positions=positions)
1711
+
1712
+ curl_space = fem.make_polynomial_space(geo, element_basis=fem.ElementBasis.NEDELEC_FIRST_KIND)
1713
+ curl_test = fem.make_test(curl_space)
1714
+
1715
+ curl_field = curl_space.make_field()
1716
+ curl_field.dof_values = wp.array(np.linspace(0.0, 1.0, curl_space.node_count()), dtype=float)
1717
+
1718
+ fem.interpolate(
1719
+ _expect_tangential_continuity,
1720
+ quadrature=fem.RegularQuadrature(fem.Sides(geo), order=2),
1721
+ fields={"field": curl_field.trace()},
1722
+ )
1723
+
1724
+ div_space = fem.make_polynomial_space(geo, element_basis=fem.ElementBasis.RAVIART_THOMAS)
1725
+ div_test = fem.make_test(div_space)
1726
+
1727
+ div_field = div_space.make_field()
1728
+ div_field.dof_values = wp.array(np.linspace(0.0, 1.0, div_space.node_count()), dtype=float)
1729
+
1730
+ fem.interpolate(
1731
+ _expect_normal_continuity,
1732
+ quadrature=fem.RegularQuadrature(fem.Sides(geo), order=2),
1733
+ fields={"field": div_field.trace()},
1734
+ )
1735
+
1736
+ return
1737
+
1738
+ with wp.ScopedDevice(device):
1739
+ positions, tri_vidx = _gen_trimesh(3, 5)
1740
+
1741
+ geo = fem.Trimesh2D(tri_vertex_indices=tri_vidx, positions=positions)
1742
+
1743
+ curl_space = fem.make_polynomial_space(geo, element_basis=fem.ElementBasis.NEDELEC_FIRST_KIND)
1744
+ curl_test = fem.make_test(curl_space)
1745
+
1746
+ fem.integrate(_expect_pure_curl, fields={"field": curl_test}, assembly="generic")
1747
+
1748
+ curl_field = curl_space.make_field()
1749
+ curl_field.dof_values.fill_(1.0)
1750
+ fem.interpolate(
1751
+ _expect_pure_curl, quadrature=fem.RegularQuadrature(fem.Cells(geo), order=2), fields={"field": curl_field}
1752
+ )
1753
+
1754
+ fem.interpolate(
1755
+ _expect_tangential_continuity,
1756
+ quadrature=fem.RegularQuadrature(fem.Sides(geo), order=2),
1757
+ fields={"field": curl_field.trace()},
1758
+ )
1759
+
1760
+ div_space = fem.make_polynomial_space(geo, element_basis=fem.ElementBasis.RAVIART_THOMAS)
1761
+ div_test = fem.make_test(div_space)
1762
+
1763
+ fem.integrate(_expect_pure_spherical, fields={"field": div_test}, assembly="generic")
1764
+
1765
+ div_field = div_space.make_field()
1766
+ div_field.dof_values.fill_(1.0)
1767
+ fem.interpolate(
1768
+ _expect_pure_spherical,
1769
+ quadrature=fem.RegularQuadrature(fem.Cells(geo), order=2),
1770
+ fields={"field": div_field},
1771
+ )
1772
+
1773
+ fem.interpolate(
1774
+ _expect_normal_continuity,
1775
+ quadrature=fem.RegularQuadrature(fem.Sides(geo), order=2),
1776
+ fields={"field": div_field.trace()},
1777
+ )
1778
+
1779
+ with wp.ScopedDevice(device):
1780
+ positions, tet_vidx = _gen_tetmesh(3, 5, 7)
1781
+
1782
+ geo = fem.Tetmesh(tet_vertex_indices=tet_vidx, positions=positions)
1783
+
1784
+ curl_space = fem.make_polynomial_space(geo, element_basis=fem.ElementBasis.NEDELEC_FIRST_KIND)
1785
+ curl_test = fem.make_test(curl_space)
1786
+
1787
+ fem.integrate(_expect_pure_curl, fields={"field": curl_test}, assembly="generic")
1788
+
1789
+ curl_field = curl_space.make_field()
1790
+ curl_field.dof_values.fill_(1.0)
1791
+ fem.interpolate(
1792
+ _expect_pure_curl, quadrature=fem.RegularQuadrature(fem.Cells(geo), order=2), fields={"field": curl_field}
1793
+ )
1794
+
1795
+ fem.interpolate(
1796
+ _expect_tangential_continuity,
1797
+ quadrature=fem.RegularQuadrature(fem.Sides(geo), order=1),
1798
+ fields={"field": curl_field.trace()},
1799
+ )
1800
+
1801
+ div_space = fem.make_polynomial_space(geo, element_basis=fem.ElementBasis.RAVIART_THOMAS)
1802
+ div_test = fem.make_test(div_space)
1803
+
1804
+ fem.integrate(_expect_pure_spherical, fields={"field": div_test}, assembly="generic")
1805
+
1806
+ div_field = div_space.make_field()
1807
+ div_field.dof_values.fill_(1.0)
1808
+ fem.interpolate(
1809
+ _expect_pure_spherical,
1810
+ quadrature=fem.RegularQuadrature(fem.Cells(geo), order=2),
1811
+ fields={"field": div_field},
1812
+ )
1813
+
1814
+ fem.interpolate(
1815
+ _expect_normal_continuity,
1816
+ quadrature=fem.RegularQuadrature(fem.Sides(geo), order=0),
1817
+ fields={"field": div_field.trace()},
1818
+ )
1819
+
1820
+
1511
1821
  @wp.kernel
1512
1822
  def test_qr_eigenvalues():
1513
1823
  tol = 1.0e-8
@@ -1588,6 +1898,28 @@ def test_qr_inverse():
1588
1898
  wp.expect_near(wp.ddot(Err, Err), 0.0, tol)
1589
1899
 
1590
1900
 
1901
+ def test_array_axpy(test, device):
1902
+ N = 10
1903
+ alpha = 0.5
1904
+ beta = 4.0
1905
+
1906
+ x = wp.full(N, 2.0, device=device, dtype=float, requires_grad=True)
1907
+ y = wp.array(np.arange(N), device=device, dtype=wp.float64, requires_grad=True)
1908
+
1909
+ tape = wp.Tape()
1910
+ with tape:
1911
+ fem.utils.array_axpy(x=x, y=y, alpha=alpha, beta=beta)
1912
+
1913
+ assert_np_equal(x.numpy(), np.full(N, 2.0))
1914
+ assert_np_equal(y.numpy(), alpha * x.numpy() + beta * np.arange(N))
1915
+
1916
+ y.grad.fill_(1.0)
1917
+ tape.backward()
1918
+
1919
+ assert_np_equal(x.grad.numpy(), alpha * np.ones(N))
1920
+ assert_np_equal(y.grad.numpy(), beta * np.ones(N))
1921
+
1922
+
1591
1923
  devices = get_test_devices()
1592
1924
  cuda_devices = get_selected_cuda_test_devices()
1593
1925
 
@@ -1612,13 +1944,21 @@ add_function_test(TestFem, "test_hex_mesh", test_hex_mesh, devices=devices)
1612
1944
  add_function_test(TestFem, "test_nanogrid", test_nanogrid, devices=cuda_devices)
1613
1945
  add_function_test(TestFem, "test_adaptive_nanogrid", test_adaptive_nanogrid, devices=cuda_devices)
1614
1946
  add_function_test(TestFem, "test_deformed_geometry", test_deformed_geometry, devices=devices)
1947
+ add_function_test(TestFem, "test_vector_spaces", test_vector_spaces, devices=devices)
1615
1948
  add_function_test(TestFem, "test_dof_mapper", test_dof_mapper)
1616
1949
  add_function_test(TestFem, "test_point_basis", test_point_basis)
1617
1950
  add_function_test(TestFem, "test_particle_quadratures", test_particle_quadratures)
1618
1951
  add_function_test(TestFem, "test_nodal_quadrature", test_nodal_quadrature)
1619
1952
  add_function_test(TestFem, "test_implicit_fields", test_implicit_fields)
1620
- add_kernel_test(TestFem, test_qr_eigenvalues, dim=1, devices=devices)
1621
- add_kernel_test(TestFem, test_qr_inverse, dim=100, devices=devices)
1953
+
1954
+
1955
+ class TestFemUtilities(unittest.TestCase):
1956
+ pass
1957
+
1958
+
1959
+ add_kernel_test(TestFemUtilities, test_qr_eigenvalues, dim=1, devices=devices)
1960
+ add_kernel_test(TestFemUtilities, test_qr_inverse, dim=100, devices=devices)
1961
+ add_function_test(TestFemUtilities, "test_array_axpy", test_array_axpy)
1622
1962
 
1623
1963
 
1624
1964
  class TestFemShapeFunctions(unittest.TestCase):