import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
from IPython.display import HTML
from devito import SubDomain, Grid, Function, Eq, Operator, \
\
SparseFunction, ConditionalDimension, TimeFunction, solve,
TensorTimeFunction, VectorTimeFunction, diag, grad, divfrom examples.seismic import TimeAxis, RickerSource, Model, Receiver
Defining Function
s on SubDomain
s
In many contexts, fields are required over only a subset of the computational Grid
. In Devito, such fields can be implemented via the Function
s on SubDomain
s API, allocating arrays only over the required region of the Grid
, thereby reducing memory consumption. This functionality is seamlessly compatible with MPI, as with any other Devito object, and these arrays are aligned with the domain decomposition under the hood.
We will begin with some necessary imports.
Basic usage
As usual, we define a subdomain template by subclassing SubDomain
and overriding the define
method. The 'middle'
subdomain we have defined in this case excludes two points from each side of the domain in each dimension.
class Middle(SubDomain):
= 'middle'
name
def define(self, dimensions):
return {d: ('middle', 2, 2) for d in dimensions}
We then create a Grid
, followed by an instance of Middle
. We can then use this subdomain as the computational grid when defining a Function
. For comparison, we also create a Function
defined on a Grid
.
= Grid(shape=(11, 11), extent=(10., 10.))
grid0
= Middle(grid=grid0)
middle
= Function(name='f', grid=middle) # Define Function on SubDomain
f = Function(name='g', grid=grid0) g
Inspecting the shape
of f
, it is apparent that it is smaller than the Grid
on which middle
is defined. Instead, its shape is that of middle
.
f.shape
(7, 7)
Furthermore, f
has the dimensions of the SubDomain
rather than the Grid
.
f.dimensions
(ix, iy)
By contrast, when inspecting g
, we observe that its shape and dimensions match those of the Grid
.
g.shape
(11, 11)
g.dimensions
(x, y)
To contrast the behaviour of a Function
defined on a SubDomain
to one defined on a Grid
, we implement a trivial Operator
, setting both f
and g
equal to one within the SubDomain
middle
.
#NBVAL_IGNORE_OUTPUT
= Eq(f, 1)
eq_f = Eq(g, 1, subdomain=middle)
eq_g
Operator([eq_f, eq_g])()
NUMA domain count autodetection failed
Operator `Kernel` ran in 0.01 s
PerformanceSummary([(PerfKey(name='section0', rank=None),
PerfEntry(time=0.000196, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[]))])
Note that if there is only one SubDomain
on which Function
s are defined present in the Eq
expressions, Devito will automatically infer the subdomain
kwarg if none is supplied. If multiple SubDomain
s are used to define Function
s, but no subdomain
is specified in Eq
, then an error will be thrown. Inspecting eq_f
, we see that the SubDomain
is inferred:
eq_f.subdomain
Middle[middle(ix, iy)]
Examining f.data
, we see that it has been entirely set to one by the Operator
. By contrast, g.data
is only set to one in the central region.
#NBVAL_IGNORE_OUTPUT
=0, vmax=3, origin='lower', extent=(1.5, 8.5, 1.5, 8.5))
plt.imshow(f.data.T, vmin
plt.colorbar()"f.data")
plt.title("x (m)")
plt.xlabel("y (m)")
plt.ylabel(-0.5, 10.5)
plt.xlim(-0.5, 10.5)
plt.ylim(
plt.show()
=0, vmax=3, origin='lower', extent=(-0.5, 10.5, -0.5, 10.5))
plt.imshow(g.data.T, vmin
plt.colorbar()"g.data")
plt.title("x (m)")
plt.xlabel("y (m)")
plt.ylabel( plt.show()
When operating on a Function
defined on a SubDomain
, Devito keeps track of the alignment of the fields relative to the base Grid
. As such, we can perform operations combining Function
s defined on SubDomain
s with those defined on the Grid
. For example, we can add f
to g
as follows:
#NBVAL_IGNORE_OUTPUT
= Eq(g, g + f)
eq_fg Operator(eq_fg)()
Operator `Kernel` ran in 0.01 s
PerformanceSummary([(PerfKey(name='section0', rank=None),
PerfEntry(time=0.000103, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[]))])
Plotting g.data
, we see that the central region, corresponding to middle
, has been incremented by the value of f
.
#NBVAL_IGNORE_OUTPUT
=0, vmax=3, origin='lower', extent=(-0.5, 10.5, -0.5, 10.5))
plt.imshow(g.data.T, vmin
plt.colorbar()"g.data")
plt.title("x (m)")
plt.xlabel("y (m)")
plt.ylabel( plt.show()
Note that we need not only iterate over the SubDomain
on which our Function
is defined. One can also iterate over any subset of this SubDomain
. In this case, we define a new Left
domain, alongside Intersection
, which is the intersection of Left
and Middle
. We then define another Function
h
on left
.
class Left(SubDomain):
= 'left'
name
def define(self, dimensions):
= dimensions
x, y return {x: ('left', 6), y: y}
class Intersection(SubDomain):
# Intersection of Left and Middle
= 'intersection'
name
def define(self, dimensions):
= dimensions
x, y return {x: ('middle', 2, 5), y: ('middle', 2, 2)}
# Create the SubDomain instances
= Left(grid=grid0)
left = Intersection(grid=grid0)
intersection
= Function(name='h', grid=left)
h = 1 h.data[:]
Printing h.shape
, we see that its size is also smaller than that of the Grid
.
h.shape
(6, 11)
We can then define equations which act over the intersection of Middle
and Left
. These equations can operate on any of the Function
s defined thus far.
#NBVAL_IGNORE_OUTPUT
# Equations operating on Functions defined on SubDomains must be applied over
# the SubDomain, or a SubDomain representing some subset thereof.
# Add h (defined on Left) to g (defined on Grid) over Intersection, and store result in g
= Eq(g, g + h, subdomain=intersection)
eq_gh # Add h (defined on Left) to f (defined on Middle) over Intersection, and store result in f
= Eq(f, f + h, subdomain=intersection)
eq_fh
Operator([eq_gh, eq_fh])()
Operator `Kernel` ran in 0.01 s
PerformanceSummary([(PerfKey(name='section0', rank=None),
PerfEntry(time=0.000108, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[]))])
Plotting both g
and f
, we observe that both have been incremented by one (the value stored inh
) over the intersection.
#NBVAL_IGNORE_OUTPUT
=0, vmax=3, origin='lower', extent=(-0.5, 10.5, -0.5, 10.5))
plt.imshow(g.data.T, vmin
plt.colorbar()"g.data")
plt.title("x (m)")
plt.xlabel("y (m)")
plt.ylabel(
plt.show()
=0, vmax=3, origin='lower', extent=(1.5, 8.5, 1.5, 8.5))
plt.imshow(f.data.T, vmin
plt.colorbar()"f.data")
plt.title("x (m)")
plt.xlabel("y (m)")
plt.ylabel(-0.5, 10.5)
plt.xlim(-0.5, 10.5)
plt.ylim( plt.show()
Finally, we can also take values from a Function
defined on the Grid
, operate on them, and store them in a Function
defined on a SubDomain
.
#NBVAL_IGNORE_OUTPUT
= Eq(f, g)
eq_gf Operator(eq_gf)()
Operator `Kernel` ran in 0.01 s
PerformanceSummary([(PerfKey(name='section0', rank=None),
PerfEntry(time=6.7e-05, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[]))])
Note that f
now contains the same values as the central region of g
.
#NBVAL_IGNORE_OUTPUT
=0, vmax=3, origin='lower', extent=(1.5, 8.5, 1.5, 8.5))
plt.imshow(f.data.T, vmin
plt.colorbar()"f.data")
plt.title("x (m)")
plt.xlabel("y (m)")
plt.ylabel(-0.5, 10.5)
plt.xlim(-0.5, 10.5)
plt.ylim( plt.show()
Sparse operations
Similarly, one can perform sparse operations such as injection into or interpolation off of Function
s defined on SubDomain
s. This is performed as per the usual interface. Note however that the operation will be windowed to the SubDomain
, not the Grid
, even if it involves the interpolation of an expression featuring both Function
s defined on a SubDomain
and those defined on a Grid
. This is because such an operation would be undefined outside a SubDomain
.
We begin by constructing a SparseFunction
.
= SparseFunction(name='srcrec', grid=grid0, npoint=1)
src_rec = np.array([[3., 3.]]) src_rec.coordinates.data[:]
A simple interpolation operator is constructed as follows:
#NBVAL_IGNORE_OUTPUT
= src_rec.interpolate(expr=f)
rec Operator(rec)()
Operator `Kernel` ran in 0.01 s
PerformanceSummary([(PerfKey(name='section0', rank=None),
PerfEntry(time=9.999999999999999e-05, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[]))])
Printing srcrec.data
, it is apparent that it now contains the value interpolated off of f
.
src_rec.data
Data([3.], dtype=float32)
As aforementioned, one can also interpolate an expression featuring Function
s defined on both the Grid
and a SubDomain
. This can be straightforwardly achieved as follows:
#NBVAL_IGNORE_OUTPUT
= src_rec.interpolate(expr=f+g)
rec Operator(rec)()
Operator `Kernel` ran in 0.01 s
PerformanceSummary([(PerfKey(name='section0', rank=None),
PerfEntry(time=7e-05, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[]))])
Note that since one of our previous operators set f
equal to g
over Middle
, srcrec.data
is now equal to 6.
src_rec.data
Data([6.], dtype=float32)
We can also inject into a Function
defined on a SubDomain
.
#NBVAL_IGNORE_OUTPUT
= src_rec.inject(field=h, expr=1)
src Operator(src)()
Operator `Kernel` ran in 0.01 s
PerformanceSummary([(PerfKey(name='section0', rank=None),
PerfEntry(time=8.599999999999999e-05, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[]))])
Plotting h.data
, we see that a value of 1 has been injected. It is also apparent that this injection is aligned with the global grid.
#NBVAL_IGNORE_OUTPUT
=0, vmax=3, origin='lower', extent=(-0.5, 5.5, -0.5, 10.5))
plt.imshow(h.data.T, vmin
plt.colorbar()"h.data")
plt.title("x (m)")
plt.xlabel("y (m)")
plt.ylabel(-0.5, 10.5)
plt.xlim(-0.5, 10.5)
plt.ylim( plt.show()
Snapshotting on a subset of a domain
We will now consider a practical application of this functionality. In many applications, it is necessary to capture snapshots of a simulation as it progresses. However, such snapshots may not be needed over the entirety of the domain. We can use a Function
defined on the subset of the domain that we aim to capture to minimize the size of the arrays allocated.
In this case, we will consider the snapshotting of a seismic wavefield. In imaging workflows, there is no need to calculate an imaging condition in the water column, and thus no need to store snapshots of this portion of the domain.
We begin by setting up the grid parameters and velocity model.
= (0., 0.)
origin = (201, 201)
shape = (5., 5.)
spacing = tuple((sh-1)*sp for sh, sp in zip(shape, spacing))
extent
# Layered model
= np.full(shape, 1.5)
vp for i in range(6):
50+i*25:] += 0.3 vp[:,
We can then construct a Model
. To highlight the use of Function
s on SubDomain
s, we plot the padded velocity model, overlaying the interior domain (i.e. the core computational domain, excluding boundary layers) and the area of interest for snapshotting (excluding the water layer at the top).
#NBVAL_IGNORE_OUTPUT
= Model(vp=vp, origin=origin, shape=shape, spacing=spacing,
model =2, nbl=20, bcs="damp")
space_order
# Plot velocity model
=(-100, 1100., 1100., -100.))
plt.imshow(model.vp.data.T, extent0, 1000, 1000, 0, 0), (0, 0, 1000, 1000, 0), 'k', label="Main Domain")
plt.plot((0, 1000, 1000, 0, 0), (245, 245, 1000, 1000, 245), 'r--', label="Snapshotting Region")
plt.plot((='lower left')
plt.legend(loc="km/s")
plt.colorbar(label"x (m)")
plt.xlabel("z (m)")
plt.ylabel( plt.show()
Operator `initdamp` ran in 0.01 s
We then set up a SubDomain
corresponding to this region.
class SnapshotDomain(SubDomain):
= 'snapshot'
name
def define(self, dimensions):
= dimensions
x, y # Exclude damping layers and water column
return {x: ('middle', 20, 20), y: ('middle', 70, 20)}
= SnapshotDomain(grid=model.grid) snapshotdomain
A trivial Operator
can be used to check that the specified SubDomain
is aligned as intended.
#NBVAL_IGNORE_OUTPUT
# Make a Function and set it equal to one to check the alignment of subdomain
= Function(name='testfunc', grid=model.grid)
test_func 1, subdomain=snapshotdomain))()
Operator(Eq(test_func,
# Plotting the resultant field, we see that the incremented region aligns with our
# desired snapshotting region.
=(-100, 1100., 1100., -100.))
plt.imshow(test_func.data.T, extent0, 1000, 1000, 0, 0), (0, 0, 1000, 1000, 0), 'k', label="Main Domain")
plt.plot((0, 1000, 1000, 0, 0), (245, 245, 1000, 1000, 245), 'r--', label="Snapshotting Region")
plt.plot((='lower left')
plt.legend(loc
plt.colorbar()"x (m)")
plt.xlabel("z (m)")
plt.ylabel( plt.show()
Operator `Kernel` ran in 0.01 s
Now we set up a source.
# Set time range, source, source coordinates and receiver coordinates
= 0. # Simulation starts a t=0
t0 = 500. # Simulation lasts tn milliseconds
tn = 0.8*model.critical_dt # Time step from model grid spacing
dt = TimeAxis(start=t0, stop=tn, step=dt)
time_range = time_range.num # number of time steps
nt
= 0.040 # Source peak frequency is 10Hz (0.010 kHz)
f0 = RickerSource(name='src', grid=model.grid, f0=f0, time_range=time_range)
src
0, :] = np.array(model.domain_size) * .5
src.coordinates.data[0, -1] = 20. # Depth is 20m src.coordinates.data[
Snapshotting is specified as usual. The only difference is that grid=snapshotdomain
is passed when instantiating the snapshot TimeFunction
. For comparison, we also instantiate usavegrid
in the conventional manner.
= 30
nsnaps = int(np.ceil(nt/nsnaps))
factor
= ConditionalDimension('t_sub', parent=model.grid.time_dim, factor=factor)
time_subsampled
= TimeFunction(name='usave', grid=snapshotdomain, time_order=0, space_order=2,
u_save =nsnaps, time_dim=time_subsampled)
save
# "Normal" snapshotting for comparison
= TimeFunction(name='usavegrid', grid=model.grid, time_order=0, space_order=2,
u_save_grid =nsnaps, time_dim=time_subsampled) save
Comparing the size of usave
, allocated on snapshotdomain
, to usavegrid
, allocated on model.grid
, we see that the former has a substantially smaller underlying array than the latter. This yields notable memory savings.
= round(float(100*(u_save_grid.size - u_save.size)/u_save_grid.size), 2)
memreduction
print(f"Using `Function`s on `SubDomain`s for this snapshot reduces memory requirements by {memreduction}%")
Using `Function`s on `SubDomain`s for this snapshot reduces memory requirements by 47.06%
We will solve the 2nd-order acoustic wave equation in this case. Note that both “normal” and subdomain snapshots are saved for comparison and plotting. In practice, one would only save the subdomain snapshots to minimise memory consumption.
#NBVAL_IGNORE_OUTPUT
= TimeFunction(name="u", grid=model.grid, time_order=2, space_order=8)
u
= model.m * u.dt2 - u.laplace + model.damp * u.dt
pde = Eq(u.forward, solve(pde, u.forward))
stencil = src.inject(field=u.forward, expr=src)
src_term
# Save both "normal" and subdomain snapshots for comparison
= Operator([stencil] + [Eq(u_save, u, subdomain=snapshotdomain), Eq(u_save_grid, u)] + src_term)
op apply(t_M=nt-2, dt=dt) op.
Operator `Kernel` ran in 0.09 s
PerformanceSummary([(PerfKey(name='section0', rank=None),
PerfEntry(time=0.0491209999999999, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[])),
(PerfKey(name='section1', rank=None),
PerfEntry(time=0.001683, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[])),
(PerfKey(name='section2', rank=None),
PerfEntry(time=0.002006, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[])),
(PerfKey(name='section3', rank=None),
PerfEntry(time=0.037044, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[]))])
We then visualise this process with an animation.
#NBVAL_SKIP
def animate_wavefield(f, fg, v, interval=100):
"""
Create an animation of the wavefield.
Parameters:
f : Snapshots on SubDomain
fg : Snapshots on Grid
v : Velocity model
interval : Delay between frames in milliseconds (default: 100)
"""
# Determine color limits based on full dataset (so they remain constant throughout)
= 0.5
clip = clip * np.amax(np.abs(f.data))
vmax = -vmax
vmin
# Create the figure and axis
= plt.subplots()
fig, ax
# Draw static elements
# Velocity model (constant background)
="Greys", extent=(-100, 1100, 1100, -100),
ax.imshow(v.data.T, cmap=0.4, zorder=3)
alpha# Snapshot region (red dashed line)
0, 1000, 1000, 0, 0), (245, 245, 1000, 1000, 245),
ax.plot(('r--', label="Snapshotting Region", zorder=4)
# Draw time-dependent images
# Initialize with the first frame (frame index 0)
= ax.imshow(fg.data[0].T, vmin=vmin, vmax=vmax, cmap="Greys",
im_fg =(-100, 1100, 1100, -100), zorder=1)
extent= ax.imshow(f.data[0].T, vmin=vmin, vmax=vmax, cmap='seismic',
im_f =(0, 1000, 1000, 245), zorder=2)
extent
# Set axis limits
-100, 1100)
ax.set_xlim(1100, -100)
ax.set_ylim(
# Set axis labels
"x (m)")
ax.set_xlabel("z (m)")
ax.set_ylabel(
def update(frame):
"""Update the images for frame 'frame'."""
im_fg.set_data(fg.data[frame].T)
im_f.set_data(f.data[frame].T)return im_fg, im_f
# Determine the total number of frames from the first dimension of f.data
= f.data.shape[0]
n_frames
# Create the animation
= animation.FuncAnimation(fig, update, frames=n_frames, interval=interval, blit=True)
ani
plt.close()
return ani
= animate_wavefield(u_save, u_save_grid, model.vp)
ani
HTML(ani.to_html5_video())
assert np.isclose(np.linalg.norm(u_save.data), 54.026115, atol=0, rtol=1e-4)
assert np.isclose(np.linalg.norm(u_save_grid.data), 77.89853, atol=0, rtol=1e-4)
Acoustic-elastic coupling
Another application of the Function
s on SubDomain
s functionality is for coupled physics problems in which certain fields are only required over a subset of the domain. By minimising the size of the allocated arrays, the memory required to run such complex models can be substantially reduced.
One such application is for efficiently handling propagation of seismic waves through both fluid and solid media. In the former, wave propagation is purely acoustic, whilst it is elastic in the latter. However, the elastic wave equation is substantially more expensive than its acoustic counterpart and features a greatly increased number of fields. Leveraging Function
s defined on only a subset of the grid, such computations can be made substantially more memory-efficient.
In this particular case, we will have an upper acoustic domain and a lower elastic domain, overlapping in the center to enable coupling of the two simulations. The transition point will lie above the seafloor to simplify the coupling.
The two domains will be laid out as follows, with the overlap representing the transition zone.
#NBVAL_IGNORE_OUTPUT
-100., 1100., 1100., -100., -100.),
plt.plot((-100., -100., 520., 520., -100.),
('k--', label='Acoustic')
-100., 1100., 1100., -100., -100.),
plt.plot((1100., 1100., 475., 475., 1100.),
('r--', label='Elastic')
='lower left')
plt.legend(loc"x (m)")
plt.xlabel("z (m)")
plt.ylabel(-150, 1150)
plt.xlim(1150, -150)
plt.ylim(
plt.show()
To achieve this setup, we will require a total of six SubDomain
s. These are as follows: * Two overlapping SubDomain
s, one for fields only required for acoustic propagation, and one for fields only required for elastic propagation * Two non-overlapping SubDomain
s, one for acoustic propagation, and one for elastic propagation * Two SubDomain
s to act as ‘halos’ for coupling the acoustic and elastic models in the upper and lower halves of the transition zone.
= 8 # Space order
so = Grid(shape=(241, 241), extent=(1200., 1200.), origin=(-100., -100.))
grid1
# Acoustic-elastic transition at index 120 (this index is the first elastic one)
class Upper(SubDomain):
"""Upper iteration subdomain"""
= 'upper'
name
def define(self, dimensions):
= dimensions
x, y # Note: uses middle to allow MPI distribution. If MPI is not needed or anticipated
# then one could use left/right instead
return {x: x, y: ('middle', 0, 121)} # 121 + so//2
class Lower(SubDomain):
"""Lower iteration subdomain"""
= 'lower'
name
def define(self, dimensions):
= dimensions
x, y return {x: x, y: ('middle', 120, 0)} # 120 + so//2
class UpperTransition(SubDomain):
"""Upper transition zone"""
= 'uppertransition'
name
def define(self, dimensions):
= dimensions
x, y return {x: x, y: ('middle', 116, 121)} # 116 + 121 = 241 - so//2
class LowerTransition(SubDomain):
"""Lower transition zone"""
= 'lowertransition'
name
def define(self, dimensions):
= dimensions
x, y return {x: x, y: ('middle', 120, 117)} # 120 + 117 = 241 - so//2
class UpperField(SubDomain):
"""Upper subdomain to define fields over"""
= 'upperfields'
name
def define(self, dimensions):
= dimensions
x, y return {x: x, y: ('middle', 0, 117)} # 121 - so//2
class LowerField(SubDomain):
"""Lower subdomain to define fields over"""
= 'lowerfields'
name
def define(self, dimensions):
= dimensions
x, y return {x: x, y: ('middle', 116, 0)} # 120 - so//2
# Iteration domains
= Upper(grid=grid1)
upper = Lower(grid=grid1)
lower = UpperTransition(grid=grid1)
uppertransition = LowerTransition(grid=grid1)
lowertransition
# Field domains
= UpperField(grid=grid1)
upperfield = LowerField(grid=grid1) lowerfield
As before, it is often helpful to create a trivial Operator
to check the alignment and layout of the SubDomain
s created, particularly when a large number are present. Doing so also illustrates how the transition zone is split into upper and lower regions, acting as halos for the elastic and acoustic models respectively.
#NBVAL_IGNORE_OUTPUT
# Make a trivial operator to check location of fields
= Function(name='testfunc', grid=grid1)
test_func 1, subdomain=upper),
Operator([Eq(test_func, 1, subdomain=lower),
Eq(test_func, + 1, subdomain=uppertransition),
Eq(test_func, test_func + 2, subdomain=lowertransition)])()
Eq(test_func, test_func
=(-100., 1100., 1100., -100.))
plt.imshow(test_func.data.T, extent
plt.colorbar()"x (m)")
plt.xlabel("z (m)")
plt.ylabel( plt.show()
Operator `Kernel` ran in 0.01 s
We now define the necessary fields. Only the damping and P-wave velocity fields are required throughout the domain. Thus all remaining fields are defined on either upperfield
or lowerfield
, these being the overlapped SubDomain
s.
# Define the fields
# Fields present everywhere
= Function(name='damp', grid=grid1, space_order=so)
damp = Function(name='cp', grid=grid1, space_order=so)
cp
# Fields only present in the upper domain
= TimeFunction(name='p', grid=upperfield, space_order=so, time_order=2)
p
# Fields only present in the lower domain
= Function(name='cs', grid=lowerfield, space_order=so)
cs = Function(name='ro', grid=lowerfield, space_order=so)
ro = TensorTimeFunction(name='tau', grid=lowerfield, space_order=so)
tau = VectorTimeFunction(name='v', grid=lowerfield, space_order=so)
v
# Intialise parameter fields
= 1
ro.data[:] = 1.5
cp.data[:]
# Add an elastic layer below the acoustic-elastic transition
-100:] = 2.5
cp.data[:, -100:] = 1.5
cs.data[:, -75:] = 3.0
cp.data[:, -75:] = 2.0
cs.data[:, -50:] = 3.5
cp.data[:, -50:] = 2.5
cs.data[:, -25:] = 4.
cp.data[:, -25:] = 3.
cs.data[:,
# Fill the damping field
= 0.5
maxtaper = maxtaper*(0.5 - 0.5*np.cos(np.linspace(0., np.pi, 20)))
taper -20:] += taper[:, np.newaxis]
damp.data[20] += taper[::-1, np.newaxis]
damp.data[:-20:] += taper[:]
damp.data[:, 20] += taper[::-1] damp.data[:, :
Source terms are defined as per usual.
# Set time range, source, source coordinates and receiver coordinates
= 0. # Simulation starts a t=0
t0 = 400. # Simulation lasts tn milliseconds
tn = float(0.1*np.amin(grid1.spacing)/np.amax(cp.data)) # Time step from grid spacing
dt = TimeAxis(start=t0, stop=tn, step=dt)
time_range = time_range.num # number of time steps
nt
= 0.030 # Source peak frequency is 30Hz (0.030 kHz)
f0 = RickerSource(name='src', grid=grid1, f0=f0, time_range=time_range)
src
0, :] = 500.
src.coordinates.data[0, -1] = 350. # Depth is 350m src.coordinates.data[
Receivers will be placed on the seafloor. Note that this is below the transition, in the elastic zone.
= Receiver(name='rec', grid=grid1, npoint=101, time_range=time_range)
rec
0] = np.linspace(0., 1000., 101)
rec.coordinates.data[:, 1] = 600. rec.coordinates.data[:,
Define some material parameters.
= 1/ro
b = cs**2*ro
mu = cp**2*ro - 2*mu lam
Update and coupling equations are specified. Note that it is important to specify a SubDomain
when an expression contains Function
s defined on SubDomain
s.
# Acoustic update
= p.dt2 - cp**2*p.laplace + damp*p.dt
pde_p = Eq(p.forward, solve(pde_p, p.forward), subdomain=upper)
eq_p
# Elastic update
= v.dt - b*div(tau) + damp*v.forward
pde_v = tau.dt - lam*diag(div(v.forward)) - mu*(grad(v.forward) + grad(v.forward).transpose(inner=False)) + damp*tau.forward
pde_tau
= Eq(v.forward, solve(pde_v, v.forward), subdomain=lowerfield)
eq_v = Eq(tau.forward, solve(pde_tau, tau.forward), subdomain=lower)
eq_t
# Coupling: p -> txx, tyy
= Eq(tau[0, 0].forward, p.forward, subdomain=uppertransition)
eq_txx_tr = Eq(tau[1, 1].forward, p.forward, subdomain=uppertransition)
eq_tyy_tr # Coupling: txx, tyy -> p
= Eq(p.forward, (tau[0, 0].forward + tau[1, 1].forward)/2, subdomain=lowertransition) eq_p_tr
Source and receiver terms are defined.
= src.inject(field=p.forward, expr=src)
src_term = rec.interpolate(expr=tau[0, 0].forward+tau[1, 1].forward) rec_term
Now build and run the Operator
.
#NBVAL_IGNORE_OUTPUT
from devito import switchconfig
# Note: switchconfig(safe_math=True) is only required here to get consistent norms for testing purposes
# This is not required more widely and can be omitted in practical applications
with switchconfig(safe_math=True):
= Operator([eq_v,
op1
eq_p, eq_t,
eq_p_tr, eq_txx_tr, eq_tyy_tr]+ src_term + rec_term)
=dt) op1(dt
Operator `Kernel` ran in 0.74 s
Overlaying the wavefields, we see that they are correctly coupled and aligned.
#NBVAL_IGNORE_OUTPUT
= np.amax(np.abs(p.data[-1]))
vmax_p = np.amax(np.abs((tau[0, 0].data[-1] + tau[1, 1].data[-1])/2))
vmax_tau = max(vmax_p, vmax_tau)
vmax
='Greys', extent=(-100, 1100, 1100, -100))
plt.imshow(cp.data.T, cmap-1].T,
plt.imshow(p.data[=vmax, vmin=-vmax, cmap='Greys',
vmax=(-100, 1100, 520, -100),
extent=0.6)
alpha0, 0].data[-1].T + tau[1, 1].data[-1].T)/2,
plt.imshow((tau[=vmax, vmin=-vmax, cmap='seismic',
vmax=(-100, 1100, 1100, 475), alpha=0.6)
extent
-100., 1100., 1100., -100., -100.),
plt.plot((-100., -100., 520., 520., -100.),
('k--', label='Acoustic')
-100., 1100., 1100., -100., -100.),
plt.plot((1100., 1100., 475., 475., 1100.),
('r--', label='Elastic')
plt.legend()
-100, 1100)
plt.xlim(1100, -100)
plt.ylim("x (m)")
plt.xlabel("z (m)")
plt.ylabel( plt.show()
We can also view the shot record recorded at the ocean-bottom receivers.
#NBVAL_IGNORE_OUTPUT
= 0.05*np.amax(np.abs(rec.data))
vmax ='Greys', aspect='auto', vmax=vmax, vmin=-vmax)
plt.imshow(rec.data, cmap"Receiver number")
plt.xlabel("Sample")
plt.ylabel( plt.show()
assert np.isclose(np.linalg.norm(rec.data), 4263.511, atol=0, rtol=1e-4)