|
| 1 | +import pytest |
| 2 | + |
| 3 | +import numpy as np |
| 4 | +import pandas as pd |
| 5 | + |
| 6 | +from examples.seismic import Model, TimeAxis, RickerSource, Receiver |
| 7 | +from devito import TimeFunction, Eq, solve, Operator |
| 8 | +from devitoboundary import ImmersedBoundary |
| 9 | +from devitoboundary.symbolics.symbols import x_b |
| 10 | +from devitoboundary.stencils.stencil_utils import generic_function |
| 11 | + |
| 12 | + |
| 13 | +def reference_shot(model, time_range, f0): |
| 14 | + """ |
| 15 | + Produce a reference shot gather with a level, conventional free-surface |
| 16 | + implementation. |
| 17 | + """ |
| 18 | + src = RickerSource(name='src', grid=model.grid, f0=f0, |
| 19 | + npoint=1, time_range=time_range) |
| 20 | + |
| 21 | + # First, position source centrally in all dimensions, then set depth |
| 22 | + src.coordinates.data[0, :] = np.array(model.domain_size) * .5 |
| 23 | + # Remember that 0, 0, 0 is top left corner |
| 24 | + # Depth is 100m from free-surface boundary |
| 25 | + src.coordinates.data[0, -1] = 600. |
| 26 | + |
| 27 | + # Create symbol for 101 receivers |
| 28 | + rec = Receiver(name='rec', grid=model.grid, npoint=101, |
| 29 | + time_range=time_range) |
| 30 | + |
| 31 | + # Prescribe even spacing for receivers along the x-axis |
| 32 | + rec.coordinates.data[:, 0] = np.linspace(0, model.domain_size[0], num=101) |
| 33 | + rec.coordinates.data[:, 1] = 500. # Centered on y axis |
| 34 | + rec.coordinates.data[:, 2] = 650. # Depth is 150m from free surface |
| 35 | + |
| 36 | + # Define the wavefield with the size of the model and the time dimension |
| 37 | + u = TimeFunction(name="u", grid=model.grid, time_order=2, space_order=4) |
| 38 | + |
| 39 | + # We can now write the PDE |
| 40 | + pde = model.m * u.dt2 - u.laplace + model.damp * u.dt |
| 41 | + |
| 42 | + stencil = Eq(u.forward, solve(pde, u.forward)) |
| 43 | + |
| 44 | + # Finally we define the source injection and receiver read function |
| 45 | + src_term = src.inject(field=u.forward, |
| 46 | + expr=src * model.critical_dt**2 / model.m) |
| 47 | + |
| 48 | + # Create interpolation expression for receivers |
| 49 | + rec_term = rec.interpolate(expr=u.forward) |
| 50 | + |
| 51 | + x, y, z = model.grid.dimensions |
| 52 | + time = u.grid.stepping_dim |
| 53 | + # Throw a free surface in here |
| 54 | + free_surface_0 = Eq(u[time+1, x, y, 60], 0) |
| 55 | + free_surface_1 = Eq(u[time+1, x, y, 59], u[time+1, x, y, 61]) |
| 56 | + free_surface_2 = Eq(u[time+1, x, y, 58], u[time+1, x, y, 62]) |
| 57 | + free_surface = [free_surface_0, free_surface_1, free_surface_2] |
| 58 | + |
| 59 | + op = Operator([stencil] + src_term + rec_term + free_surface) |
| 60 | + |
| 61 | + op(time=time_range.num-1, dt=model.critical_dt) |
| 62 | + |
| 63 | + return rec.data |
| 64 | + |
| 65 | + |
| 66 | +def tilted_shot(model, time_range, f0, tilt): |
| 67 | + """ |
| 68 | + Produce a shot for the same setup, but tilted with immersed free surface |
| 69 | + """ |
| 70 | + src = RickerSource(name='src', grid=model.grid, f0=f0, |
| 71 | + npoint=1, time_range=time_range) |
| 72 | + |
| 73 | + # First, position source, then set depth |
| 74 | + src.coordinates.data[0, 0] = 500. - 100.*np.sin(np.radians(tilt)) |
| 75 | + src.coordinates.data[0, 1] = 500. |
| 76 | + # Remember that 0, 0, 0 is top left corner |
| 77 | + # Depth is 100m from free-surface boundary |
| 78 | + src.coordinates.data[0, 2] = 500. + 100.*np.cos(np.radians(tilt)) |
| 79 | + |
| 80 | + # Create symbol for 101 receivers |
| 81 | + rec = Receiver(name='rec', grid=model.grid, npoint=101, |
| 82 | + time_range=time_range) |
| 83 | + |
| 84 | + # Prescribe even spacing for receivers along the x-axis |
| 85 | + rec_center_x = 500. - 150.*np.sin(np.radians(tilt)) |
| 86 | + rec_center_z = 500. + 150.*np.cos(np.radians(tilt)) |
| 87 | + |
| 88 | + rec_top_x = rec_center_x - 500.*np.cos(np.radians(tilt)) |
| 89 | + rec_bottom_x = rec_center_x + 500.*np.cos(np.radians(tilt)) |
| 90 | + |
| 91 | + rec_top_z = rec_center_z - 500.*np.sin(np.radians(tilt)) |
| 92 | + rec_bottom_z = rec_center_z + 500.*np.sin(np.radians(tilt)) |
| 93 | + |
| 94 | + rec.coordinates.data[:, 0] = np.linspace(rec_top_x, rec_bottom_x, num=101) |
| 95 | + rec.coordinates.data[:, 1] = 500. # Centered on y axis |
| 96 | + rec.coordinates.data[:, 2] = np.linspace(rec_top_z, rec_bottom_z, num=101) |
| 97 | + |
| 98 | + # Define the wavefield with the size of the model and the time dimension |
| 99 | + u = TimeFunction(name="u", grid=model.grid, time_order=2, space_order=4, |
| 100 | + coefficients='symbolic') |
| 101 | + |
| 102 | + infile = 'tests/trial_surfaces/angled_surface_'+str(tilt)+'.ply' |
| 103 | + |
| 104 | + # Zero even derivatives on the boundary |
| 105 | + bcs_u = [Eq(generic_function(x_b, 2*i), 0) |
| 106 | + for i in range(1+u.space_order//2)] |
| 107 | + functions = pd.DataFrame({'function': [u], |
| 108 | + 'bcs': [bcs_u]}, |
| 109 | + columns=['function', 'bcs']) |
| 110 | + |
| 111 | + # Create the immersed boundary surface |
| 112 | + surface = ImmersedBoundary('topography', infile, functions) |
| 113 | + # Configure derivative needed |
| 114 | + derivs = pd.DataFrame({'function': [u], |
| 115 | + 'derivative': [2]}, |
| 116 | + columns=['function', 'derivative']) |
| 117 | + coeffs = surface.subs(derivs) |
| 118 | + |
| 119 | + # We can now write the PDE |
| 120 | + pde = model.m * u.dt2 - u.laplace + model.damp * u.dt |
| 121 | + |
| 122 | + stencil = Eq(u.forward, solve(pde, u.forward), |
| 123 | + coefficients=coeffs['substitution'].values[0]) |
| 124 | + |
| 125 | + # Finally we define the source injection and receiver read function |
| 126 | + src_term = src.inject(field=u.forward, |
| 127 | + expr=src * model.critical_dt**2 / model.m) |
| 128 | + |
| 129 | + # Create interpolation expression for receivers |
| 130 | + rec_term = rec.interpolate(expr=u.forward) |
| 131 | + |
| 132 | + op = Operator([stencil] + src_term + rec_term) |
| 133 | + op(time=time_range.num-1, dt=model.critical_dt) |
| 134 | + |
| 135 | + return rec.data |
| 136 | + |
| 137 | + |
| 138 | +class TestGathers: |
| 139 | + """ |
| 140 | + A class for testing the accuracy of gathers resulting from a reflection off |
| 141 | + the immersed boundary. |
| 142 | + """ |
| 143 | + |
| 144 | + @pytest.mark.parametrize('tilt', [5, 10, 15, 20, 25, 30, 35, 40, 45]) |
| 145 | + def test_tilted_boundary(self, tilt): |
| 146 | + """ |
| 147 | + Check that gathers for a tilted boundary match those generated with a |
| 148 | + conventional horizontal free surface and the same geometry. |
| 149 | + """ |
| 150 | + max_thres = 0.09 |
| 151 | + avg_thres = 0.006 |
| 152 | + # Define a physical size |
| 153 | + shape = (101, 101, 101) # Number of grid point (nx, ny, nz) |
| 154 | + spacing = (10., 10., 10.) # Grid spacing in m. The domain size is 1x1x1km |
| 155 | + origin = (0., 0., 0.) |
| 156 | + |
| 157 | + v = 1.5 |
| 158 | + |
| 159 | + model = Model(vp=v, origin=origin, shape=shape, spacing=spacing, |
| 160 | + space_order=4, nbl=10, bcs="damp") |
| 161 | + |
| 162 | + t0 = 0. # Simulation starts a t=0 |
| 163 | + tn = 500. # Simulation last 0.5 seconds (500 ms) |
| 164 | + dt = model.critical_dt # Time step from model grid spacing |
| 165 | + |
| 166 | + time_range = TimeAxis(start=t0, stop=tn, step=dt) |
| 167 | + |
| 168 | + f0 = 0.010 # Source peak frequency is 10Hz (0.010 kHz) |
| 169 | + |
| 170 | + ref = reference_shot(model, time_range, f0) |
| 171 | + tilted = tilted_shot(model, time_range, f0, tilt) |
| 172 | + |
| 173 | + assert np.amax(np.absolute(ref - tilted)) < max_thres |
| 174 | + assert np.mean(np.absolute(ref-tilted)) < avg_thres |
0 commit comments