cuwave 0.7.1__tar.gz → 0.7.2__tar.gz

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.
Files changed (39) hide show
  1. {cuwave-0.7.1 → cuwave-0.7.2}/PKG-INFO +1 -1
  2. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/boundary.py +12 -0
  3. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/decomposition.py +3 -1
  4. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/elastic.py +87 -25
  5. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/kernels/common.cuh +256 -10
  6. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/kernels/elastic.cu +317 -338
  7. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/kernels/elastic_sensitivity.cu +30 -37
  8. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/kernels/maxwell.cu +29 -17
  9. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/kernels/maxwell_sensitivity.cu +20 -8
  10. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/kernels/scalar.cu +180 -75
  11. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/kernels/scalar_sensitivity.cu +190 -152
  12. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/maxwell.py +8 -3
  13. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/scalar.py +13 -6
  14. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/sensitivity.py +2 -1
  15. cuwave-0.7.2/cuwave/stencils.py +453 -0
  16. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/wave.py +35 -12
  17. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave.egg-info/PKG-INFO +1 -1
  18. {cuwave-0.7.1 → cuwave-0.7.2}/pyproject.toml +1 -1
  19. cuwave-0.7.1/cuwave/stencils.py +0 -80
  20. {cuwave-0.7.1 → cuwave-0.7.2}/LICENSE +0 -0
  21. {cuwave-0.7.1 → cuwave-0.7.2}/README.md +0 -0
  22. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/__init__.py +0 -0
  23. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/anisotropic.py +0 -0
  24. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/evals.py +0 -0
  25. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/geometry.py +0 -0
  26. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/kernels/__init__.py +0 -0
  27. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/kernels/anisotropic.cu +0 -0
  28. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/kernels/anisotropic_sensitivity.cu +0 -0
  29. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/nn.py +0 -0
  30. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/optimization.py +0 -0
  31. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/postprocessing.py +0 -0
  32. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/regularization.py +0 -0
  33. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/signals.py +0 -0
  34. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave/utils.py +0 -0
  35. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave.egg-info/SOURCES.txt +0 -0
  36. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave.egg-info/dependency_links.txt +0 -0
  37. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave.egg-info/requires.txt +0 -0
  38. {cuwave-0.7.1 → cuwave-0.7.2}/cuwave.egg-info/top_level.txt +0 -0
  39. {cuwave-0.7.1 → cuwave-0.7.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cuwave
3
- Version: 0.7.1
3
+ Version: 0.7.2
4
4
  Summary: GPU finite-difference wave solver with differentiable adjoints
5
5
  Author-email: Leon Herrmann <herrmann.leon@pm.me>
6
6
  License-Expression: MIT
@@ -92,6 +92,18 @@ def face_mask(sim: Simulation, condition: BoundaryCondition) -> np.int32:
92
92
  return np.int32(sum(1 << f for f in faces_with(sim, condition)))
93
93
 
94
94
 
95
+ def require_off_held_walls(sim: Simulation, position: cpt.NDArray[cp.int32]) -> None:
96
+ """Raise if a source at `position` sits on the wall node of a `Dirichlet` face, which the step holds at zero."""
97
+ rows = position[-sim.ndim :]
98
+ for face in faces_with(sim, Dirichlet):
99
+ d, side = divmod(face, 2)
100
+ wall = sim.Nx[d] - 2 if side else 1
101
+ if bool(cp.any(rows[d] == wall)):
102
+ raise ValueError(
103
+ f"a source on the wall node {wall} of Dirichlet face {face}, held at zero"
104
+ )
105
+
106
+
95
107
  def canonical_boundary(
96
108
  boundary: BoundaryCondition | Sequence | None,
97
109
  ndim: int,
@@ -20,7 +20,7 @@ import cupy as cp
20
20
  import cupy.typing as cpt
21
21
  import numpy as np
22
22
 
23
- from .boundary import _canonical_faces, pml_widths
23
+ from .boundary import _canonical_faces, pml_widths, require_off_held_walls
24
24
 
25
25
  if TYPE_CHECKING:
26
26
  from .wave import Simulation
@@ -295,6 +295,8 @@ def decomposable(function: Callable) -> Callable:
295
295
  def run(*args, **kwargs):
296
296
  bound = signature.bind(*args, **kwargs)
297
297
  sim = bound.arguments["sim"]
298
+ # checked whole, since a subdomain's walls at the cuts are halo rows
299
+ require_off_held_walls(sim, bound.arguments["source"].position)
298
300
  if sim.devices is None:
299
301
  return function(*args, **kwargs)
300
302
  if bound.arguments.get("record_every") is not None:
@@ -6,7 +6,8 @@ axes. Each is a pure per-axis staggered difference, which is what carries the cr
6
6
  terms a per-axis flux cannot and keeps the cost linear in the stencil radius, where the
7
7
  cell gather of `AnisotropicElasticWave` pays `(2r)**(2 ndim)`. The operator is
8
8
  `-B^T C B` with the material sampled on the stress points, so it stays the exact
9
- transpose at every order.
9
+ transpose at every order. Its walls are the closures of a staggered SBP pair, whose norms
10
+ weigh the stress and component points, so the walls keep the interior order.
10
11
  """
11
12
 
12
13
  from __future__ import annotations
@@ -23,13 +24,13 @@ import numpy as np
23
24
  import numpy.typing as npt
24
25
 
25
26
  from .boundary import Clamped, Traction, face_mask, faces_with
27
+ from .stencils import CLOSURES, closure_band, closure_norms, closure_reach
26
28
  from .wave import (
27
29
  PAIRS,
28
30
  Blocked,
29
31
  LaunchSlots,
30
32
  Simulation,
31
33
  Source,
32
- apply_cell_weights,
33
34
  axis_geometry,
34
35
  blocked_launches,
35
36
  compile_kernels,
@@ -41,7 +42,6 @@ from .wave import (
41
42
  point_average_adjoint,
42
43
  tile_counts,
43
44
  tile_lists,
44
- wall_weights,
45
45
  )
46
46
 
47
47
  KERNEL_PATH = Path(__file__).parent / "kernels" / "elastic.cu"
@@ -138,6 +138,19 @@ def zener(relaxation: Relaxation, dt: float) -> Zener:
138
138
  return Zener(rates, weights, float(np.real(1.0 / np.sqrt(modulus)) ** 2))
139
139
 
140
140
 
141
+ def _band_free_origin(n: int, tile: int, core: int, halo: int, band: int) -> int | None:
142
+ """The latest first core start, 1 or before, that puts no blocked tile's edge inside a wall's closure band."""
143
+ for origin in range(1, 1 - core, -1):
144
+ count = -(-(n - 1 - origin) // core)
145
+ firsts = [origin + b * core - halo for b in range(1, count)]
146
+ lasts = [first + tile - 1 for first in [origin - halo, *firsts][:-1]]
147
+ if all(f <= 1 or f > band for f in firsts) and all(
148
+ e >= n - 2 or e < n - 2 - band for e in lasts
149
+ ):
150
+ return origin
151
+ return None
152
+
153
+
141
154
  # ------------------------------- discretization setup --------------------------------
142
155
  @dataclass
143
156
  class ElasticWave(Simulation):
@@ -206,6 +219,16 @@ class ElasticWave(Simulation):
206
219
  if condition not in (Traction, Clamped):
207
220
  raise ValueError(f"elastic faces are Traction or Clamped: {pair}")
208
221
 
222
+ # the order the walls close at, and room for both closures
223
+ if self.space_order not in CLOSURES:
224
+ raise ValueError(f"the walls close up to order 8: {self.space_order}")
225
+ least = 2 * closure_band(self.space_order, self.stencil) + 3
226
+ if min(self.Nx) < least:
227
+ raise ValueError(
228
+ f"Nx {self.Nx} at order {self.space_order} needs {least} nodes an "
229
+ f"axis for both closures"
230
+ )
231
+
209
232
  # the relaxation, fitted to the step's dt
210
233
  if self.relaxation is not None:
211
234
  if self.pml is not None:
@@ -255,14 +278,32 @@ class ElasticWave(Simulation):
255
278
  return 1.0 if self.zener is None else self.zener.unrelaxed
256
279
 
257
280
  def inverse_inertia(self, indicator: cpt.NDArray) -> cpt.NDArray:
258
- """Nodal `1 / (gamma rho0 W)`, what a sponge scales its damping by."""
259
- mass = (
260
- indicator
261
- * self.density
262
- * apply_cell_weights(self, cp.ones(self.Nx_padded, dtype=self.dtype))
263
- )
281
+ """Nodal `1 / (gamma rho0)`, the inertia the update damps by and a sponge scales its damping by."""
282
+ mass = indicator * self.density
264
283
  return 1.0 / cp.maximum(mass, cp.finfo(self.dtype).tiny)
265
284
 
285
+ def point_weights(self, half: tuple[int, ...]) -> cpt.NDArray:
286
+ """The norm W of the points on the half grid of the axes `half` and on the nodes of the rest."""
287
+ w = cp.ones(self.Nx_padded, dtype=self.dtype)
288
+ for d, line in enumerate(self._norm_lines(half)):
289
+ shape = [1] * self.ndim
290
+ shape[d] = -1
291
+ w *= cp.asarray(line.reshape(shape), dtype=self.dtype)
292
+ return w
293
+
294
+ def _norm_lines(self, half: tuple[int, ...]) -> list[npt.NDArray[np.float64]]:
295
+ """Per axis the closure norms along its padded extent, 1 on the ghosts and past the walls."""
296
+ lines = []
297
+ for d, n in enumerate(self.Nx):
298
+ node, half_points = closure_norms(self.space_order, int(n) - 3)
299
+ line = np.ones(self.Nx_padded[d])
300
+ if d in half:
301
+ line[1 : n - 2] = half_points
302
+ else:
303
+ line[1 : n - 1] = node
304
+ lines.append(line)
305
+ return lines
306
+
266
307
  def build_materials(self, indicator: cpt.NDArray) -> dict:
267
308
  """The design field alone: the kernels average it onto the stress and component points."""
268
309
  mat = {"gamma": cp.ascontiguousarray(indicator, dtype=self.dtype)}
@@ -273,17 +314,17 @@ class ElasticWave(Simulation):
273
314
  def point_inverse_inertia(
274
315
  self, gamma: cpt.NDArray, lin_index: cpt.NDArray[cp.int32]
275
316
  ) -> cpt.NDArray:
276
- """`1 / (rho0 W mean gamma)` on the component points `lin_index`, as `point_minv` forms it."""
317
+ """`1 / (rho0 W mean gamma)` on the component points `lin_index`, the inverse mass a point load divides by.
318
+
319
+ The kernels' `point_minv` leaves the norm W out, which the summation by parts
320
+ cancels against the stresses' (`_point_norms`), so it is this times W.
321
+ """
277
322
  component, node = lin_index // self.comp_stride, lin_index % self.comp_stride
278
323
  rows = [node // s % n for s, n in zip(self.strides, self.Nx_padded)]
279
324
  step = cp.asarray(self.strides, dtype=lin_index.dtype)[component]
280
325
  flat = gamma.ravel()
281
326
  mean = 0.5 * (flat[node] + flat[node + step])
282
- weight = cp.ones(lin_index.shape, dtype=self.dtype)
283
- for d, a in enumerate(rows):
284
- for index in (1, self.Nx[d] - 2):
285
- weight = cp.where((a == index) & (component != d), 0.5 * weight, weight)
286
- mass = self.density * weight * mean
327
+ mass = self.density * self._point_norms(lin_index) * mean
287
328
  minv = 1.0 / cp.maximum(mass, cp.finfo(self.dtype).tiny)
288
329
  for face in faces_with(self, Clamped):
289
330
  d = face // 2
@@ -291,6 +332,17 @@ class ElasticWave(Simulation):
291
332
  minv = cp.where((rows[d] == wall) & (component != d), 0.0, minv)
292
333
  return minv.astype(self.dtype)
293
334
 
335
+ def _point_norms(self, lin_index: cpt.NDArray[cp.int32]) -> cpt.NDArray:
336
+ """The norm W on the component points `lin_index`: the half points' along its own axis."""
337
+ component, node = np.divmod(lin_index.get(), self.comp_stride)
338
+ nodal = self._norm_lines(())
339
+ half = self._norm_lines(tuple(range(self.ndim)))
340
+ weight = np.ones(len(node))
341
+ for d in range(self.ndim):
342
+ a = node // self.strides[d] % self.Nx_padded[d]
343
+ weight *= np.where(component == d, half[d][a], nodal[d][a])
344
+ return cp.asarray(weight, dtype=self.dtype)
345
+
294
346
  def define_step(self, kernels: cp.RawModule, mat: dict) -> Callable:
295
347
  """Closure launching the stress kernel and then the update over (u0, u1, u2)."""
296
348
  return self._define_pair(kernels, mat, adjoint=False)
@@ -341,6 +393,7 @@ class ElasticWave(Simulation):
341
393
  ring = math.prod(t + self.space_order for t in tile)
342
394
  return (
343
395
  self.ndim >= 2
396
+ and min(tile) > closure_reach(self.space_order, self.stencil)
344
397
  and self.pml is None
345
398
  and self.relaxation is None
346
399
  and fields > device["L2CacheSize"]
@@ -483,7 +536,15 @@ class ElasticWave(Simulation):
483
536
  tile, rows, steps = block
484
537
  halo = steps * reach
485
538
  core = [t - 2 * halo for t in tile]
486
- cores = tile_counts(self.Nx, tile, halo)
539
+
540
+ # the first cores moved until no tile edge cuts a wall's closure band
541
+ band = closure_band(self.space_order, self.stencil)
542
+ origin = [
543
+ _band_free_origin(*axis, halo, band) for axis in zip(self.Nx, tile, core)
544
+ ]
545
+ if None in origin:
546
+ return None
547
+ cores = tile_counts(self.Nx, tile, halo, origin=origin)
487
548
 
488
549
  # sources on points the step moves, sensors in the interior, none twice
489
550
  sources = flatten_indices(self, source.position)
@@ -509,12 +570,12 @@ class ElasticWave(Simulation):
509
570
 
510
571
  # a tile's sources wherever its nodes reach, its sensors where its core does
511
572
  hits = [
512
- covering_hits(points[0], tile, core, halo, cores),
513
- owner_hits(points[1], points[1], core),
573
+ covering_hits(points[0], tile, core, halo, cores, origin=origin),
574
+ owner_hits(points[1], points[1], core, origin=origin),
514
575
  ]
515
576
  lists = []
516
577
  for hit, layer in zip(hits, layers):
517
- lists += tile_lists(hit, tile, core, halo, cores, layer)
578
+ lists += tile_lists(hit, tile, core, halo, cores, layer, origin=origin)
518
579
 
519
580
  # the kernel and its arguments, the pointers swapped per launch
520
581
  flags = (f"-DSTEPS={steps}", f"-DTILE0={tile[0]}", f"-DTILE1={tile[1]}")
@@ -534,7 +595,7 @@ class ElasticWave(Simulation):
534
595
  slots = LaunchSlots(len(args) - 10, len(args) - 7, len(args) - 1, None)
535
596
  args += [face_mask(self, Clamped), np.int32(self.comp_stride)]
536
597
  args += [self.dtype(1.0 / d) for d in self.dx]
537
- args += axis_geometry(self, self.step_factors())
598
+ args += [*axis_geometry(self, self.step_factors()), *map(np.int32, origin)]
538
599
  launches = [(kernel, tuple(cores[::-1]), None)]
539
600
  return blocked_launches(steps, launches, (tile[1], rows), shared, args, slots)
540
601
 
@@ -614,9 +675,10 @@ class ElasticWave(Simulation):
614
675
  minv = self.point_inverse_inertia(mat["gamma"], lin_index)
615
676
  weight = minv * volume
616
677
  if self.damping is not None:
678
+ # the kernel's beta, whose inertia leaves the norm out
617
679
  node = lin_index % np.int32(self.comp_stride)
618
- beta = 0.5 * minv * mat["damping"].ravel()[node] * self.dt
619
- weight = weight / (1.0 + beta)
680
+ beta = 0.5 * minv * self._point_norms(lin_index)
681
+ weight = weight / (1.0 + beta * mat["damping"].ravel()[node] * self.dt)
620
682
  return (self.dtype(self.dt**2 * self.source_factor()) * weight).astype(
621
683
  self.dtype
622
684
  )
@@ -655,12 +717,12 @@ class ElasticWave(Simulation):
655
717
  gamma = grads["design"]
656
718
  g_mass = cp.zeros(self.Nx_padded, dtype=self.dtype)
657
719
  for c in range(self.ncomp):
658
- density = grads["mass"][c] * wall_weights(self, (c,)) * self.density
720
+ density = grads["mass"][c] * self.point_weights((c,)) * self.density
659
721
  g_mass += point_average_adjoint(self, density, c)
660
722
  # the normal density sits on the nodes, so its chain rule is the weight alone
661
- g_stiff = apply_cell_weights(self, grads["normal"].copy())
723
+ g_stiff = grads["normal"] * self.point_weights(())
662
724
  for p, axes in enumerate(PAIRS[self.ndim][self.ndim :]):
663
- density = grads["shear"][p] * wall_weights(self, axes)
725
+ density = grads["shear"][p] * self.point_weights(axes)
664
726
  g_stiff += pair_average_adjoint(self, density, gamma, axes)
665
727
  return {"mass": g_mass, "stiff": g_stiff}
666
728
 
@@ -5,6 +5,8 @@
5
5
  // STENCIL_RADIUS
6
6
  // OP_COEFFS
7
7
  // STAG_COEFFS
8
+ // NODE_CLOSURE, HALF_CLOSURE, NODE_SPAN, HALF_SPAN, NODE_ROWS_C, HALF_ROWS_C
9
+ // DIRICHLET_FACES (default 0)
8
10
 
9
11
  #ifdef USE_FLOAT
10
12
  typedef float real_t;
@@ -12,8 +14,8 @@ typedef float real_t;
12
14
  #define FMA_RN __fmaf_rn
13
15
  #else
14
16
  typedef double real_t;
15
- #define MUL_RN(a, b) ((a) * (b)) // left to nvcc, as only float32 blocks steps
16
- #define FMA_RN(a, b, c) ((a) * (b) + (c))
17
+ #define MUL_RN __dmul_rn // pinned too, so code around it rounds nothing anew
18
+ #define FMA_RN __fma_rn
17
19
  #endif
18
20
 
19
21
  // ----------------------------- finite difference helpers
@@ -23,18 +25,105 @@ typedef double real_t;
23
25
 
24
26
  #if STENCIL_RADIUS == 1
25
27
  #define OP_W(r, k) ((real_t)1)
26
- #define CLOSURE(a, N) 1
27
28
  #define SG_W(r, k) ((real_t)1)
28
29
  #else
29
30
  // cache
30
31
  __constant__ real_t OP_C[STENCIL_RADIUS][STENCIL_RADIUS] = OP_COEFFS;
31
32
  #define OP_W(r, k) OP_C[(r) - 1][(k) - 1] // 1-indexed adjustment
32
- // grade radius down towards wall so the stencil never reaches past ghost nodes
33
- #define CLOSURE(a, N) min(STENCIL_RADIUS, min(a, (N) - 1 - (a)))
34
33
  __constant__ real_t SG_C[STENCIL_RADIUS][STENCIL_RADIUS] = STAG_COEFFS;
35
34
  #define SG_W(r, k) SG_C[(r) - 1][(k) - 1] // 1-indexed adjustment
36
35
  #endif
37
36
 
37
+ // ---------------------------------- mirrored taps
38
+ #ifndef DIRICHLET_FACES
39
+ #define DIRICHLET_FACES 0
40
+ #endif
41
+
42
+ // whether node a is the wall node of a face of `odd` (bit 0 low, bit 1 high)
43
+ __device__ __forceinline__ bool held_wall(const int a, const int N,
44
+ const int odd) {
45
+ return (odd & 1 && a == 1) || (odd & 2 && a == N - 2);
46
+ }
47
+
48
+ // node b of an axis of N nodes, past the stored ghost its mirror about the
49
+ // wall node, odd past a face of `odd`
50
+ __device__ __forceinline__ int mirror_node(const int b, const int N,
51
+ const int odd, int &sign) {
52
+ sign = (b < 0 && odd & 1) || (b > N - 1 && odd & 2) ? -1 : 1;
53
+ return b < 0 ? 2 - b : b > N - 1 ? 2 * (N - 2) - b : b;
54
+ }
55
+
56
+ // a value read off a mirror with its sign
57
+ __device__ __forceinline__ real_t signed_value(const real_t v, const int sign) {
58
+ return sign > 0 ? v : sign < 0 ? -v : (real_t)0;
59
+ }
60
+
61
+ // u at node a + j of the line through idx, read through mirror_node
62
+ __device__ __forceinline__ real_t mirrored(const real_t *__restrict__ u,
63
+ const int idx, const int s,
64
+ const int a, const int N,
65
+ const int odd, const int j) {
66
+ int sign;
67
+ const int b = mirror_node(a + j, N, odd, sign);
68
+ return signed_value(u[idx + (b - a) * s], sign);
69
+ }
70
+
71
+ // node b past the wall node its image, for a field no refill keeps beyond it:
72
+ // odd across a face of `odd`
73
+ __device__ __forceinline__ int node_image(const int b, const int N,
74
+ const int odd, int &sign) {
75
+ sign = (b < 1 && odd & 1) || (b > N - 2 && odd & 2) ? -1 : 1;
76
+ return b < 1 ? 2 - b : b > N - 2 ? 2 * (N - 2) - b : b;
77
+ }
78
+
79
+ // half point b, half a node up from node b, past the wall its image
80
+ __device__ __forceinline__ int half_image(const int b, const int N,
81
+ const int odd, int &sign) {
82
+ sign = (b < 1 && odd & 1) || (b > N - 3 && odd & 2) ? -1 : 1;
83
+ return b < 1 ? 1 - b : b > N - 3 ? 2 * N - 5 - b : b;
84
+ }
85
+
86
+ // a node-centred field at a + j of the line through idx, through node_image
87
+ __device__ __forceinline__ real_t node_imaged(const real_t *__restrict__ u,
88
+ const int idx, const int s,
89
+ const int a, const int N,
90
+ const int odd, const int j) {
91
+ int sign;
92
+ const int b = node_image(a + j, N, odd, sign);
93
+ return signed_value(u[idx + (b - a) * s], sign);
94
+ }
95
+
96
+ // a half-point field at a + j of the line through idx, through half_image
97
+ __device__ __forceinline__ real_t half_imaged(const real_t *__restrict__ u,
98
+ const int idx, const int s,
99
+ const int a, const int N,
100
+ const int odd, const int j) {
101
+ int sign;
102
+ const int b = half_image(a + j, N, odd, sign);
103
+ return signed_value(u[idx + (b - a) * s], sign);
104
+ }
105
+
106
+ // t[j - first] = a field at a + j, j = first .. first + 2R - 1, read in place
107
+ // where every tap lies on stored points `lo .. hi`, else through its image
108
+ template <bool HALF>
109
+ __device__ __forceinline__ void staged_taps(real_t *t,
110
+ const real_t *__restrict__ u,
111
+ const int idx, const int s,
112
+ const int a, const int N,
113
+ const int odd, const int first,
114
+ const int lo, const int hi) {
115
+ if (a + first >= lo && a + first + 2 * STENCIL_RADIUS - 1 <= hi) {
116
+ #pragma unroll
117
+ for (int j = 0; j < 2 * STENCIL_RADIUS; ++j)
118
+ t[j] = u[idx + (first + j) * s];
119
+ } else {
120
+ #pragma unroll
121
+ for (int j = 0; j < 2 * STENCIL_RADIUS; ++j)
122
+ t[j] = HALF ? half_imaged(u, idx, s, a, N, odd, first + j)
123
+ : node_imaged(u, idx, s, a, N, odd, first + j);
124
+ }
125
+ }
126
+
38
127
  // --------------------------------- staggered lattice
39
128
  #define NPAIRS (NDIM * (NDIM - 1) / 2)
40
129
  #define NVOIGT (NDIM + NPAIRS)
@@ -44,11 +133,6 @@ __constant__ real_t SG_C[STENCIL_RADIUS][STENCIL_RADIUS] = STAG_COEFFS;
44
133
  #define PAIR_ROW(k, l) 2
45
134
  #endif
46
135
 
47
- // radius of the node-centred derivative of a staggered field, zero on the wall
48
- __device__ __forceinline__ int rad_node(const int a, const int N) {
49
- return min(STENCIL_RADIUS, min(a - 1, N - 2 - a));
50
- }
51
-
52
136
  // radius of a half-point derivative, whose taps may sit on the wall
53
137
  __device__ __forceinline__ int rad_half(const int a, const int N) {
54
138
  return min(STENCIL_RADIUS, min(a, N - 2 - a));
@@ -65,6 +149,154 @@ __device__ __forceinline__ bool clamped_face(const int clamped, const int d,
65
149
  return (clamped >> (2 * d + side)) & 1;
66
150
  }
67
151
 
152
+ // ---------------------------------- staggered closure
153
+ #ifdef NODE_CLOSURE
154
+ // the low wall's rows of the staggered SBP pair, row 0 the wall's; the high
155
+ // wall reads them mirrored and negated
156
+ __device__ const real_t NODE_ROWS[NODE_CLOSURE][NODE_SPAN] = NODE_ROWS_C;
157
+ __device__ const real_t HALF_ROWS[HALF_CLOSURE][HALF_SPAN] = HALF_ROWS_C;
158
+ #define CLOSURE_BAND \
159
+ (NODE_CLOSURE > HALF_CLOSURE ? NODE_CLOSURE : HALF_CLOSURE)
160
+
161
+ // the axes along which a node lies past both walls' closure bands, a bit each
162
+ __device__ __forceinline__ int deep_axes(const int *A, const int *NN) {
163
+ int deep = 0;
164
+ #pragma unroll
165
+ for (int d = 0; d < NDIM; ++d)
166
+ deep |= (A[d] > CLOSURE_BAND && A[d] < NN[d] - 2 - CLOSURE_BAND) << d;
167
+ return deep;
168
+ }
169
+
170
+ // every axis deep, or every one but the fast axis
171
+ #define DEEP_ALL ((1 << NDIM) - 1)
172
+ #define DEEP_SLOW (DEEP_ALL >> 1)
173
+
174
+ // the deep axes the warp's active lanes share, as DEEP_ALL, DEEP_SLOW or none,
175
+ // so the warp runs one path
176
+ __device__ __forceinline__ int warp_deep(const int *A, const int *NN) {
177
+ const unsigned lanes = __activemask();
178
+ const int deep = deep_axes(A, NN);
179
+ if (__all_sync(lanes, deep == DEEP_ALL))
180
+ return DEEP_ALL;
181
+ return __all_sync(lanes, (deep & DEEP_SLOW) == DEEP_SLOW) ? DEEP_SLOW : 0;
182
+ }
183
+
184
+ // f with the template mask of the deep axes a warp_deep result names
185
+ #define BY_DEEP(deep, f, ...) \
186
+ ((deep) == DEEP_ALL ? f<DEEP_ALL>(__VA_ARGS__) \
187
+ : (deep) == DEEP_SLOW ? f<DEEP_SLOW>(__VA_ARGS__) \
188
+ : f(__VA_ARGS__))
189
+
190
+ // a line through a field: u(j) the value j points along the axis from idx
191
+ struct Line {
192
+ const real_t *__restrict__ p;
193
+ int idx, s;
194
+ __device__ real_t operator()(const int j) const { return p[idx + j * s]; }
195
+ };
196
+
197
+ // the closure rows off either wall
198
+ template <class L>
199
+ __device__ __forceinline__ real_t node_rows(const L &u, const int lo,
200
+ const int hi) {
201
+ real_t acc = (real_t)0;
202
+ if (lo < NODE_CLOSURE) {
203
+ #pragma unroll
204
+ for (int m = 0; m < NODE_SPAN; ++m)
205
+ acc += NODE_ROWS[lo][m] * u(m - lo);
206
+ } else {
207
+ #pragma unroll
208
+ for (int m = 0; m < NODE_SPAN; ++m)
209
+ acc -= NODE_ROWS[hi][m] * u(hi - 1 - m);
210
+ }
211
+ return acc;
212
+ }
213
+
214
+ template <class L>
215
+ __device__ __forceinline__ real_t half_rows(const L &u, const int lo,
216
+ const int hi) {
217
+ real_t acc = (real_t)0;
218
+ if (lo < HALF_CLOSURE) {
219
+ #pragma unroll
220
+ for (int i = 0; i < HALF_SPAN; ++i)
221
+ acc += HALF_ROWS[lo][i] * u(i - lo);
222
+ } else {
223
+ #pragma unroll
224
+ for (int i = 0; i < HALF_SPAN; ++i)
225
+ acc -= HALF_ROWS[hi][i] * u(hi + 1 - i);
226
+ }
227
+ return acc;
228
+ }
229
+
230
+ // d/dx at node a of a half-point field along a line; past a wall it reads the
231
+ // half-grid wall point as zero; deep (a constant where it is set) skips the
232
+ // closure rows
233
+ template <class L>
234
+ __device__ __forceinline__ real_t node_derivative(const L &u, const int a,
235
+ const int N,
236
+ const bool deep = false) {
237
+ const int lo = a - 1, hi = N - 2 - a;
238
+ if (!deep && (lo < NODE_CLOSURE || hi < NODE_CLOSURE))
239
+ return node_rows(u, lo, hi);
240
+ real_t acc = (real_t)0;
241
+ #pragma unroll
242
+ for (int k = 1; k <= STENCIL_RADIUS; ++k)
243
+ acc += SG_W(STENCIL_RADIUS, k) * (u(k - 1) - u(-k));
244
+ return acc;
245
+ }
246
+
247
+ // d/dx at the half point a (a + 1/2) of a nodal field along a line
248
+ template <class L>
249
+ __device__ __forceinline__ real_t half_derivative(const L &u, const int a,
250
+ const int N,
251
+ const bool deep = false) {
252
+ const int lo = a - 1, hi = N - 3 - a;
253
+ if (!deep && (lo < HALF_CLOSURE || hi < HALF_CLOSURE))
254
+ return half_rows(u, lo, hi);
255
+ real_t acc = (real_t)0;
256
+ #pragma unroll
257
+ for (int k = 1; k <= STENCIL_RADIUS; ++k)
258
+ acc += SG_W(STENCIL_RADIUS, k) * (u(k) - u(1 - k));
259
+ return acc;
260
+ }
261
+
262
+ // the divergence of a nodal stress at the half point a: half_derivative's rows,
263
+ // which are minus node_derivative's transpose in the norms, so the norms cancel
264
+ // against the inertia's; inside, the taps in the order the deep path rounds
265
+ template <class L>
266
+ __device__ __forceinline__ real_t node_divergence(const L &sg, const int a,
267
+ const int N,
268
+ const bool deep = false) {
269
+ const int lo = a - 1, hi = N - 3 - a;
270
+ if (!deep && (lo < HALF_CLOSURE || hi < HALF_CLOSURE))
271
+ return half_rows(sg, lo, hi);
272
+ real_t acc = (real_t)0;
273
+ #pragma unroll
274
+ for (int k = 1; k <= STENCIL_RADIUS; ++k) {
275
+ acc += SG_W(STENCIL_RADIUS, k) * sg(k);
276
+ acc -= SG_W(STENCIL_RADIUS, k) * sg(1 - k);
277
+ }
278
+ return acc;
279
+ }
280
+
281
+ // the divergence of a half-point stress at node a: node_derivative's rows, the
282
+ // stress at the wall point never read
283
+ template <class L>
284
+ __device__ __forceinline__ real_t half_divergence(const L &sg, const int a,
285
+ const int N,
286
+ const bool deep = false) {
287
+ const int lo = a - 1, hi = N - 2 - a;
288
+ if (!deep && (lo < NODE_CLOSURE || hi < NODE_CLOSURE))
289
+ return node_rows(sg, lo, hi);
290
+ real_t acc = (real_t)0;
291
+ #pragma unroll
292
+ for (int k = 1; k <= STENCIL_RADIUS; ++k) {
293
+ acc += SG_W(STENCIL_RADIUS, k) * sg(k - 1);
294
+ acc -= SG_W(STENCIL_RADIUS, k) * sg(-k);
295
+ }
296
+ return acc;
297
+ }
298
+ #endif
299
+
68
300
  // --------------------------------------- pml slabs
69
301
  #if NDIM == 1
70
302
  #define PML_PARAMS const int N0
@@ -140,6 +372,20 @@ __device__ __forceinline__ int slab_index(const int a, const int off,
140
372
  return d < NDIM - 1 ? row * plane + off : off * (lo + hi) + row;
141
373
  }
142
374
 
375
+ // whether the node at idx is the wall node of a face DIRICHLET_FACES names
376
+ __device__ __forceinline__ bool held_node(const int idx, const int *n,
377
+ const int *s) {
378
+ bool held = false;
379
+ int rest = idx;
380
+ #pragma unroll
381
+ for (int e = 0; e < NDIM; ++e) {
382
+ const int ae = rest / s[e];
383
+ rest -= ae * s[e];
384
+ held |= held_wall(ae, n[e], DIRICHLET_FACES >> 2 * e);
385
+ }
386
+ return held;
387
+ }
388
+
143
389
  // ------------------------------- pml memory accessors
144
390
  // component k of a field j nodes along the axis from a node
145
391
  struct Field {