cuwave 0.1.0__py3-none-any.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.
- cuwave/__init__.py +8 -0
- cuwave/anisotropic.py +337 -0
- cuwave/boundary.py +255 -0
- cuwave/elastic.py +342 -0
- cuwave/evals.py +130 -0
- cuwave/geometry.py +226 -0
- cuwave/kernels/__init__.py +0 -0
- cuwave/kernels/anisotropic.cu +174 -0
- cuwave/kernels/anisotropic_sensitivity.cu +226 -0
- cuwave/kernels/common.cuh +95 -0
- cuwave/kernels/elastic.cu +225 -0
- cuwave/kernels/elastic_sensitivity.cu +217 -0
- cuwave/kernels/maxwell.cu +154 -0
- cuwave/kernels/maxwell_sensitivity.cu +139 -0
- cuwave/kernels/scalar.cu +164 -0
- cuwave/kernels/scalar_sensitivity.cu +140 -0
- cuwave/maxwell.py +416 -0
- cuwave/nn.py +99 -0
- cuwave/optimization.py +123 -0
- cuwave/postprocessing.py +181 -0
- cuwave/regularization.py +243 -0
- cuwave/scalar.py +224 -0
- cuwave/sensitivity.py +535 -0
- cuwave/signals.py +71 -0
- cuwave/stencils.py +48 -0
- cuwave/utils.py +472 -0
- cuwave/wave.py +518 -0
- cuwave-0.1.0.dist-info/METADATA +134 -0
- cuwave-0.1.0.dist-info/RECORD +32 -0
- cuwave-0.1.0.dist-info/WHEEL +5 -0
- cuwave-0.1.0.dist-info/licenses/LICENSE +21 -0
- cuwave-0.1.0.dist-info/top_level.txt +1 -0
cuwave/wave.py
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
"""The grid, the compile-time configuration, and the time loop every equation shares.
|
|
2
|
+
|
|
3
|
+
`Simulation` holds both and leaves the physics to a subclass in its own module
|
|
4
|
+
(`scalar.py`, `elastic.py`, `anisotropic.py`, `maxwell.py`), which names its kernel
|
|
5
|
+
sources and supplies the material and factor hooks. The `define_*` factories bind
|
|
6
|
+
compiled kernels to one such configuration, and `simulate` loops over the closures they
|
|
7
|
+
return.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import itertools
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import cupy as cp
|
|
18
|
+
import cupy.typing as cpt
|
|
19
|
+
import numpy as np
|
|
20
|
+
import numpy.typing as npt
|
|
21
|
+
|
|
22
|
+
from .boundary import canonical_boundary, define_boundary
|
|
23
|
+
from .stencils import preamble, weights
|
|
24
|
+
|
|
25
|
+
# Voigt row order per dimension as (k, l) strain pairs; the shear rows are PAIRS[d][d:]
|
|
26
|
+
PAIRS = {
|
|
27
|
+
1: ((0, 0),),
|
|
28
|
+
2: ((0, 0), (1, 1), (0, 1)),
|
|
29
|
+
3: ((0, 0), (1, 1), (2, 2), (1, 2), (0, 2), (0, 1)),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ------------------------------------- utilities -------------------------------------
|
|
34
|
+
def stable_dt(dx: tuple[float, ...], wavespeed: float, space_order: int = 2) -> float:
|
|
35
|
+
"""CFL-stable timestep for an explicit scheme with grid spacing `dx`."""
|
|
36
|
+
lam = float(np.abs(weights(space_order // 2)).sum())
|
|
37
|
+
return 2.0 / (wavespeed * float(np.sqrt(lam * sum(1.0 / d**2 for d in dx))))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def stable_timestep(
|
|
41
|
+
sim: Simulation,
|
|
42
|
+
indicator: cpt.NDArray,
|
|
43
|
+
iterations: int = 60,
|
|
44
|
+
safety: float = 0.95,
|
|
45
|
+
) -> float:
|
|
46
|
+
"""Largest stable timestep for `sim` under `indicator`, measured not estimated.
|
|
47
|
+
|
|
48
|
+
The leapfrog is stable while the spectral radius of `dt**2 minv L` stays under 4,
|
|
49
|
+
and `L` is symmetric with `minv` diagonal, so a power iteration on the step kernel
|
|
50
|
+
itself converges to that radius. Exact for any order, any material and any boundary
|
|
51
|
+
layout, where `stable_dt` only knows the wave speed and the spacing.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
sim: the simulation to measure, whose own `dt` sets the scale of the answer.
|
|
55
|
+
indicator: the design field, which is what a high contrast enters through.
|
|
56
|
+
iterations: power iterations, 60 being ample for three digits.
|
|
57
|
+
safety: fraction of the bound to return.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
the timestep to build `sim` with. A wide stencil over a strong contrast can put
|
|
61
|
+
this far below `stable_dt`, which is the signal to drop `space_order` rather
|
|
62
|
+
than to shrink `dt`.
|
|
63
|
+
"""
|
|
64
|
+
mat = sim.build_materials(indicator)
|
|
65
|
+
step = define_step_method(sim, compile_kernels(sim), mat)
|
|
66
|
+
shifts = sim.component_offsets
|
|
67
|
+
if shifts is None:
|
|
68
|
+
shifts = np.zeros((sim.ncomp, sim.ndim))
|
|
69
|
+
slices = [
|
|
70
|
+
(c, *(slice(1, n - 1 - (shifts[c][d] > 0)) for d, n in enumerate(sim.Nx)))
|
|
71
|
+
for c in range(sim.ncomp)
|
|
72
|
+
]
|
|
73
|
+
field = cp.asarray(
|
|
74
|
+
np.random.default_rng(0).standard_normal((sim.ncomp, *sim.Nx_padded)),
|
|
75
|
+
dtype=sim.dtype,
|
|
76
|
+
)
|
|
77
|
+
zero = cp.zeros_like(field)
|
|
78
|
+
out = cp.zeros_like(field)
|
|
79
|
+
|
|
80
|
+
def masked(values):
|
|
81
|
+
kept = cp.zeros_like(values)
|
|
82
|
+
for sl in slices:
|
|
83
|
+
kept[sl] = values[sl]
|
|
84
|
+
return kept
|
|
85
|
+
|
|
86
|
+
field = masked(field)
|
|
87
|
+
value = 0.0
|
|
88
|
+
for _ in range(iterations):
|
|
89
|
+
field /= cp.linalg.norm(field)
|
|
90
|
+
out[...] = 0.0
|
|
91
|
+
step(zero, field, out)
|
|
92
|
+
applied = masked(2.0 * field - out)
|
|
93
|
+
value = float(cp.sum(field * applied))
|
|
94
|
+
field = applied
|
|
95
|
+
if value <= 0.0:
|
|
96
|
+
raise ValueError(f"the operator came back non-positive: {value}")
|
|
97
|
+
return safety * sim.dt * float(np.sqrt(4.0 / value))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# -------------------------------------- helpers --------------------------------------
|
|
101
|
+
def padded_shape(Nx: tuple[int, ...]) -> tuple[int, ...]:
|
|
102
|
+
"""Pad the fastest (last) axis of `Nx` up to a multiple of 32, for coalesced access."""
|
|
103
|
+
return (*Nx[:-1], ((Nx[-1] + 31) // 32) * 32)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def mirror_ghosts(sim: Simulation, field: cpt.NDArray) -> cpt.NDArray:
|
|
107
|
+
"""Mirror `field`'s ghost layer onto its second-interior node, for homogeneous Neumann."""
|
|
108
|
+
for d in range(sim.ndim):
|
|
109
|
+
for ghost, mirror in ((0, 2), (sim.Nx[d] - 1, sim.Nx[d] - 3)):
|
|
110
|
+
dst = [slice(None)] * sim.ndim
|
|
111
|
+
dst[d] = ghost
|
|
112
|
+
src = [slice(None)] * sim.ndim
|
|
113
|
+
src[d] = mirror
|
|
114
|
+
field[tuple(dst)] = field[tuple(src)]
|
|
115
|
+
return field
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def grid_coords(
|
|
119
|
+
Nx: tuple[int, ...], dx: tuple[float, ...], dtype: npt.DTypeLike = cp.float64
|
|
120
|
+
) -> list[cpt.NDArray]:
|
|
121
|
+
"""Padded grid coordinates for shape `Nx` at spacing `dx`, with node 1 at the origin."""
|
|
122
|
+
# indices 0 and -1 are ghost nodes outside the domain
|
|
123
|
+
axes = [(cp.arange(n, dtype=dtype) - 1) * d for n, d in zip(padded_shape(Nx), dx)]
|
|
124
|
+
return cp.meshgrid(*axes, indexing="ij")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class Source:
|
|
129
|
+
position: cpt.NDArray[cp.int32] # (ndim, num_sources) grid indices
|
|
130
|
+
signal: cpt.NDArray # (N, num_sources) time series
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# W is the cell volume a node owns; docs/sensitivity.md derives it
|
|
134
|
+
def apply_cell_weights(sim: Simulation, field: cpt.NDArray) -> cpt.NDArray:
|
|
135
|
+
"""In-place multiply of `field` by the cell weights W."""
|
|
136
|
+
# axes in sequence, so a corner compounds to 1/4 (1/8 in 3D)
|
|
137
|
+
for d in range(sim.ndim):
|
|
138
|
+
for index in (1, sim.Nx[d] - 2):
|
|
139
|
+
face = [slice(None)] * sim.ndim
|
|
140
|
+
face[d] = index
|
|
141
|
+
field[tuple(face)] *= 0.5
|
|
142
|
+
return field
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def sensor_cell_weights(sim: Simulation, sensors: cpt.NDArray[cp.int32]) -> cpt.NDArray:
|
|
146
|
+
"""Cell weights W at the sensor nodes only, as a (num_sensors,) vector."""
|
|
147
|
+
# the nested loop of apply_cell_weights, so the two agree on a degenerate axis too
|
|
148
|
+
w = cp.ones(sensors.shape[1], dtype=sim.dtype)
|
|
149
|
+
for d in range(sim.ndim):
|
|
150
|
+
for index in (1, sim.Nx[d] - 2):
|
|
151
|
+
w = cp.where(grid_rows(sim, sensors)[d] == index, w * 0.5, w)
|
|
152
|
+
return w
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def grid_rows(
|
|
156
|
+
sim: Simulation, position: cpt.NDArray[cp.int32]
|
|
157
|
+
) -> cpt.NDArray[cp.int32]:
|
|
158
|
+
"""The `ndim` spatial rows of `position`, dropping a leading component row."""
|
|
159
|
+
return position[-sim.ndim :]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def flatten_indices(
|
|
163
|
+
sim: Simulation, position: cpt.NDArray[cp.int32]
|
|
164
|
+
) -> cpt.NDArray[cp.int32]:
|
|
165
|
+
"""Collapse (node_rows, num) indices `position` into flat indices of the field.
|
|
166
|
+
|
|
167
|
+
A vector unknown takes a leading component row, folded in as
|
|
168
|
+
`component * comp_stride`, so the gather and scatter kernels stay scalar.
|
|
169
|
+
"""
|
|
170
|
+
if position.shape[0] != sim.node_rows:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"position needs {sim.node_rows} rows for ncomp={sim.ncomp} in "
|
|
173
|
+
f"{sim.ndim}D, not {position.shape[0]}"
|
|
174
|
+
)
|
|
175
|
+
lin = cp.zeros(position.shape[1], dtype=cp.int32)
|
|
176
|
+
for d in range(sim.ndim):
|
|
177
|
+
lin += grid_rows(sim, position)[d] * cp.int32(sim.strides[d])
|
|
178
|
+
if sim.ncomp > 1:
|
|
179
|
+
lin += position[0] * cp.int32(sim.comp_stride)
|
|
180
|
+
return lin
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# --------------------------------- staggered lattice ---------------------------------
|
|
184
|
+
def component_weights(sim: Simulation, c: int) -> cpt.NDArray:
|
|
185
|
+
"""Cell weights of component `c`: halved on the walls of the unstaggered axes."""
|
|
186
|
+
w = cp.ones(sim.Nx_padded, dtype=sim.dtype)
|
|
187
|
+
for d in range(sim.ndim):
|
|
188
|
+
if d == c:
|
|
189
|
+
continue
|
|
190
|
+
for index in (1, sim.Nx[d] - 2):
|
|
191
|
+
wall = [slice(None)] * sim.ndim
|
|
192
|
+
wall[d] = index
|
|
193
|
+
w[tuple(wall)] *= 0.5
|
|
194
|
+
return w
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def pair_weights(sim: Simulation, axes: tuple[int, int]) -> cpt.NDArray:
|
|
198
|
+
"""Cell weights of a pair point: halved on the walls of the remaining axes."""
|
|
199
|
+
w = cp.ones(sim.Nx_padded, dtype=sim.dtype)
|
|
200
|
+
for d in range(sim.ndim):
|
|
201
|
+
if d in axes:
|
|
202
|
+
continue
|
|
203
|
+
for index in (1, sim.Nx[d] - 2):
|
|
204
|
+
wall = [slice(None)] * sim.ndim
|
|
205
|
+
wall[d] = index
|
|
206
|
+
w[tuple(wall)] *= 0.5
|
|
207
|
+
return w
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def point_average(sim: Simulation, field: cpt.NDArray, c: int) -> cpt.NDArray:
|
|
211
|
+
"""Arithmetic mean of `field` over the two nodes component `c` sits between."""
|
|
212
|
+
out = cp.zeros(sim.Nx_padded, dtype=sim.dtype)
|
|
213
|
+
lo = tuple(slice(0, n - 1) if d == c else slice(0, n) for d, n in enumerate(sim.Nx))
|
|
214
|
+
hi = tuple(slice(1, n) if d == c else slice(0, n) for d, n in enumerate(sim.Nx))
|
|
215
|
+
out[lo] = 0.5 * (field[lo] + field[hi])
|
|
216
|
+
return out
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def point_average_adjoint(sim: Simulation, density: cpt.NDArray, c: int) -> cpt.NDArray:
|
|
220
|
+
"""Transpose of `point_average`: half of `density` back onto each node it spans."""
|
|
221
|
+
out = 0.5 * density
|
|
222
|
+
to = [slice(None)] * sim.ndim
|
|
223
|
+
fro = [slice(None)] * sim.ndim
|
|
224
|
+
to[c], fro[c] = slice(1, None), slice(0, -1)
|
|
225
|
+
out[tuple(to)] += 0.5 * density[tuple(fro)]
|
|
226
|
+
return out
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def pair_average(
|
|
230
|
+
sim: Simulation, field: cpt.NDArray, axes: tuple[int, int]
|
|
231
|
+
) -> cpt.NDArray:
|
|
232
|
+
"""Harmonic mean of `field` over the four nodes a pair point straddles.
|
|
233
|
+
|
|
234
|
+
Harmonic for the reason the cell scheme uses it: it keeps the coefficient single
|
|
235
|
+
valued across a material jump.
|
|
236
|
+
"""
|
|
237
|
+
out = cp.zeros(sim.Nx_padded, dtype=sim.dtype)
|
|
238
|
+
safe = cp.maximum(field, cp.finfo(sim.dtype).tiny)
|
|
239
|
+
inner = tuple(
|
|
240
|
+
slice(0, n - 1) if d in axes else slice(0, n) for d, n in enumerate(sim.Nx)
|
|
241
|
+
)
|
|
242
|
+
for bits in itertools.product((0, 1), repeat=2):
|
|
243
|
+
shifted = tuple(
|
|
244
|
+
slice(bits[axes.index(d)], n - 1 + bits[axes.index(d)])
|
|
245
|
+
if d in axes
|
|
246
|
+
else slice(0, n)
|
|
247
|
+
for d, n in enumerate(sim.Nx)
|
|
248
|
+
)
|
|
249
|
+
out[inner] += 1.0 / safe[shifted]
|
|
250
|
+
out[inner] = 4.0 / out[inner]
|
|
251
|
+
return out
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def pair_average_adjoint(
|
|
255
|
+
sim: Simulation, density: cpt.NDArray, field: cpt.NDArray, axes: tuple[int, int]
|
|
256
|
+
) -> cpt.NDArray:
|
|
257
|
+
"""Transpose of `pair_average`: d(harmonic mean)/d(node) is `(mean / node)**2 / 4`."""
|
|
258
|
+
safe = cp.maximum(field, cp.finfo(sim.dtype).tiny)
|
|
259
|
+
mean = pair_average(sim, field, axes)
|
|
260
|
+
scattered = density * mean * mean * 0.25
|
|
261
|
+
out = cp.zeros(sim.Nx_padded, dtype=sim.dtype)
|
|
262
|
+
for bits in itertools.product((0, 1), repeat=2):
|
|
263
|
+
to = [slice(None)] * sim.ndim
|
|
264
|
+
fro = [slice(None)] * sim.ndim
|
|
265
|
+
for d, b in zip(axes, bits):
|
|
266
|
+
to[d], fro[d] = slice(b, None), slice(0, -b if b else None)
|
|
267
|
+
out[tuple(to)] += scattered[tuple(fro)] / safe[tuple(to)] ** 2
|
|
268
|
+
return out
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# -------------------------------- discretization setup -------------------------------
|
|
272
|
+
@dataclass
|
|
273
|
+
class Simulation:
|
|
274
|
+
"""Grid, timestepping, and compile-time configuration shared by all wave equations."""
|
|
275
|
+
|
|
276
|
+
Nx: tuple[int, ...] # logical grid points per axis (incl. ghost nodes)
|
|
277
|
+
dx: tuple[float, ...]
|
|
278
|
+
N: int # number of time steps
|
|
279
|
+
dt: float
|
|
280
|
+
threads: tuple[int, ...] # threads per block, per axis
|
|
281
|
+
precision: str = "float32" # "float32" or "float64"
|
|
282
|
+
space_order: int = 2 # finite difference order: any even number
|
|
283
|
+
boundary: tuple = None # ((low, high),) per axis; None is the equation's default
|
|
284
|
+
damping: cpt.NDArray | None = None # nodal field d, None for a lossless operator
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def compile_flags(self) -> tuple[str, ...]:
|
|
288
|
+
"""`-DUSE_DAMPING` when a damping field is set, else no extra flags."""
|
|
289
|
+
return ("-DUSE_DAMPING",) if self.damping is not None else ()
|
|
290
|
+
|
|
291
|
+
kernel_path = None # forward source this equation compiles, set by the subclass
|
|
292
|
+
sensitivity_path = None # and the adjoint one
|
|
293
|
+
default_boundary = None # what `boundary=None` means for this equation
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def ncomp(self) -> int:
|
|
297
|
+
"""Field components per node: 1 for a scalar unknown, `ndim` for a vector one."""
|
|
298
|
+
return 1
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def component_offsets(self) -> npt.NDArray[np.float64] | None:
|
|
302
|
+
"""(ncomp, ndim) grid offsets of each component in units of `dx`, or None for nodal."""
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def reach(self) -> int:
|
|
307
|
+
"""Nodes one step reads past a point, which a reconstruction strip must cover."""
|
|
308
|
+
return self.space_order // 2
|
|
309
|
+
|
|
310
|
+
def __post_init__(self) -> None:
|
|
311
|
+
"""Derive `ndim`, padded shape, strides, dtype, and canonical `boundary`."""
|
|
312
|
+
self.ndim = len(self.Nx)
|
|
313
|
+
self.Nx_padded = padded_shape(self.Nx)
|
|
314
|
+
# C-contiguous strides over the padded shape (last axis has unit stride)
|
|
315
|
+
strides = [1] * self.ndim
|
|
316
|
+
for d in range(self.ndim - 2, -1, -1):
|
|
317
|
+
strides[d] = strides[d + 1] * self.Nx_padded[d + 1]
|
|
318
|
+
self.strides = tuple(strides)
|
|
319
|
+
self.dtype = cp.float32 if self.precision == "float32" else cp.float64
|
|
320
|
+
self.boundary = canonical_boundary(
|
|
321
|
+
self.boundary, self.ndim, self.default_boundary
|
|
322
|
+
)
|
|
323
|
+
if self.space_order % 2 != 0 or self.space_order < 2:
|
|
324
|
+
raise ValueError("space_order must be an even integer >= 2")
|
|
325
|
+
self.comp_stride = int(np.prod(self.Nx_padded))
|
|
326
|
+
self.node_rows = self.ndim + (self.ncomp > 1)
|
|
327
|
+
self.field_shape = (
|
|
328
|
+
self.Nx_padded if self.ncomp == 1 else (self.ncomp, *self.Nx_padded)
|
|
329
|
+
)
|
|
330
|
+
# flatten_indices accumulates in int32, so the whole field has to address in it
|
|
331
|
+
if self.ncomp * self.comp_stride >= 2**31:
|
|
332
|
+
raise ValueError(
|
|
333
|
+
f"{self.ncomp} x {self.comp_stride} nodes overflow the int32 flat "
|
|
334
|
+
f"index; coarsen the grid"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def define_step(self, kernels: cp.RawModule, mat: dict) -> Callable:
|
|
338
|
+
"""Closure launching the finite-difference step kernel over (u0, u1, u2)."""
|
|
339
|
+
fd_kernel = kernels.get_function("fd_kernel")
|
|
340
|
+
grid, block = grid_block(self)
|
|
341
|
+
args = [
|
|
342
|
+
None,
|
|
343
|
+
None,
|
|
344
|
+
None,
|
|
345
|
+
*self.step_kernel_args(mat),
|
|
346
|
+
*axis_geometry(self, self.step_factors()),
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
def fd_step(u0, u1, u2):
|
|
350
|
+
args[0], args[1], args[2] = u0, u1, u2
|
|
351
|
+
fd_kernel(grid, block, args)
|
|
352
|
+
return u2
|
|
353
|
+
|
|
354
|
+
return fd_step
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ----------------------------------- kernel helpers ----------------------------------
|
|
358
|
+
COMMON_PATH = Path(__file__).parent / "kernels" / "common.cuh"
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def compile_kernels(sim: Simulation, path: Path | None = None) -> cp.RawModule:
|
|
362
|
+
"""Compile `path` for `sim`, defaulting to its own source, stencil table injected."""
|
|
363
|
+
path = sim.kernel_path if path is None else path
|
|
364
|
+
options = ["--use_fast_math", f"-DNDIM={sim.ndim}", *sim.compile_flags]
|
|
365
|
+
if sim.precision == "float32":
|
|
366
|
+
options.append("-DUSE_FLOAT")
|
|
367
|
+
# injected as source, so the module cache keys on the order without a -D flag
|
|
368
|
+
code = preamble(sim.space_order) + COMMON_PATH.read_text() + Path(path).read_text()
|
|
369
|
+
return cp.RawModule(code=code, options=tuple(options))
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def grid_block(sim: Simulation) -> tuple[tuple[int, ...], tuple[int, ...]]:
|
|
373
|
+
"""CUDA (grid, block) dimensions for `sim`, fastest axis mapped to x."""
|
|
374
|
+
# map the fastest axis to grid/block x, the next to y, the next to z
|
|
375
|
+
extent = sim.Nx_padded
|
|
376
|
+
block = tuple(sim.threads[::-1])
|
|
377
|
+
grid = tuple(
|
|
378
|
+
(extent[d] + sim.threads[d] - 1) // sim.threads[d] for d in range(sim.ndim)
|
|
379
|
+
)[::-1]
|
|
380
|
+
return grid, block
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def axis_geometry(sim: Simulation, factors: list) -> list:
|
|
384
|
+
"""Interleave `factors` with axis extents and strides, in the layout the step kernel expects."""
|
|
385
|
+
# kernel args after the material arrays: f0, N0, [f1, N1, s0], [f2, N2, s1]
|
|
386
|
+
geom = [factors[0], sim.Nx[0]]
|
|
387
|
+
for d in range(1, sim.ndim):
|
|
388
|
+
geom += [factors[d], sim.Nx[d], sim.strides[d - 1]]
|
|
389
|
+
return geom
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# -------------------------------- simulation functions -------------------------------
|
|
393
|
+
def define_step_method(sim: Simulation, kernels: cp.RawModule, mat: dict) -> Callable:
|
|
394
|
+
"""The step closure `sim.define_step` builds, a hook so a scheme may launch several kernels."""
|
|
395
|
+
return sim.define_step(kernels, mat)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def define_excitation(
|
|
399
|
+
sim: Simulation,
|
|
400
|
+
position: cpt.NDArray[cp.int32],
|
|
401
|
+
kernels: cp.RawModule,
|
|
402
|
+
mat: dict,
|
|
403
|
+
) -> Callable:
|
|
404
|
+
"""Closure injecting `signal` at `position` into `u` at timestep `t_index`."""
|
|
405
|
+
excitation_kernel = kernels.get_function("excitation_kernel")
|
|
406
|
+
threads = 256
|
|
407
|
+
num_sources = position.shape[1]
|
|
408
|
+
blocks = (num_sources + threads - 1) // threads
|
|
409
|
+
lin_index = flatten_indices(sim, position)
|
|
410
|
+
weight = sim.excitation_weights(mat, lin_index)
|
|
411
|
+
# the whole (N, num_sources) record plus a row offset, not a row view
|
|
412
|
+
args = [None, None, np.int32(0), lin_index, np.int32(num_sources), weight]
|
|
413
|
+
|
|
414
|
+
def excitation_step(u, signal, t_index):
|
|
415
|
+
args[0], args[1] = u, signal
|
|
416
|
+
args[2] = np.int32(t_index * num_sources)
|
|
417
|
+
excitation_kernel((blocks,), (threads,), args)
|
|
418
|
+
return u
|
|
419
|
+
|
|
420
|
+
return excitation_step
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def define_get_signal(
|
|
424
|
+
sim: Simulation, sensors: cpt.NDArray[cp.int32], kernels: cp.RawModule
|
|
425
|
+
) -> Callable:
|
|
426
|
+
"""Closure writing row `t_index` of the (N, num_sensors) record `um` from `u`."""
|
|
427
|
+
get_signal_kernel = kernels.get_function("get_signal_kernel")
|
|
428
|
+
threads = 256
|
|
429
|
+
num_sensors = sensors.shape[1]
|
|
430
|
+
blocks = (num_sensors + threads - 1) // threads
|
|
431
|
+
lin_index = flatten_indices(sim, sensors)
|
|
432
|
+
args = [None, None, np.int32(0), lin_index, np.int32(num_sensors)]
|
|
433
|
+
|
|
434
|
+
# writes row t of the whole (N, num_sensors) record, so the caller never slices
|
|
435
|
+
def get_signal_step(u, um, t_index):
|
|
436
|
+
args[0], args[1] = u, um
|
|
437
|
+
args[2] = np.int32(t_index * num_sensors)
|
|
438
|
+
get_signal_kernel((blocks,), (threads,), args)
|
|
439
|
+
return um
|
|
440
|
+
|
|
441
|
+
return get_signal_step
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def define_set_signal(
|
|
445
|
+
sim: Simulation, sensors: cpt.NDArray[cp.int32], kernels: cp.RawModule
|
|
446
|
+
) -> Callable:
|
|
447
|
+
"""Closure writing `u` at `sensors` back from row `t_index` of the record `um`."""
|
|
448
|
+
set_signal_kernel = kernels.get_function("set_signal_kernel")
|
|
449
|
+
threads = 256
|
|
450
|
+
num_sensors = sensors.shape[1]
|
|
451
|
+
blocks = (num_sensors + threads - 1) // threads
|
|
452
|
+
lin_index = flatten_indices(sim, sensors)
|
|
453
|
+
args = [None, None, np.int32(0), lin_index, np.int32(num_sensors)]
|
|
454
|
+
|
|
455
|
+
# assignment rather than the atomicAdd of define_excitation, so it restores a state
|
|
456
|
+
def set_signal_step(u, um, t_index):
|
|
457
|
+
args[0], args[1] = u, um
|
|
458
|
+
args[2] = np.int32(t_index * num_sensors)
|
|
459
|
+
set_signal_kernel((blocks,), (threads,), args)
|
|
460
|
+
return u
|
|
461
|
+
|
|
462
|
+
return set_signal_step
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def simulate(
|
|
466
|
+
sim: Simulation,
|
|
467
|
+
source: Source,
|
|
468
|
+
indicator: cpt.NDArray,
|
|
469
|
+
sensors: cpt.NDArray[cp.int32] | None = None,
|
|
470
|
+
record_every: int | None = None,
|
|
471
|
+
) -> cpt.NDArray | tuple:
|
|
472
|
+
"""Run `sim` forward under `source` and material `indicator`.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
sim: the simulation to step, which fixes the grid and the kernels compiled.
|
|
476
|
+
source: the shot to inject, its signal an (N, num_sources) record.
|
|
477
|
+
indicator: the design field the materials are built from.
|
|
478
|
+
sensors: (ndim, num_sensors) grid indices to record at, or None for no record.
|
|
479
|
+
record_every: snapshot the interior field every this many steps, or None.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
the final interior field, followed by the (N, num_sensors) record when
|
|
483
|
+
`sensors` is given and the stacked host snapshots when `record_every` is.
|
|
484
|
+
"""
|
|
485
|
+
U = cp.zeros((2, *sim.field_shape), dtype=sim.dtype)
|
|
486
|
+
u0, u1 = U[0], U[1]
|
|
487
|
+
|
|
488
|
+
mat = sim.build_materials(indicator)
|
|
489
|
+
kernels = compile_kernels(sim)
|
|
490
|
+
fd_step = define_step_method(sim, kernels, mat)
|
|
491
|
+
bc_step = define_boundary(sim, kernels)
|
|
492
|
+
excitation_step = define_excitation(sim, source.position, kernels, mat)
|
|
493
|
+
if sensors is not None:
|
|
494
|
+
get_signal = define_get_signal(sim, sensors, kernels)
|
|
495
|
+
um = cp.zeros((sim.N, sensors.shape[1]), dtype=sim.dtype)
|
|
496
|
+
interior = (Ellipsis, *(slice(0, n) for n in sim.Nx))
|
|
497
|
+
snapshots = []
|
|
498
|
+
|
|
499
|
+
def field(u):
|
|
500
|
+
return u[interior]
|
|
501
|
+
|
|
502
|
+
for t in range(sim.N):
|
|
503
|
+
u0 = fd_step(u0, u1, u0)
|
|
504
|
+
u0 = excitation_step(u0, source.signal, t)
|
|
505
|
+
u0 = bc_step(u0)
|
|
506
|
+
u1, u0 = u0, u1
|
|
507
|
+
if sensors is not None:
|
|
508
|
+
get_signal(u1, um, t)
|
|
509
|
+
if record_every is not None and t % record_every == 0:
|
|
510
|
+
snapshots.append(field(u1).get())
|
|
511
|
+
|
|
512
|
+
if sensors is not None and record_every is not None:
|
|
513
|
+
return field(u1), um, np.stack(snapshots)
|
|
514
|
+
if sensors is not None:
|
|
515
|
+
return field(u1), um
|
|
516
|
+
if record_every is not None:
|
|
517
|
+
return field(u1), np.stack(snapshots)
|
|
518
|
+
return field(u1)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cuwave
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: GPU finite-difference wave solver with differentiable adjoints
|
|
5
|
+
Author-email: Leon Herrmann <herrmann.leon@pm.me>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/cmpmech/cuwave
|
|
8
|
+
Project-URL: Documentation, https://github.com/cmpmech/cuwave/blob/main/docs/Home.md
|
|
9
|
+
Project-URL: Changelog, https://github.com/cmpmech/cuwave/releases
|
|
10
|
+
Keywords: wave-equation,finite-difference,adjoint,full-waveform-inversion,topology-optimization,cuda,cupy
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: GPU :: NVIDIA CUDA
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: numpy>=1.24
|
|
20
|
+
Provides-Extra: examples
|
|
21
|
+
Requires-Dist: matplotlib>=3.7; extra == "examples"
|
|
22
|
+
Requires-Dist: pillow>=9.1; extra == "examples"
|
|
23
|
+
Provides-Extra: nn
|
|
24
|
+
Requires-Dist: torch>=2.0; extra == "nn"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# CuWave
|
|
28
|
+
|
|
29
|
+
<picture>
|
|
30
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/cmpmech/cuwave/main/.assets/readme-dark.webp">
|
|
31
|
+
<img src="https://raw.githubusercontent.com/cmpmech/cuwave/main/.assets/readme-light.webp" width="100%">
|
|
32
|
+
</picture>
|
|
33
|
+
|
|
34
|
+
**CuWave** is a single-GPU, differentiable finite difference wave propagation code.
|
|
35
|
+
Possible applications include
|
|
36
|
+
|
|
37
|
+
<table width="100%">
|
|
38
|
+
<tr>
|
|
39
|
+
<td valign="middle"><a href="https://www.sciencedirect.com/science/article/pii/S0045782523000166"><strong>nondestructive testing via full waveform inversion</strong></a></td>
|
|
40
|
+
<td width="60%" align="right" valign="middle"><picture>
|
|
41
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/cmpmech/cuwave/main/.assets/fwi-dark.png">
|
|
42
|
+
<img width="100%" src="https://raw.githubusercontent.com/cmpmech/cuwave/main/.assets/fwi-light.png" alt="nondestructive testing via full waveform inversion">
|
|
43
|
+
</picture></td>
|
|
44
|
+
</tr>
|
|
45
|
+
<tr>
|
|
46
|
+
<td valign="middle"><a href="https://doi.org/10.1007/s00158-025-04237-y"><strong>transient acoustic topology optimization</strong></a></td>
|
|
47
|
+
<td width="60%" align="center" valign="middle"><picture>
|
|
48
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/cmpmech/cuwave/main/.assets/tato-dark.webp">
|
|
49
|
+
<img width="46.512%" src="https://raw.githubusercontent.com/cmpmech/cuwave/main/.assets/tato-light.webp" alt="transient acoustic topology optimization">
|
|
50
|
+
</picture></td>
|
|
51
|
+
</tr>
|
|
52
|
+
<tr>
|
|
53
|
+
<td valign="middle"><a href="https://www.science.org/doi/10.1126/sciadv.aay6946"><strong>analog neural networks</strong></a></td>
|
|
54
|
+
<td width="60%" align="right" valign="middle"></td>
|
|
55
|
+
</tr>
|
|
56
|
+
<tr>
|
|
57
|
+
<td valign="middle"><a href="https://opg.optica.org/josab/fulltext.cfm?uri=josab-38-2-496"><strong>transient photonic topology optimization</strong></a></td>
|
|
58
|
+
<td width="60%" align="right" valign="middle"><picture>
|
|
59
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/cmpmech/cuwave/main/.assets/tpto-dark.webp">
|
|
60
|
+
<img width="100%" src="https://raw.githubusercontent.com/cmpmech/cuwave/main/.assets/tpto-light.webp" alt="transient photonic topology optimization">
|
|
61
|
+
</picture></td>
|
|
62
|
+
</tr>
|
|
63
|
+
</table>
|
|
64
|
+
|
|
65
|
+
## Documentation
|
|
66
|
+
|
|
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
|
+
- see [examples](https://github.com/cmpmech/cuwave/tree/main/examples) for how to apply the code
|
|
69
|
+
|
|
70
|
+
## Development status
|
|
71
|
+
|
|
72
|
+
**Scalar** & **acoustic wave** equations have been developed over the last 2 years and are thoroughly validated.
|
|
73
|
+
|
|
74
|
+
> [!IMPORTANT]
|
|
75
|
+
> **Elastic** & **electromagnetic wave** equations were developed with AI assistance (Claude) and have undergone less validation. The elastic wave equation is currently being validated against experimental results.
|
|
76
|
+
|
|
77
|
+
## Performance
|
|
78
|
+
|
|
79
|
+
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:
|
|
80
|
+
- **scalar wave equation**
|
|
81
|
+
- [Deepwave](https://github.com/ar4/deepwave) (forward: ~1.1x, sensitivity: ~1x)
|
|
82
|
+
- [NVIDIA Warp](https://github.com/NVIDIA/warp) (forward: ~2.1x, sensitivity: ~1.8x)
|
|
83
|
+
- [SeismicWaves.jl](https://github.com/GinvLab/SeismicWaves.jl) (forward: ~4.4x, sensitivity: ~4x)
|
|
84
|
+
- [Devito](https://github.com/devitocodes/devito) on CPU (forward: ~3.6x, sensitivity: ~3.8x)
|
|
85
|
+
- **elastic wave equation**
|
|
86
|
+
- Deepwave (forward: ~1.1x, sensitivity: ~1.3x)
|
|
87
|
+
- SeismicWaves.jl (forward: ~4x, sensitivity: ~5x)
|
|
88
|
+
Tested on one NVIDIA RTX PRO 500 Blackwell laptop GPU (6 GB) on the largest possible grids with CuWave's `superposition_sensitivity` as reference for the sensitivities. The specific numbers need to be taken with a grain of salt, as they are subject to specific hardware and simulation setup. All implementations operate on the same order of magnitude.
|
|
89
|
+
|
|
90
|
+
Additional benefits of **CuWave** are
|
|
91
|
+
- the built-in **higher order finite difference** schemes, allowing for fewer grid points
|
|
92
|
+
- a sensitivity analysis whose **memory is independent of the number of timesteps**, allowing for orders of magnitude larger grids
|
|
93
|
+
|
|
94
|
+
## Install
|
|
95
|
+
|
|
96
|
+
Dependencies are kept **lightweight**. Only CuPy is required beyond standard Python library.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pip install cupy-cuda12x # or cupy-cuda11x, to match your CUDA
|
|
100
|
+
pip install cuwave # or `pip install -e .` from a checkout
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
CuPy must be installed separately because the wheel depends on your CUDA toolkit;
|
|
104
|
+
all remaining dependencies are declared in `pyproject.toml`.
|
|
105
|
+
|
|
106
|
+
PyTorch is optional for the regularization via neural optimization; see [pytorch](https://pytorch.org/get-started/locally/) for the installation. Otherwise it is not needed.
|
|
107
|
+
|
|
108
|
+
> [!NOTE]
|
|
109
|
+
> Match PyTorch's CUDA version to CuPy's, or the two runtimes clash at the first kernel launch.
|
|
110
|
+
> With `cupy-cuda12x`:
|
|
111
|
+
> ```bash
|
|
112
|
+
> pip install torch --index-url https://download.pytorch.org/whl/cu128
|
|
113
|
+
> ```
|
|
114
|
+
|
|
115
|
+
The tests under `tests/` are `unittest` classes, but `pytest` is the recommended runner:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pip install pytest
|
|
119
|
+
python -m pytest tests/ -q # ~20 s on a GPU, ~3 s without: CUDA and PyTorch tests skip when unavailable
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## References
|
|
123
|
+
|
|
124
|
+
If you use our code for your scientific research, please acknowledge this by referring to the following publication:
|
|
125
|
+
|
|
126
|
+
_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)_
|
|
127
|
+
|
|
128
|
+
## Contact
|
|
129
|
+
|
|
130
|
+
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).
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT; see [LICENSE](https://github.com/cmpmech/cuwave/blob/main/LICENSE).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
cuwave/__init__.py,sha256=PNj76PUFei2Sw1Z4N5-wB9q9Y16sCV4FhbQZARsDQR0,277
|
|
2
|
+
cuwave/anisotropic.py,sha256=wD_JLTbPTAHOMCnowoKdRI9Gcib4F7s_eDlAsj8qIOg,14631
|
|
3
|
+
cuwave/boundary.py,sha256=E-EmhcKuwF0UhGBOoCj_jnpGtQ2QSHm8_1rrkbHEMe4,9930
|
|
4
|
+
cuwave/elastic.py,sha256=POxrD6ru4x3fwr9kWP4Z5GeOYOnWvhHFI49MX2E9BhY,13873
|
|
5
|
+
cuwave/evals.py,sha256=8rJmB4yRZF7FLGMSswBejBHoaj8kEE8TEI1yNqs2ObM,5488
|
|
6
|
+
cuwave/geometry.py,sha256=XvaFawIWwawKkHZF6RPZ1mUaol-9WtYbNfy9AtIxSEQ,7901
|
|
7
|
+
cuwave/maxwell.py,sha256=5vQ4I4unxoRm_-LBj_pH0XAa86PD2D5jVRlVes-AkXo,17812
|
|
8
|
+
cuwave/nn.py,sha256=QM7Q9H3PkLbxF_05vJ6_QcZonCAmf6f9h11cpeZdw58,3834
|
|
9
|
+
cuwave/optimization.py,sha256=3oj-8iULKKZtm39xWkeqnvKsnrRfbhP2ia-8szt98x4,4325
|
|
10
|
+
cuwave/postprocessing.py,sha256=aCByHTlcwZA7EA-rqpSkIuMEYyevHdnQgwCkhvtuu5M,7064
|
|
11
|
+
cuwave/regularization.py,sha256=PbTf2AE0oESnxPNH9GQDLcgAgXhLSE47weuNITM1OBI,10517
|
|
12
|
+
cuwave/scalar.py,sha256=4c-npaeEZw006eVebi20yusjga8Kgs1UGmqr2N73mjE,9418
|
|
13
|
+
cuwave/sensitivity.py,sha256=n_qJgbWXVatM7cWQc20GAKt92EIejC3Vpem0sWhaJhY,22508
|
|
14
|
+
cuwave/signals.py,sha256=CV8QxTpHq4YjCMf47n0ZwAUmqUQBbMj9aPZtuE5FHNw,2196
|
|
15
|
+
cuwave/stencils.py,sha256=0w81sBKi1wNfEi-a_M3me29CG8IBDBnwYjGlylLOxC0,1621
|
|
16
|
+
cuwave/utils.py,sha256=bH3tmEWsfozg00bKh4lvH9pviu09phrBQ7eb7iQlEgU,18588
|
|
17
|
+
cuwave/wave.py,sha256=Yazqg_3_R3_Gl7n51_ZGAxO38fk4dKu6x-tLMnoVPdQ,20273
|
|
18
|
+
cuwave/kernels/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
cuwave/kernels/anisotropic.cu,sha256=Arxw_ybY9IHs2snUPXk0HvboRbEin4CJ2xC-zCVGnA4,6478
|
|
20
|
+
cuwave/kernels/anisotropic_sensitivity.cu,sha256=wresk5Qk9A3tKsIO3pC5e_Aru69i_2nl_Gtmowehesk,8162
|
|
21
|
+
cuwave/kernels/common.cuh,sha256=uwHSY6lePaEKVZ2yM-xPboOnjC-mreaEuYyTBW5ESoc,3396
|
|
22
|
+
cuwave/kernels/elastic.cu,sha256=Vt3uC26m_QXp_JU0Hy5tMyRfYgmVWOwzJ9dGXYsTTHU,8890
|
|
23
|
+
cuwave/kernels/elastic_sensitivity.cu,sha256=cA2Av22_gA7ywSQDDsXaplXLle2ZBApHlDevpOXXfRk,8939
|
|
24
|
+
cuwave/kernels/maxwell.cu,sha256=VriqARV3-xnR8jN8roDLt2JCasXZIxnUaiIVlxLINio,6234
|
|
25
|
+
cuwave/kernels/maxwell_sensitivity.cu,sha256=guFrAbPKyjwlgn7dokzu9dd95D7wohKRSt2Moy4LjI4,5544
|
|
26
|
+
cuwave/kernels/scalar.cu,sha256=HEgte8QSHKszwwdXT0-D6-3oF9mZrkVQrNj-d0d_i9E,6052
|
|
27
|
+
cuwave/kernels/scalar_sensitivity.cu,sha256=2LsnA9AjiX8eV066cFQKKti_t7XIZxc8MNBra4flPlY,5984
|
|
28
|
+
cuwave-0.1.0.dist-info/licenses/LICENSE,sha256=Zo_H3adB0WTzDHG7DL0dYMjyXrR8PFzwlcJX1F-IBvc,1070
|
|
29
|
+
cuwave-0.1.0.dist-info/METADATA,sha256=iiTyX7i-sbajNYLAWPi_fmqLgmVP1BKBGLvoKsYQ_ic,7082
|
|
30
|
+
cuwave-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
31
|
+
cuwave-0.1.0.dist-info/top_level.txt,sha256=HZSy3rgj7_raEX1LvNk5DOFGe6b2MingOIqjwdcc4tM,7
|
|
32
|
+
cuwave-0.1.0.dist-info/RECORD,,
|