Getting Started

These tutorials provide instructions of how to set up various modeling or inversion scenarios with JUDI. For a list of runnable Julia scripts and reproducable research, please also check out the examples:

  • The examples scripts contain simple modeling and inversion examples such as FWI, LSRTM, and medical modeling.
  • The machine-learning scripts contain examples of machine learning using Flux.

    2D Modeling Quickstart

    To set up a simple 2D modeling experiment with JUDI with an OBN-type acquisition (receivers everywhere), we start by loading the module and building a two layer model:

    using JUDI
    
    # Grid
    n = (120, 100)   # (x,z)
    d = (10., 10.)
    o = (0., 0.)
    
    # Velocity [km/s]
    v = ones(Float32, n) .* 1.4f0
    v[:, 50:end] .= 5f0
    
    # Squared slowness
    m = (1f0 ./ v).^2

    For working with JUDI operators, we need to set up a model structure, which contains the grid information, as well as the slowness. Optionally, we can provide an array of the density in g/cm^3 (by default a density of 1 is used):

    # Density (optional)
    rho = ones(Float32, n)
    
    # Model structure:
    model = Model(n, d, o, m; rho=rho)

    Next, we define our source acquisition geometry, which needs to be defined as a Geometry structure. The Geometry function requires the x-, y- and z-coordinates of the source locations as input, as well as the modeling time and samping interval of the wavelet. In general, each parameter can be passed as a cell array, where each cell entry provides the information for the respective source location. The helper function convertToCell converts a Julia range to a cell array, which makes defining the source geometry easier:

    # Set up source geometry
    nsrc = 4    # no. of sources
    xsrc = convertToCell(range(400f0, stop=800f0, length=nsrc))
    ysrc = convertToCell(range(0f0, stop=0f0, length=nsrc))
    zsrc = convertToCell(range(20f0, stop=20f0, length=nsrc))
    
    # Modeling time and sampling interval
    time = 1000f0  # ms
    dt = 2f0   # ms
    
    # Set up source structure
    src_geometry = Geometry(xsrc, ysrc, zsrc; dt=dt, t=time)

    Now we can define our source wavelet. The source must be defined as a judiVector, which takes the source geometry, as well as the source data (i.e. the wavelet) as an input argument:

    # Source wavelet
    f0 = 0.01f0     # kHz
    wavelet = ricker_wavelet(time, dt, f0)
    q = judiVector(src_geometry, wavelet)

    In general, wavelet can be a cell array with a different wavelet in each cell, i.e. for every source location. Here, we want to use the same wavelet for all 4 source experiments, so we can simply pass a single vector. As we already specified in our src_geometry object that we want to have 4 source locations, judiVector will automaticallty copy the wavelet for every experiment.

    Next, we set up the receiver acquisition geometry. Here, we define an OBN acquisition, where the receivers are spread out over the entire domain and each source experiment uses the same set of receivers. Again, we can in principle pass the coordinates as cell arrays, with one cell per source location. Since we want to use the same geometry for every source, we can use a short cut and define the coordinates as Julia ranges and pass nsrc=nsrc as an optional argument to the Geometry function. This tells the function that we want to use our receiver set up for nsrc distinct source experiments:

    # Set up receiver geometry (for 2D, set yrec to zero)
    nxrec = 120
    xrec = range(50f0, stop=1150f0, length=nxrec)
    yrec = 0f0
    zrec = range(50f0, stop=50f0, length=nxrec)
    
    # Set up receiver structure
    rec_geometry = Geometry(xrec, yrec, zrec; dt=dt, t=time, nsrc=nsrc)

    Next, we can define separate operators for source/receiver projections and a forward modeling operator:

    # Setup operators
    Pr = judiProjection(rec_geometry)
    A_inv = judiModeling(model)
    Ps = judiProjection(src_geometry)

    We can see, that from JUDI's perspective, source and receivers are treated equally and are represented by the same operators (judiProjection) and vectors (judiVector).

    We also could've skipped setting up the projection operators and directly created:

    F = judiModeling(model, src_geometry, rec_geometry)

    which is equivalent to creating the combined operator:

    F = Pr*A_inv*Ps'

    Finally, to model our seismic data, we run:

    d_obs = Pr*A_inv*Ps'*q
    # or
    d_obs = F*q

    We can plot a 2D shot record by accessing the .data field of the judiVector, which contains the data in the original (non-vectorized) dimensions:

    using PythonPlot
    imshow(d_obs.data[1], vmin=-5, vmax=5, cmap="seismic", aspect="auto")

    We can also set up a Jacobian operator for Born modeling and reverse-time migration. First we set up a (constant) migration velocity model:

    v0 = ones(Float32, n) .* 1.4f0
    m0 = (1f0 ./ v0).^2
    dm = m - m0     # model perturbation/image
    
    # Model structure
    model0 = Model(n, d, o, m0)

    We can create the Jacobian directly from a (non-linear) modeling operator and a source vector:

    A0_inv = judiModeling(model0) # modeling operator for migration velocity
    J = judiJacobian(Pr*A0_inv*Ps', q)

    We can use this operator to model single scattered data, as well as for migration our previous data:

    d_lin = J*vec(dm)
    
    # RTM
    rtm = J'*d_obs

    To plot, first reshape the image:

    rtm = reshape(rtm, model0.n)
    imshow(rtm', cmap="gray", vmin=-1e3, vmax=1e3)

    3D Modeling Quickstart

    Setting up a 3D experiment largely follows the instructions for the 2D example. Instead of a 2D model, we define our velocity model as:

    using JUDI
    
    # Grid
    n = (120, 100, 80)   # (x,y,z)
    d = (10., 10., 10.)
    o = (0., 0., 0.)
    
    # Velocity [km/s]
    v = ones(Float32, n) .* 1.4f0
    v[:, :, 40:end] .= 5f0
    
    # Squared slowness and model structure
    m = (1f0 ./ v).^2
    model = Model(n, d, o, m)

    Our source coordinates now also need to have the y-coordinate defined:

    # Set up source geometry
    nsrc = 4    # no. of sources
    xsrc = convertToCell(range(400f0, stop=800f0, length=nsrc))
    ysrc = convertToCell(range(200f0, stop=1000f0, length=nsrc))
    zsrc = convertToCell(range(20f0, stop=20f0, length=nsrc))
    
    # Modeling time and sampling interval
    time = 1000f0  # ms
    dt = 2f0   # ms
    
    # Set up source structure
    src_geometry = Geometry(xsrc, ysrc, zsrc; dt=dt, t=time)

    Our source wavelet, is set up as in the 2D case:

    # Source wavelet
    f0 = 0.01f0     # kHz
    wavelet = ricker_wavelet(time, dt, f0)
    q = judiVector(src_geometry, wavelet)

    For the receivers, we generally need to define each coordinate (x, y, z) for every receiver. I.e. xrec, yrec and zrec each have the length of the total number of receivers. However, oftentimes we are interested in a regular receiver grid, which can be defined by two basis vectors and a constant depth value for all receivers. We can then use the setup_3D_grid helper function to create the full set of coordinates:

    # Receiver geometry
    nxrec = 120
    nyrec = 100
    xrec = range(50f0, stop=1150f0, length=nxrec)
    yrec = range(100f0, stop=900f0, length=nyrec)
    zrec = 50f0
    
    # Construct 3D grid from basis vectors
    (xrec, yrec, zrec) = setup_3D_grid(xrec, yrec, zrec)
    
    # Set up receiver structure
    rec_geometry = Geometry(xrec, yrec, zrec; dt=dt, t=time, nsrc=nsrc)

    Setting up the modeling operators is done as in the previous 2D case:

    # Setup operators
    Pr = judiProjection(rec_geometry)
    A_inv = judiModeling(model)
    Ps = judiProjection(src_geometry)
    
    # Model data
    d_obs = Pr*A_inv*Ps'*q

    The 3D shot records are still saved as 2D arrays of dimensions time x (nxrec*nyrec):

    using PythonPlot
    imshow(d_obs.data[1], vmin=-.4, vmax=.4, cmap="seismic", aspect="auto")

    Vertical and tilted-transverse isotropic modeling (VTI, TTI)

    JUDI supports both VTI and TTI modeling based on a coupled pseudo-acoustic wave equation. To enable VTI/TTI modeling, simply pass Thomsen parameters as well as the tilt angles to the Model structure as optional keyword arguments:

    # Grid and model
    n = (120, 100, 80)
    d = (10., 10., 10)
    o = (0., 0., 0.)
    
    # Velocity
    v = ones(Float32, n) .* 1.5f0
    m = 1f0 ./ v.^2
    
    # Thomsen parameters
    epsilon = ones(Float32, n) .* 0.2f0
    delta = ones(Float32, n) .* 0.1f0
    
    # Tile angles for TTI
    theta = ones(Float32, n) .* pi/2f0
    phi = ones(Float32, n) .* pi/3f0    # 3D only
    
    # Set up model structure with Thomsen parameters
    model = Model(n, d, o, m; rho=rho, epsilon=epsilon, delta=delta, theta=theta, delta=delta)

    Modeling with density

    To use density, pass rho in the units of [g/cm^3] as an optional keyword argument to the Model structure. The default density is rho=1f0 (i.e. density of water):

    # Grid and model
    n = (120, 100)
    d = (10., 10.)
    o = (0., 0.)
    v = ones(Float32, n) .* 1.5f0
    m = 1f0 ./ v.^2
    rho = ones(Float32, n) .* 1.1f0
    
    # Set up model structure with density
    model = Model(n, d, o, m; rho=rho)

    2D Marine streamer acquisition

    For a marine streamer acquisition, we need to define a moving set of receivers representing a streamer that is towed behind a seismic source vessel. In JUDI, this is easily done by defining a different set of receivers for each source location. Here, we explain how to set up the Geometry objects for a 2D marine streamer acquisition.

    If we define that our streamer is to the right side of the source vessel, this has the effect that part of the streamer is outside the grid while our vessel is in the right side of the model. To circumvent this, we can say that our streamer is on the right side of the source while the vessel is in the left-hand side of the model and vice versa. This way, we get the full maximum offset coverage for every source location (assuming that the maximum offset is less or equal than half the domain size).

    First, we have to specify our domain size (the physical extent of our model), as well as the number of receivers and the minimum and maximum offset:

    domain_x = (model.n[1] - 1)*model.d[1]    # horizontal extent of model
    nrec = 120     # no. of receivers
    xmin = 50f0    # leave buffer zone w/o source and receivers of this size
    xmax = domain_x - 50f0
    min_offset = 10f0      # distance between source and first receiver
    max_offset = 400f0    # distance between source and last
    xmid = domain_x / 2     # midpoint of model
    source_spacing = 25f0   # source interval [m]

    For the JUDI Geometry objects, we need to create cell arrays for the source and receiver coordinates, with one cell entry per source location:

    # Source/receivers
    nsrc = 20   # number of shot locations
    
    # Receiver coordinates
    xrec = Array{Any}(undef, nsrc)
    yrec = Array{Any}(undef, nsrc)
    zrec = Array{Any}(undef, nsrc)
    
    # Source coordinates
    xsrc = Array{Any}(undef, nsrc)
    ysrc = Array{Any}(undef, nsrc)
    zsrc = Array{Any}(undef, nsrc)

    Next, we compute the source and receiver coordinates for when the vessel moves from left to right in the right-hand side of the model:

    # Vessel goes from left to right in right-hand side of model
    nsrc_half = Int(nsrc/2)
    for j=1:nsrc_half
        xloc = xmid + (j-1)*source_spacing
    
        # Current receiver locations
        xrec[j] = range(xloc - max_offset, xloc - min_offset, length=nrec)
        yrec[j] = 0.
        zrec[j] = range(50f0, 50f0, length=nrec)
        
        # Current source
        xsrc[j] = xloc
        ysrc[j] = 0f0
        zsrc[j] = 20f0
    end

    Then, we repeat this for the case where the vessel goes from right to left in the left-hand model side:

    # Vessel goes from right to left in left-hand side of model
    for j=1:nsrc_half
        xloc = xmid - (j-1)*source_spacing
        
        # Current receiver locations
        xrec[nsrc_half + j] = range(xloc + min_offset, xloc + max_offset, length=nrec)
        yrec[nsrc_half + j] = 0f0
        zrec[nsrc_half + j] = range(50f0, 50f0, length=nrec)
        
        # Current source
        xsrc[nsrc_half + j] = xloc
        ysrc[nsrc_half + j] = 0f0
        zsrc[nsrc_half + j] = 20f0
    end

    Finally, we can set the modeling time and sampling interval and create the Geometry objects:

    # receiver sampling and recording time
    time = 10000f0   # receiver recording time [ms]
    dt = 4f0    # receiver sampling interval
    
    # Set geometry objects
    rec_geometry = Geometry(xrec, yrec, zrec; dt=dt, t=time)
    src_geometry = Geometry(xsrc, ysrc, zsrc; dt=dt, t=time)

    You can find a full (reproducable) example for generating a marine streamer data set for the Sigsbee 2A model here.

    Simultaneous sources

    To set up a simultaneous source with JUDI, we first create a cell array with nsrc cells, where nsrc is the number of separate experiments (here nsrc=1). For a simultaneous source, we create an array of source coordinates for each cell entry. In fact, this is exactly like setting up the receiver geometry, in which case we define multiple receivers per shot location. Here, we define a single experiment with a simultaneous source consisting of four sources:

    nsrc = 1    # single simultaneous source
    xsrc = Vector{Float32}(undef, nsrc)
    ysrc = Vector{Float32}(undef, nsrc)
    zsrc = Vector{Float32}(undef, nsrc)
    
    # Set up source geometry
    xsrc[1] = [250f0, 500f0, 750f0, 1000f0]     # four simultaneous sources
    ysrc[1] = 0f0
    zsrc[1] = [50f0, 50f0, 50f0, 50f0]	
    
    # Source sampling and number of time steps
    time = 2000f0
    dt = 4f0
    
    # Set up source structure
    src_geometry = Geometry(xsrc, ysrc, zsrc; dt=dt, t=time)

    With the simultaneous source geometry in place, we can now create our simultaneous data. As we have four sources per sim. source, we create an array of dimensions 4 x src_geometry.nt[1] and fill it with wavelets of different time shifts:

    # Create wavelet
    f0 = 0.01	# source peak frequencies
    q = ricker_wavelet(500f0, dt, f0)  # 500 ms wavelet
    
    # Create array with different time shifts of the wavelet
    wavelet = zeros(Float32, 4, src_geometry.nt[1])
    wavelet[1, 1:1+length(q)-1] = q
    wavelet[2, 41:41+length(q)-1] = q
    wavelet[3, 121:121+length(q)-1] = q
    wavelet[4, 201:201+length(q)-1] = q

    Finally, we create our simultaneous source as a judiVector:

    # Source wavelet
    q = judiVector(src_geometry, wavelet)

    Computational simultaneous sources (super shots)

    The computational simultaneous sources refer to superposition of sequentially-fired sources and shot records from the field. These computational simultaneous shot records are not acquired in the field simultaneously, but computational simultaneous sources introduce randomness to the optimization problems like FWI and LS-RTM, which takes advantage of the knowledge in randomized linear algebra to make optimization faster and more robust.

    The implementation of computational simultaneous sources follows the journal article Fast randomized full-waveform inversion with compressive sensing The simultaneous sources experiments are generated by superposition of shot records with random weights drawn from Gaussian distribution.

    # assume dobs is generated by sequentially fired point sources q
    nsimsrc = 8
    # Set up random weights
    weights = randn(Float32,nsimsrc,q.nsrc)
    # Create SimSource
    q_sim = weights * q
    data_sim = weights * dobs
    # We can also apply the weights to the operator and check the equivalence
    d_sim = (weights * F) * q_sim
    isapprox(data_sim, d_sim)

    Working with wavefields

    JUDI allows computing full time domain wavefields and using them as right-hand sides for wave equations solves. This tutorial shows how. We start by setting up a basic 2D experiment:

    using JUDI
    
    # Grid
    n = (120, 100)   # (x,z)
    d = (10., 10.)
    o = (0., 0.)
    
    # Velocity [km/s]
    v = ones(Float32, n) .* 1.4f0
    v[:, 50:end] .= 5f0
    
    # Squared slowness
    m = (1f0 ./ v).^2
    
    # Model structure:
    model = Model(n, d, o, m)

    Next, we set up the source geometry for a single source experiment:

    # Set up source geometry
    nsrc = 1    # no. of sources
    xsrc = convertToCell([600f0])
    ysrc = convertToCell([0f0])
    zsrc = convertToCell([20f0])
    
    # Modeling time and sampling interval
    time = 600f0  # ms
    dt = 4f0   # ms
    
    # Set up source structure
    src_geometry = Geometry(xsrc, ysrc, zsrc; dt=dt, t=time)
    
    # Source wavelet
    f0 = 0.01f0     # kHz
    wavelet = ricker_wavelet(time, dt, f0)
    q = judiVector(src_geometry, wavelet)

    As in the 2D quick start tutorial, we create our modeling operator and source projection operator:

    # Setup operators
    A_inv = judiModeling(model)
    Ps = judiProjection(src_geometry)

    To model a wavefield, we simply omit the receiver sampling operator:

    u = A_inv*Ps'*q

    This return an abstract data vector called judiWavefield. Similar to judiVectors, we can access the data for each source number i via u.data[i]. The data is a 3D array of size (nt, nx, nz) for 2D and a 4D array of size (nt, nx, ny, nz) for 3D. We can plot the wavefield of the 600th time step with:

    using PythonPlot
    imshow(u.data[1][600, :, :]', vmin=-5, vmax=5, cmap="seismic", aspect="auto")

    We can also use the computed wavefield u as a right-hand side for forward and adjoint wave equation solves:

    v = A_inv*u
    w = A_inv'*u

    Similarly, by setting up a receiver projection operator, we can use wavefields as right-hand sides, but restrict the output to the receiver locations.

    Extended source modeling

    JUDI supports extened source modeling, which injects a 1D wavelet q at every point in the subsurface weighted by a spatially varying extended source. To demonstrate extended source modeling, we first set up a runnable 2D experiment with JUDI. We start with defining the model:

    using JUDI
    
    # Grid
    n = (120, 100)   # (x,z)
    d = (10., 10.)
    o = (0., 0.)
    
    # Velocity [km/s]
    v = ones(Float32, n) .* 1.4f0
    v[:, 50:end] .= 5f0
    
    # Squared slowness
    m = (1f0 ./ v).^2
    
    # Model structure:
    model = Model(n, d, o, m)

    Next, we set up the receiver geometry:

    # Number of experiments
    nsrc = 2
    
    # Set up receiver geometry
    nxrec = 120
    xrec = range(50f0, stop=1150f0, length=nxrec)
    yrec = 0f0
    zrec = range(50f0, stop=50f0, length=nxrec)
    
    # Modeling time and receiver sampling interval
    time = 2000
    dt = 4
    
    # Set up receiver structure
    rec_geometry = Geometry(xrec, yrec, zrec; dt=dt, t=time, nsrc=nsrc)

    For the extended source, we do not need to set up a source geometry object, but we need to define a wavelet function:

    # Source wavelet
    f0 = 0.01f0     # MHz
    wavelet = ricker_wavelet(time, dt, f0)

    As before, we set up a modeling operator and a receiver sampling operator:

    # Setup operators
    A_inv = judiModeling(model)
    Pr = judiProjection(rec_geometry)

    We define our extended source as a so called judiWeights vector. Similar to a judiVector, the data of this abstract vector is stored as a cell array, where each cell corresponds to one source experiment. We create a cell array of length two and create a random array of the size of the model as our extended source:

    weights = Array{Array}(undef, nsrc)
    for j=1:nsrc
        weights[j] = randn(Float32, model.n)
    end
    w = judiWeights(weights)

    To inject the extended source into the model and weight it by the wavelet, we create a special projection operator called judiLRWF (for JUDI low-rank wavefield). This operator needs to know the wavelet we defined earlier. We can then create our full modeling operator, by combining Pw with A_inv and the receiver sampling operator:

    # Create operator for injecting the weights, multiplied by the provided wavelet(s)
    Pw = judiLRWF(wavelet)
    
    # Model observed data w/ extended source
    F = Pr*A_inv*adjoint(Pw)

    Extended source modeling supports both forward and adjoint modeling:

    # Simultaneous observed data
    d_sim = F*w
    dw = adjoint(F)*d_sim

    As for regular modeling, we can create a Jacobian for linearized modeling and migration. First we define a migration velocity model and the corresponding modeling operator A0_inv:

    # Migration velocity and squared slowness
    v0 = ones(Float32, n) .* 1.4f0
    m0 = (1f0 ./ v0).^2
    
    # Model structure and modeling operator for migration velocity
    model0 = Model(n, d, o, m0)
    A0_inv = judiModeling(model0)
    
    # Jacobian and RTM
    J = judiJacobian(Pr*A0_inv*adjoint(Pw), w)
    rtm = adjoint(J)*d_sim

    As before, we can plot the image after reshaping it into its original dimensions:

    rtm = reshape(rtm, model.n)
    imshow(rtm', cmap="gray", vmin=-3e6, vmax=3e6)

    Please also refer to the reproducable example on github for 2D and 3D extended modeling.

    Impedance imaging (inverse scattering)

    JUDI supports imaging (RTM) and demigration (linearized modeling) using the linearized inverse scattering imaging condition (ISIC) and its corresponding adjoint. ISIC can be enabled via the Options class. You can set this options when you initially create the modeling operator:

    # Options strucuture
    opt = Options(isic=true)
    
    # Set up modeling operator
    A0_inv = judiModeling(model0; options=opt)

    When you create a Jacobian from a forward modeling operator, the Jacobian inherits the options from A0_inv:

    J = judiJacobian(Pr*A0_inv*Ps', q)
    J.options.isic
    # -> true

    Alternatively, you can directly set the option in your Jacobian:

    J.options.isic = true   # enable isic
    J.options.isic = false  # disable isic

    Optimal checkpointing

    JUDI supports optimal checkpointing via Devito's interface to the Revolve library. To enable checkpointing, use the Options class:

    # Options strucuture
    opt = Options(optimal_checkpointing=true)
    
    # Set up modeling operator
    A0_inv = judiModeling(model0; options=opt)

    When you create a Jacobian from a forward modeling operator, the Jacobian inherits the options from A0_inv:

    J = judiJacobian(Pr*A0_inv*Ps', q)
    J.options.optimal_checkpointing
    # -> true

    Alternatively, you can directly set the option in your Jacobian:

    J.options.optimal_checkpointing = true   # enable checkpointing
    J.options.optimal_checkpointing = false  # disable checkpointing

    On-the-fly Fourier transforms

    JUDI supports seismic imaging in the frequency domain using on-the-fly discrete Fourier transforms (DFTs). To compute an RTM image in the frequency domain for a given set of frequencies, we first create a cell array for the frequencies of each source experiment:

    nsrc = 4    # assume 4 source experiments
    frequencies = Array{Any}(undef, nsrc)

    Now we can define single or multiple frequencies for each shot location for which the RTM image will be computed:

    # For every source location, compute RTM image for 10 and 20 Hz
    for j=1:nsrc
        frequencies[j] = [0.001, 0.002]
    end

    The frequencies are passed to the Jacobian via the options field. Assuming we already have a Jacobian set up, we set the frequencies via:

    J.options.frequencies = frequencies

    Instead of the same two frequencies for each source experiment, we could have chosen different random sets of frequencies, which creates an RTM with incoherent noise. We can also draw random frequencies using the frequency spectrum of the true source as the probability density function. To create a distribution for a given source q (judiVector) from which we can draw frequency samples, use:

    q_dist = generate_distribution(q)

    Then we can assigne a random set of frequencies in a specified range as follows:

    nfreq = 10  # no. of frequencies per source location
    for j=1:nsrc
        J.options.frequencies[j] = select_frequencies(q_dist; fmin=0.003, fmax=0.04, nf=nfreq)
    end

    Once the options.frequencies field is set, on-the-fly DFTs are used for both born modeling and RTM. To save computational cost, we can limit the number of DFTs that are performed. Rather than computing the DFT at every time step, we can define a subsampling factor as follows:

    # Compute DFT every 4 time steps
    J.options.dft_subsampling_factor=4