cuwave 0.3.0__tar.gz → 0.6.0__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.
- {cuwave-0.3.0 → cuwave-0.6.0}/PKG-INFO +13 -7
- {cuwave-0.3.0 → cuwave-0.6.0}/README.md +12 -6
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/anisotropic.py +103 -9
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/boundary.py +110 -2
- cuwave-0.6.0/cuwave/decomposition.py +412 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/elastic.py +98 -52
- cuwave-0.6.0/cuwave/kernels/anisotropic.cu +503 -0
- cuwave-0.6.0/cuwave/kernels/common.cuh +264 -0
- cuwave-0.6.0/cuwave/kernels/elastic.cu +667 -0
- cuwave-0.6.0/cuwave/kernels/maxwell.cu +367 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/kernels/scalar.cu +138 -0
- cuwave-0.6.0/cuwave/kernels/scalar_sensitivity.cu +632 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/maxwell.py +47 -16
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/regularization.py +100 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/scalar.py +172 -5
- cuwave-0.6.0/cuwave/sensitivity.py +1072 -0
- cuwave-0.6.0/cuwave/stencils.py +80 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/utils.py +173 -5
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/wave.py +249 -27
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave.egg-info/PKG-INFO +13 -7
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave.egg-info/SOURCES.txt +1 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/pyproject.toml +1 -1
- cuwave-0.3.0/cuwave/kernels/anisotropic.cu +0 -174
- cuwave-0.3.0/cuwave/kernels/common.cuh +0 -111
- cuwave-0.3.0/cuwave/kernels/elastic.cu +0 -225
- cuwave-0.3.0/cuwave/kernels/maxwell.cu +0 -154
- cuwave-0.3.0/cuwave/kernels/scalar_sensitivity.cu +0 -302
- cuwave-0.3.0/cuwave/sensitivity.py +0 -589
- cuwave-0.3.0/cuwave/stencils.py +0 -48
- {cuwave-0.3.0 → cuwave-0.6.0}/LICENSE +0 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/__init__.py +0 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/evals.py +0 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/geometry.py +0 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/kernels/__init__.py +0 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/kernels/anisotropic_sensitivity.cu +0 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/kernels/elastic_sensitivity.cu +0 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/kernels/maxwell_sensitivity.cu +0 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/nn.py +0 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/optimization.py +0 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/postprocessing.py +0 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave/signals.py +0 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave.egg-info/dependency_links.txt +0 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave.egg-info/requires.txt +0 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/cuwave.egg-info/top_level.txt +0 -0
- {cuwave-0.3.0 → cuwave-0.6.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cuwave
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
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
|
|
@@ -31,7 +31,7 @@ Dynamic: license-file
|
|
|
31
31
|
<img src="https://raw.githubusercontent.com/cmpmech/cuwave/main/.assets/readme-light.webp" width="100%">
|
|
32
32
|
</picture>
|
|
33
33
|
|
|
34
|
-
**CuWave** is a single
|
|
34
|
+
**CuWave** is a GPU-accelerated (single or multi), differentiable finite difference wave propagation code.
|
|
35
35
|
Possible applications include
|
|
36
36
|
|
|
37
37
|
<table width="100%">
|
|
@@ -67,13 +67,16 @@ Possible applications include
|
|
|
67
67
|
- see the [documentation](https://github.com/cmpmech/cuwave/blob/main/docs/Home.md) for how the code works (AI-assisted; verify with sources for critical details)
|
|
68
68
|
- see [examples](https://github.com/cmpmech/cuwave/tree/main/examples) for how to apply the code
|
|
69
69
|
|
|
70
|
-
##
|
|
71
|
-
|
|
72
|
-
**Scalar** & **acoustic wave** equations have been developed over the last 2 years and are thoroughly validated.
|
|
70
|
+
## Multiple GPUs
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
Several GPUs take one argument, and the driver otherwise stays as it is:
|
|
73
|
+
- `Simulation(..., devices=(0, 1))` splits **one grid** along its first axis over the listed GPUs, for grids too large or too slow for one. `simulate`, `sensitivity`, `reconstruction_sensitivity` and `superposition_sensitivity` then run split and return the same result as on one GPU, bitwise. Try it with the `DEVICES` setting of
|
|
74
|
+
- [examples/forward/scalarND.py](https://github.com/cmpmech/cuwave/blob/main/examples/forward/scalarND.py), the forward problem in 2D or 3D
|
|
75
|
+
- [examples/sensitivity/scalar2D_sensitivity.py](https://github.com/cmpmech/cuwave/blob/main/examples/sensitivity/scalar2D_sensitivity.py), the gradient
|
|
76
|
+
- `misfit_gradient(..., devices=[0, 1])` (and `measure`, `misfit`) deals the **shots** of an inversion out over the GPUs, which needs no communication and is the faster choice whenever one shot fits one GPU.
|
|
77
|
+
## Development status
|
|
76
78
|
|
|
79
|
+
**Scalar** & **acoustic wave** equations have been developed over the last 2 years and are thoroughly validated. The **elastic wave** and **electromagnetic wave** equations were developed with AI assistance (Claude). The first is validated on experimental data, while the latter are cross-validated against the independent FDTD code [FDTDX](https://github.com/ymahlau/fdtdx).
|
|
77
80
|
## Performance
|
|
78
81
|
|
|
79
82
|
CuWave's runtime for identical discretizations is comparable to that of other established wave propagation finite difference codes. Speedups in 2D with reflecting boundaries (**a ratio above 1 means CuWave is that many times faster**) compared to the following frameworks:
|
|
@@ -105,6 +108,9 @@ If you use our code for your scientific research, please acknowledge this by ref
|
|
|
105
108
|
|
|
106
109
|
_Herrmann, L., Bürchner, T., Kudela, L., Kollmannsberger, S., 2026, **A memory-efficient adjoint method to enable billion parameter optimization on a single GPU in dynamic problems**, Structural and Multidisciplinary Optimization, Volume 69, 52 (2026), DOI: [10.1007/s00158-025-04237-y](https://doi.org/10.1007/s00158-025-04237-y)_
|
|
107
110
|
|
|
111
|
+
## Acknowledgements
|
|
112
|
+
|
|
113
|
+
Ideas adapted from [FDTDX](https://github.com/ymahlau/fdtdx) ([Mahlau, Schubert, Berg & Rosenhahn 2026](https://doi.org/10.21105/joss.08912)), which also serves as the reference for validating the electromagnetic solvers.
|
|
108
114
|
## Contact
|
|
109
115
|
|
|
110
116
|
For questions, bug reports, or collaboration inquiries, please don't hesitate to contact Leon Herrmann at [herrmann.leon@pm.me](mailto:herrmann.leon@pm.me).
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<img src="https://raw.githubusercontent.com/cmpmech/cuwave/main/.assets/readme-light.webp" width="100%">
|
|
6
6
|
</picture>
|
|
7
7
|
|
|
8
|
-
**CuWave** is a single
|
|
8
|
+
**CuWave** is a GPU-accelerated (single or multi), differentiable finite difference wave propagation code.
|
|
9
9
|
Possible applications include
|
|
10
10
|
|
|
11
11
|
<table width="100%">
|
|
@@ -41,13 +41,16 @@ Possible applications include
|
|
|
41
41
|
- see the [documentation](https://github.com/cmpmech/cuwave/blob/main/docs/Home.md) for how the code works (AI-assisted; verify with sources for critical details)
|
|
42
42
|
- see [examples](https://github.com/cmpmech/cuwave/tree/main/examples) for how to apply the code
|
|
43
43
|
|
|
44
|
-
##
|
|
45
|
-
|
|
46
|
-
**Scalar** & **acoustic wave** equations have been developed over the last 2 years and are thoroughly validated.
|
|
44
|
+
## Multiple GPUs
|
|
47
45
|
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
Several GPUs take one argument, and the driver otherwise stays as it is:
|
|
47
|
+
- `Simulation(..., devices=(0, 1))` splits **one grid** along its first axis over the listed GPUs, for grids too large or too slow for one. `simulate`, `sensitivity`, `reconstruction_sensitivity` and `superposition_sensitivity` then run split and return the same result as on one GPU, bitwise. Try it with the `DEVICES` setting of
|
|
48
|
+
- [examples/forward/scalarND.py](https://github.com/cmpmech/cuwave/blob/main/examples/forward/scalarND.py), the forward problem in 2D or 3D
|
|
49
|
+
- [examples/sensitivity/scalar2D_sensitivity.py](https://github.com/cmpmech/cuwave/blob/main/examples/sensitivity/scalar2D_sensitivity.py), the gradient
|
|
50
|
+
- `misfit_gradient(..., devices=[0, 1])` (and `measure`, `misfit`) deals the **shots** of an inversion out over the GPUs, which needs no communication and is the faster choice whenever one shot fits one GPU.
|
|
51
|
+
## Development status
|
|
50
52
|
|
|
53
|
+
**Scalar** & **acoustic wave** equations have been developed over the last 2 years and are thoroughly validated. The **elastic wave** and **electromagnetic wave** equations were developed with AI assistance (Claude). The first is validated on experimental data, while the latter are cross-validated against the independent FDTD code [FDTDX](https://github.com/ymahlau/fdtdx).
|
|
51
54
|
## Performance
|
|
52
55
|
|
|
53
56
|
CuWave's runtime for identical discretizations is comparable to that of other established wave propagation finite difference codes. Speedups in 2D with reflecting boundaries (**a ratio above 1 means CuWave is that many times faster**) compared to the following frameworks:
|
|
@@ -79,6 +82,9 @@ If you use our code for your scientific research, please acknowledge this by ref
|
|
|
79
82
|
|
|
80
83
|
_Herrmann, L., Bürchner, T., Kudela, L., Kollmannsberger, S., 2026, **A memory-efficient adjoint method to enable billion parameter optimization on a single GPU in dynamic problems**, Structural and Multidisciplinary Optimization, Volume 69, 52 (2026), DOI: [10.1007/s00158-025-04237-y](https://doi.org/10.1007/s00158-025-04237-y)_
|
|
81
84
|
|
|
85
|
+
## Acknowledgements
|
|
86
|
+
|
|
87
|
+
Ideas adapted from [FDTDX](https://github.com/ymahlau/fdtdx) ([Mahlau, Schubert, Berg & Rosenhahn 2026](https://doi.org/10.21105/joss.08912)), which also serves as the reference for validating the electromagnetic solvers.
|
|
82
88
|
## Contact
|
|
83
89
|
|
|
84
90
|
For questions, bug reports, or collaboration inquiries, please don't hesitate to contact Leon Herrmann at [herrmann.leon@pm.me](mailto:herrmann.leon@pm.me).
|
|
@@ -23,9 +23,17 @@ import cupy.typing as cpt
|
|
|
23
23
|
import numpy as np
|
|
24
24
|
import numpy.typing as npt
|
|
25
25
|
|
|
26
|
-
from .boundary import Clamped, Traction, faces_with
|
|
26
|
+
from .boundary import Clamped, Traction, faces_with, pml_widths
|
|
27
27
|
from .elastic import voigt
|
|
28
|
-
from .wave import
|
|
28
|
+
from .wave import (
|
|
29
|
+
PAIRS,
|
|
30
|
+
PML_THREADS,
|
|
31
|
+
Simulation,
|
|
32
|
+
apply_cell_weights,
|
|
33
|
+
axis_geometry,
|
|
34
|
+
grid_block,
|
|
35
|
+
slab_grid,
|
|
36
|
+
)
|
|
29
37
|
|
|
30
38
|
KERNEL_PATH = Path(__file__).parent / "kernels" / "anisotropic.cu"
|
|
31
39
|
SENSITIVITY_PATH = Path(__file__).parent / "kernels" / "anisotropic_sensitivity.cu"
|
|
@@ -96,6 +104,21 @@ def cell_stencil(
|
|
|
96
104
|
return K
|
|
97
105
|
|
|
98
106
|
|
|
107
|
+
def backward_axes(C: npt.NDArray, ndim: int, samples: int = 4096) -> list[int]:
|
|
108
|
+
"""Axes along which a wave of the Voigt `C` runs against its phase, sampled over directions."""
|
|
109
|
+
row = np.zeros((ndim, ndim), dtype=int)
|
|
110
|
+
for r, (k, m) in enumerate(PAIRS[ndim]):
|
|
111
|
+
row[k, m] = row[m, k] = r
|
|
112
|
+
T = C[row[:, :, None, None], row[None, None]]
|
|
113
|
+
n = np.random.default_rng(0).standard_normal((samples, ndim))
|
|
114
|
+
n /= np.linalg.norm(n, axis=1, keepdims=True)
|
|
115
|
+
_, v = np.linalg.eigh(np.einsum("ikjl,nk,nl->nij", T, n, n))
|
|
116
|
+
# the group velocity of each mode, up to its positive 1 / (2 omega)
|
|
117
|
+
group = np.einsum("nip,imjl,nl,njp->npm", v, T + T.transpose(0, 3, 2, 1), n, v)
|
|
118
|
+
group /= np.linalg.norm(group, axis=2, keepdims=True)
|
|
119
|
+
return [d for d in range(ndim) if (n[:, None, d] * group[..., d]).min() < -1e-6]
|
|
120
|
+
|
|
121
|
+
|
|
99
122
|
def cell_average(sim: Simulation, field: cpt.NDArray) -> cpt.NDArray:
|
|
100
123
|
"""Harmonic mean of `field` over the `2**ndim` corners of a cell, at its low corner.
|
|
101
124
|
|
|
@@ -137,6 +160,9 @@ class AnisotropicElasticWave(Simulation):
|
|
|
137
160
|
sensitivity_path = SENSITIVITY_PATH
|
|
138
161
|
default_boundary = Traction
|
|
139
162
|
gradient_names = ("mass", "stiff", "cell")
|
|
163
|
+
accepts_pml = True
|
|
164
|
+
pml_shifted = True
|
|
165
|
+
pml_walls = (Clamped,)
|
|
140
166
|
|
|
141
167
|
@property
|
|
142
168
|
def ncomp(self) -> int:
|
|
@@ -172,6 +198,14 @@ class AnisotropicElasticWave(Simulation):
|
|
|
172
198
|
raise ValueError(f"C must be ({nvoigt}, {nvoigt}): {self.C.shape}")
|
|
173
199
|
if not np.allclose(self.C, self.C.T):
|
|
174
200
|
raise ValueError("C must be symmetric")
|
|
201
|
+
widths = pml_widths(self) if self.pml else []
|
|
202
|
+
lined = [d for d, w in enumerate(widths) if sum(w)]
|
|
203
|
+
backward = [d for d in backward_axes(self.C, self.ndim) if d in lined]
|
|
204
|
+
if backward:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"C carries a wave against its phase along axes {backward}, which "
|
|
207
|
+
f"a pml grows (Becache et al. 2003): use sponge"
|
|
208
|
+
)
|
|
175
209
|
|
|
176
210
|
@property
|
|
177
211
|
def lame(self) -> float:
|
|
@@ -183,14 +217,17 @@ class AnisotropicElasticWave(Simulation):
|
|
|
183
217
|
"""Second Lame parameter `rho0 * c_s**2`."""
|
|
184
218
|
return self.density * self.wavespeed_s**2
|
|
185
219
|
|
|
186
|
-
def
|
|
220
|
+
def stiffness_matrix(self) -> npt.NDArray:
|
|
221
|
+
"""The Voigt stiffness at `gamma = 1`: `C`, or the isotropic one."""
|
|
222
|
+
if self.C is not None:
|
|
223
|
+
return self.C
|
|
224
|
+
return voigt(self.ndim, self.lame, self.shear, self.plane)
|
|
225
|
+
|
|
226
|
+
def stencil_table(self) -> cpt.NDArray:
|
|
187
227
|
"""Stencil table at `gamma = 1`: one cell's `-B^T C B`, flattened for the kernel."""
|
|
188
228
|
if self._stencil is not None:
|
|
189
229
|
return self._stencil
|
|
190
|
-
|
|
191
|
-
if C is None:
|
|
192
|
-
C = voigt(self.ndim, self.lame, self.shear, self.plane)
|
|
193
|
-
K = cell_stencil(self.ndim, self.dx, C)
|
|
230
|
+
K = cell_stencil(self.ndim, self.dx, self.stiffness_matrix())
|
|
194
231
|
self._stencil = cp.asarray(K.ravel(), dtype=self.dtype)
|
|
195
232
|
return self._stencil
|
|
196
233
|
|
|
@@ -215,7 +252,7 @@ class AnisotropicElasticWave(Simulation):
|
|
|
215
252
|
"minv": cp.ascontiguousarray(minv, dtype=self.dtype),
|
|
216
253
|
"gamma": cp.ascontiguousarray(indicator, dtype=self.dtype),
|
|
217
254
|
"cell": cell_average(self, indicator),
|
|
218
|
-
"stencil": self.
|
|
255
|
+
"stencil": self.stencil_table(),
|
|
219
256
|
}
|
|
220
257
|
if self.damping is not None:
|
|
221
258
|
mat["damping"] = self.damping
|
|
@@ -245,6 +282,63 @@ class AnisotropicElasticWave(Simulation):
|
|
|
245
282
|
"""Gamma scales inertia and stiffness alike, so both derivatives are 1."""
|
|
246
283
|
return 1.0, 1.0
|
|
247
284
|
|
|
285
|
+
def define_step(self, kernels: cp.RawModule, mat: dict) -> Callable:
|
|
286
|
+
"""The cell step, followed by its pml terms if there are any."""
|
|
287
|
+
passes = ("pml_cell", "pml_force")
|
|
288
|
+
return self._define_pml(kernels, mat, passes)
|
|
289
|
+
|
|
290
|
+
def define_adjoint_step(
|
|
291
|
+
self, kernels: cp.RawModule, sens_kernels: cp.RawModule, mat: dict
|
|
292
|
+
) -> Callable:
|
|
293
|
+
"""The same step with the pml terms transposed, which breaks its symmetry."""
|
|
294
|
+
passes = ("pml_adjoint_memory", "pml_adjoint_cell", "pml_adjoint_force")
|
|
295
|
+
return self._define_pml(kernels, mat, passes)
|
|
296
|
+
|
|
297
|
+
def _define_pml(self, kernels: cp.RawModule, mat: dict, passes: tuple) -> Callable:
|
|
298
|
+
"""The cell step, then each pass over every lined axis, all axes in one memory."""
|
|
299
|
+
fd_step = super().define_step(kernels, mat)
|
|
300
|
+
if self.pml is None:
|
|
301
|
+
return fd_step
|
|
302
|
+
slabs = self.pml_slabs()
|
|
303
|
+
sizes = [2 * self.ndim * (lo + hi) * plane for _, lo, hi, _, plane in slabs]
|
|
304
|
+
starts = np.cumsum([0, *sizes])
|
|
305
|
+
offsets = np.cumsum([0, *(slab[3].size for slab in slabs)])
|
|
306
|
+
# per axis: rows lo and hi, then where its memory and profile start
|
|
307
|
+
table = np.zeros((self.ndim, 4), dtype=np.int32)
|
|
308
|
+
for k, (d, lo, hi, _, _) in enumerate(slabs):
|
|
309
|
+
table[d] = lo, hi, starts[k], offsets[k]
|
|
310
|
+
cv = float(np.prod(self.dx)) * self.stiffness_matrix().ravel()
|
|
311
|
+
common = [
|
|
312
|
+
cp.zeros(int(starts[-1]), dtype=self.dtype),
|
|
313
|
+
mat["minv"],
|
|
314
|
+
mat["cell"],
|
|
315
|
+
cp.asarray(cv, dtype=self.dtype),
|
|
316
|
+
self.dtype(self.dt**2),
|
|
317
|
+
np.int32(self.comp_stride),
|
|
318
|
+
cp.concatenate([slab[3].ravel() for slab in slabs]),
|
|
319
|
+
cp.asarray(table.ravel()),
|
|
320
|
+
]
|
|
321
|
+
geometry = axis_geometry(self, [self.dtype(1.0 / d) for d in self.dx])
|
|
322
|
+
launches = [
|
|
323
|
+
(
|
|
324
|
+
kernels.get_function(f"{name}_kernel"),
|
|
325
|
+
slab_grid(lo, hi, plane),
|
|
326
|
+
[None, None, *common, np.int32(d), *geometry],
|
|
327
|
+
)
|
|
328
|
+
for name in passes
|
|
329
|
+
for d, lo, hi, _, plane in slabs
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
def pml_step(u0, u1, u2):
|
|
333
|
+
fd_step(u0, u1, u2)
|
|
334
|
+
for kernel, grid, args in launches:
|
|
335
|
+
args[0], args[1] = u1, u2
|
|
336
|
+
kernel(grid, (PML_THREADS,), args)
|
|
337
|
+
return u2
|
|
338
|
+
|
|
339
|
+
pml_step.memory = [common[0]]
|
|
340
|
+
return pml_step
|
|
341
|
+
|
|
248
342
|
def step_factors(self) -> list:
|
|
249
343
|
"""`dt**2`; the grid spacing already sits in the stencil table."""
|
|
250
344
|
return [self.dtype(self.dt**2)] * self.ndim
|
|
@@ -312,7 +406,7 @@ class AnisotropicElasticWave(Simulation):
|
|
|
312
406
|
grid, block = grid_block(self)
|
|
313
407
|
volume = float(np.prod(self.dx))
|
|
314
408
|
args = [accs["mass"], accs["cell"], None, None, None] + [
|
|
315
|
-
self.
|
|
409
|
+
self.stencil_table(),
|
|
316
410
|
self.dtype(sign * self.density * volume / (2.0 * self.dt) ** 2),
|
|
317
411
|
self.dtype(-sign),
|
|
318
412
|
np.int32(self.comp_stride),
|
|
@@ -5,8 +5,9 @@ setup can place one per face in `Simulation.boundary` long before the module is
|
|
|
5
5
|
compiled. `define_boundary` turns the markers into the launch closures once
|
|
6
6
|
`compile_kernels` has run, which is why `simulate` needs no separate preparation phase.
|
|
7
7
|
|
|
8
|
-
`sponge`
|
|
9
|
-
face rather than acting on the ghost ring
|
|
8
|
+
`sponge` and `Pml` open the domain from the other side: they absorb in the material
|
|
9
|
+
behind a face rather than acting on the ghost ring. The sponge is a damping field; the
|
|
10
|
+
PML is declarative like a condition, its profiles built once the step is defined.
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
from __future__ import annotations
|
|
@@ -253,3 +254,110 @@ def sponge(
|
|
|
253
254
|
return cp.ascontiguousarray(
|
|
254
255
|
(2.0 * beta * taper**2 / (sim.dt * minv)).astype(sim.dtype)
|
|
255
256
|
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@dataclass(frozen=True)
|
|
260
|
+
class Pml:
|
|
261
|
+
"""A convolutional perfectly matched layer behind `faces`, for `Simulation.pml`.
|
|
262
|
+
|
|
263
|
+
Every derivative along an axis carries a memory variable inside the `width` nodes
|
|
264
|
+
behind that axis' lined faces, its graded profile set by the target `reflection`
|
|
265
|
+
at the fastest `wavespeed` (Komatitsch & Martin 2007,
|
|
266
|
+
https://doi.org/10.1190/1.2757586).
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
width: int # layer depth in nodes, the wall node included
|
|
270
|
+
wavespeed: float # fastest wave speed in the layer, which sizes the damping
|
|
271
|
+
reflection: float = 1e-6 # target reflection at normal incidence
|
|
272
|
+
frequency: float = 0.0 # CFS shift pi * frequency through the layer, 0 for none
|
|
273
|
+
faces: tuple[int, ...] | None = None # the `2 * axis + side` codes, None for all
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def validate_pml(sim: Simulation) -> None:
|
|
277
|
+
"""Raise if `sim.pml` cannot be stepped: unsupported, combined, or too wide."""
|
|
278
|
+
layer = sim.pml
|
|
279
|
+
if not sim.accepts_pml:
|
|
280
|
+
raise NotImplementedError(f"{type(sim).__name__} has no pml; use sponge")
|
|
281
|
+
if sim.damping is not None or sim.masked:
|
|
282
|
+
raise ValueError("a pml takes neither a damping field nor a domain mask")
|
|
283
|
+
least = 2 + sim.pml_margin
|
|
284
|
+
if layer.width < least or layer.wavespeed <= 0.0 or not 0.0 < layer.reflection < 1:
|
|
285
|
+
raise ValueError(
|
|
286
|
+
f"a pml needs width >= {least}, a speed and 0 < reflection < 1: {layer}"
|
|
287
|
+
)
|
|
288
|
+
if sim.pml_shifted and layer.frequency <= 0.0:
|
|
289
|
+
raise ValueError(
|
|
290
|
+
f"{type(sim).__name__} keeps a static mode in an unshifted pml: set "
|
|
291
|
+
f"frequency, near the low end of the band"
|
|
292
|
+
)
|
|
293
|
+
# the transposed taps reach `reach` nodes past the layer, which must not meet
|
|
294
|
+
faces = _validate_layer(sim, layer.width + sim.reach, layer.faces)
|
|
295
|
+
if sim.pml_walls is not None:
|
|
296
|
+
for face in faces:
|
|
297
|
+
wall = sim.boundary[face // 2][face % 2]
|
|
298
|
+
if wall not in sim.pml_walls:
|
|
299
|
+
raise ValueError(
|
|
300
|
+
f"face {face} ends the pml on {wall}, which traps a growing wave: "
|
|
301
|
+
f"make it {' or '.join(map(repr, sim.pml_walls))}"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def pml_widths(sim: Simulation) -> list[tuple[int, int]]:
|
|
306
|
+
"""Per axis, the (low, high) layer depths in nodes, 0 for a face left unlined."""
|
|
307
|
+
faces = _canonical_faces(sim.ndim, sim.pml.faces)
|
|
308
|
+
return [
|
|
309
|
+
tuple(sim.pml.width if 2 * d + side in faces else 0 for side in (0, 1))
|
|
310
|
+
for d in range(sim.ndim)
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def pml_profiles(sim: Simulation) -> list[cpt.NDArray | None]:
|
|
315
|
+
"""Per lined axis, the (4, Nx[d]) recursion coefficients `a, b` at nodes and half points.
|
|
316
|
+
|
|
317
|
+
Row 0 and 1 are the node values, rows 2 and 3 those of the half point `a + 1/2`,
|
|
318
|
+
graded from 0 at the front to the wall and mirrored across it, so the ghost cell
|
|
319
|
+
sees what its mirror does. An axis with no lined face gets None.
|
|
320
|
+
"""
|
|
321
|
+
layer = sim.pml
|
|
322
|
+
# the front node is undamped, and so are the margin nodes in front of it
|
|
323
|
+
L = layer.width - 1 - sim.pml_margin
|
|
324
|
+
profiles = []
|
|
325
|
+
for d, (lo, hi) in enumerate(pml_widths(sim)):
|
|
326
|
+
if not lo + hi:
|
|
327
|
+
profiles.append(None)
|
|
328
|
+
continue
|
|
329
|
+
n = sim.Nx[d]
|
|
330
|
+
damping = (
|
|
331
|
+
-3.0 * layer.wavespeed * np.log(layer.reflection) / (2.0 * L * sim.dx[d])
|
|
332
|
+
)
|
|
333
|
+
rows = []
|
|
334
|
+
for position in (np.arange(n) + shift for shift in (0.0, 0.5)):
|
|
335
|
+
distance = np.full(n, np.inf)
|
|
336
|
+
if lo:
|
|
337
|
+
distance = np.minimum(distance, np.abs(position - 1))
|
|
338
|
+
if hi:
|
|
339
|
+
distance = np.minimum(distance, np.abs(n - 2 - position))
|
|
340
|
+
depth = np.clip((L - distance) / L, 0.0, 1.0)
|
|
341
|
+
d0 = damping * depth**2
|
|
342
|
+
alpha = np.pi * layer.frequency
|
|
343
|
+
inside = depth > 0.0
|
|
344
|
+
b = np.where(inside, np.exp(-(d0 + alpha) * sim.dt), 1.0)
|
|
345
|
+
a = np.where(inside, d0 / np.maximum(d0 + alpha, 1e-300) * (b - 1.0), 0.0)
|
|
346
|
+
rows += [a, b]
|
|
347
|
+
profiles.append(cp.asarray(np.stack(rows), dtype=sim.dtype))
|
|
348
|
+
return profiles
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def pml_nodes(sim: Simulation) -> cpt.NDArray:
|
|
352
|
+
"""Interior nodes inside the layer, where the step carries memory terms."""
|
|
353
|
+
inside = cp.zeros(sim.Nx_padded, dtype=bool)
|
|
354
|
+
if sim.pml is None:
|
|
355
|
+
return inside
|
|
356
|
+
for d, (lo, hi) in enumerate(pml_widths(sim)):
|
|
357
|
+
index = [slice(None)] * sim.ndim
|
|
358
|
+
for rows in (slice(1, 1 + lo), slice(sim.Nx[d] - 1 - hi, sim.Nx[d] - 1)):
|
|
359
|
+
index[d] = rows
|
|
360
|
+
inside[tuple(index)] = True
|
|
361
|
+
interior = cp.zeros_like(inside)
|
|
362
|
+
interior[tuple(slice(1, n - 1) for n in sim.Nx)] = True
|
|
363
|
+
return inside & interior
|