Skip to content

Commit 8094963

Browse files
authored
Merge pull request #593 from mrava87/feature-devitowav
feature: added updatesrc method to AcousticWave2D
2 parents 3a27486 + 733533e commit 8094963

File tree

2 files changed

+272
-8
lines changed

2 files changed

+272
-8
lines changed

examples/plot_twoway.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
r"""
2+
Acoustic Wave Equation modelling
3+
================================
4+
5+
This example shows how to perform acoustic wave equation modelling
6+
using the :class:`pylops.waveeqprocessing.AcousticWave2D` operator,
7+
which brings the power of finite-difference modelling via the Devito
8+
modelling engine to PyLops.
9+
"""
10+
import matplotlib.pyplot as plt
11+
import numpy as np
12+
from scipy.ndimage import gaussian_filter
13+
14+
import pylops
15+
16+
plt.close("all")
17+
np.random.seed(0)
18+
19+
20+
###############################################################################
21+
# To begin with, we will create a simple layered velocity model. We will also
22+
# define a background velocity model by smoothing the original velocity model
23+
# which will be responsible of the kinematic of the wavefield modelled via
24+
# Born modelling, and the perturbation velocity model which will lead to
25+
# scattering effects and therefore guide the dynamic of the modelled wavefield.
26+
27+
# Velocity Model
28+
nx, nz = 61, 40
29+
dx, dz = 4, 4
30+
x, z = np.arange(nx) * dx, np.arange(nz) * dz
31+
vel = 1000 * np.ones((nx, nz))
32+
vel[:, 15:] = 1200
33+
vel[:, 35:] = 1600
34+
35+
# Smooth velocity model
36+
v0 = gaussian_filter(vel, sigma=10)
37+
38+
# Born perturbation from m - m0
39+
dv = vel ** (-2) - v0 ** (-2)
40+
41+
# Receivers
42+
nr = 101
43+
rx = np.linspace(0, x[-1], nr)
44+
rz = 20 * np.ones(nr)
45+
recs = np.vstack((rx, rz))
46+
dr = recs[0, 1] - recs[0, 0]
47+
48+
# Sources
49+
ns = 3
50+
sx = np.linspace(0, x[-1], ns)
51+
sz = 10 * np.ones(ns)
52+
sources = np.vstack((sx, sz))
53+
54+
plt.figure(figsize=(10, 5))
55+
im = plt.imshow(vel.T, cmap="summer", extent=(x[0], x[-1], z[-1], z[0]))
56+
plt.scatter(recs[0], recs[1], marker="v", s=150, c="b", edgecolors="k")
57+
plt.scatter(sources[0], sources[1], marker="*", s=150, c="r", edgecolors="k")
58+
cb = plt.colorbar(im)
59+
cb.set_label("[m/s]")
60+
plt.axis("tight")
61+
plt.xlabel("x [m]"), plt.ylabel("z [m]")
62+
plt.title("Velocity")
63+
plt.xlim(x[0], x[-1])
64+
plt.tight_layout()
65+
66+
plt.figure(figsize=(10, 5))
67+
im = plt.imshow(dv.T, cmap="seismic", extent=(x[0], x[-1], z[-1], z[0]))
68+
plt.scatter(recs[0], recs[1], marker="v", s=150, c="b", edgecolors="k")
69+
plt.scatter(sources[0], sources[1], marker="*", s=150, c="r", edgecolors="k")
70+
cb = plt.colorbar(im)
71+
cb.set_label("[m/s]")
72+
plt.axis("tight")
73+
plt.xlabel("x [m]"), plt.ylabel("z [m]")
74+
plt.title("Velocity perturbation")
75+
plt.xlim(x[0], x[-1])
76+
plt.tight_layout()
77+
78+
###############################################################################
79+
# Let us now define the Born modelling operator
80+
81+
Aop = pylops.waveeqprocessing.AcousticWave2D(
82+
(nx, nz),
83+
(0, 0),
84+
(dx, dz),
85+
v0,
86+
sources[0],
87+
sources[1],
88+
recs[0],
89+
recs[1],
90+
0.0,
91+
0.5 * 1e3,
92+
"Ricker",
93+
space_order=4,
94+
nbl=100,
95+
f0=15,
96+
dtype="float32",
97+
)
98+
99+
###############################################################################
100+
# And we use it to model our data
101+
102+
dobs = Aop @ dv
103+
104+
fig, axs = plt.subplots(1, 3, sharey=True, figsize=(10, 6))
105+
fig.suptitle("FD modelling with Ricker", y=0.99)
106+
107+
for isrc in range(ns):
108+
axs[isrc].imshow(
109+
dobs[isrc].reshape(Aop.geometry.nrec, Aop.geometry.nt).T,
110+
cmap="gray",
111+
vmin=-1e-7,
112+
vmax=1e-7,
113+
extent=(
114+
recs[0, 0],
115+
recs[0, -1],
116+
Aop.geometry.time_axis.time_values[-1] * 1e-3,
117+
0,
118+
),
119+
)
120+
axs[isrc].axis("tight")
121+
axs[isrc].set_xlabel("rec [m]")
122+
axs[0].set_ylabel("t [s]")
123+
fig.tight_layout()
124+
125+
###############################################################################
126+
# Finally, we are going to show how despite the
127+
# :class:`pylops.waveeqprocessing.AcousticWave2D` operator allows a user to
128+
# specify a limited number of source wavelets (this is directly borrowed from
129+
# Devito), a simple modification can be applied to pass any user defined wavelet.
130+
# We are going to do that with a Ormsby wavelet
131+
132+
# Extract Ricker wavelet
133+
wav = Aop.geometry.src.data[:, 0]
134+
wavc = np.argmax(wav)
135+
136+
# Define Ormsby wavelet
137+
wavest = pylops.utils.wavelets.ormsby(
138+
Aop.geometry.time_axis.time_values[:wavc] * 1e-3, f=[3, 20, 30, 45]
139+
)[0]
140+
141+
# Update wavelet in operator and model new data
142+
Aop.updatesrc(wavest)
143+
144+
dobs1 = Aop @ dv
145+
146+
fig, axs = plt.subplots(1, 3, sharey=True, figsize=(10, 6))
147+
fig.suptitle("FD modelling with Ormsby", y=0.99)
148+
149+
for isrc in range(ns):
150+
axs[isrc].imshow(
151+
dobs1[isrc].reshape(Aop.geometry.nrec, Aop.geometry.nt).T,
152+
cmap="gray",
153+
vmin=-1e-7,
154+
vmax=1e-7,
155+
extent=(
156+
recs[0, 0],
157+
recs[0, -1],
158+
Aop.geometry.time_axis.time_values[-1] * 1e-3,
159+
0,
160+
),
161+
)
162+
axs[isrc].axis("tight")
163+
axs[isrc].set_xlabel("rec [m]")
164+
axs[0].set_ylabel("t [s]")
165+
fig.tight_layout()
166+
167+
fig, axs = plt.subplots(1, 2, figsize=(10, 3))
168+
axs[0].plot(wav[: 2 * wavc], "k")
169+
axs[0].plot(wavest, "r")
170+
axs[1].plot(
171+
dobs[isrc].reshape(Aop.geometry.nrec, Aop.geometry.nt)[nr // 2], "k", label="Ricker"
172+
)
173+
axs[1].plot(
174+
dobs1[isrc].reshape(Aop.geometry.nrec, Aop.geometry.nt)[nr // 2],
175+
"r",
176+
label="Ormsby",
177+
)
178+
axs[1].legend()
179+
fig.tight_layout()

pylops/waveeqprocessing/twoway.py

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,49 @@
1414
if devito_message is None:
1515
from examples.seismic import AcquisitionGeometry, Model
1616
from examples.seismic.acoustic import AcousticWaveSolver
17+
from examples.seismic.utils import PointSource
18+
19+
20+
class _CustomSource(PointSource):
21+
"""Custom source
22+
23+
This class creates a Devito symbolic object that encapsulates a set of
24+
sources with a user defined source signal wavelet ``wav``
25+
26+
Parameters
27+
----------
28+
name : :obj:`str`
29+
Name for the resulting symbol.
30+
grid : :obj:`devito.types.grid.Grid`
31+
The computational domain.
32+
time_range : :obj:`examples.seismic.source.TimeAxis`
33+
TimeAxis(start, step, num) object.
34+
wav : :obj:`numpy.ndarray`
35+
Wavelet of size
36+
37+
"""
38+
39+
__rkwargs__ = PointSource.__rkwargs__ + ["wav"]
40+
41+
@classmethod
42+
def __args_setup__(cls, *args, **kwargs):
43+
kwargs.setdefault("npoint", 1)
44+
45+
return super().__args_setup__(*args, **kwargs)
46+
47+
def __init_finalize__(self, *args, **kwargs):
48+
super().__init_finalize__(*args, **kwargs)
49+
50+
self.wav = kwargs.get("wav")
51+
52+
if not self.alias:
53+
for p in range(kwargs["npoint"]):
54+
self.data[:, p] = self.wavelet
55+
56+
@property
57+
def wavelet(self):
58+
"""Return user-provided wavelet"""
59+
return self.wav
1760

1861

1962
class AcousticWave2D(LinearOperator):
@@ -38,9 +81,9 @@ class AcousticWave2D(LinearOperator):
3881
rec_z : :obj:`numpy.ndarray` or :obj:`float`
3982
Receiver z-coordinates in m
4083
t0 : :obj:`float`
41-
Initial time
84+
Initial time in ms
4285
tn : :obj:`int`
43-
Number of time samples
86+
Final time in ms
4487
src_type : :obj:`str`
4588
Source type
4689
space_order : :obj:`int`, optional
@@ -79,7 +122,7 @@ def __init__(
79122
rec_x: NDArray,
80123
rec_z: NDArray,
81124
t0: float,
82-
tn: int,
125+
tn: float,
83126
src_type: str = "Ricker",
84127
space_order: int = 6,
85128
nbl: int = 20,
@@ -155,7 +198,7 @@ def _create_geometry(
155198
rec_x: NDArray,
156199
rec_z: NDArray,
157200
t0: float,
158-
tn: int,
201+
tn: float,
159202
src_type: str,
160203
f0: float = 20.0,
161204
) -> None:
@@ -174,7 +217,7 @@ def _create_geometry(
174217
t0 : :obj:`float`
175218
Initial time
176219
tn : :obj:`int`
177-
Number of time samples
220+
Final time in ms
178221
src_type : :obj:`str`
179222
Source type
180223
f0 : :obj:`float`, optional
@@ -201,6 +244,28 @@ def _create_geometry(
201244
f0=None if f0 is None else f0 * 1e-3,
202245
)
203246

247+
def updatesrc(self, wav):
248+
"""Update source wavelet
249+
250+
This routines is used to allow users to pass a custom source
251+
wavelet to replace the source wavelet generated when the
252+
object is initialized
253+
254+
Parameters
255+
----------
256+
wav : :obj:`numpy.ndarray`
257+
Wavelet
258+
259+
"""
260+
wav_padded = np.pad(wav, (0, self.geometry.nt - len(wav)))
261+
262+
self.wav = _CustomSource(
263+
name="src",
264+
grid=self.model.grid,
265+
wav=wav_padded,
266+
time_range=self.geometry.time_axis,
267+
)
268+
204269
def _srcillumination_oneshot(self, isrc: int) -> Tuple[NDArray, NDArray]:
205270
"""Source wavefield and illumination for one shot
206271
@@ -229,8 +294,15 @@ def _srcillumination_oneshot(self, isrc: int) -> Tuple[NDArray, NDArray]:
229294
)
230295
solver = AcousticWaveSolver(self.model, geometry, space_order=self.space_order)
231296

297+
# assign source location to source object with custom wavelet
298+
if hasattr(self, "wav"):
299+
self.wav.coordinates.data[0, :] = self.geometry.src_positions[isrc, :]
300+
232301
# source wavefield
233-
u0 = solver.forward(save=True)[1]
302+
u0 = solver.forward(
303+
save=True, src=None if not hasattr(self, "wav") else self.wav
304+
)[1]
305+
234306
# source illumination
235307
src_ill = self._crop_model((u0.data**2).sum(axis=0), self.model.nbl)
236308
return u0, src_ill
@@ -286,9 +358,15 @@ def _born_oneshot(self, isrc: int, dm: NDArray) -> NDArray:
286358
dmext = np.zeros(self.model.grid.shape, dtype=np.float32)
287359
dmext[self.model.nbl : -self.model.nbl, self.model.nbl : -self.model.nbl] = dm
288360

361+
# assign source location to source object with custom wavelet
362+
if hasattr(self, "wav"):
363+
self.wav.coordinates.data[0, :] = self.geometry.src_positions[isrc, :]
364+
289365
# solve
290366
solver = AcousticWaveSolver(self.model, geometry, space_order=self.space_order)
291-
d = solver.jacobian(dmext)[0]
367+
d = solver.jacobian(dmext, src=None if not hasattr(self, "wav") else self.wav)[
368+
0
369+
]
292370
d = d.resample(geometry.dt).data[:][: geometry.nt].T
293371
return d
294372

@@ -347,11 +425,18 @@ def _bornadj_oneshot(self, isrc, dobs):
347425

348426
solver = AcousticWaveSolver(self.model, geometry, space_order=self.space_order)
349427

428+
# assign source location to source object with custom wavelet
429+
if hasattr(self, "wav"):
430+
self.wav.coordinates.data[0, :] = self.geometry.src_positions[isrc, :]
431+
350432
# source wavefield
351433
if hasattr(self, "src_wavefield"):
352434
u0 = self.src_wavefield[isrc]
353435
else:
354-
u0 = solver.forward(save=True)[1]
436+
u0 = solver.forward(
437+
save=True, src=None if not hasattr(self, "wav") else self.wav
438+
)[1]
439+
355440
# adjoint modelling (reverse wavefield plus imaging condition)
356441
model = solver.jacobian_adjoint(
357442
rec=recs, u=u0, checkpointing=self.checkpointing

0 commit comments

Comments
 (0)