#!/usr/bin/env python # coding: utf-8 # # 02 - Reverse Time Migration # # This notebook is the second in a series of tutorial highlighting various aspects of seismic inversion based on Devito operators. In this second example we aim to highlight the core ideas behind seismic inversion, where we create an image of the subsurface from field recorded data. This tutorial follows on the modelling tutorial and will reuse the modelling operator and velocity model. # # ## Imaging requirement # # Seismic imaging relies on two known parameters: # # - **Field data** - or also called **recorded data**. This is a shot record corresponding to the true velocity model. In practice this data is acquired as described in the first tutorial. In order to simplify this tutorial we will generate synthetic field data by modelling it with the **true velocity model**. # # - **Background velocity model**. This is a velocity model that has been obtained by processing and inverting the field data. We will look at this methods in the following tutorial as it relies on the method we are describing here. This velocity model is usually a **smooth version** of the true velocity model. # ## Imaging computational setup # # In this tutorial, we will introduce the back-propagation operator. This operator simulates the adjoint wave-equation, that is a wave-equation solved in a reversed time order. This time reversal led to the naming of the method we present here, called Reverse Time Migration. The notion of adjoint in exploration geophysics is fundamental as most of the wave-equation based imaging and inversion methods rely on adjoint based optimization methods. # # ## Notes on the operators # # As we have already described the creation of a forward modelling operator, we will use a thin wrapper function instead. This wrapper is provided by a utility class called `AcousticWaveSolver`, which provides all the necessary operators for seismic modeling, imaging and inversion. The `AcousticWaveSolver` provides a more concise API for common wave propagation operators and caches the Devito `Operator` objects to avoid unnecessary recompilation. Operators introduced for the first time in this tutorial will be properly described. # # As before we initialize printing and import some utilities. We also raise the Devito log level to avoid excessive logging for repeated operator invocations. # In[1]: import numpy as np get_ipython().run_line_magic('matplotlib', 'inline') from devito import configuration configuration['log-level'] = 'WARNING' # ## Computational considerations # # Seismic inversion algorithms are generally very computationally demanding and require a large amount of memory to store the forward wavefield. In order to keep this tutorial as lightweight as possible we are using a very simple # velocity model that requires low temporal and spatial resolution. For a more realistic model, a second set of preset parameters for a reduced version of the 2D Marmousi data set [1] is provided below in comments. This can be run to create some more realistic subsurface images. However, this second preset is more computationally demanding and requires a slightly more powerful workstation. # In[2]: # Configure model presets from examples.seismic import demo_model # Enable model presets here: preset = 'layers-isotropic' # A simple but cheap model (recommended) # preset = 'marmousi2d-isotropic' # A larger more realistic model # Standard preset with a simple two-layer model if preset == 'layers-isotropic': def create_model(grid=None): return demo_model('layers-isotropic', origin=(0., 0.), shape=(101, 101), spacing=(10., 10.), nbl=20, grid=grid, nlayers=2) filter_sigma = (1, 1) nshots = 21 nreceivers = 101 t0 = 0. tn = 1000. # Simulation last 1 second (1000 ms) f0 = 0.010 # Source peak frequency is 10Hz (0.010 kHz) # A more computationally demanding preset based on the 2D Marmousi model if preset == 'marmousi2d-isotropic': def create_model(grid=None): return demo_model('marmousi2d-isotropic', data_path='../../../../data/', grid=grid, nbl=20) filter_sigma = (6, 6) nshots = 301 # Need good covergae in shots, one every two grid points nreceivers = 601 # One recevier every grid point t0 = 0. tn = 3500. # Simulation last 3.5 second (3500 ms) f0 = 0.025 # Source peak frequency is 25Hz (0.025 kHz) # # True and smooth velocity models # # First, we create the model data for the "true" model from a given demonstration preset. This model represents the subsurface topology for the purposes of this example and we will later use it to generate our synthetic data readings. We also generate a second model and apply a smoothing filter to it, which represents our initial model for the imaging algorithm. The perturbation between these two models can be thought of as the image we are trying to recover. # In[3]: #NBVAL_IGNORE_OUTPUT from examples.seismic import plot_velocity, plot_perturbation from devito import gaussian_smooth # Create true model from a preset model = create_model() # Create initial model and smooth the boundaries model0 = create_model(grid=model.grid) gaussian_smooth(model0.vp, sigma=filter_sigma) # Plot the true and initial model and the perturbation between them plot_velocity(model) plot_velocity(model0) plot_perturbation(model0, model) # ## Acquisition geometry # # Next we define the positioning and the wave signal of our source, as well as the location of our receivers. To generate the wavelet for our source we require the discretized values of time that we are going to use to model a single "shot", # which again depends on the grid spacing used in our model. For consistency this initial setup will look exactly as in the previous modelling tutorial, although we will vary the position of our source later on during the actual imaging algorithm. # In[4]: #NBVAL_IGNORE_OUTPUT # Define acquisition geometry: source from examples.seismic import AcquisitionGeometry # First, position source centrally in all dimensions, then set depth src_coordinates = np.empty((1, 2)) src_coordinates[0, :] = np.array(model.domain_size) * .5 src_coordinates[0, -1] = 20. # Depth is 20m # Define acquisition geometry: receivers # Initialize receivers for synthetic and imaging data rec_coordinates = np.empty((nreceivers, 2)) rec_coordinates[:, 0] = np.linspace(0, model.domain_size[0], num=nreceivers) rec_coordinates[:, 1] = 30. # Geometry geometry = AcquisitionGeometry(model, rec_coordinates, src_coordinates, t0, tn, f0=.010, src_type='Ricker') # We can plot the time signature to see the wavelet geometry.src.show() # # True and smooth data # # We can now generate the shot record (receiver readings) corresponding to our true and initial models. The difference between these two records will be the basis of the imaging procedure. # # For this purpose we will use the same forward modelling operator that was introduced in the previous tutorial, provided by the `AcousticWaveSolver` utility class. This object instantiates a set of pre-defined operators according to an initial definition of the acquisition geometry, consisting of source and receiver symbols. The solver objects caches the individual operators and provides a slightly more high-level API that allows us to invoke the modelling modelling operators from the initial tutorial in a single line. In the following cells we use this to generate shot data by only specifying the respective model symbol `m` to use, and the solver will create and return a new `Receiver` object the represents the readings at the previously defined receiver coordinates. # In[5]: # Compute synthetic data with forward operator from examples.seismic.acoustic import AcousticWaveSolver solver = AcousticWaveSolver(model, geometry, space_order=4) true_d , _, _ = solver.forward(vp=model.vp) # In[6]: # Compute initial data with forward operator smooth_d, _, _ = solver.forward(vp=model0.vp) # In[7]: #NBVAL_IGNORE_OUTPUT # Plot shot record for true and smooth velocity model and the difference from examples.seismic import plot_shotrecord plot_shotrecord(true_d.data, model, t0, tn) plot_shotrecord(smooth_d.data, model, t0, tn) plot_shotrecord(smooth_d.data - true_d.data, model, t0, tn) # # Imaging with back-propagation # # As explained in the introduction of this tutorial, this method is based on back-propagation. # # ## Adjoint wave equation # # If we go back to the modelling part, we can rewrite the simulation as a linear system solve: # # \begin{equation} # \mathbf{A}(\mathbf{m}) \mathbf{u} = \mathbf{q} # \end{equation} # # where $\mathbf{m}$ is the discretized square slowness, $\mathbf{q}$ is the discretized source and $\mathbf{A}(\mathbf{m})$ is the discretized wave-equation. The discretized wave-equation matricial representation is a lower triangular matrix that can be solve with forward substitution. The pointwise writing or the forward substitution leads to the time-stepping stencil. # # On a small problem one could form the matrix explicitly and transpose it to obtain the adjoint discrete wave-equation: # # \begin{equation} # \mathbf{A}(\mathbf{m})^T \mathbf{v} = \delta \mathbf{d} # \end{equation} # # where $\mathbf{v}$ is the discrete **adjoint wavefield** and $\delta \mathbf{d}$ is the data residual defined as the difference between the field/observed data and the synthetic data $\mathbf{d}_s = \mathbf{P}_r \mathbf{u}$. In our case we derive the discrete adjoint wave-equation from the discrete forward wave-equation to get its stencil. # # ## Imaging # # Wave-equation based imaging relies on one simple concept: # # - If the background velocity model is cinematically correct, the forward wavefield $\mathbf{u}$ and the adjoint wavefield $\mathbf{v}$ meet at the reflectors position at zero time offset. # # The sum over time of the zero time-offset correlation of these two fields then creates an image of the subsurface. Mathematically this leads to the simple imaging condition: # # \begin{equation} # \text{Image} = \sum_{t=1}^{n_t} \mathbf{u}[t] \mathbf{v}[t] # \end{equation} # # In the following tutorials we will describe a more advanced imaging condition that produces shaper and more accurate results. # # ## Operator # # We will now define the imaging operator that computes the adjoint wavefield $\mathbf{v}$ and correlates it with the forward wavefield $\mathbf{u}$. This operator essentially consists of three components: # * Stencil update of the adjoint wavefield `v` # * Injection of the data residual at the adjoint source (forward receiver) location # * Correlation of `u` and `v` to compute the image contribution at each timestep # In[8]: # Define gradient operator for imaging from devito import TimeFunction, Operator, Eq, solve from examples.seismic import PointSource def ImagingOperator(model, image): # Define the wavefield with the size of the model and the time dimension v = TimeFunction(name='v', grid=model.grid, time_order=2, space_order=4) u = TimeFunction(name='u', grid=model.grid, time_order=2, space_order=4, save=geometry.nt) # Define the wave equation, but with a negated damping term eqn = model.m * v.dt2 - v.laplace + model.damp * v.dt.T # Use `solve` to rearrange the equation into a stencil expression stencil = Eq(v.backward, solve(eqn, v.backward)) # Define residual injection at the location of the forward receivers dt = model.critical_dt residual = PointSource(name='residual', grid=model.grid, time_range=geometry.time_axis, coordinates=geometry.rec_positions) res_term = residual.inject(field=v.backward, expr=residual * dt**2 / model.m) # Correlate u and v for the current time step and add it to the image image_update = Eq(image, image - u * v) return Operator([stencil] + res_term + [image_update], subs=model.spacing_map) # ## Implementation of the imaging loop # # As just explained, the forward wave-equation is solved forward in time while the adjoint wave-equation is solved in a reversed time order. Therefore, the correlation of these two fields over time requires to store one of the two fields. The computational procedure for imaging follows: # # - Simulate the forward wave-equation with the background velocity model to get the synthetic data and save the full wavefield $\mathbf{u}$ # - Compute the data residual # - Back-propagate the data residual and compute on the fly the image contribution at each time step. # # This procedure is applied to multiple source positions (shots) and summed to obtain the full image of the subsurface. We can first visualize the varying locations of the sources that we will use. # In[9]: #NBVAL_IGNORE_OUTPUT # Prepare the varying source locations source_locations = np.empty((nshots, 2), dtype=np.float32) source_locations[:, 0] = np.linspace(0., 1000, num=nshots) source_locations[:, 1] = 30. plot_velocity(model, source=source_locations) # In[10]: # Run imaging loop over shots from devito import Function # Create image symbol and instantiate the previously defined imaging operator image = Function(name='image', grid=model.grid) op_imaging = ImagingOperator(model, image) for i in range(nshots): print('Imaging source %d out of %d' % (i+1, nshots)) # Update source location geometry.src_positions[0, :] = source_locations[i, :] # Generate synthetic data from true model true_d, _, _ = solver.forward(vp=model.vp) # Compute smooth data and full forward wavefield u0 smooth_d, u0, _ = solver.forward(vp=model0.vp, save=True) # Compute gradient from the data residual v = TimeFunction(name='v', grid=model.grid, time_order=2, space_order=4) residual = smooth_d.data - true_d.data op_imaging(u=u0, v=v, vp=model0.vp, dt=model0.critical_dt, residual=residual) # In[11]: #NBVAL_IGNORE_OUTPUT from examples.seismic import plot_image # Plot the inverted image plot_image(np.diff(image.data, axis=1)) # In[12]: from devito import norm assert np.isclose(norm(image), 1e7, rtol=1e1) # And we have an image of the subsurface with a strong reflector at the original location. # ## References # # [1] _Versteeg, R.J. & Grau, G. (eds.) (1991): The Marmousi experience. Proc. EAGE workshop on Practical Aspects of Seismic Data Inversion (Copenhagen, 1990), Eur. Assoc. Explor. Geophysicists, Zeist._