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/utils.py
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
from collections.abc import Callable, Sequence
|
|
3
|
+
|
|
4
|
+
import cupy as cp
|
|
5
|
+
import cupy.typing as cpt
|
|
6
|
+
import numpy as np
|
|
7
|
+
import numpy.typing as npt
|
|
8
|
+
|
|
9
|
+
from .sensitivity import l2_misfit, sensitivity
|
|
10
|
+
from .wave import Simulation, Source, simulate
|
|
11
|
+
|
|
12
|
+
TOL = 1e-6 # for `distribute`, so exact-boundary coordinates survive rounding
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# -------------------------------------- helpers --------------------------------------
|
|
16
|
+
def _reparametrize(
|
|
17
|
+
sim: Simulation, indicator: cpt.NDArray, grads: dict[str, cpt.NDArray]
|
|
18
|
+
) -> cpt.NDArray:
|
|
19
|
+
"""Chain d(cost)/d(mass, stiff) onto the indicator with `parametrization_jacobian`."""
|
|
20
|
+
d_mass, d_stiff = sim.parametrization_jacobian(indicator)
|
|
21
|
+
return d_mass * grads["mass"] + d_stiff * grads["stiff"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# -------------------------------------- general --------------------------------------
|
|
25
|
+
def interior_slice(sim: Simulation) -> tuple[slice, ...]:
|
|
26
|
+
"""Index tuple selecting the interior nodes, dropping the ghost ring and the padding."""
|
|
27
|
+
return tuple(slice(1, n - 1) for n in sim.Nx)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def threshold(
|
|
31
|
+
field: cpt.NDArray,
|
|
32
|
+
eta: float = 0.5,
|
|
33
|
+
low: float = 0.0,
|
|
34
|
+
high: float = 1.0,
|
|
35
|
+
dtype: npt.DTypeLike | None = None,
|
|
36
|
+
) -> cpt.NDArray:
|
|
37
|
+
"""Snap a grey design to `low` below the threshold `eta` and `high` above it.
|
|
38
|
+
|
|
39
|
+
`dtype` defaults to the dtype of `field`, since `cp.where` against python floats
|
|
40
|
+
otherwise promotes a float32 design to float64 and the kernels read it as garbage.
|
|
41
|
+
"""
|
|
42
|
+
snapped = cp.where(field < eta, low, high)
|
|
43
|
+
if dtype is None:
|
|
44
|
+
dtype = field.dtype if field.dtype.kind == "f" else snapped.dtype
|
|
45
|
+
return snapped.astype(dtype)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def resample(
|
|
49
|
+
signal: cpt.NDArray | npt.NDArray,
|
|
50
|
+
dt: float,
|
|
51
|
+
dt_new: float,
|
|
52
|
+
N_new: int | None = None,
|
|
53
|
+
) -> cpt.NDArray | npt.NDArray:
|
|
54
|
+
"""Linearly resample `signal` from timestep `dt` to `dt_new`.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
signal: samples along the leading axis, on either array module.
|
|
58
|
+
dt: the timestep `signal` is sampled at.
|
|
59
|
+
dt_new: the timestep to resample onto.
|
|
60
|
+
N_new: number of output samples, defaulting to the original span. Pass it
|
|
61
|
+
explicitly to force a common length across separately resampled signals.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
the resampled signal, zero past the end of the original span.
|
|
65
|
+
"""
|
|
66
|
+
xp = cp.get_array_module(signal)
|
|
67
|
+
values = xp.asarray(signal)
|
|
68
|
+
steps = values.shape[0]
|
|
69
|
+
if steps < 2:
|
|
70
|
+
raise ValueError(f"resampling needs at least two samples, got {steps}")
|
|
71
|
+
if N_new is None:
|
|
72
|
+
span = (steps - 1) * dt / dt_new
|
|
73
|
+
N_new = int(span + 1e-9 * max(1.0, span)) + 1
|
|
74
|
+
|
|
75
|
+
dtype = xp.dtype(values.dtype if values.dtype.kind == "f" else xp.float64)
|
|
76
|
+
t = xp.arange(N_new, dtype=dtype) * dtype.type(dt_new / dt) # in input samples
|
|
77
|
+
left = xp.clip(xp.floor(t), 0, steps - 2)
|
|
78
|
+
weight = t - left
|
|
79
|
+
index = left.astype(xp.int32)
|
|
80
|
+
if values.ndim > 1:
|
|
81
|
+
weight = weight.reshape(N_new, *(1,) * (values.ndim - 1))
|
|
82
|
+
interpolated = (1 - weight) * values[index] + weight * values[index + 1]
|
|
83
|
+
return interpolated * (t.reshape(weight.shape) <= steps - 1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def line(
|
|
87
|
+
start: npt.ArrayLike, stop: npt.ArrayLike, count: int
|
|
88
|
+
) -> npt.NDArray[np.float64]:
|
|
89
|
+
"""`count` coordinates evenly spaced from `start` to `stop`, endpoints included."""
|
|
90
|
+
return np.linspace(start, stop, count)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _corner_weights(
|
|
94
|
+
sim: Simulation, coords: npt.NDArray, shift: npt.NDArray
|
|
95
|
+
) -> tuple[npt.NDArray, npt.NDArray]:
|
|
96
|
+
"""Cell corners and multilinear weights of `coords` on the grid shifted by `shift`.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
(nodes, weights) shaped (ndim, num, 2**ndim) and (num, 2**ndim), the nodes
|
|
100
|
+
array indices on the component's own grid, whose unknowns end half a cell
|
|
101
|
+
short of a wall along a shifted axis.
|
|
102
|
+
"""
|
|
103
|
+
index = np.empty_like(coords)
|
|
104
|
+
for d in range(sim.ndim):
|
|
105
|
+
nodal = coords[:, d] / sim.dx[d] + 1.0
|
|
106
|
+
# checked in index units, where the interior runs from node 1 to Nx[d] - 2
|
|
107
|
+
if nodal.min() < 1.0 - TOL or nodal.max() > sim.Nx[d] - 2.0 + TOL:
|
|
108
|
+
length = (sim.Nx[d] - 3) * sim.dx[d] # spanned by the interior nodes
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"axis {d} coordinate outside the domain [0, {length:g}]: "
|
|
111
|
+
f"[{float(coords[:, d].min()):g}, {float(coords[:, d].max()):g}]"
|
|
112
|
+
)
|
|
113
|
+
# a wall coordinate on a shifted axis lands on the unknown half a cell inside
|
|
114
|
+
high = sim.Nx[d] - 2.0 - (shift[d] > 0.0)
|
|
115
|
+
index[:, d] = np.clip(nodal - shift[d], 1.0, high)
|
|
116
|
+
base = np.stack(
|
|
117
|
+
[
|
|
118
|
+
np.clip(np.floor(index[:, d]), 1, sim.Nx[d] - 3 - (shift[d] > 0.0)).astype(
|
|
119
|
+
np.int32
|
|
120
|
+
)
|
|
121
|
+
for d in range(sim.ndim)
|
|
122
|
+
]
|
|
123
|
+
)
|
|
124
|
+
offset = index.T - base # (ndim, num) position inside that cell, in [0, 1]
|
|
125
|
+
corners = np.array(list(itertools.product((0, 1), repeat=sim.ndim))) # (K, ndim)
|
|
126
|
+
nodes = base[:, :, None] + corners.T[:, None, :] # (ndim, num, K)
|
|
127
|
+
weights = np.ones((len(coords), len(corners)))
|
|
128
|
+
for d in range(sim.ndim):
|
|
129
|
+
w = offset[d][:, None]
|
|
130
|
+
weights *= np.where(corners[None, :, d] == 1, w, 1.0 - w)
|
|
131
|
+
return nodes, weights
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def distribute(
|
|
135
|
+
sim: Simulation, coords: npt.ArrayLike, direction: npt.ArrayLike | None = None
|
|
136
|
+
) -> tuple[cpt.NDArray[cp.int32], cpt.NDArray]:
|
|
137
|
+
"""Multilinear interpolation of `coords` onto the grid, along `direction`.
|
|
138
|
+
|
|
139
|
+
A vector unknown drives and reads along a direction, which enters as a factor on
|
|
140
|
+
the weights, so `traces` and `scatter` stay exact transposes of one another. A
|
|
141
|
+
staggered unknown declares `sim.component_offsets`, and each component is then
|
|
142
|
+
interpolated on its own shifted grid.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
sim: the simulation whose grid the coordinates land on.
|
|
146
|
+
coords: (num, ndim) physical coordinates, inside the domain.
|
|
147
|
+
direction: (ncomp,) or (num, ncomp) components to act along, required where the
|
|
148
|
+
unknown has more than one.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
(nodes, weights) of the surrounding 2**ndim cell corners, shaped
|
|
152
|
+
(node_rows, num * 2**ndim * ncomp) and (num, 2**ndim * ncomp), the component
|
|
153
|
+
running fastest.
|
|
154
|
+
"""
|
|
155
|
+
coords = np.atleast_2d(np.asarray(coords, dtype=float))
|
|
156
|
+
if coords.shape[1] != sim.ndim:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"coordinates need {sim.ndim} components, not {coords.shape[1]}"
|
|
159
|
+
)
|
|
160
|
+
shifts = sim.component_offsets
|
|
161
|
+
if shifts is None:
|
|
162
|
+
shifts = np.zeros((sim.ncomp, sim.ndim))
|
|
163
|
+
if direction is None:
|
|
164
|
+
if sim.ncomp > 1:
|
|
165
|
+
raise ValueError(f"a {sim.ncomp}-component unknown needs a direction")
|
|
166
|
+
nodes, weights = _corner_weights(sim, coords, shifts[0])
|
|
167
|
+
return (
|
|
168
|
+
cp.asarray(nodes.reshape(sim.ndim, -1), dtype=cp.int32),
|
|
169
|
+
cp.asarray(weights, dtype=sim.dtype),
|
|
170
|
+
)
|
|
171
|
+
n = np.atleast_2d(np.asarray(direction, dtype=float))
|
|
172
|
+
if n.shape[1] != sim.ncomp:
|
|
173
|
+
raise ValueError(f"direction needs {sim.ncomp} components, not {n.shape[1]}")
|
|
174
|
+
if len(n) == 1:
|
|
175
|
+
n = np.repeat(n, len(coords), axis=0)
|
|
176
|
+
if len(n) != len(coords):
|
|
177
|
+
raise ValueError(f"{len(n)} directions for {len(coords)} coordinates")
|
|
178
|
+
per = [_corner_weights(sim, coords, shifts[c]) for c in range(sim.ncomp)]
|
|
179
|
+
weights = np.stack([w for _, w in per], axis=2) * n[:, None, :]
|
|
180
|
+
weights = weights.reshape(len(coords), -1)
|
|
181
|
+
nodes = np.stack([nodes for nodes, _ in per], axis=3) # (ndim, num, K, ncomp)
|
|
182
|
+
if sim.node_rows == sim.ndim:
|
|
183
|
+
# a single component needs no row of its own, so the direction is a scale
|
|
184
|
+
return (
|
|
185
|
+
cp.asarray(nodes.reshape(sim.ndim, -1), dtype=cp.int32),
|
|
186
|
+
cp.asarray(weights, dtype=sim.dtype),
|
|
187
|
+
)
|
|
188
|
+
# the component runs fastest, so one column per (coordinate, corner, component)
|
|
189
|
+
component = np.broadcast_to(
|
|
190
|
+
np.arange(sim.ncomp)[None, None, None, :], (1, *nodes.shape[1:])
|
|
191
|
+
)
|
|
192
|
+
rows = np.concatenate((component, nodes), axis=0)
|
|
193
|
+
return (
|
|
194
|
+
cp.asarray(rows.reshape(sim.node_rows, -1), dtype=cp.int32),
|
|
195
|
+
cp.asarray(weights, dtype=sim.dtype),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def point_source(
|
|
200
|
+
sim: Simulation,
|
|
201
|
+
coords: npt.ArrayLike,
|
|
202
|
+
signal: cpt.NDArray | npt.NDArray,
|
|
203
|
+
direction: npt.ArrayLike | None = None,
|
|
204
|
+
) -> Source:
|
|
205
|
+
"""Build a `Source` injecting `signal` at `coords`, distributed by `distribute`.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
sim: the simulation the source fires into.
|
|
209
|
+
coords: (num, ndim) physical coordinates of the point sources.
|
|
210
|
+
signal: one (N,) trace broadcast to every coordinate, or (N, num) one each.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
the `Source`, its signal divided by the cell volume so the amplitude is
|
|
214
|
+
independent of the grid spacing.
|
|
215
|
+
"""
|
|
216
|
+
nodes, weights = distribute(sim, coords, direction)
|
|
217
|
+
count = weights.shape[0]
|
|
218
|
+
signal = cp.asarray(signal, dtype=sim.dtype)
|
|
219
|
+
if signal.ndim == 1:
|
|
220
|
+
signal = cp.broadcast_to(signal[:, None], (signal.shape[0], count))
|
|
221
|
+
if signal.shape[1] != count:
|
|
222
|
+
raise ValueError(f"{signal.shape[1]} signal columns for {count} sources")
|
|
223
|
+
columns = (signal[:, :, None] * weights).reshape(signal.shape[0], -1)
|
|
224
|
+
return Source(
|
|
225
|
+
nodes, cp.ascontiguousarray(columns / np.prod(sim.dx), dtype=sim.dtype)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def collect_source(
|
|
230
|
+
sim: Simulation,
|
|
231
|
+
coords: npt.ArrayLike,
|
|
232
|
+
columns: cpt.NDArray,
|
|
233
|
+
direction: npt.ArrayLike | None = None,
|
|
234
|
+
) -> cpt.NDArray:
|
|
235
|
+
"""Transpose of `point_source`: an (N, num * 2**ndim) node gradient onto `coords`.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
sim: the simulation the columns were injected into.
|
|
239
|
+
coords: (num, ndim) physical coordinates the source was built from.
|
|
240
|
+
columns: the derivative with respect to the node columns `point_source` made.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
the (N, num) derivative with respect to the signal of each coordinate.
|
|
244
|
+
"""
|
|
245
|
+
_, weights = distribute(sim, coords, direction)
|
|
246
|
+
columns = columns.reshape(columns.shape[0], len(weights), -1)
|
|
247
|
+
# cast, since dividing a float32 record by a float64 cell volume would promote it
|
|
248
|
+
return (columns * weights).sum(2) / sim.dtype(np.prod(sim.dx))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def shots(
|
|
252
|
+
sim: Simulation,
|
|
253
|
+
coords: npt.ArrayLike,
|
|
254
|
+
signal: cpt.NDArray | npt.NDArray,
|
|
255
|
+
direction: npt.ArrayLike | None = None,
|
|
256
|
+
) -> list[Source]:
|
|
257
|
+
"""One single-coordinate `Source` per coordinate: the shot list of an inversion"""
|
|
258
|
+
return [point_source(sim, c, signal, direction) for c in np.atleast_2d(coords)]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def stack(sources: Sequence[Source]) -> Source:
|
|
262
|
+
"""Fire several shots in a single simulation.
|
|
263
|
+
|
|
264
|
+
Positions and signal columns are concatenated, so a stacked shot costs one
|
|
265
|
+
simulation instead of `len(sources)` and returns one record.
|
|
266
|
+
|
|
267
|
+
Encode the shots (random signs, phase shifts) by scaling their signals first.
|
|
268
|
+
"""
|
|
269
|
+
return Source(
|
|
270
|
+
cp.concatenate([s.position for s in sources], axis=1),
|
|
271
|
+
cp.ascontiguousarray(cp.concatenate([s.signal for s in sources], axis=1)),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class Sensors:
|
|
276
|
+
"""Receivers at arbitrary coordinates, multilinearly interpolated onto the grid.
|
|
277
|
+
|
|
278
|
+
The interpolation `nodes` and `weights` are precomputed once, so `traces` and its
|
|
279
|
+
transpose `scatter` are the only per-record work.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
sim: the simulation whose grid the receivers land on.
|
|
283
|
+
coords: (count, ndim) physical coordinates of the receivers.
|
|
284
|
+
direction: (ndim,) or (count, ndim) components each receiver measures along,
|
|
285
|
+
required where the unknown is a vector.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
def __init__(
|
|
289
|
+
self,
|
|
290
|
+
sim: Simulation,
|
|
291
|
+
coords: npt.ArrayLike,
|
|
292
|
+
direction: npt.ArrayLike | None = None,
|
|
293
|
+
) -> None:
|
|
294
|
+
self.sim = sim
|
|
295
|
+
self.coordinates = np.atleast_2d(np.asarray(coords, dtype=float))
|
|
296
|
+
self.count = len(self.coordinates)
|
|
297
|
+
self.nodes, self.weights = distribute(sim, self.coordinates, direction)
|
|
298
|
+
|
|
299
|
+
def traces(self, record: cpt.NDArray) -> cpt.NDArray:
|
|
300
|
+
"""(N, count * 2**ndim) node record -> (N, count) receiver traces"""
|
|
301
|
+
return (record.reshape(record.shape[0], self.count, -1) * self.weights).sum(2)
|
|
302
|
+
|
|
303
|
+
def scatter(self, dphi: cpt.NDArray) -> cpt.NDArray:
|
|
304
|
+
"""transpose of `traces`: (N, count) -> (N, count * 2**ndim)"""
|
|
305
|
+
return (dphi[:, :, None] * self.weights).reshape(dphi.shape[0], -1)
|
|
306
|
+
|
|
307
|
+
def objective(self, objective: Callable) -> Callable:
|
|
308
|
+
"""Lift a receiver-space `objective` into the node space `sensitivity` wants"""
|
|
309
|
+
|
|
310
|
+
def wrapped(record):
|
|
311
|
+
cost, dphi = objective(self.traces(record))
|
|
312
|
+
return cost, self.scatter(dphi)
|
|
313
|
+
|
|
314
|
+
return wrapped
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ---------------------------------------- fwi ----------------------------------------
|
|
318
|
+
def measure(
|
|
319
|
+
sim: Simulation,
|
|
320
|
+
sources: Sequence[Source],
|
|
321
|
+
indicator: cpt.NDArray,
|
|
322
|
+
sensors: Sensors,
|
|
323
|
+
) -> list[cpt.NDArray]:
|
|
324
|
+
"""Receiver traces per shot: the synthetic experiment an inversion is fitted to"""
|
|
325
|
+
return [
|
|
326
|
+
sensors.traces(simulate(sim, source, indicator, sensors=sensors.nodes)[1])
|
|
327
|
+
for source in sources
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def misfit(
|
|
332
|
+
sim: Simulation,
|
|
333
|
+
sources: Sequence[Source],
|
|
334
|
+
indicator: cpt.NDArray,
|
|
335
|
+
sensors: Sensors,
|
|
336
|
+
observed: Sequence[cpt.NDArray],
|
|
337
|
+
objective: Callable = l2_misfit,
|
|
338
|
+
) -> float:
|
|
339
|
+
"""The cost `misfit_gradient` returns, without its gradient: forward passes only."""
|
|
340
|
+
return sum(
|
|
341
|
+
objective(data)(traces)[0]
|
|
342
|
+
for traces, data in zip(measure(sim, sources, indicator, sensors), observed)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def misfit_gradient(
|
|
347
|
+
sim: Simulation,
|
|
348
|
+
sources: Sequence[Source],
|
|
349
|
+
indicator: cpt.NDArray,
|
|
350
|
+
sensors: Sensors,
|
|
351
|
+
observed: Sequence[cpt.NDArray],
|
|
352
|
+
objective: Callable = l2_misfit,
|
|
353
|
+
adjoint: Callable = sensitivity,
|
|
354
|
+
) -> tuple[float, cpt.NDArray]:
|
|
355
|
+
"""Misfit summed over the shots and its derivative with respect to `indicator`.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
sim: the simulation each shot is run in.
|
|
359
|
+
sources: the shot list, one adjoint solve each.
|
|
360
|
+
indicator: the design field the gradient is taken with respect to.
|
|
361
|
+
sensors: the receiver array the objective is evaluated on.
|
|
362
|
+
observed: the measured traces, one (N, count) record per shot.
|
|
363
|
+
objective: factory taking one record and returning `objective(traces)`.
|
|
364
|
+
adjoint: which variant computes each shot, `reconstruction_sensitivity`
|
|
365
|
+
where the stored history no longer fits.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
(cost, gradient), the gradient a field over the padded grid, already
|
|
369
|
+
reparametrized from (mass, stiff) onto `indicator`.
|
|
370
|
+
"""
|
|
371
|
+
cost = 0.0
|
|
372
|
+
gradient = cp.zeros(sim.Nx_padded, dtype=sim.dtype)
|
|
373
|
+
for source, data in zip(sources, observed):
|
|
374
|
+
shot_cost, grads, _, _ = adjoint(
|
|
375
|
+
sim, source, indicator, sensors.nodes, sensors.objective(objective(data))
|
|
376
|
+
)
|
|
377
|
+
cost += shot_cost
|
|
378
|
+
gradient += _reparametrize(sim, indicator, grads)
|
|
379
|
+
return cost, gradient
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# ---------------------------------------- tato ---------------------------------------
|
|
383
|
+
def energy(sim: Simulation) -> Callable:
|
|
384
|
+
"""Objective factory: J = 1/2 int_region int_t p^2, the energy reaching the sensors."""
|
|
385
|
+
scale = float(np.prod(sim.dx)) * sim.dt
|
|
386
|
+
|
|
387
|
+
def objective(traces):
|
|
388
|
+
return 0.5 * scale * float(cp.sum(traces**2)), scale * traces
|
|
389
|
+
|
|
390
|
+
return objective
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def response(
|
|
394
|
+
sim: Simulation,
|
|
395
|
+
source: Source,
|
|
396
|
+
indicator: cpt.NDArray,
|
|
397
|
+
sensors: cpt.NDArray[cp.int32],
|
|
398
|
+
objective: Callable,
|
|
399
|
+
) -> tuple[float, cpt.NDArray]:
|
|
400
|
+
"""The cost `response_gradient` returns, without its gradient: one forward pass.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
(cost, wavefield), the field at the last step over the logical grid, so the
|
|
404
|
+
design a cost was read from is plotted together with the wave that scored it.
|
|
405
|
+
"""
|
|
406
|
+
wavefield, traces = simulate(sim, source, indicator, sensors=sensors)
|
|
407
|
+
return objective(traces)[0], wavefield
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def response_gradient(
|
|
411
|
+
sim: Simulation,
|
|
412
|
+
source: Source,
|
|
413
|
+
indicator: cpt.NDArray,
|
|
414
|
+
sensors: cpt.NDArray[cp.int32],
|
|
415
|
+
objective: Callable,
|
|
416
|
+
adjoint: Callable = sensitivity,
|
|
417
|
+
) -> tuple[float, cpt.NDArray]:
|
|
418
|
+
"""Cost of one shot and its derivative with respect to `indicator`.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
sim: the simulation the forward and adjoint passes both step.
|
|
422
|
+
source: the shot to differentiate, its position interior nodes only.
|
|
423
|
+
indicator: the design field the gradient is taken with respect to.
|
|
424
|
+
sensors: (ndim, num_sensors) interior grid indices the objective reads.
|
|
425
|
+
objective: takes the (N, num_sensors) record, returns (cost, dcost/dtraces).
|
|
426
|
+
adjoint: which variant computes the gradient, `reconstruction_sensitivity`
|
|
427
|
+
where the stored history no longer fits.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
(cost, gradient), the gradient a field over the padded grid, already
|
|
431
|
+
reparametrized from (mass, stiff) onto `indicator`.
|
|
432
|
+
"""
|
|
433
|
+
cost, grads, _, _ = adjoint(sim, source, indicator, sensors, objective)
|
|
434
|
+
return cost, _reparametrize(sim, indicator, grads)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# ---------------------------------------- tpto ---------------------------------------
|
|
438
|
+
def intensity(
|
|
439
|
+
sim: Simulation,
|
|
440
|
+
frequencies: npt.ArrayLike,
|
|
441
|
+
weights: cpt.NDArray | npt.ArrayLike | None = None,
|
|
442
|
+
) -> Callable:
|
|
443
|
+
"""Objective factory: J = sum |u_hat(f)|**2, the spectral intensity at the sensors.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
sim: the simulation the record comes from, whose `dt` and `N` fix the
|
|
447
|
+
transform.
|
|
448
|
+
frequencies: one frequency, or several scored by the same run.
|
|
449
|
+
weights: (num_frequencies, num_sensors), or anything broadcasting to it, so one
|
|
450
|
+
record scores several ports each at its own frequency. Defaults to 1.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
the objective `sensitivity` takes. The transform is linear in the record, so
|
|
454
|
+
its adjoint is the same pair of tables read backwards.
|
|
455
|
+
"""
|
|
456
|
+
# the phase reaches 1e5 radians over a long run, so it is built in double
|
|
457
|
+
f = np.atleast_1d(np.asarray(frequencies, dtype=np.float64))
|
|
458
|
+
t = np.arange(sim.N, dtype=np.float64) * sim.dt
|
|
459
|
+
phase = 2.0 * np.pi * t[:, None] * f[None, :]
|
|
460
|
+
cos = cp.asarray(np.cos(phase), dtype=sim.dtype)
|
|
461
|
+
sin = cp.asarray(np.sin(phase), dtype=sim.dtype)
|
|
462
|
+
scale = sim.dtype(sim.dt)
|
|
463
|
+
w = 1.0 if weights is None else cp.asarray(weights, dtype=sim.dtype)
|
|
464
|
+
|
|
465
|
+
# real tables rather than one complex one: half the work, and no conjugate to drop
|
|
466
|
+
def objective(traces):
|
|
467
|
+
re = scale * (cos.T @ traces)
|
|
468
|
+
im = -scale * (sin.T @ traces)
|
|
469
|
+
cost = float(cp.sum(w * (re * re + im * im)))
|
|
470
|
+
return cost, 2.0 * scale * (cos @ (w * re) - sin @ (w * im))
|
|
471
|
+
|
|
472
|
+
return objective
|