├── .github └── workflows │ ├── deploy_doc.yaml │ ├── example.yaml │ └── update-tutos.yaml ├── LICENSE ├── README.md ├── conftest.py ├── mkdocs.yml ├── seismic ├── __init__.py ├── acoustic │ ├── __init__.py │ ├── accuracy.ipynb │ ├── acoustic_example.py │ ├── operators.py │ └── wavesolver.py ├── checkpointing │ └── checkpoint.py ├── elastic │ ├── __init__.py │ ├── elastic_example.py │ ├── operators.py │ └── wavesolver.py ├── model.py ├── plotting.py ├── preset_models.py ├── source.py ├── tti │ ├── __init__.py │ ├── operators.py │ ├── rotated_fd.py │ ├── tti_example.py │ └── wavesolver.py ├── tutorials │ ├── 01_modelling.ipynb │ ├── 02_rtm.ipynb │ ├── 03_fwi.ipynb │ ├── 04_dask.ipynb │ ├── 05_staggered_acoustic.ipynb │ ├── 06_elastic.ipynb │ ├── 06_elastic_varying_parameters.ipynb │ ├── TLE_Adjoint.ipynb │ ├── TLE_Forward.ipynb │ └── TLE_fwi.ipynb ├── utils.py └── viscoelastic │ ├── __init__.py │ ├── operators.py │ ├── viscoelastic_example.py │ └── wavesolver.py └── setup.py /.github/workflows/deploy_doc.yaml: -------------------------------------------------------------------------------- 1 | name: Publish docs via GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build: 9 | name: Deploy docs 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout master 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Python 3.10 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: "3.10" 19 | 20 | - name: Install dependencies 21 | run: | 22 | pip install --upgrade pip 23 | pip install mkdocs jupyter pymdown-extensions python-markdown-math 24 | 25 | - name: Build doc 26 | run: | 27 | for f in *.ipynb;do jupyter nbconvert --to markdown --output-dir='../../docs/tutorials' $f; done 28 | jupyter nbconvert --to markdown --output-dir='../../docs/tutorials' ../acoustic/accuracy.ipynb 29 | cp ../../README.md ../../docs/index.md 30 | working-directory: seismic/tutorials/ 31 | 32 | - run: mkdocs build 33 | 34 | - name: Deploy 35 | uses: peaceiris/actions-gh-pages@v4 36 | with: 37 | github_token: ${{ secrets.GITHUB_TOKEN }} 38 | publish_dir: ./site 39 | -------------------------------------------------------------------------------- /.github/workflows/example.yaml: -------------------------------------------------------------------------------- 1 | name: Examples 2 | 3 | on: 4 | # Trigger the workflow on push or pull request, 5 | # but only for the master branch 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | tutorials: 15 | name: Examples 16 | runs-on: ubuntu-latest 17 | 18 | env: 19 | DEVITO_ARCH: gcc 20 | DEVITO_LANGUAGE: "openmp" 21 | 22 | strategy: 23 | # Prevent all build to stop if a single one fails 24 | fail-fast: false 25 | matrix: 26 | ndim: ['1', '2', '3'] 27 | 28 | steps: 29 | - name: Checkout devito 30 | uses: actions/checkout@v4 31 | 32 | - name: Set up Python 3.10 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: "3.10" 36 | 37 | - name: Install dependencies 38 | run: | 39 | pip install --upgrade pip 40 | pip install -e . 41 | 42 | - name: Seismic acoustic examples 43 | run: | 44 | python seismic/acoustic/acoustic_example.py --full -nd ${{ matrix.ndim }} 45 | python seismic/acoustic/acoustic_example.py --constant --full -nd ${{ matrix.ndim }} 46 | 47 | - name: Seismic advanced examples 48 | if: matrix.ndim > 1 49 | run: | 50 | python seismic/acoustic/acoustic_example.py --full --checkpointing -nd ${{ matrix.ndim }} 51 | python seismic/tti/tti_example.py -a basic -nd ${{ matrix.ndim }} 52 | python seismic/tti/tti_example.py -a basic --noazimuth -nd ${{ matrix.ndim }} 53 | python seismic/elastic/elastic_example.py -nd ${{ matrix.ndim }} 54 | python seismic/viscoelastic/viscoelastic_example.py -nd ${{ matrix.ndim }} 55 | 56 | - name: Tutorials 57 | if: matrix.ndim > 1 58 | run: py.test --nbval --ignore-glob=seismic/tutorials/TLE*.ipynb seismic/tutorials/ 59 | -------------------------------------------------------------------------------- /.github/workflows/update-tutos.yaml: -------------------------------------------------------------------------------- 1 | name: Sync Some Directory 2 | 3 | on: 4 | schedule: 5 | - cron: '0 6 * * *' # Runs daily at 6 AM UTC 6 | workflow_dispatch: # Allow manual trigger too 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | sync-directory: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout this repo 17 | uses: actions/checkout@v4 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - name: Clone lastest release of devito to update examples 22 | run: | 23 | REPO="devitocodes/devito" 24 | TAG=$(curl -s "https://api.github.com/repos/${REPO}/releases/latest" | jq -r .tag_name) 25 | git clone --branch "$TAG" --depth=1 "https://github.com/${REPO}.git" 26 | 27 | 28 | - name: Update only existing files in local 29 | run: | 30 | LOCAL_DIR="${GITHUB_WORKSPACE}/seismic" 31 | SRC_DIR="${GITHUB_WORKSPACE}/devito/examples/seismic" 32 | cd "$SRC_DIR" 33 | find . -type f | while read -r f; do 34 | if [ -f "${LOCAL_DIR}/$f" ]; then 35 | cp "$f" "${LOCAL_DIR}/$f" 36 | fi 37 | done 38 | 39 | - name: Commit and push 40 | run: | 41 | git config user.name "github-actions[bot]" 42 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 43 | git add -u 44 | git commit -m "Sync local from external repo" || echo "Nothing to commit" 45 | git push origin master 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SLIM group @ Georgia Institute of Technology 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Devito-Examples 3 | 4 | [![Examples](https://github.com/slimgroup/Devito-Examples/workflows/Examples/badge.svg)](https://github.com/slimgroup/Devito-Examples/actions?query=workflow%3AExamples) 5 | [![](https://img.shields.io/badge/docs-stable-blue.svg)](https://slimgroup.github.io/Devito-Examples/) 6 | 7 | This repository contains a set of examples and tutorials for seismic modeling and inversion using [Devito]. 8 | These examples use four different wave equations, namely 9 | 10 | - The acoustic isotropic wave equation in`seismic/acoustic` 11 | - The TTI pseudo-acoustic wave equation in `seismic/tti` 12 | - The elastic isotropic wave equation in `seismic/elastic` 13 | - The viscoelastic isotropic wave equation in `seismic/elastic` 14 | 15 | Currently, the acoustic isotropic wave equation solver also contains the propagator associated with the adjoint and linearized (Born) wave-equation solution and the gradient of the FWI objective (application of the Jacobian to data residual) 16 | 17 | ## Disclaimer 18 | 19 | The majority of these examples can also be found in the [Devito] examples directory, which is a fork of this repository. These examples for seismic applications have been developed and implemented by [Mathias Louboutin] at the Georgia Institute of Technology. For additional introductory examples, including tutorials on the [Devito] compiler, we refer to the [Devito example directory] on [github] since these were developed primarily by people from the [Devito] team at Imperial College London. The contributions by [Mathias Louboutin] were made as part of actvities at the Georgia Tech's Seismic Laboratory for Imaging and modeling ([SLIM). 20 | 21 | [Devito example directory]:https://github.com/devitocodes/devito/tree/master/examples/seismic 22 | [github]:https://github.com 23 | [SLIM]:https://slim.gatech.edu 24 | [Mathias Louboutin]:https://slim.gatech.edu/people/mathias-louboutin 25 | 26 | ## Installation 27 | 28 | To install this set of examples with its dependencies run in your terminal (OSX, Ubuntu): 29 | 30 | ``` 31 | git clone https://github.com/slimgroup/Devito-Examples 32 | cd Devito-Examples 33 | pip install -e . 34 | ``` 35 | 36 | This command will install all dependencies including [Devito] and will allow you to run the examples. To verify your installation you can run: 37 | 38 | ``` 39 | python seismic/acoustic/acoustic_example.py -nd 1 40 | ``` 41 | 42 | Some of the examples require velocity models such as the marmousi-ii model. These models can be downloaded at [devito-data](https://github.com/devitocodes/data) to be used in the tutorials. 43 | 44 | ## How to navigate this directory 45 | 46 | Examples and tutorials are provided in the form of single Python files and as Jupyter 47 | notebooks.Jupyter notebooks are files with extension `.ipynb`. To execute these, run 48 | `jupyter notebook`, and then click on the desired notebook in the window that 49 | pops up in your browser. 50 | 51 | The seismic examples and tutorials are organized as follows: 52 | 53 | * `seismic/tutorials`: A series of Jupyter notebooks of incremental complexity, 54 | showing a variety of Devito features in the context of seismic inversion 55 | operators. Among the discussed features are modeling, adjoint modeling, computing a gradient and a seismic image, FWI and elastic modeling on a staggered grid. 56 | * `seismic/acoustic`: Example implementations of isotropic acoustic forward, 57 | adjoint, gradient and born operators, suitable for full-waveform inversion 58 | methods (FWI). 59 | * `seismic/tti`: Example implementations of several anisotropic acoustic 60 | forward operators (TTI). 61 | * `seismic/elastic`: Example implementation of an isotropic elastic forward 62 | operator. `elastic`, unlike `acoustic` and `tti`, fully exploits the 63 | tensorial nature of the Devito symbolic language. 64 | * `seismic/viscoelastic`: Example implementation of an isotropic viscoelastic 65 | forward operator. Like `elastic`, `viscoelastic` exploits tensor functions 66 | for a neat and compact representation of the discretized partial differential 67 | equations. 68 | 69 | ## Related literature 70 | 71 | Some of these examples are described in the following papers: 72 | 73 | - [Devito's symbolic API](https://slim.gatech.edu/content/devito-embedded-domain-specific-language-finite-differences-and-geophysical-exploration) for a description of the Devito API and symbolic capabilities. 74 | - [TTI imaging](https://slim.gatech.edu/content/effects-wrong-adjoints-rtm-tti-media) for small overview of imging in a TTI media (SEG abstract). 75 | - [Mathias Louboutin's thesis](https://slim.gatech.edu/content/modeling-inversion-exploration-geophysics) for [Mathias Louboutin]'s Thesis. 76 | 77 | More advanced geophysical application can be found in the [JUDI] repository. [JUDI] is a linear algebra DSL built on top of [Devito] for large scale inverse problems and includes abstractions for source/receivers and handles large SEG-Y datasets with [SegyIO](https://github.com/slimgroup/SegyIO.jl). A complete description of [JUDI] and the related seismic inversion application can be found in [Philipp Witte's thesis](https://slim.gatech.edu/content/modeling-inversion-exploration-geophysics). 78 | 79 | [JUDI]:https://github.com/slimgroup/JUDI.jl 80 | [Devito]:https://www.devitoproject.org 81 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.hookimpl(tryfirst=True) 5 | def pytest_collectstart(collector): 6 | if hasattr(collector, "fspath") and \ 7 | str(collector.fspath).endswith(".ipynb"): 8 | collector.skip_compare += ('text/latex', 'stderr') 9 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: SLIM's Devito examples 2 | site_author: Mathias Louboutin 3 | 4 | repo_url: https://github.com/mkdocs/mkdocs/ 5 | edit_uri: "" 6 | 7 | nav: 8 | - 'Home': index.md 9 | - 'Tutorials': 10 | - Acoustic modeling: tutorials/01_modelling.md 11 | - Convergence analysis for the acoustic wave-equation: tutorials/accuracy.md 12 | - Acoustic RTM: tutorials/02_rtm.md 13 | - Acoustic FWI: tutorials/03_fwi.md 14 | - Parallel acoustic FWI with Dask: tutorials/04_dask.md 15 | - First order acoustic modeling: tutorials/05_staggered_acoustic.md 16 | - Elastic modeling with constant parameters: tutorials/06_elastic.md 17 | - Elastic modeling with varying parameters: tutorials/06_elastic_varying_parameters.md 18 | - 'The Leading Edge tutorials': 19 | - Forward modeling: tutorials/TLE_Forward.md 20 | - Adjoint Modeling: tutorials/TLE_Adjoint.md 21 | - FWI and algorithms: tutorials/TLE_fwi.md 22 | 23 | theme: readthedocs 24 | 25 | copyright: Copyright (c) 2020 SLIM group @ Georgia Institute of Technology. 26 | 27 | plugins: 28 | - search 29 | 30 | extra_javascript: 31 | - https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML 32 | 33 | markdown_extensions: 34 | - mdx_math 35 | - pymdownx.arithmatex 36 | - pymdownx.betterem: 37 | smart_enable: all 38 | - pymdownx.caret 39 | - pymdownx.critic 40 | - pymdownx.details 41 | - pymdownx.emoji: 42 | emoji_generator: !!python/name:pymdownx.emoji.to_svg 43 | - pymdownx.inlinehilite 44 | - pymdownx.magiclink 45 | - pymdownx.mark 46 | - pymdownx.smartsymbols 47 | - pymdownx.superfences 48 | - pymdownx.tasklist: 49 | custom_checkbox: true 50 | -------------------------------------------------------------------------------- /seismic/__init__.py: -------------------------------------------------------------------------------- 1 | from .model import * # noqa 2 | from .source import * # noqa 3 | from .plotting import * # noqa 4 | from .preset_models import * # noqa 5 | from .utils import * # noqa 6 | -------------------------------------------------------------------------------- /seismic/acoustic/__init__.py: -------------------------------------------------------------------------------- 1 | from .operators import * # noqa 2 | from .wavesolver import * # noqa 3 | from .acoustic_example import * # noqa 4 | -------------------------------------------------------------------------------- /seismic/acoustic/acoustic_example.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | try: 3 | import pytest 4 | except ImportError: 5 | pass 6 | 7 | from devito.logger import info 8 | from devito import Constant, Function, smooth, norm 9 | from examples.seismic.acoustic import AcousticWaveSolver 10 | from examples.seismic import demo_model, setup_geometry, seismic_args 11 | 12 | 13 | def acoustic_setup(shape=(50, 50, 50), spacing=(15.0, 15.0, 15.0), 14 | tn=500., kernel='OT2', space_order=4, nbl=10, 15 | preset='layers-isotropic', fs=False, **kwargs): 16 | model = demo_model(preset, space_order=space_order, shape=shape, nbl=nbl, 17 | dtype=kwargs.pop('dtype', np.float32), spacing=spacing, 18 | fs=fs, **kwargs) 19 | 20 | # Source and receiver geometries 21 | geometry = setup_geometry(model, tn, **kwargs) 22 | 23 | # Create solver object to provide relevant operators 24 | solver = AcousticWaveSolver(model, geometry, kernel=kernel, 25 | space_order=space_order, **kwargs) 26 | return solver 27 | 28 | 29 | def run(shape=(50, 50, 50), spacing=(20.0, 20.0, 20.0), tn=1000.0, 30 | space_order=4, kernel='OT2', nbl=40, full_run=False, fs=False, 31 | autotune=False, preset='layers-isotropic', checkpointing=False, **kwargs): 32 | 33 | solver = acoustic_setup(shape=shape, spacing=spacing, nbl=nbl, tn=tn, 34 | space_order=space_order, kernel=kernel, fs=fs, 35 | preset=preset, **kwargs) 36 | 37 | info("Applying Forward") 38 | # Whether or not we save the whole time history. We only need the full wavefield 39 | # with 'save=True' if we compute the gradient without checkpointing, if we use 40 | # checkpointing, PyRevolve will take care of the time history 41 | save = full_run and not checkpointing 42 | # Define receiver geometry (spread across x, just below surface) 43 | rec, u, summary = solver.forward(save=save, autotune=autotune) 44 | 45 | if preset == 'constant-isotropic': 46 | # With a new m as Constant 47 | v0 = Constant(name="v", value=2.0, dtype=np.float32) 48 | solver.forward(save=save, vp=v0) 49 | # With a new vp as a scalar value 50 | solver.forward(save=save, vp=2.0) 51 | 52 | if not full_run: 53 | return summary.gflopss, summary.oi, summary.timings, [rec, u.data] 54 | 55 | # Smooth velocity 56 | initial_vp = Function(name='v0', grid=solver.model.grid, space_order=space_order) 57 | smooth(initial_vp, solver.model.vp) 58 | dm = np.float32(initial_vp.data**(-2) - solver.model.vp.data**(-2)) 59 | 60 | info("Applying Adjoint") 61 | solver.adjoint(rec, autotune=autotune) 62 | info("Applying Born") 63 | solver.jacobian(dm, autotune=autotune) 64 | info("Applying Gradient") 65 | solver.jacobian_adjoint(rec, u, autotune=autotune, checkpointing=checkpointing) 66 | return summary.gflopss, summary.oi, summary.timings, [rec, u.data._local] 67 | 68 | 69 | @pytest.mark.parametrize('shape', [(101,), (51, 51), (16, 16, 16)]) 70 | @pytest.mark.parametrize('k', ['OT2', 'OT4']) 71 | @pytest.mark.parametrize('interp', ['linear', 'sinc']) 72 | def test_isoacoustic_stability(shape, k, interp): 73 | spacing = tuple([20]*len(shape)) 74 | _, _, _, [rec, _] = run(shape=shape, spacing=spacing, tn=20000.0, nbl=0, kernel=k, 75 | interpolation=interp) 76 | assert np.isfinite(norm(rec)) 77 | 78 | 79 | @pytest.mark.parametrize('fs, normrec, dtype, interp', [ 80 | (True, 369.955, np.float32, 'linear'), 81 | (False, 459.1678, np.float64, 'linear'), 82 | (True, 402.216, np.float32, 'sinc'), 83 | (False, 509.0681, np.float64, 'sinc')]) 84 | def test_isoacoustic(fs, normrec, dtype, interp): 85 | _, _, _, [rec, _] = run(fs=fs, dtype=dtype, interpolation=interp) 86 | assert np.isclose(norm(rec), normrec, rtol=1e-3, atol=0) 87 | 88 | 89 | if __name__ == "__main__": 90 | description = ("Example script for a set of acoustic operators.") 91 | parser = seismic_args(description) 92 | parser.add_argument('--fs', dest='fs', default=False, action='store_true', 93 | help="Whether or not to use a freesurface") 94 | parser.add_argument("-k", dest="kernel", default='OT2', 95 | choices=['OT2', 'OT4'], 96 | help="Choice of finite-difference kernel") 97 | args = parser.parse_args() 98 | 99 | # 3D preset parameters 100 | ndim = args.ndim 101 | shape = args.shape[:args.ndim] 102 | spacing = tuple(ndim * [15.0]) 103 | tn = args.tn if args.tn > 0 else (750. if ndim < 3 else 1250.) 104 | 105 | preset = 'constant-isotropic' if args.constant else 'layers-isotropic' 106 | run(shape=shape, spacing=spacing, nbl=args.nbl, tn=tn, fs=args.fs, 107 | space_order=args.space_order, preset=preset, kernel=args.kernel, 108 | autotune=args.autotune, opt=args.opt, full_run=args.full, 109 | checkpointing=args.checkpointing, dtype=args.dtype, interpolation=args.interp) 110 | -------------------------------------------------------------------------------- /seismic/acoustic/operators.py: -------------------------------------------------------------------------------- 1 | from devito import Eq, Operator, Function, TimeFunction, Inc, solve, sign 2 | from devito.symbolics import retrieve_functions, INT, retrieve_derivatives 3 | 4 | 5 | def freesurface(model, eq): 6 | """ 7 | Generate the stencil that mirrors the field as a free surface modeling for 8 | the acoustic wave equation. 9 | 10 | Parameters 11 | ---------- 12 | model : Model 13 | Physical model. 14 | eq : Eq 15 | Time-stepping stencil (time update) to mirror at the freesurface. 16 | """ 17 | lhs, rhs = eq.args 18 | # Get vertical dimension and corresponding subdimension 19 | fsdomain = model.grid.subdomains['fsdomain'] 20 | zfs = fsdomain.dimensions[-1] 21 | z = zfs.parent 22 | 23 | # Retrieve vertical derivatives 24 | dzs = {d for d in retrieve_derivatives(rhs) if z in d.dims} 25 | # Remove inner duplicate 26 | dzs = dzs - {d for D in dzs for d in retrieve_derivatives(D.expr) if z in d.dims} 27 | dzs = {d: d._eval_at(lhs).evaluate for d in dzs} 28 | 29 | # Finally get functions for evaluated derivatives 30 | funcs = {f for f in retrieve_functions(dzs.values())} 31 | 32 | mapper = {} 33 | # Antisymmetric mirror at negative indices 34 | # TODO: Make a proper "mirror_indices" tool function 35 | for f in funcs: 36 | zind = f.indices[-1] 37 | if (zind - z).as_coeff_Mul()[0] < 0: 38 | s = sign(zind.subs({z: zfs, z.spacing: 1})) 39 | mapper.update({f: s * f.subs({zind: INT(abs(zind))})}) 40 | 41 | # Mapper for vertical derivatives 42 | dzmapper = {d: v.subs(mapper) for d, v in dzs.items()} 43 | 44 | fs_eq = [eq.func(lhs, rhs.subs(dzmapper), subdomain=fsdomain)] 45 | fs_eq.append(eq.func(lhs._subs(z, 0), 0, subdomain=fsdomain)) 46 | 47 | return fs_eq 48 | 49 | 50 | def laplacian(field, model, kernel): 51 | """ 52 | Spatial discretization for the isotropic acoustic wave equation. For a 4th 53 | order in time formulation, the 4th order time derivative is replaced by a 54 | double laplacian: 55 | H = (laplacian + s**2/12 laplacian(1/m*laplacian)) 56 | 57 | Parameters 58 | ---------- 59 | field : TimeFunction 60 | The computed solution. 61 | model : Model 62 | Physical model. 63 | """ 64 | if kernel not in ['OT2', 'OT4']: 65 | raise ValueError("Unrecognized kernel") 66 | s = model.grid.time_dim.spacing 67 | biharmonic = field.biharmonic(1/model.m) if kernel == 'OT4' else 0 68 | return field.laplace + s**2/12 * biharmonic 69 | 70 | 71 | def iso_stencil(field, model, kernel, **kwargs): 72 | """ 73 | Stencil for the acoustic isotropic wave-equation: 74 | u.dt2 - H + damp*u.dt = 0. 75 | 76 | Parameters 77 | ---------- 78 | field : TimeFunction 79 | The computed solution. 80 | model : Model 81 | Physical model. 82 | kernel : str, optional 83 | Type of discretization, 'OT2' or 'OT4'. 84 | q : TimeFunction, Function or float 85 | Full-space/time source of the wave-equation. 86 | forward : bool, optional 87 | Whether to propagate forward (True) or backward (False) in time. 88 | """ 89 | # Forward or backward 90 | forward = kwargs.get('forward', True) 91 | # Define time step to be updated 92 | unext = field.forward if forward else field.backward 93 | udt = field.dt if forward else field.dt.T 94 | # Get the spacial FD 95 | lap = laplacian(field, model, kernel) 96 | # Get source 97 | q = kwargs.get('q', 0) 98 | # Define PDE and update rule 99 | eq_time = solve(model.m * field.dt2 - lap - q + model.damp * udt, unext) 100 | 101 | # Time-stepping stencil. 102 | eqns = [Eq(unext, eq_time, subdomain=model.grid.subdomains['physdomain'])] 103 | 104 | # Add free surface 105 | if model.fs: 106 | eqns.append(freesurface(model, Eq(unext, eq_time))) 107 | return eqns 108 | 109 | 110 | def ForwardOperator(model, geometry, space_order=4, 111 | save=False, kernel='OT2', **kwargs): 112 | """ 113 | Construct a forward modelling operator in an acoustic medium. 114 | 115 | Parameters 116 | ---------- 117 | model : Model 118 | Object containing the physical parameters. 119 | geometry : AcquisitionGeometry 120 | Geometry object that contains the source (SparseTimeFunction) and 121 | receivers (SparseTimeFunction) and their position. 122 | space_order : int, optional 123 | Space discretization order. 124 | save : int or Buffer, optional 125 | Saving flag, True saves all time steps. False saves three timesteps. 126 | Defaults to False. 127 | kernel : str, optional 128 | Type of discretization, 'OT2' or 'OT4'. 129 | """ 130 | m = model.m 131 | 132 | # Create symbols for forward wavefield, source and receivers 133 | u = TimeFunction(name='u', grid=model.grid, 134 | save=geometry.nt if save else None, 135 | time_order=2, space_order=space_order) 136 | src = geometry.src 137 | rec = geometry.rec 138 | 139 | s = model.grid.stepping_dim.spacing 140 | eqn = iso_stencil(u, model, kernel) 141 | 142 | # Construct expression to inject source values 143 | src_term = src.inject(field=u.forward, expr=src * s**2 / m) 144 | 145 | # Create interpolation expression for receivers 146 | rec_term = rec.interpolate(expr=u) 147 | 148 | # Substitute spacing terms to reduce flops 149 | return Operator(eqn + src_term + rec_term, subs=model.spacing_map, 150 | name='Forward', **kwargs) 151 | 152 | 153 | def AdjointOperator(model, geometry, space_order=4, 154 | kernel='OT2', **kwargs): 155 | """ 156 | Construct an adjoint modelling operator in an acoustic media. 157 | 158 | Parameters 159 | ---------- 160 | model : Model 161 | Object containing the physical parameters. 162 | geometry : AcquisitionGeometry 163 | Geometry object that contains the source (SparseTimeFunction) and 164 | receivers (SparseTimeFunction) and their position. 165 | space_order : int, optional 166 | Space discretization order. 167 | kernel : str, optional 168 | Type of discretization, 'OT2' or 'OT4'. 169 | """ 170 | m = model.m 171 | 172 | v = TimeFunction(name='v', grid=model.grid, save=None, 173 | time_order=2, space_order=space_order) 174 | srca = geometry.new_src(name='srca', src_type=None) 175 | rec = geometry.rec 176 | 177 | s = model.grid.stepping_dim.spacing 178 | eqn = iso_stencil(v, model, kernel, forward=False) 179 | 180 | # Construct expression to inject receiver values 181 | receivers = rec.inject(field=v.backward, expr=rec * s**2 / m) 182 | 183 | # Create interpolation expression for the adjoint-source 184 | source_a = srca.interpolate(expr=v) 185 | 186 | # Substitute spacing terms to reduce flops 187 | return Operator(eqn + receivers + source_a, subs=model.spacing_map, 188 | name='Adjoint', **kwargs) 189 | 190 | 191 | def GradientOperator(model, geometry, space_order=4, save=True, 192 | kernel='OT2', **kwargs): 193 | """ 194 | Construct a gradient operator in an acoustic media. 195 | 196 | Parameters 197 | ---------- 198 | model : Model 199 | Object containing the physical parameters. 200 | geometry : AcquisitionGeometry 201 | Geometry object that contains the source (SparseTimeFunction) and 202 | receivers (SparseTimeFunction) and their position. 203 | space_order : int, optional 204 | Space discretization order. 205 | save : int or Buffer, optional 206 | Option to store the entire (unrolled) wavefield. 207 | kernel : str, optional 208 | Type of discretization, centered or shifted. 209 | """ 210 | m = model.m 211 | 212 | # Gradient symbol and wavefield symbols 213 | grad = Function(name='grad', grid=model.grid) 214 | u = TimeFunction(name='u', grid=model.grid, save=geometry.nt if save 215 | else None, time_order=2, space_order=space_order) 216 | v = TimeFunction(name='v', grid=model.grid, save=None, 217 | time_order=2, space_order=space_order) 218 | rec = geometry.rec 219 | 220 | s = model.grid.stepping_dim.spacing 221 | eqn = iso_stencil(v, model, kernel, forward=False) 222 | 223 | if kernel == 'OT2': 224 | gradient_update = Inc(grad, - u * v.dt2) 225 | elif kernel == 'OT4': 226 | gradient_update = Inc(grad, - u * v.dt2 - s**2 / 12.0 * u.biharmonic(m**(-2)) * v) 227 | # Add expression for receiver injection 228 | receivers = rec.inject(field=v.backward, expr=rec * s**2 / m) 229 | 230 | # Substitute spacing terms to reduce flops 231 | return Operator(eqn + receivers + [gradient_update], subs=model.spacing_map, 232 | name='Gradient', **kwargs) 233 | 234 | 235 | def BornOperator(model, geometry, space_order=4, 236 | kernel='OT2', **kwargs): 237 | """ 238 | Construct an Linearized Born operator in an acoustic media. 239 | 240 | Parameters 241 | ---------- 242 | model : Model 243 | Object containing the physical parameters. 244 | geometry : AcquisitionGeometry 245 | Geometry object that contains the source (SparseTimeFunction) and 246 | receivers (SparseTimeFunction) and their position. 247 | space_order : int, optional 248 | Space discretization order. 249 | kernel : str, optional 250 | Type of discretization, centered or shifted. 251 | """ 252 | m = model.m 253 | 254 | # Create source and receiver symbols 255 | src = geometry.src 256 | rec = geometry.rec 257 | 258 | # Create wavefields and a dm field 259 | u = TimeFunction(name="u", grid=model.grid, save=None, 260 | time_order=2, space_order=space_order) 261 | U = TimeFunction(name="U", grid=model.grid, save=None, 262 | time_order=2, space_order=space_order) 263 | dm = Function(name="dm", grid=model.grid, space_order=0) 264 | 265 | s = model.grid.stepping_dim.spacing 266 | eqn1 = iso_stencil(u, model, kernel) 267 | eqn2 = iso_stencil(U, model, kernel, q=-dm*u.dt2) 268 | 269 | # Add source term expression for u 270 | source = src.inject(field=u.forward, expr=src * s**2 / m) 271 | 272 | # Create receiver interpolation expression from U 273 | receivers = rec.interpolate(expr=U) 274 | 275 | # Substitute spacing terms to reduce flops 276 | return Operator(eqn1 + source + eqn2 + receivers, subs=model.spacing_map, 277 | name='Born', **kwargs) 278 | -------------------------------------------------------------------------------- /seismic/acoustic/wavesolver.py: -------------------------------------------------------------------------------- 1 | from devito import Function, TimeFunction, DevitoCheckpoint, CheckpointOperator, Revolver 2 | from devito.tools import memoized_meth 3 | from examples.seismic.acoustic.operators import ( 4 | ForwardOperator, AdjointOperator, GradientOperator, BornOperator 5 | ) 6 | 7 | 8 | class AcousticWaveSolver: 9 | """ 10 | Solver object that provides operators for seismic inversion problems 11 | and encapsulates the time and space discretization for a given problem 12 | setup. 13 | 14 | Parameters 15 | ---------- 16 | model : Model 17 | Physical model with domain parameters. 18 | geometry : AcquisitionGeometry 19 | Geometry object that contains the source (SparseTimeFunction) and 20 | receivers (SparseTimeFunction) and their position. 21 | kernel : str, optional 22 | Type of discretization, centered or shifted. 23 | space_order: int, optional 24 | Order of the spatial stencil discretisation. Defaults to 4. 25 | """ 26 | def __init__(self, model, geometry, kernel='OT2', space_order=4, **kwargs): 27 | self.model = model 28 | self.model._initialize_bcs(bcs="damp") 29 | self.geometry = geometry 30 | 31 | assert self.model.grid == geometry.grid 32 | 33 | self.space_order = space_order 34 | self.kernel = kernel 35 | 36 | # Cache compiler options 37 | self._kwargs = kwargs 38 | 39 | @property 40 | def dt(self): 41 | # Time step can be \sqrt{3}=1.73 bigger with 4th order 42 | if self.kernel == 'OT4': 43 | return self.model.dtype(1.73 * self.model.critical_dt) 44 | return self.model.critical_dt 45 | 46 | @memoized_meth 47 | def op_fwd(self, save=None): 48 | """Cached operator for forward runs with buffered wavefield""" 49 | return ForwardOperator(self.model, save=save, geometry=self.geometry, 50 | kernel=self.kernel, space_order=self.space_order, 51 | **self._kwargs) 52 | 53 | @memoized_meth 54 | def op_adj(self): 55 | """Cached operator for adjoint runs""" 56 | return AdjointOperator(self.model, save=None, geometry=self.geometry, 57 | kernel=self.kernel, space_order=self.space_order, 58 | **self._kwargs) 59 | 60 | @memoized_meth 61 | def op_grad(self, save=True): 62 | """Cached operator for gradient runs""" 63 | return GradientOperator(self.model, save=save, geometry=self.geometry, 64 | kernel=self.kernel, space_order=self.space_order, 65 | **self._kwargs) 66 | 67 | @memoized_meth 68 | def op_born(self): 69 | """Cached operator for born runs""" 70 | return BornOperator(self.model, save=None, geometry=self.geometry, 71 | kernel=self.kernel, space_order=self.space_order, 72 | **self._kwargs) 73 | 74 | def forward(self, src=None, rec=None, u=None, model=None, save=None, **kwargs): 75 | """ 76 | Forward modelling function that creates the necessary 77 | data objects for running a forward modelling operator. 78 | 79 | Parameters 80 | ---------- 81 | src : SparseTimeFunction or array_like, optional 82 | Time series data for the injected source term. 83 | rec : SparseTimeFunction or array_like, optional 84 | The interpolated receiver data. 85 | u : TimeFunction, optional 86 | Stores the computed wavefield. 87 | model : Model, optional 88 | Object containing the physical parameters. 89 | vp : Function or float, optional 90 | The time-constant velocity. 91 | save : bool, optional 92 | Whether or not to save the entire (unrolled) wavefield. 93 | 94 | Returns 95 | ------- 96 | Receiver, wavefield and performance summary 97 | """ 98 | # Source term is read-only, so re-use the default 99 | src = src or self.geometry.src 100 | # Create a new receiver object to store the result 101 | rec = rec or self.geometry.rec 102 | 103 | # Create the forward wavefield if not provided 104 | u = u or TimeFunction(name='u', grid=self.model.grid, 105 | save=self.geometry.nt if save else None, 106 | time_order=2, space_order=self.space_order) 107 | 108 | model = model or self.model 109 | # Pick vp from model unless explicitly provided 110 | kwargs.update(model.physical_params(**kwargs)) 111 | 112 | # Execute operator and return wavefield and receiver data 113 | summary = self.op_fwd(save).apply(src=src, rec=rec, u=u, 114 | dt=kwargs.pop('dt', self.dt), **kwargs) 115 | 116 | return rec, u, summary 117 | 118 | def adjoint(self, rec, srca=None, v=None, model=None, **kwargs): 119 | """ 120 | Adjoint modelling function that creates the necessary 121 | data objects for running an adjoint modelling operator. 122 | 123 | Parameters 124 | ---------- 125 | rec : SparseTimeFunction or array-like 126 | The receiver data. Please note that 127 | these act as the source term in the adjoint run. 128 | srca : SparseTimeFunction or array-like 129 | The resulting data for the interpolated at the 130 | original source location. 131 | v: TimeFunction, optional 132 | The computed wavefield. 133 | model : Model, optional 134 | Object containing the physical parameters. 135 | vp : Function or float, optional 136 | The time-constant velocity. 137 | 138 | Returns 139 | ------- 140 | Adjoint source, wavefield and performance summary. 141 | """ 142 | # Create a new adjoint source and receiver symbol 143 | srca = srca or self.geometry.new_src(name='srca', src_type=None) 144 | 145 | # Create the adjoint wavefield if not provided 146 | v = v or TimeFunction(name='v', grid=self.model.grid, 147 | time_order=2, space_order=self.space_order) 148 | 149 | model = model or self.model 150 | # Pick vp from model unless explicitly provided 151 | kwargs.update(model.physical_params(**kwargs)) 152 | 153 | # Execute operator and return wavefield and receiver data 154 | summary = self.op_adj().apply(srca=srca, rec=rec, v=v, 155 | dt=kwargs.pop('dt', self.dt), **kwargs) 156 | return srca, v, summary 157 | 158 | def jacobian_adjoint(self, rec, u, src=None, v=None, grad=None, model=None, 159 | checkpointing=False, **kwargs): 160 | """ 161 | Gradient modelling function for computing the adjoint of the 162 | Linearized Born modelling function, ie. the action of the 163 | Jacobian adjoint on an input data. 164 | 165 | Parameters 166 | ---------- 167 | rec : SparseTimeFunction 168 | Receiver data. 169 | u : TimeFunction 170 | Full wavefield `u` (created with save=True). 171 | v : TimeFunction, optional 172 | Stores the computed wavefield. 173 | grad : Function, optional 174 | Stores the gradient field. 175 | model : Model, optional 176 | Object containing the physical parameters. 177 | vp : Function or float, optional 178 | The time-constant velocity. 179 | 180 | Returns 181 | ------- 182 | Gradient field and performance summary. 183 | """ 184 | dt = kwargs.pop('dt', self.dt) 185 | # Gradient symbol 186 | grad = grad or Function(name='grad', grid=self.model.grid) 187 | 188 | # Create the forward wavefield 189 | v = v or TimeFunction(name='v', grid=self.model.grid, 190 | time_order=2, space_order=self.space_order) 191 | 192 | model = model or self.model 193 | # Pick vp from model unless explicitly provided 194 | kwargs.update(model.physical_params(**kwargs)) 195 | 196 | if checkpointing: 197 | u = TimeFunction(name='u', grid=self.model.grid, 198 | time_order=2, space_order=self.space_order) 199 | cp = DevitoCheckpoint([u]) 200 | n_checkpoints = None 201 | wrap_fw = CheckpointOperator(self.op_fwd(save=False), 202 | src=src or self.geometry.src, 203 | u=u, dt=dt, **kwargs) 204 | wrap_rev = CheckpointOperator(self.op_grad(save=False), u=u, v=v, 205 | rec=rec, dt=dt, grad=grad, **kwargs) 206 | 207 | # Run forward 208 | wrp = Revolver(cp, wrap_fw, wrap_rev, n_checkpoints, rec.data.shape[0]-2) 209 | wrp.apply_forward() 210 | summary = wrp.apply_reverse() 211 | else: 212 | summary = self.op_grad().apply(rec=rec, grad=grad, v=v, u=u, dt=dt, 213 | **kwargs) 214 | return grad, summary 215 | 216 | def jacobian(self, dmin, src=None, rec=None, u=None, U=None, model=None, **kwargs): 217 | """ 218 | Linearized Born modelling function that creates the necessary 219 | data objects for running an adjoint modelling operator. 220 | 221 | Parameters 222 | ---------- 223 | src : SparseTimeFunction or array_like, optional 224 | Time series data for the injected source term. 225 | rec : SparseTimeFunction or array_like, optional 226 | The interpolated receiver data. 227 | u : TimeFunction, optional 228 | The forward wavefield. 229 | U : TimeFunction, optional 230 | The linearized wavefield. 231 | model : Model, optional 232 | Object containing the physical parameters. 233 | vp : Function or float, optional 234 | The time-constant velocity. 235 | """ 236 | # Source term is read-only, so re-use the default 237 | src = src or self.geometry.src 238 | # Create a new receiver object to store the result 239 | rec = rec or self.geometry.rec 240 | 241 | # Create the forward wavefields u and U if not provided 242 | u = u or TimeFunction(name='u', grid=self.model.grid, 243 | time_order=2, space_order=self.space_order) 244 | U = U or TimeFunction(name='U', grid=self.model.grid, 245 | time_order=2, space_order=self.space_order) 246 | 247 | model = model or self.model 248 | # Pick vp from model unless explicitly provided 249 | kwargs.update(model.physical_params(**kwargs)) 250 | 251 | # Execute operator and return wavefield and receiver data 252 | summary = self.op_born().apply(dm=dmin, u=u, U=U, src=src, rec=rec, 253 | dt=kwargs.pop('dt', self.dt), **kwargs) 254 | return rec, u, U, summary 255 | 256 | # Backward compatibility 257 | born = jacobian 258 | gradient = jacobian_adjoint 259 | -------------------------------------------------------------------------------- /seismic/checkpointing/checkpoint.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2016, Imperial College, London 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | # this software and associated documentation files (the "Software"), to deal in the 7 | # Software without restriction, including without limitation the rights to use, copy, 8 | # modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 9 | # and to permit persons to whom the Software is furnished to do so, subject to the 10 | # following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 16 | # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 17 | # FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 18 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | from pyrevolve import Checkpoint, Operator 21 | from devito import TimeFunction 22 | from devito.tools import flatten 23 | 24 | 25 | class CheckpointOperator(Operator): 26 | """Devito's concrete implementation of the ABC pyrevolve.Operator. This class wraps 27 | devito.Operator so it conforms to the pyRevolve API. pyRevolve will call apply 28 | with arguments t_start and t_end. Devito calls these arguments t_s and t_e so 29 | the following dict is used to perform the translations between different names. 30 | 31 | Parameters 32 | ---------- 33 | op : Operator 34 | devito.Operator object that this object will wrap. 35 | args : dict 36 | If devito.Operator.apply() expects any arguments, they can be provided 37 | here to be cached. Any calls to CheckpointOperator.apply() will 38 | automatically include these cached arguments in the call to the 39 | underlying devito.Operator.apply(). 40 | """ 41 | t_arg_names = {'t_start': 'time_m', 't_end': 'time_M'} 42 | 43 | def __init__(self, op, **kwargs): 44 | self.op = op 45 | self.args = kwargs 46 | op_default_args = self.op._prepare_arguments(**kwargs) 47 | self.start_offset = op_default_args[self.t_arg_names['t_start']] 48 | 49 | def _prepare_args(self, t_start, t_end): 50 | args = self.args.copy() 51 | args[self.t_arg_names['t_start']] = t_start + self.start_offset 52 | args[self.t_arg_names['t_end']] = t_end - 1 + self.start_offset 53 | return args 54 | 55 | def apply(self, t_start, t_end): 56 | """ If the devito operator requires some extra arguments in the call to apply 57 | they can be stored in the args property of this object so pyRevolve calls 58 | pyRevolve.Operator.apply() without caring about these extra arguments while 59 | this method passes them on correctly to devito.Operator 60 | """ 61 | # Build the arguments list to invoke the kernel function 62 | args = self.op.arguments(**self._prepare_args(t_start, t_end)) 63 | # Invoke kernel function with args 64 | arg_values = [args[p.name] for p in self.op.parameters] 65 | self.op.cfunction(*arg_values) 66 | 67 | 68 | class DevitoCheckpoint(Checkpoint): 69 | """Devito's concrete implementation of the Checkpoint abstract base class provided by 70 | pyRevolve. Holds a list of symbol objects that hold data. 71 | """ 72 | def __init__(self, objects): 73 | """Intialise a checkpoint object. Upon initialisation, a checkpoint 74 | stores only a reference to the objects that are passed into it.""" 75 | assert(all(isinstance(o, TimeFunction) for o in objects)) 76 | dtypes = set([o.dtype for o in objects]) 77 | assert(len(dtypes) == 1) 78 | self._dtype = dtypes.pop() 79 | self.objects = objects 80 | 81 | @property 82 | def dtype(self): 83 | return self._dtype 84 | 85 | def get_data(self, timestep): 86 | data = flatten([get_symbol_data(s, timestep) for s in self.objects]) 87 | return data 88 | 89 | def get_data_location(self, timestep): 90 | return self.get_data(timestep) 91 | 92 | @property 93 | def size(self): 94 | """The memory consumption of the data contained in a checkpoint.""" 95 | return sum([int((o.size_allocated/(o.time_order+1))*o.time_order) 96 | for o in self.objects]) 97 | 98 | def save(*args): 99 | raise RuntimeError("Invalid method called. Did you check your version" + 100 | " of pyrevolve?") 101 | 102 | def load(*args): 103 | raise RuntimeError("Invalid method called. Did you check your version" + 104 | " of pyrevolve?") 105 | 106 | 107 | def get_symbol_data(symbol, timestep): 108 | timestep += symbol.time_order - 1 109 | ptrs = [] 110 | for i in range(symbol.time_order): 111 | # Use `._data`, instead of `.data`, as `.data` is a view of the DOMAIN 112 | # data region which is non-contiguous in memory. The performance hit from 113 | # dealing with non-contiguous memory is so big (introduces >1 copy), it's 114 | # better to checkpoint unneccesarry stuff to get a contiguous chunk of memory. 115 | ptr = symbol._data[timestep - i, :, :] 116 | ptrs.append(ptr) 117 | return ptrs 118 | -------------------------------------------------------------------------------- /seismic/elastic/__init__.py: -------------------------------------------------------------------------------- 1 | from .operators import * # noqa 2 | from .wavesolver import * # noqa 3 | from .elastic_example import * # noqa 4 | -------------------------------------------------------------------------------- /seismic/elastic/elastic_example.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | try: 3 | import pytest 4 | except ImportError: 5 | pass 6 | from devito import norm 7 | from devito.logger import info 8 | from examples.seismic.elastic import ElasticWaveSolver 9 | from examples.seismic import demo_model, setup_geometry, seismic_args 10 | 11 | 12 | def elastic_setup(shape=(50, 50), spacing=(15.0, 15.0), tn=500., space_order=4, 13 | nbl=10, constant=False, **kwargs): 14 | 15 | preset = 'constant-elastic' if constant else 'layers-elastic' 16 | model = demo_model(preset, space_order=space_order, shape=shape, nbl=nbl, 17 | dtype=kwargs.pop('dtype', np.float32), spacing=spacing) 18 | 19 | # Source and receiver geometries 20 | geometry = setup_geometry(model, tn) 21 | 22 | # Create solver object to provide relevant operators 23 | solver = ElasticWaveSolver(model, geometry, space_order=space_order, **kwargs) 24 | return solver 25 | 26 | 27 | def run(shape=(50, 50), spacing=(20.0, 20.0), tn=1000.0, 28 | space_order=4, nbl=40, autotune=False, constant=False, **kwargs): 29 | 30 | solver = elastic_setup(shape=shape, spacing=spacing, nbl=nbl, tn=tn, 31 | space_order=space_order, constant=constant, **kwargs) 32 | info("Applying Forward") 33 | # Define receiver geometry (spread across x, just below surface) 34 | rec1, rec2, v, tau, summary = solver.forward(autotune=autotune) 35 | return (summary.gflopss, summary.oi, summary.timings, 36 | [rec1, rec2, v, tau]) 37 | 38 | 39 | @pytest.mark.parametrize("dtype", [np.float32, np.float64]) 40 | def test_elastic(dtype): 41 | _, _, _, [rec1, rec2, v, tau] = run(dtype=dtype) 42 | assert np.isclose(norm(rec1), 19.9367, atol=1e-3, rtol=0) 43 | assert np.isclose(norm(rec2), 0.6512, atol=1e-3, rtol=0) 44 | 45 | 46 | @pytest.mark.parametrize('shape', [(51, 51), (16, 16, 16)]) 47 | def test_elastic_stability(shape): 48 | spacing = tuple([20]*len(shape)) 49 | _, _, _, [rec1, rec2, v, tau] = run(shape=shape, spacing=spacing, tn=20000.0, nbl=0) 50 | assert np.isfinite(norm(rec1)) 51 | 52 | 53 | if __name__ == "__main__": 54 | description = ("Example script for a set of elastic operators.") 55 | args = seismic_args(description).parse_args() 56 | # Preset parameters 57 | ndim = args.ndim 58 | shape = args.shape[:args.ndim] 59 | spacing = tuple(ndim * [10.0]) 60 | tn = args.tn if args.tn > 0 else (750. if ndim < 3 else 1250.) 61 | 62 | run(shape=shape, spacing=spacing, nbl=args.nbl, tn=tn, opt=args.opt, 63 | space_order=args.space_order, autotune=args.autotune, constant=args.constant, 64 | dtype=args.dtype) 65 | -------------------------------------------------------------------------------- /seismic/elastic/operators.py: -------------------------------------------------------------------------------- 1 | from devito import Eq, Operator, VectorTimeFunction, TensorTimeFunction 2 | from devito import div, grad, diag, solve 3 | 4 | 5 | def src_rec(v, tau, model, geometry): 6 | """ 7 | Source injection and receiver interpolation 8 | """ 9 | s = model.grid.time_dim.spacing 10 | # Source symbol with input wavelet 11 | src = geometry.src 12 | rec1 = geometry.new_rec(name="rec1") 13 | rec2 = geometry.new_rec(name="rec2") 14 | 15 | # The source injection term 16 | src_expr = src.inject(tau.forward.diagonal(), expr=src * s) 17 | 18 | # Create interpolation expression for receivers 19 | rec_term1 = rec1.interpolate(expr=tau[-1, -1]) 20 | rec_term2 = rec2.interpolate(expr=div(v)) 21 | 22 | return src_expr + rec_term1 + rec_term2 23 | 24 | 25 | def ForwardOperator(model, geometry, space_order=4, save=False, **kwargs): 26 | """ 27 | Construct method for the forward modelling operator in an elastic media. 28 | 29 | Parameters 30 | ---------- 31 | model : Model 32 | Object containing the physical parameters. 33 | geometry : AcquisitionGeometry 34 | Geometry object that contains the source (SparseTimeFunction) and 35 | receivers (SparseTimeFunction) and their position. 36 | space_order : int, optional 37 | Space discretization order. 38 | save : int or Buffer 39 | Saving flag, True saves all time steps, False saves three buffered 40 | indices (last three time steps). Defaults to False. 41 | """ 42 | 43 | v = VectorTimeFunction(name='v', grid=model.grid, 44 | save=geometry.nt if save else None, 45 | space_order=space_order, time_order=1) 46 | tau = TensorTimeFunction(name='tau', grid=model.grid, 47 | save=geometry.nt if save else None, 48 | space_order=space_order, time_order=1) 49 | 50 | lam, mu, b = model.lam, model.mu, model.b 51 | 52 | # Particle velocity 53 | eq_v = v.dt - b * div(tau) 54 | # Stress 55 | e = (grad(v.forward) + grad(v.forward).transpose(inner=False)) 56 | eq_tau = tau.dt - lam * diag(div(v.forward)) - mu * e 57 | 58 | u_v = Eq(v.forward, model.damp * solve(eq_v, v.forward)) 59 | u_t = Eq(tau.forward, model.damp * solve(eq_tau, tau.forward)) 60 | 61 | srcrec = src_rec(v, tau, model, geometry) 62 | op = Operator([u_v] + [u_t] + srcrec, subs=model.spacing_map, name="ForwardElastic", 63 | **kwargs) 64 | # Substitute spacing terms to reduce flops 65 | return op 66 | -------------------------------------------------------------------------------- /seismic/elastic/wavesolver.py: -------------------------------------------------------------------------------- 1 | from devito.tools import memoized_meth 2 | from devito import VectorTimeFunction, TensorTimeFunction 3 | 4 | from examples.seismic.elastic.operators import ForwardOperator 5 | 6 | 7 | class ElasticWaveSolver: 8 | """ 9 | Solver object that provides operators for seismic inversion problems 10 | and encapsulates the time and space discretization for a given problem 11 | setup. 12 | 13 | Parameters 14 | ---------- 15 | model : Model 16 | Physical model with domain parameters. 17 | geometry : AcquisitionGeometry 18 | Geometry object that contains the source (SparseTimeFunction) and 19 | receivers (SparseTimeFunction) and their position. 20 | space_order : int, optional 21 | Order of the spatial stencil discretisation. Defaults to 4. 22 | """ 23 | def __init__(self, model, geometry, space_order=4, **kwargs): 24 | self.model = model 25 | self.model._initialize_bcs(bcs="mask") 26 | self.geometry = geometry 27 | 28 | self.space_order = space_order 29 | # Cache compiler options 30 | self._kwargs = kwargs 31 | 32 | @property 33 | def dt(self): 34 | return self.model.critical_dt 35 | 36 | @memoized_meth 37 | def op_fwd(self, save=None): 38 | """Cached operator for forward runs with buffered wavefield""" 39 | return ForwardOperator(self.model, save=save, geometry=self.geometry, 40 | space_order=self.space_order, **self._kwargs) 41 | 42 | def forward(self, src=None, rec1=None, rec2=None, v=None, tau=None, 43 | model=None, save=None, **kwargs): 44 | """ 45 | Forward modelling function that creates the necessary 46 | data objects for running a forward modelling operator. 47 | 48 | Parameters 49 | ---------- 50 | src : SparseTimeFunction or array_like, optional 51 | Time series data for the injected source term. 52 | rec1 : SparseTimeFunction or array_like, optional 53 | The interpolated receiver data of the pressure (tzz). 54 | rec2 : SparseTimeFunction or array_like, optional 55 | The interpolated receiver data of the particle velocities. 56 | v : VectorTimeFunction, optional 57 | The computed particle velocity. 58 | tau : TensorTimeFunction, optional 59 | The computed symmetric stress tensor. 60 | model : Model, optional 61 | Object containing the physical parameters. 62 | lam : Function, optional 63 | The time-constant first Lame parameter `rho * (vp**2 - 2 * vs **2)`. 64 | mu : Function, optional 65 | The Shear modulus `(rho * vs*2)`. 66 | b : Function, optional 67 | The time-constant inverse density (b=1 for water). 68 | save : bool, optional 69 | Whether or not to save the entire (unrolled) wavefield. 70 | 71 | Returns 72 | ------- 73 | Rec1(tzz), Rec2(div(v)), particle velocities v, stress tensor tau and 74 | performance summary. 75 | """ 76 | # Source term is read-only, so re-use the default 77 | src = src or self.geometry.src 78 | # Create a new receiver object to store the result 79 | rec1 = rec1 or self.geometry.new_rec(name='rec1') 80 | rec2 = rec2 or self.geometry.new_rec(name='rec2') 81 | 82 | # Create all the fields vx, vz, tau_xx, tau_zz, tau_xz 83 | save_t = src.nt if save else None 84 | v = v or VectorTimeFunction(name='v', grid=self.model.grid, save=save_t, 85 | space_order=self.space_order, time_order=1) 86 | tau = tau or TensorTimeFunction(name='tau', grid=self.model.grid, save=save_t, 87 | space_order=self.space_order, time_order=1) 88 | kwargs.update({k.name: k for k in v}) 89 | kwargs.update({k.name: k for k in tau}) 90 | 91 | model = model or self.model 92 | # Pick Lame parameters from model unless explicitly provided 93 | kwargs.update(model.physical_params(**kwargs)) 94 | 95 | # Execute operator and return wavefield and receiver data 96 | summary = self.op_fwd(save).apply(src=src, rec1=rec1, rec2=rec2, 97 | dt=kwargs.pop('dt', self.dt), **kwargs) 98 | return rec1, rec2, v, tau, summary 99 | -------------------------------------------------------------------------------- /seismic/model.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from sympy import finite_diff_weights as fd_w 3 | try: 4 | import pytest 5 | except: 6 | pass 7 | 8 | from devito import (Grid, SubDomain, Function, Constant, warning, 9 | SubDimension, Eq, Inc, Operator, div, sin, Abs) 10 | from devito.builtins import initialize_function, gaussian_smooth, mmax, mmin 11 | from devito.tools import as_tuple 12 | 13 | __all__ = ['SeismicModel', 'Model', 'ModelElastic', 14 | 'ModelViscoelastic', 'ModelViscoacoustic'] 15 | 16 | 17 | def initialize_damp(damp, padsizes, spacing, abc_type="damp", fs=False): 18 | """ 19 | Initialize damping field with an absorbing boundary layer. 20 | 21 | Parameters 22 | ---------- 23 | damp : Function 24 | The damping field for absorbing boundary condition. 25 | nbl : int 26 | Number of points in the damping layer. 27 | spacing : 28 | Grid spacing coefficient. 29 | mask : bool, optional 30 | whether the dampening is a mask or layer. 31 | mask => 1 inside the domain and decreases in the layer 32 | not mask => 0 inside the domain and increase in the layer 33 | """ 34 | 35 | eqs = [Eq(damp, 1.0 if abc_type == "mask" else 0.0)] 36 | for (nbl, nbr), d in zip(padsizes, damp.dimensions): 37 | if not fs or d is not damp.dimensions[-1]: 38 | dampcoeff = 1.5 * np.log(1.0 / 0.001) / (nbl) 39 | # left 40 | dim_l = SubDimension.left(name='abc_%s_l' % d.name, parent=d, 41 | thickness=nbl) 42 | pos = Abs((nbl - (dim_l - d.symbolic_min) + 1) / float(nbl)) 43 | val = dampcoeff * (pos - sin(2*np.pi*pos)/(2*np.pi)) 44 | val = -val if abc_type == "mask" else val 45 | eqs += [Inc(damp.subs({d: dim_l}), val/d.spacing)] 46 | # right 47 | dampcoeff = 1.5 * np.log(1.0 / 0.001) / (nbr) 48 | dim_r = SubDimension.right(name='abc_%s_r' % d.name, parent=d, 49 | thickness=nbr) 50 | pos = Abs((nbr - (d.symbolic_max - dim_r) + 1) / float(nbr)) 51 | val = dampcoeff * (pos - sin(2*np.pi*pos)/(2*np.pi)) 52 | val = -val if abc_type == "mask" else val 53 | eqs += [Inc(damp.subs({d: dim_r}), val/d.spacing)] 54 | 55 | Operator(eqs, name='initdamp')() 56 | 57 | 58 | class PhysicalDomain(SubDomain): 59 | 60 | name = 'physdomain' 61 | 62 | def __init__(self, so, fs=False): 63 | super().__init__() 64 | self.so = so 65 | self.fs = fs 66 | 67 | def define(self, dimensions): 68 | map_d = {d: d for d in dimensions} 69 | if self.fs: 70 | map_d[dimensions[-1]] = ('middle', self.so, 0) 71 | return map_d 72 | 73 | 74 | class FSDomain(SubDomain): 75 | 76 | name = 'fsdomain' 77 | 78 | def __init__(self, so): 79 | super().__init__() 80 | self.size = so 81 | 82 | def define(self, dimensions): 83 | """ 84 | Definition of the upper section of the domain for wrapped indices FS. 85 | """ 86 | 87 | return {d: (d if not d == dimensions[-1] else ('left', self.size)) 88 | for d in dimensions} 89 | 90 | 91 | class GenericModel: 92 | """ 93 | General model class with common properties 94 | """ 95 | def __init__(self, origin, spacing, shape, space_order, nbl=20, 96 | dtype=np.float32, subdomains=(), bcs="damp", grid=None, 97 | fs=False, topology=None): 98 | self.shape = shape 99 | self.space_order = space_order 100 | self.nbl = int(nbl) 101 | self.origin = tuple([dtype(o) for o in origin]) 102 | self.fs = fs 103 | # Default setup 104 | origin_pml = [dtype(o - s*nbl) for o, s in zip(origin, spacing)] 105 | shape_pml = np.array(shape) + 2 * self.nbl 106 | 107 | # Model size depending on freesurface 108 | physdomain = PhysicalDomain(space_order, fs=fs) 109 | subdomains = subdomains + (physdomain,) 110 | if fs: 111 | fsdomain = FSDomain(space_order) 112 | subdomains = subdomains + (fsdomain,) 113 | origin_pml[-1] = origin[-1] 114 | shape_pml[-1] -= self.nbl 115 | 116 | # Origin of the computational domain with boundary to inject/interpolate 117 | # at the correct index 118 | if grid is None: 119 | # Physical extent is calculated per cell, so shape - 1 120 | extent = tuple(np.array(spacing) * (shape_pml - 1)) 121 | self.grid = Grid(extent=extent, shape=shape_pml, origin=origin_pml, 122 | dtype=dtype, subdomains=subdomains, topology=topology) 123 | else: 124 | self.grid = grid 125 | 126 | self._physical_parameters = set() 127 | self.damp = None 128 | self._initialize_bcs(bcs=bcs) 129 | 130 | def _initialize_bcs(self, bcs="damp"): 131 | # Create dampening field as symbol `damp` 132 | if self.nbl == 0: 133 | self.damp = 1 if bcs == "mask" else 0 134 | return 135 | 136 | # First initialization 137 | init = self.damp is None 138 | # Get current Function if already initialized 139 | self.damp = self.damp or Function(name="damp", grid=self.grid, 140 | space_order=self.space_order) 141 | if callable(bcs): 142 | bcs(self.damp, self.nbl) 143 | else: 144 | re_init = ((bcs == "mask" and mmin(self.damp) == 0) or 145 | (bcs == "damp" and mmax(self.damp) == 1)) 146 | if init or re_init: 147 | if re_init and not init: 148 | bcs_o = "damp" if bcs == "mask" else "mask" 149 | warning("Re-initializing damp profile from %s to %s" % (bcs_o, bcs)) 150 | warning("Model has to be created with `bcs=\"%s\"`" 151 | "for this WaveSolver" % bcs) 152 | initialize_damp(self.damp, self.padsizes, self.spacing, 153 | abc_type=bcs, fs=self.fs) 154 | self._physical_parameters.update(['damp']) 155 | 156 | @property 157 | def padsizes(self): 158 | """ 159 | Padding size for each dimension. 160 | """ 161 | padsizes = [(self.nbl, self.nbl) for _ in range(self.dim-1)] 162 | padsizes.append((0 if self.fs else self.nbl, self.nbl)) 163 | return padsizes 164 | 165 | def physical_params(self, **kwargs): 166 | """ 167 | Return all set physical parameters and update to input values if provided 168 | """ 169 | known = [getattr(self, i) for i in self.physical_parameters] 170 | return {i.name: kwargs.get(i.name, i) or i for i in known} 171 | 172 | def _gen_phys_param(self, field, name, space_order, is_param=True, 173 | default_value=0, avg_mode='arithmetic', **kwargs): 174 | if field is None: 175 | return default_value 176 | if isinstance(field, np.ndarray): 177 | function = Function(name=name, grid=self.grid, space_order=space_order, 178 | parameter=is_param, avg_mode=avg_mode) 179 | initialize_function(function, field, self.padsizes) 180 | else: 181 | function = Constant(name=name, value=field, dtype=self.grid.dtype) 182 | self._physical_parameters.update([name]) 183 | return function 184 | 185 | @property 186 | def physical_parameters(self): 187 | return as_tuple(self._physical_parameters) 188 | 189 | @property 190 | def dim(self): 191 | """ 192 | Spatial dimension of the problem and model domain. 193 | """ 194 | return self.grid.dim 195 | 196 | @property 197 | def spacing(self): 198 | """ 199 | Grid spacing for all fields in the physical model. 200 | """ 201 | return self.grid.spacing 202 | 203 | @property 204 | def space_dimensions(self): 205 | """ 206 | Spatial dimensions of the grid 207 | """ 208 | return self.grid.dimensions 209 | 210 | @property 211 | def spacing_map(self): 212 | """ 213 | Map between spacing symbols and their values for each `SpaceDimension`. 214 | """ 215 | return self.grid.spacing_map 216 | 217 | @property 218 | def dtype(self): 219 | """ 220 | Data type for all assocaited data objects. 221 | """ 222 | return self.grid.dtype 223 | 224 | @property 225 | def domain_size(self): 226 | """ 227 | Physical size of the domain as determined by shape and spacing 228 | """ 229 | return tuple((d-1) * s for d, s in zip(self.shape, self.spacing)) 230 | 231 | 232 | class SeismicModel(GenericModel): 233 | """ 234 | The physical model used in seismic inversion processes. 235 | 236 | Parameters 237 | ---------- 238 | origin : tuple of floats 239 | Origin of the model in m as a tuple in (x,y,z) order. 240 | spacing : tuple of floats 241 | Grid size in m as a Tuple in (x,y,z) order. 242 | shape : tuple of int 243 | Number of grid points size in (x,y,z) order. 244 | space_order : int 245 | Order of the spatial stencil discretisation. 246 | vp : array_like or float 247 | Velocity in km/s. 248 | nbl : int, optional 249 | The number of absorbin layers for boundary damping. 250 | bcs: str or callable 251 | Absorbing boundary type ("damp" or "mask") or initializer. 252 | dtype : np.float32 or np.float64 253 | Defaults to np.float32. 254 | epsilon : array_like or float, optional 255 | Thomsen epsilon parameter (0 2 and preset.lower() not in ['constant-tti-noazimuth']: 113 | phi = .35 114 | if density: 115 | kwargs['b'] = 1 116 | return SeismicModel(space_order=space_order, vp=vp, origin=origin, shape=shape, 117 | dtype=dtype, spacing=spacing, nbl=nbl, epsilon=epsilon, 118 | delta=delta, theta=theta, phi=phi, bcs="damp", **kwargs) 119 | 120 | elif preset.lower() in ['layers-isotropic']: 121 | # A n-layers model in a 2D or 3D domain with two different 122 | # velocities split across the height dimension: 123 | # By default, the top part of the domain has 1.5 km/s, 124 | # and the bottom part of the domain has 2.5 km/s. 125 | vp_top = kwargs.pop('vp_top', 1.5) 126 | vp_bottom = kwargs.pop('vp_bottom', 3.5) 127 | 128 | # Define a velocity profile in km/s 129 | v = np.empty(shape, dtype=dtype) 130 | v[:] = vp_top # Top velocity (background) 131 | vp_i = np.linspace(vp_top, vp_bottom, nlayers) 132 | for i in range(1, nlayers): 133 | v[..., i*int(shape[-1] / nlayers):] = vp_i[i] # Bottom velocity 134 | 135 | if density: 136 | kwargs['b'] = Gardners(v) 137 | 138 | return SeismicModel(space_order=space_order, vp=v, origin=origin, shape=shape, 139 | dtype=dtype, spacing=spacing, nbl=nbl, bcs="damp", 140 | fs=fs, **kwargs) 141 | 142 | elif preset.lower() in ['layers-elastic']: 143 | # A n-layers model in a 2D or 3D domain with two different 144 | # velocities split across the height dimension: 145 | # By default, the top part of the domain has 1.5 km/s, 146 | # and the bottom part of the domain has 2.5 km/s. 147 | vp_top = kwargs.pop('vp_top', 1.5) 148 | vp_bottom = kwargs.pop('vp_bottom', 3.5) 149 | 150 | # Define a velocity profile in km/s 151 | v = np.empty(shape, dtype=dtype) 152 | v[:] = vp_top # Top velocity (background) 153 | vp_i = np.linspace(vp_top, vp_bottom, nlayers) 154 | for i in range(1, nlayers): 155 | v[..., i*int(shape[-1] / nlayers):] = vp_i[i] # Bottom velocity 156 | 157 | vs = 0.5 * v[:] 158 | b = Gardners(v) 159 | vs[v < 1.51] = 0.0 160 | 161 | return SeismicModel(space_order=space_order, vp=v, vs=vs, b=b, 162 | origin=origin, shape=shape, 163 | dtype=dtype, spacing=spacing, nbl=nbl, **kwargs) 164 | 165 | elif preset.lower() in ['layers-viscoelastic', 'twolayer-viscoelastic', 166 | '2layer-viscoelastic']: 167 | # A two-layer model in a 2D or 3D domain with two different 168 | # velocities split across the height dimension: 169 | # By default, the top part of the domain has 1.6 km/s, 170 | # and the bottom part of the domain has 2.2 km/s. 171 | ratio = kwargs.pop('ratio', 3) 172 | vp_top = kwargs.pop('vp_top', 1.6) 173 | qp_top = kwargs.pop('qp_top', 40.) 174 | vs_top = kwargs.pop('vs_top', 0.4) 175 | qs_top = kwargs.pop('qs_top', 30.) 176 | b_top = kwargs.pop('b_top', 1/1.3) 177 | vp_bottom = kwargs.pop('vp_bottom', 2.2) 178 | qp_bottom = kwargs.pop('qp_bottom', 100.) 179 | vs_bottom = kwargs.pop('vs_bottom', 1.2) 180 | qs_bottom = kwargs.pop('qs_bottom', 70.) 181 | b_bottom = kwargs.pop('b_bottom', 1/2.) 182 | 183 | # Define a velocity profile in km/s 184 | vp = np.empty(shape, dtype=dtype) 185 | qp = np.empty(shape, dtype=dtype) 186 | vs = np.empty(shape, dtype=dtype) 187 | qs = np.empty(shape, dtype=dtype) 188 | b = np.empty(shape, dtype=dtype) 189 | # Top and bottom P-wave velocity 190 | vp[:] = vp_top 191 | vp[..., int(shape[-1] / ratio):] = vp_bottom 192 | # Top and bottom P-wave quality factor 193 | qp[:] = qp_top 194 | qp[..., int(shape[-1] / ratio):] = qp_bottom 195 | # Top and bottom S-wave velocity 196 | vs[:] = vs_top 197 | vs[..., int(shape[-1] / ratio):] = vs_bottom 198 | # Top and bottom S-wave quality factor 199 | qs[:] = qs_top 200 | qs[..., int(shape[-1] / ratio):] = qs_bottom 201 | # Top and bottom density 202 | b[:] = b_top 203 | b[..., int(shape[-1] / ratio):] = b_bottom 204 | 205 | return SeismicModel(space_order=space_order, vp=vp, qp=qp, 206 | vs=vs, qs=qs, b=b, origin=origin, 207 | shape=shape, dtype=dtype, spacing=spacing, 208 | nbl=nbl, **kwargs) 209 | 210 | elif preset.lower() in ['layers-tti', 'layers-tti-noazimuth']: 211 | # A n-layers model in a 2D or 3D domain with two different 212 | # velocities split across the height dimension: 213 | # By default, the top part of the domain has 1.5 km/s, 214 | # and the bottom part of the domain has 2.5 km/s.\ 215 | vp_top = kwargs.pop('vp_top', 1.5) 216 | vp_bottom = kwargs.pop('vp_bottom', 3.5) 217 | 218 | # Define a velocity profile in km/s 219 | v = np.empty(shape, dtype=dtype) 220 | v[:] = vp_top # Top velocity (background) 221 | vp_i = np.linspace(vp_top, vp_bottom, nlayers) 222 | for i in range(1, nlayers): 223 | v[..., i*int(shape[-1] / nlayers):] = vp_i[i] # Bottom velocity 224 | 225 | epsilon = .1*(v - vp_top) 226 | delta = .05*(v - vp_top) 227 | theta = .5*(v - vp_top) 228 | phi = None 229 | if len(shape) > 2 and preset.lower() not in ['layers-tti-noazimuth']: 230 | phi = .25*(v - vp_top) 231 | 232 | if density: 233 | kwargs['b'] = Gardners(v) 234 | 235 | model = SeismicModel(space_order=space_order, vp=v, origin=origin, shape=shape, 236 | dtype=dtype, spacing=spacing, nbl=nbl, epsilon=epsilon, 237 | delta=delta, theta=theta, phi=phi, bcs="damp", 238 | fs=fs, **kwargs) 239 | 240 | if kwargs.get('smooth', False): 241 | if len(shape) > 2 and preset.lower() not in ['layers-tti-noazimuth']: 242 | model.smooth(('epsilon', 'delta', 'theta', 'phi')) 243 | else: 244 | model.smooth(('epsilon', 'delta', 'theta')) 245 | 246 | return model 247 | 248 | elif preset.lower() in ['circle-isotropic']: 249 | # A simple circle in a 2D domain with a background velocity. 250 | # By default, the circle velocity is 2.5 km/s, 251 | # and the background veloity is 3.0 km/s. 252 | vp = kwargs.pop('vp_circle', 3.0) 253 | vp_background = kwargs.pop('vp_background', 2.5) 254 | r = kwargs.pop('r', 15) 255 | 256 | # Only a 2D preset is available currently 257 | assert(len(shape) == 2) 258 | 259 | v = np.empty(shape, dtype=dtype) 260 | v[:] = vp_background 261 | 262 | a, b = shape[0] / 2, shape[1] / 2 263 | y, x = np.ogrid[-a:shape[0]-a, -b:shape[1]-b] 264 | v[x*x + y*y <= r*r] = vp 265 | 266 | if density: 267 | kwargs['b'] = Gardners(v) 268 | 269 | return SeismicModel(space_order=space_order, vp=v, origin=origin, shape=shape, 270 | dtype=dtype, spacing=spacing, nbl=nbl, bcs="damp", 271 | fs=fs, **kwargs) 272 | 273 | elif preset.lower() in ['marmousi-isotropic', 'marmousi2d-isotropic']: 274 | shape = (1601, 401) 275 | spacing = (7.5, 7.5) 276 | origin = (0., 0.) 277 | nbl = kwargs.pop('nbl', 20) 278 | 279 | # Read 2D Marmousi model from devitocodes/data repo 280 | data_path = kwargs.get('data_path', None) 281 | if data_path is None: 282 | raise ValueError("Path to devitocodes/data not found! Please specify with " 283 | "'data_path='") 284 | path = os.path.join(data_path, 'Simple2D/vp_marmousi_bi') 285 | v = np.fromfile(path, dtype='float32', sep="") 286 | v = v.reshape(shape) 287 | 288 | # Cut the model to make it slightly cheaper 289 | v = v[301:-300, :] 290 | 291 | return SeismicModel(space_order=space_order, vp=v, origin=origin, shape=v.shape, 292 | dtype=np.float32, spacing=spacing, nbl=nbl, bcs="damp", 293 | fs=fs, **kwargs) 294 | 295 | elif preset.lower() in ['marmousi-tti2d', 'marmousi2d-tti', 296 | 'marmousi-tti3d', 'marmousi3d-tti']: 297 | 298 | shape = (201, 201, 70) 299 | nbl = kwargs.pop('nbl', 20) 300 | 301 | # Read 2D Marmousi model from devitocodes/data repo 302 | data_path = kwargs.pop('data_path', None) 303 | if data_path is None: 304 | raise ValueError("Path to devitocodes/data not found! Please specify with " 305 | "'data_path='") 306 | path = os.path.join(data_path, 'marmousi3D/vp_marmousi_bi') 307 | 308 | # velocity 309 | vp = 1e-3 * np.fromfile(os.path.join(data_path, 'marmousi3D/MarmousiVP.raw'), 310 | dtype='float32', sep="") 311 | vp = vp.reshape(shape) 312 | 313 | # Epsilon, in % in file, resale between 0 and 1 314 | epsilon = np.fromfile(os.path.join(data_path, 'marmousi3D/MarmousiEps.raw'), 315 | dtype='float32', sep="") * 1e-2 316 | epsilon = epsilon.reshape(shape) 317 | 318 | # Delta, in % in file, resale between 0 and 1 319 | delta = np.fromfile(os.path.join(data_path, 'marmousi3D/MarmousiDelta.raw'), 320 | dtype='float32', sep="") * 1e-2 321 | delta = delta.reshape(shape) 322 | 323 | # Theta, in degrees in file, resale in radian 324 | theta = np.fromfile(os.path.join(data_path, 'marmousi3D/MarmousiTilt.raw'), 325 | dtype='float32', sep="") 326 | theta = np.float32(np.pi / 180 * theta.reshape(shape)) 327 | 328 | if preset.lower() in ['marmousi-tti3d', 'marmousi3d-tti']: 329 | # Phi, in degrees in file, resale in radian 330 | phi = np.fromfile(os.path.join(data_path, 'marmousi3D/Azimuth.raw'), 331 | dtype='float32', sep="") 332 | phi = np.float32(np.pi / 180 * phi.reshape(shape)) 333 | else: 334 | vp = vp[101, :, :] 335 | epsilon = epsilon[101, :, :] 336 | delta = delta[101, :, :] 337 | theta = theta[101, :, :] 338 | shape = vp.shape 339 | phi = None 340 | 341 | spacing = tuple([10.0]*len(shape)) 342 | origin = tuple([0.0]*len(shape)) 343 | 344 | return SeismicModel(space_order=space_order, vp=vp, origin=origin, shape=shape, 345 | dtype=np.float32, spacing=spacing, nbl=nbl, epsilon=epsilon, 346 | delta=delta, theta=theta, phi=phi, bcs="damp", **kwargs) 347 | 348 | elif preset.lower() in ['layers-viscoacoustic']: 349 | # A n-layers model in a 2D or 3D domain with two different 350 | # velocities split across the height dimension: 351 | # By default, the top part of the domain has 1.5 km/s, 352 | # and the bottom part of the domain has 3.5 km/s. 353 | 354 | # Define a velocity profile in km/s 355 | vp = np.empty(shape, dtype=dtype) 356 | qp = np.empty(shape, dtype=dtype) 357 | 358 | # Top and bottom P-wave velocity 359 | vp_top = kwargs.pop('vp_top', 1.5) 360 | vp_bottom = kwargs.pop('vp_bottom', 3.5) 361 | 362 | # Define a velocity profile in km/s 363 | vp = np.empty(shape, dtype=dtype) 364 | vp[:] = vp_top # Top velocity (background) 365 | vp_i = np.linspace(vp_top, vp_bottom, nlayers) 366 | for i in range(1, nlayers): 367 | vp[..., i*int(shape[-1] / nlayers):] = vp_i[i] # Bottom velocity 368 | 369 | qp[:] = 3.516*((vp[:]*1000.)**2.2)*10**(-6) # Li's empirical formula 370 | 371 | b = Gardners(vp, normalize=False) 372 | 373 | return SeismicModel(space_order=space_order, vp=vp, qp=qp, b=b, nbl=nbl, 374 | dtype=dtype, origin=origin, shape=shape, 375 | spacing=spacing, **kwargs) 376 | 377 | else: 378 | raise ValueError("Unknown model preset name") 379 | -------------------------------------------------------------------------------- /seismic/source.py: -------------------------------------------------------------------------------- 1 | from functools import cached_property 2 | from scipy import interpolate 3 | import numpy as np 4 | try: 5 | import matplotlib.pyplot as plt 6 | except: 7 | plt = None 8 | 9 | from devito.types import SparseTimeFunction 10 | 11 | __all__ = ['PointSource', 'Receiver', 'Shot', 'WaveletSource', 12 | 'RickerSource', 'GaborSource', 'DGaussSource', 'TimeAxis'] 13 | 14 | 15 | class TimeAxis: 16 | """ 17 | Data object to store the TimeAxis. Exactly three of the four key arguments 18 | must be prescribed. Because of remainder values, it is not possible to create 19 | a TimeAxis that exactly adheres to the inputs; therefore, start, stop, step 20 | and num values should be taken from the TimeAxis object rather than relying 21 | upon the input values. 22 | 23 | The four possible cases are: 24 | start is None: start = step*(1 - num) + stop 25 | step is None: step = (stop - start)/(num - 1) 26 | num is None: num = ceil((stop - start + step)/step); 27 | because of remainder stop = step*(num - 1) + start 28 | stop is None: stop = step*(num - 1) + start 29 | 30 | Parameters 31 | ---------- 32 | start : float, optional 33 | Start of time axis. 34 | step : float, optional 35 | Time interval. 36 | num : int, optional 37 | Number of values (Note: this is the number of intervals + 1). 38 | Stop value is reset to correct for remainder. 39 | stop : float, optional 40 | End time. 41 | """ 42 | def __init__(self, start=None, step=None, num=None, stop=None): 43 | try: 44 | if start is None: 45 | start = step*(1 - num) + stop 46 | elif step is None: 47 | step = (stop - start)/(num - 1) 48 | elif num is None: 49 | num = int(np.ceil((stop - start + step)/step)) 50 | stop = step*(num - 1) + start 51 | elif stop is None: 52 | stop = step*(num - 1) + start 53 | else: 54 | raise ValueError("Only three of start, step, num and stop may be set") 55 | except: 56 | raise ValueError("Three of args start, step, num and stop may be set") 57 | 58 | if not isinstance(num, int): 59 | raise TypeError("input argument must be of type int") 60 | 61 | self.start = float(start) 62 | self.stop = float(stop) 63 | self.step = float(step) 64 | self.num = int(num) 65 | 66 | def __str__(self): 67 | return "TimeAxis: start=%g, stop=%g, step=%g, num=%g" % \ 68 | (self.start, self.stop, self.step, self.num) 69 | 70 | def _rebuild(self): 71 | return TimeAxis(start=self.start, stop=self.stop, num=self.num) 72 | 73 | @cached_property 74 | def time_values(self): 75 | return np.linspace(self.start, self.stop, self.num) 76 | 77 | 78 | class PointSource(SparseTimeFunction): 79 | """Symbolic data object for a set of sparse point sources 80 | 81 | Parameters 82 | ---------- 83 | name : str 84 | Name of the symbol representing this source. 85 | grid : Grid 86 | The computational domain. 87 | time_range : TimeAxis 88 | TimeAxis(start, step, num) object. 89 | npoint : int, optional 90 | Number of sparse points represented by this source. 91 | data : ndarray, optional 92 | Data values to initialise point data. 93 | coordinates : ndarray, optional 94 | Point coordinates for this source. 95 | space_order : int, optional 96 | Space discretization order. 97 | time_order : int, optional 98 | Time discretization order (defaults to 2). 99 | dtype : data-type, optional 100 | Data type of the buffered data. 101 | dimension : Dimension, optional 102 | Represents the number of points in this source. 103 | """ 104 | 105 | __rkwargs__ = list(SparseTimeFunction.__rkwargs__) + ['time_range'] 106 | __rkwargs__.remove('nt') # `nt` is inferred from `time_range` 107 | 108 | @classmethod 109 | def __args_setup__(cls, *args, **kwargs): 110 | kwargs['nt'] = kwargs['time_range'].num 111 | 112 | # Either `npoint` or `coordinates` must be provided 113 | npoint = kwargs.get('npoint', kwargs.get('npoint_global')) 114 | if npoint is None: 115 | coordinates = kwargs.get('coordinates', kwargs.get('coordinates_data')) 116 | if coordinates is None: 117 | raise TypeError("Need either `npoint` or `coordinates`") 118 | kwargs['npoint'] = coordinates.shape[0] 119 | 120 | return args, kwargs 121 | 122 | def __init_finalize__(self, *args, **kwargs): 123 | time_range = kwargs.pop('time_range') 124 | data = kwargs.pop('data', None) 125 | 126 | kwargs.setdefault('time_order', 2) 127 | super().__init_finalize__(*args, **kwargs) 128 | 129 | self._time_range = time_range._rebuild() 130 | 131 | # If provided, copy initial data into the allocated buffer 132 | if data is not None: 133 | self.data[:] = data 134 | 135 | @cached_property 136 | def time_values(self): 137 | return self._time_range.time_values 138 | 139 | @property 140 | def time_range(self): 141 | return self._time_range 142 | 143 | def resample(self, dt=None, num=None, rtol=1e-5, order=3): 144 | # Only one of dt or num may be set. 145 | if dt is None: 146 | assert num is not None 147 | else: 148 | assert num is None 149 | 150 | start, stop = self._time_range.start, self._time_range.stop 151 | dt0 = self._time_range.step 152 | 153 | if dt is None: 154 | new_time_range = TimeAxis(start=start, stop=stop, num=num) 155 | dt = new_time_range.step 156 | else: 157 | new_time_range = TimeAxis(start=start, stop=stop, step=dt) 158 | 159 | if np.isclose(dt, dt0): 160 | return self 161 | 162 | nsamples, ntraces = self.data.shape 163 | 164 | new_traces = np.zeros((new_time_range.num, ntraces)) 165 | 166 | for i in range(ntraces): 167 | tck = interpolate.splrep(self._time_range.time_values, 168 | self.data[:, i], k=order) 169 | new_traces[:, i] = interpolate.splev(new_time_range.time_values, tck) 170 | 171 | # Return new object 172 | return PointSource(name=self.name, grid=self.grid, data=new_traces, 173 | time_range=new_time_range, coordinates=self.coordinates.data) 174 | 175 | 176 | Receiver = PointSource 177 | Shot = PointSource 178 | 179 | 180 | class WaveletSource(PointSource): 181 | 182 | """ 183 | Abstract base class for symbolic objects that encapsulates a set of 184 | sources with a pre-defined source signal wavelet. 185 | 186 | Parameters 187 | ---------- 188 | name : str 189 | Name for the resulting symbol. 190 | grid : Grid 191 | The computational domain. 192 | f0 : float 193 | Peak frequency for Ricker wavelet in kHz. 194 | time_values : TimeAxis 195 | Discretized values of time in ms. 196 | a : float, optional 197 | Amplitude of the wavelet (defaults to 1). 198 | t0 : float, optional 199 | Firing time (defaults to 1 / f0) 200 | """ 201 | 202 | __rkwargs__ = PointSource.__rkwargs__ + ['f0', 'a', 't0'] 203 | 204 | @classmethod 205 | def __args_setup__(cls, *args, **kwargs): 206 | kwargs.setdefault('npoint', 1) 207 | 208 | return super().__args_setup__(*args, **kwargs) 209 | 210 | def __init_finalize__(self, *args, **kwargs): 211 | super().__init_finalize__(*args, **kwargs) 212 | 213 | self.f0 = kwargs.get('f0') 214 | self.a = kwargs.get('a') 215 | self.t0 = kwargs.get('t0') 216 | 217 | if not self.alias: 218 | for p in range(kwargs['npoint']): 219 | self.data[:, p] = self.wavelet 220 | 221 | @property 222 | def wavelet(self): 223 | """ 224 | Return a wavelet with a peak frequency ``f0`` at time ``t0``. 225 | """ 226 | raise NotImplementedError('Wavelet not defined') 227 | 228 | def show(self, idx=0, wavelet=None): 229 | """ 230 | Plot the wavelet of the specified source. 231 | 232 | Parameters 233 | ---------- 234 | idx : int 235 | Index of the source point for which to plot wavelet. 236 | wavelet : ndarray or callable 237 | Prescribed wavelet instead of one from this symbol. 238 | """ 239 | wavelet = wavelet or self.data[:, idx] 240 | plt.figure() 241 | plt.plot(self.time_values, wavelet) 242 | plt.xlabel('Time (ms)') 243 | plt.ylabel('Amplitude') 244 | plt.tick_params() 245 | plt.show() 246 | 247 | 248 | class RickerSource(WaveletSource): 249 | 250 | """ 251 | Symbolic object that encapsulates a set of sources with a 252 | pre-defined Ricker wavelet: 253 | 254 | http://subsurfwiki.org/wiki/Ricker_wavelet 255 | 256 | Parameters 257 | ---------- 258 | name : str 259 | Name for the resulting symbol. 260 | grid : Grid 261 | The computational domain. 262 | f0 : float 263 | Peak frequency for Ricker wavelet in kHz. 264 | time : TimeAxis 265 | Discretized values of time in ms. 266 | 267 | Returns 268 | ---------- 269 | A Ricker wavelet. 270 | """ 271 | 272 | @property 273 | def wavelet(self): 274 | t0 = self.t0 or 1 / self.f0 275 | a = self.a or 1 276 | r = (np.pi * self.f0 * (self.time_values - t0)) 277 | return a * (1-2.*r**2)*np.exp(-r**2) 278 | 279 | 280 | class GaborSource(WaveletSource): 281 | 282 | """ 283 | Symbolic object that encapsulates a set of sources with a 284 | pre-defined Gabor wavelet: 285 | 286 | https://en.wikipedia.org/wiki/Gabor_wavelet 287 | 288 | Parameters 289 | ---------- 290 | name : str 291 | Name for the resulting symbol. 292 | grid : Grid 293 | defining the computational domain. 294 | f0 : float 295 | Peak frequency for Ricker wavelet in kHz. 296 | time : TimeAxis 297 | Discretized values of time in ms. 298 | 299 | Returns 300 | ------- 301 | A Gabor wavelet. 302 | """ 303 | 304 | @property 305 | def wavelet(self): 306 | agauss = 0.5 * self.f0 307 | tcut = self.t0 or 1.5 / agauss 308 | s = (self.time_values - tcut) * agauss 309 | a = self.a or 1 310 | return a * np.exp(-2*s**2) * np.cos(2 * np.pi * s) 311 | 312 | 313 | class DGaussSource(WaveletSource): 314 | 315 | """ 316 | Symbolic object that encapsulates a set of sources with a 317 | pre-defined 1st derivative wavelet of a Gaussian Source. 318 | 319 | Notes 320 | ----- 321 | For visualizing the second or third order derivative 322 | of Gaussian wavelets, the convention is to use the 323 | negative of the normalized derivative. In the case 324 | of the second derivative, scaling by -1 produces a 325 | wavelet with its main lobe in the positive y direction. 326 | This scaling also makes the Gaussian wavelet resemble 327 | the Mexican hat, or Ricker, wavelet. The validity of 328 | the wavelet is not affected by the -1 scaling factor. 329 | 330 | Parameters 331 | ---------- 332 | name : str 333 | Name for the resulting symbol. 334 | grid : Grid 335 | The computational domain. 336 | f0 : float 337 | Peak frequency for wavelet in kHz. 338 | time : TimeAxis 339 | Discretized values of time in ms. 340 | 341 | Returns 342 | ------- 343 | The 1st order derivative of the Gaussian wavelet. 344 | """ 345 | 346 | @property 347 | def wavelet(self): 348 | t0 = self.t0 or 1 / self.f0 349 | a = self.a or 1 350 | time = (self.time_values - t0) 351 | return -2 * a * time * np.exp(- a * time**2) 352 | -------------------------------------------------------------------------------- /seismic/tti/__init__.py: -------------------------------------------------------------------------------- 1 | from .operators import * # noqa 2 | from .wavesolver import * # noqa 3 | from .tti_example import * # noqa 4 | -------------------------------------------------------------------------------- /seismic/tti/operators.py: -------------------------------------------------------------------------------- 1 | from devito import (Eq, Operator, Function, TimeFunction, NODE, Inc, solve, 2 | cos, sin, sqrt, div, grad) 3 | from examples.seismic.acoustic.operators import freesurface 4 | 5 | 6 | def second_order_stencil(model, u, v, H0, Hz, qu, qv, forward=True): 7 | """ 8 | Creates the stencil corresponding to the second order TTI wave equation 9 | m * u.dt2 = (epsilon * H0 + delta * Hz) - damp * u.dt 10 | m * v.dt2 = (delta * H0 + Hz) - damp * v.dt 11 | """ 12 | m, damp = model.m, model.damp 13 | 14 | unext = u.forward if forward else u.backward 15 | vnext = v.forward if forward else v.backward 16 | udt = u.dt if forward else u.dt.T 17 | vdt = v.dt if forward else v.dt.T 18 | 19 | # Stencils 20 | stencilp = solve(m * u.dt2 - H0 - qu + damp * udt, unext) 21 | stencilr = solve(m * v.dt2 - Hz - qv + damp * vdt, vnext) 22 | 23 | first_stencil = Eq(unext, stencilp, subdomain=model.grid.subdomains['physdomain']) 24 | second_stencil = Eq(vnext, stencilr, subdomain=model.grid.subdomains['physdomain']) 25 | 26 | stencils = [first_stencil, second_stencil] 27 | 28 | # Add free surface 29 | if model.fs: 30 | stencils.append(freesurface(model, Eq(unext, stencilp))) 31 | stencils.append(freesurface(model, Eq(vnext, stencilr))) 32 | 33 | return stencils 34 | 35 | 36 | def trig_func(model): 37 | """ 38 | Trigonometric function of the tilt and azymuth angles. 39 | """ 40 | try: 41 | theta = model.theta 42 | except AttributeError: 43 | theta = 0 44 | 45 | costheta = cos(theta) 46 | sintheta = sin(theta) 47 | if model.dim == 3: 48 | try: 49 | phi = model.phi 50 | except AttributeError: 51 | phi = 0 52 | 53 | cosphi = cos(phi) 54 | sinphi = sin(phi) 55 | return costheta, sintheta, cosphi, sinphi 56 | return costheta, sintheta 57 | 58 | 59 | def Gzz_centered(model, field): 60 | """ 61 | 3D rotated second order derivative in the direction z. 62 | 63 | Parameters 64 | ---------- 65 | model : Model 66 | Physical parameters model structure. 67 | field : Function 68 | Input for which the derivative is computed. 69 | 70 | Returns 71 | ------- 72 | Rotated second order derivative w.r.t. z. 73 | """ 74 | b = getattr(model, 'b', 1) 75 | costheta, sintheta, cosphi, sinphi = trig_func(model) 76 | 77 | order1 = field.space_order // 2 78 | x, y, z = field.grid.dimensions 79 | dx, dy, dz = x.spacing/2, y.spacing/2, z.spacing/2 80 | 81 | Gz = (sintheta * cosphi * field.dx(fd_order=order1, x0=x+dx) + 82 | sintheta * sinphi * field.dy(fd_order=order1, x0=y+dy) + 83 | costheta * field.dz(fd_order=order1, x0=z+dz)) 84 | 85 | Gzz = (b * Gz * costheta).dz(fd_order=order1, x0=z-dz) 86 | # Add rotated derivative if angles are not zero. If angles are 87 | # zeros then `0*Gz = 0` and doesn't have any `.dy` .... 88 | if sintheta != 0: 89 | Gzz += (b * Gz * sintheta * cosphi).dx(fd_order=order1, x0=x-dx) 90 | if sinphi != 0: 91 | Gzz += (b * Gz * sintheta * sinphi).dy(fd_order=order1, x0=y-dy) 92 | 93 | return Gzz 94 | 95 | 96 | def Gzz_centered_2d(model, field): 97 | """ 98 | 2D rotated second order derivative in the direction z. 99 | 100 | Parameters 101 | ---------- 102 | model : Model 103 | Physical parameters model structure. 104 | field : Function 105 | Input for which the derivative is computed. 106 | 107 | Returns 108 | ------- 109 | Rotated second order derivative w.r.t. z. 110 | """ 111 | b = getattr(model, 'b', 1) 112 | costheta, sintheta = trig_func(model) 113 | 114 | order1 = field.space_order // 2 115 | x, y = field.grid.dimensions 116 | dx, dy = x.spacing/2, y.spacing/2 117 | 118 | Gz = (sintheta * field.dx(fd_order=order1, x0=x+dx) + 119 | costheta * field.dy(fd_order=order1, x0=y+dy)) 120 | Gzz = (b * Gz * costheta).dy(fd_order=order1, x0=y-dy) 121 | 122 | # Add rotated derivative if angles are not zero. If angles are 123 | # zeros then `0*Gz = 0` and doesn't have any `.dy` .... 124 | if sintheta != 0: 125 | Gzz += (b * Gz * sintheta).dx(fd_order=order1, x0=x-dx) 126 | return Gzz 127 | 128 | 129 | # Centered case produces directly Gxx + Gyy 130 | def Gh_centered(model, field): 131 | """ 132 | Sum of the 3D rotated second order derivative in the direction x and y. 133 | As the Laplacian is rotation invariant, it is computed as the conventional 134 | Laplacian minus the second order rotated second order derivative in the direction z 135 | Gxx + Gyy = field.laplace - Gzz 136 | 137 | Parameters 138 | ---------- 139 | model : Model 140 | Physical parameters model structure. 141 | field : Function 142 | Input field. 143 | 144 | Returns 145 | ------- 146 | Sum of the 3D rotated second order derivative in the direction x and y. 147 | """ 148 | if model.dim == 3: 149 | Gzz = Gzz_centered(model, field) 150 | else: 151 | Gzz = Gzz_centered_2d(model, field) 152 | b = getattr(model, 'b', None) 153 | if b is not None: 154 | so = field.space_order // 2 155 | lap = div(b * grad(field, shift=.5, order=so), shift=-.5, order=so) 156 | else: 157 | lap = field.laplace 158 | return lap - Gzz 159 | 160 | 161 | def kernel_centered(model, u, v, **kwargs): 162 | """ 163 | TTI finite difference kernel. The equation solved is: 164 | 165 | u.dt2 = H0 166 | v.dt2 = Hz 167 | 168 | where H0 and Hz are defined as: 169 | H0 = (1+2 *epsilon) (Gxx(u)+Gyy(u)) + sqrt(1+ 2*delta) Gzz(v) 170 | Hz = sqrt(1+ 2*delta) (Gxx(u)+Gyy(u)) + Gzz(v) 171 | 172 | and 173 | 174 | H0 = (Gxx+Gyy)((1+2 *epsilon)*u + sqrt(1+ 2*delta)*v) 175 | Hz = Gzz(sqrt(1+ 2*delta)*u + v) 176 | 177 | for the forward and adjoint cases, respectively. Epsilon and delta are the Thomsen 178 | parameters. This function computes H0 and Hz. 179 | 180 | References: 181 | * Zhang, Yu, Houzhu Zhang, and Guanquan Zhang. "A stable TTI reverse 182 | time migration and its implementation." Geophysics 76.3 (2011): WA3-WA11. 183 | * Louboutin, Mathias, Philipp Witte, and Felix J. Herrmann. "Effects of 184 | wrong adjoints for RTM in TTI media." SEG Technical Program Expanded 185 | Abstracts 2018. Society of Exploration Geophysicists, 2018. 331-335. 186 | 187 | Parameters 188 | ---------- 189 | u : TimeFunction 190 | First TTI field. 191 | v : TimeFunction 192 | Second TTI field. 193 | space_order : int 194 | Space discretization order. 195 | 196 | Returns 197 | ------- 198 | u and v component of the rotated Laplacian in 2D. 199 | """ 200 | # Forward or backward 201 | forward = kwargs.get('forward', True) 202 | 203 | delta, epsilon = model.delta, model.epsilon 204 | epsilon = 1 + 2*epsilon 205 | delta = sqrt(1 + 2*delta) 206 | 207 | # Get source 208 | qu = kwargs.get('qu', 0) 209 | qv = kwargs.get('qv', 0) 210 | 211 | Gzz = Gzz_centered_2d if model.dim == 2 else Gzz_centered 212 | 213 | if forward: 214 | Gxx = Gh_centered(model, u) 215 | Gzz = Gzz(model, v) 216 | H0 = epsilon*Gxx + delta*Gzz 217 | Hz = delta*Gxx + Gzz 218 | return second_order_stencil(model, u, v, H0, Hz, qu, qv) 219 | else: 220 | H0 = Gh_centered(model, (epsilon*u + delta*v)) 221 | Hz = Gzz(model, (delta*u + v)) 222 | return second_order_stencil(model, u, v, H0, Hz, qu, qv, forward=forward) 223 | 224 | 225 | def particle_velocity_fields(model, space_order): 226 | """ 227 | Initialize particle velocity fields for staggered TTI. 228 | """ 229 | if model.grid.dim == 2: 230 | x, z = model.space_dimensions 231 | stagg_x = x 232 | stagg_z = z 233 | # Create symbols for forward wavefield, source and receivers 234 | vx = TimeFunction(name='vx', grid=model.grid, staggered=stagg_x, 235 | time_order=1, space_order=space_order) 236 | vz = TimeFunction(name='vz', grid=model.grid, staggered=stagg_z, 237 | time_order=1, space_order=space_order) 238 | vy = None 239 | elif model.grid.dim == 3: 240 | x, y, z = model.space_dimensions 241 | stagg_x = x 242 | stagg_y = y 243 | stagg_z = z 244 | # Create symbols for forward wavefield, source and receivers 245 | vx = TimeFunction(name='vx', grid=model.grid, staggered=stagg_x, 246 | time_order=1, space_order=space_order) 247 | vy = TimeFunction(name='vy', grid=model.grid, staggered=stagg_y, 248 | time_order=1, space_order=space_order) 249 | vz = TimeFunction(name='vz', grid=model.grid, staggered=stagg_z, 250 | time_order=1, space_order=space_order) 251 | 252 | return vx, vz, vy 253 | 254 | 255 | def kernel_staggered_2d(model, u, v, **kwargs): 256 | """ 257 | TTI finite difference. The equation solved is: 258 | 259 | vx.dt = - u.dx 260 | vz.dt = - v.dx 261 | m * v.dt = - sqrt(1 + 2 delta) vx.dx - vz.dz + Fh 262 | m * u.dt = - (1 + 2 epsilon) vx.dx - sqrt(1 + 2 delta) vz.dz + Fv 263 | """ 264 | space_order = u.space_order 265 | # Forward or backward 266 | forward = kwargs.get('forward', True) 267 | 268 | dampl = 1 - model.damp 269 | m, epsilon, delta = model.m, model.epsilon, model.delta 270 | costheta, sintheta = trig_func(model) 271 | epsilon = 1 + 2 * epsilon 272 | delta = sqrt(1 + 2 * delta) 273 | s = model.grid.stepping_dim.spacing 274 | x, z = model.grid.dimensions 275 | 276 | # Get source 277 | qu = kwargs.get('qu', 0) 278 | qv = kwargs.get('qv', 0) 279 | 280 | # Staggered setup 281 | vx, vz, _ = particle_velocity_fields(model, space_order) 282 | 283 | if forward: 284 | # Stencils 285 | phdx = costheta * u.dx - sintheta * u.dyc 286 | u_vx = Eq(vx.forward, dampl * vx - dampl * s * phdx) 287 | 288 | pvdz = sintheta * v.dxc + costheta * v.dy 289 | u_vz = Eq(vz.forward, dampl * vz - dampl * s * pvdz) 290 | 291 | dvx = costheta * vx.forward.dx - sintheta * vx.forward.dyc 292 | dvz = sintheta * vz.forward.dxc + costheta * vz.forward.dy 293 | 294 | # u and v equations 295 | pv_eq = Eq(v.forward, dampl * (v - s / m * (delta * dvx + dvz)) + s / m * qv) 296 | ph_eq = Eq(u.forward, dampl * (u - s / m * (epsilon * dvx + delta * dvz)) + 297 | s / m * qu) 298 | else: 299 | # Stencils 300 | phdx = ((costheta*epsilon*u).dx - (sintheta*epsilon*u).dyc + 301 | (costheta*delta*v).dx - (sintheta*delta*v).dyc) 302 | u_vx = Eq(vx.backward, dampl * vx + dampl * s * phdx) 303 | 304 | pvdz = ((sintheta*delta*u).dxc + (costheta*delta*u).dy + 305 | (sintheta*v).dxc + (costheta*v).dy) 306 | u_vz = Eq(vz.backward, dampl * vz + dampl * s * pvdz) 307 | 308 | dvx = (costheta * vx.backward).dx - (sintheta * vx.backward).dyc 309 | dvz = (sintheta * vz.backward).dxc + (costheta * vz.backward).dy 310 | 311 | # u and v equations 312 | pv_eq = Eq(v.backward, dampl * (v + s / m * dvz)) 313 | ph_eq = Eq(u.backward, dampl * (u + s / m * dvx)) 314 | 315 | return [u_vx, u_vz] + [pv_eq, ph_eq] 316 | 317 | 318 | def kernel_staggered_3d(model, u, v, **kwargs): 319 | """ 320 | TTI finite difference. The equation solved is: 321 | 322 | vx.dt = - u.dx 323 | vy.dt = - u.dx 324 | vz.dt = - v.dx 325 | m * v.dt = - sqrt(1 + 2 delta) (vx.dx + vy.dy) - vz.dz + Fh 326 | m * u.dt = - (1 + 2 epsilon) (vx.dx + vy.dy) - sqrt(1 + 2 delta) vz.dz + Fv 327 | """ 328 | space_order = u.space_order 329 | # Forward or backward 330 | forward = kwargs.get('forward', True) 331 | 332 | dampl = 1 - model.damp 333 | m, epsilon, delta = model.m, model.epsilon, model.delta 334 | costheta, sintheta, cosphi, sinphi = trig_func(model) 335 | epsilon = 1 + 2 * epsilon 336 | delta = sqrt(1 + 2 * delta) 337 | s = model.grid.stepping_dim.spacing 338 | x, y, z = model.grid.dimensions 339 | 340 | # Get source 341 | qu = kwargs.get('qu', 0) 342 | qv = kwargs.get('qv', 0) 343 | 344 | # Staggered setup 345 | vx, vz, vy = particle_velocity_fields(model, space_order) 346 | 347 | if forward: 348 | # Stencils 349 | phdx = (costheta * cosphi * u.dx + 350 | costheta * sinphi * u.dyc - 351 | sintheta * u.dzc) 352 | u_vx = Eq(vx.forward, dampl * vx - dampl * s * phdx) 353 | 354 | phdy = -sinphi * u.dxc + cosphi * u.dy 355 | u_vy = Eq(vy.forward, dampl * vy - dampl * s * phdy) 356 | 357 | pvdz = (sintheta * cosphi * v.dxc + 358 | sintheta * sinphi * v.dyc + 359 | costheta * v.dz) 360 | u_vz = Eq(vz.forward, dampl * vz - dampl * s * pvdz) 361 | 362 | dvx = (costheta * cosphi * vx.forward.dx + 363 | costheta * sinphi * vx.forward.dyc - 364 | sintheta * vx.forward.dzc) 365 | dvy = -sinphi * vy.forward.dxc + cosphi * vy.forward.dy 366 | dvz = (sintheta * cosphi * vz.forward.dxc + 367 | sintheta * sinphi * vz.forward.dyc + 368 | costheta * vz.forward.dz) 369 | # u and v equations 370 | pv_eq = Eq(v.forward, dampl * (v - s / m * (delta * (dvx + dvy) + dvz)) + 371 | s / m * qv) 372 | 373 | ph_eq = Eq(u.forward, dampl * (u - s / m * (epsilon * (dvx + dvy) + 374 | delta * dvz)) + s / m * qu) 375 | else: 376 | # Stencils 377 | phdx = ((costheta * cosphi * epsilon*u).dx + 378 | (costheta * sinphi * epsilon*u).dyc - 379 | (sintheta * epsilon*u).dzc + (costheta * cosphi * delta*v).dx + 380 | (costheta * sinphi * delta*v).dyc - 381 | (sintheta * delta*v).dzc) 382 | u_vx = Eq(vx.backward, dampl * vx + dampl * s * phdx) 383 | 384 | phdy = (-(sinphi * epsilon*u).dxc + (cosphi * epsilon*u).dy - 385 | (sinphi * delta*v).dxc + (cosphi * delta*v).dy) 386 | u_vy = Eq(vy.backward, dampl * vy + dampl * s * phdy) 387 | 388 | pvdz = ((sintheta * cosphi * delta*u).dxc + 389 | (sintheta * sinphi * delta*u).dyc + 390 | (costheta * delta*u).dz + (sintheta * cosphi * v).dxc + 391 | (sintheta * sinphi * v).dyc + 392 | (costheta * v).dz) 393 | u_vz = Eq(vz.backward, dampl * vz + dampl * s * pvdz) 394 | 395 | dvx = ((costheta * cosphi * vx.backward).dx + 396 | (costheta * sinphi * vx.backward).dyc - 397 | (sintheta * vx.backward).dzc) 398 | dvy = (-sinphi * vy.backward).dxc + (cosphi * vy.backward).dy 399 | dvz = ((sintheta * cosphi * vz.backward).dxc + 400 | (sintheta * sinphi * vz.backward).dyc + 401 | (costheta * vz.backward).dz) 402 | # u and v equations 403 | pv_eq = Eq(v.backward, dampl * (v + s / m * dvz)) 404 | 405 | ph_eq = Eq(u.backward, dampl * (u + s / m * (dvx + dvy))) 406 | 407 | return [u_vx, u_vy, u_vz] + [pv_eq, ph_eq] 408 | 409 | 410 | def ForwardOperator(model, geometry, space_order=4, 411 | save=False, kernel='centered', **kwargs): 412 | """ 413 | Construct an forward modelling operator in an tti media. 414 | 415 | Parameters 416 | ---------- 417 | model : Model 418 | Object containing the physical parameters. 419 | geometry : AcquisitionGeometry 420 | Geometry object that contains the source (SparseTimeFunction) and 421 | receivers (SparseTimeFunction) and their position. 422 | space_order : int, optional 423 | Space discretization order. 424 | save : int or Buffer, optional 425 | Saving flag, True saves all time steps. False saves three timesteps. 426 | Defaults to False. 427 | kernel : str, optional 428 | Type of discretization, centered or shifted 429 | """ 430 | 431 | dt = model.grid.time_dim.spacing 432 | m = model.m 433 | time_order = 1 if kernel == 'staggered' else 2 434 | if kernel == 'staggered': 435 | stagg_u = stagg_v = NODE 436 | else: 437 | stagg_u = stagg_v = None 438 | 439 | # Create symbols for forward wavefield, source and receivers 440 | u = TimeFunction(name='u', grid=model.grid, staggered=stagg_u, 441 | save=geometry.nt if save else None, 442 | time_order=time_order, space_order=space_order) 443 | v = TimeFunction(name='v', grid=model.grid, staggered=stagg_v, 444 | save=geometry.nt if save else None, 445 | time_order=time_order, space_order=space_order) 446 | src = geometry.src 447 | rec = geometry.rec 448 | 449 | # FD kernels of the PDE 450 | FD_kernel = kernels[(kernel, len(model.shape))] 451 | stencils = FD_kernel(model, u, v) 452 | 453 | # Source and receivers 454 | expr = src * dt / m if kernel == 'staggered' else src * dt**2 / m 455 | stencils += src.inject(field=(u.forward, v.forward), expr=expr) 456 | stencils += rec.interpolate(expr=u + v) 457 | 458 | # Substitute spacing terms to reduce flops 459 | return Operator(stencils, subs=model.spacing_map, name='ForwardTTI', **kwargs) 460 | 461 | 462 | def AdjointOperator(model, geometry, space_order=4, 463 | kernel='centered', **kwargs): 464 | """ 465 | Construct an adjoint modelling operator in an tti media. 466 | 467 | Parameters 468 | ---------- 469 | model : Model 470 | Object containing the physical parameters. 471 | geometry : AcquisitionGeometry 472 | Geometry object that contains the source (SparseTimeFunction) and 473 | receivers (SparseTimeFunction) and their position. 474 | space_order : int, optional 475 | Space discretization order. 476 | kernel : str, optional 477 | Type of discretization, centered or shifted 478 | """ 479 | 480 | dt = model.grid.time_dim.spacing 481 | m = model.m 482 | time_order = 1 if kernel == 'staggered' else 2 483 | if kernel == 'staggered': 484 | stagg_p = stagg_r = NODE 485 | else: 486 | stagg_p = stagg_r = None 487 | 488 | # Create symbols for forward wavefield, source and receivers 489 | p = TimeFunction(name='p', grid=model.grid, staggered=stagg_p, 490 | time_order=time_order, space_order=space_order) 491 | r = TimeFunction(name='r', grid=model.grid, staggered=stagg_r, 492 | time_order=time_order, space_order=space_order) 493 | srca = geometry.new_src(name='srca', src_type=None) 494 | rec = geometry.rec 495 | 496 | # FD kernels of the PDE 497 | FD_kernel = kernels[(kernel, len(model.shape))] 498 | stencils = FD_kernel(model, p, r, forward=False) 499 | 500 | # Construct expression to inject receiver values 501 | expr = rec * dt / m if kernel == 'staggered' else rec * dt**2 / m 502 | stencils += rec.inject(field=(p.backward, r.backward), expr=expr) 503 | 504 | # Create interpolation expression for the adjoint-source 505 | stencils += srca.interpolate(expr=p + r) 506 | 507 | # Substitute spacing terms to reduce flops 508 | return Operator(stencils, subs=model.spacing_map, name='AdjointTTI', **kwargs) 509 | 510 | 511 | def JacobianOperator(model, geometry, space_order=4, 512 | **kwargs): 513 | """ 514 | Construct a Linearized Born operator in a TTI media. 515 | 516 | Parameters 517 | ---------- 518 | model : Model 519 | Object containing the physical parameters. 520 | geometry : AcquisitionGeometry 521 | Geometry object that contains the source (SparseTimeFunction) and 522 | receivers (SparseTimeFunction) and their position. 523 | space_order : int, optional 524 | Space discretization order. 525 | kernel : str, optional 526 | Type of discretization, centered or staggered. 527 | """ 528 | dt = model.grid.stepping_dim.spacing 529 | m = model.m 530 | time_order = 2 531 | 532 | # Create source and receiver symbols 533 | src = geometry.src 534 | rec = geometry.rec 535 | 536 | # Create wavefields and a dm field 537 | u0 = TimeFunction(name='u0', grid=model.grid, save=None, time_order=time_order, 538 | space_order=space_order) 539 | v0 = TimeFunction(name='v0', grid=model.grid, save=None, time_order=time_order, 540 | space_order=space_order) 541 | du = TimeFunction(name="du", grid=model.grid, save=None, 542 | time_order=2, space_order=space_order) 543 | dv = TimeFunction(name="dv", grid=model.grid, save=None, 544 | time_order=2, space_order=space_order) 545 | dm = Function(name="dm", grid=model.grid, space_order=0) 546 | 547 | # FD kernels of the PDE 548 | FD_kernel = kernels[('centered', len(model.shape))] 549 | eqn1 = FD_kernel(model, u0, v0) 550 | 551 | # Linearized source and stencil 552 | lin_usrc = -dm * u0.dt2 553 | lin_vsrc = -dm * v0.dt2 554 | 555 | eqn2 = FD_kernel(model, du, dv, qu=lin_usrc, qv=lin_vsrc) 556 | 557 | # Construct expression to inject source values, injecting at u0(t+dt)/v0(t+dt) 558 | src_term = src.inject(field=(u0.forward, v0.forward), expr=src * dt**2 / m) 559 | 560 | # Create interpolation expression for receivers, extracting at du(t)+dv(t) 561 | rec_term = rec.interpolate(expr=du + dv) 562 | 563 | # Substitute spacing terms to reduce flops 564 | return Operator(eqn1 + src_term + eqn2 + rec_term, subs=model.spacing_map, 565 | name='BornTTI', **kwargs) 566 | 567 | 568 | def JacobianAdjOperator(model, geometry, space_order=4, 569 | save=True, **kwargs): 570 | """ 571 | Construct a linearized JacobianAdjoint modeling Operator in a TTI media. 572 | 573 | Parameters 574 | ---------- 575 | model : Model 576 | Object containing the physical parameters. 577 | geometry : AcquisitionGeometry 578 | Geometry object that contains the source (SparseTimeFunction) and 579 | receivers (SparseTimeFunction) and their position. 580 | space_order : int, optional 581 | Space discretization order. 582 | save : int or Buffer, optional 583 | Option to store the entire (unrolled) wavefield. 584 | """ 585 | dt = model.grid.stepping_dim.spacing 586 | m = model.m 587 | time_order = 2 588 | 589 | # Gradient symbol and wavefield symbols 590 | u0 = TimeFunction(name='u0', grid=model.grid, save=geometry.nt if save 591 | else None, time_order=time_order, space_order=space_order) 592 | v0 = TimeFunction(name='v0', grid=model.grid, save=geometry.nt if save 593 | else None, time_order=time_order, space_order=space_order) 594 | 595 | du = TimeFunction(name="du", grid=model.grid, save=None, 596 | time_order=time_order, space_order=space_order) 597 | dv = TimeFunction(name="dv", grid=model.grid, save=None, 598 | time_order=time_order, space_order=space_order) 599 | 600 | dm = Function(name="dm", grid=model.grid) 601 | 602 | rec = geometry.rec 603 | 604 | # FD kernels of the PDE 605 | FD_kernel = kernels[('centered', len(model.shape))] 606 | eqn = FD_kernel(model, du, dv, forward=False) 607 | 608 | dm_update = Inc(dm, - (u0 * du.dt2 + v0 * dv.dt2)) 609 | 610 | # Add expression for receiver injection 611 | rec_term = rec.inject(field=(du.backward, dv.backward), expr=rec * dt**2 / m) 612 | 613 | # Substitute spacing terms to reduce flops 614 | return Operator(eqn + rec_term + [dm_update], subs=model.spacing_map, 615 | name='GradientTTI', **kwargs) 616 | 617 | 618 | kernels = {('centered', 3): kernel_centered, ('centered', 2): kernel_centered, 619 | ('staggered', 3): kernel_staggered_3d, ('staggered', 2): kernel_staggered_2d} 620 | -------------------------------------------------------------------------------- /seismic/tti/rotated_fd.py: -------------------------------------------------------------------------------- 1 | from sympy import sqrt 2 | from devito import centered, first_derivative, transpose 3 | 4 | def laplacian(v, irho): 5 | """ 6 | Laplacian with density div( 1/rho grad) (u) 7 | """ 8 | if irho is None or irho == 1: 9 | Lap = v.laplace 10 | else: 11 | if getattr(irho, 'is_Function', False): 12 | Lap = grad(irho).T * grad(v) + irho * v.laplace 13 | else: 14 | Lap = irho * v.laplace 15 | 16 | return Lap 17 | 18 | 19 | def rotated_weighted_lap(u, v, costheta, sintheta, cosphi, sinphi, 20 | epsilon, delta, irho, fw=True): 21 | """ 22 | TTI finite difference kernel. The equation we solve is: 23 | u.dt2 = (1+2 *epsilon) (Gxx(u)) + sqrt(1+ 2*delta) Gzz(v) 24 | v.dt2 = sqrt(1+ 2*delta) (Gxx(u)) + Gzz(v) 25 | where epsilon and delta are the thomsen parameters. This function computes 26 | H0 = Gxx(u) + Gyy(u) 27 | Hz = Gzz(v) 28 | 29 | Parameters 30 | ---------- 31 | u: first TTI field 32 | v: second TTI field 33 | costheta: cosine of the tilt angle 34 | sintheta: sine of the tilt angle 35 | cosphi: cosine of the azymuth angle, has to be 0 in 2D 36 | sinphi: sine of the azymuth angle, has to be 0 in 2D 37 | space_order: discretization order 38 | 39 | Returns 40 | ------- 41 | u and v component of the rotated Laplacian in 2D 42 | """ 43 | epsilon = 1 + 2 * epsilon 44 | delta = sqrt(1 * 2 * delta) 45 | if fw: 46 | Gxx = Gxxyy(u, costheta, sintheta, cosphi, sinphi, irho) 47 | Gzzr = Gzz(v, costheta, sintheta, cosphi, sinphi, irho) 48 | return (epsilon * Gxx + delta * Gzzr, delta * Gxx + Gzzr) 49 | else: 50 | a = epsilon * u + delta * v 51 | b = delta * u + v 52 | H0 = Gxxyy(a, costheta, sintheta, cosphi, sinphi, irho) 53 | H1 = Gzz(b, costheta, sintheta, cosphi, sinphi, irho) 54 | return H0, H1 55 | 56 | 57 | def Gzz(field, costheta, sintheta, cosphi, sinphi, irho): 58 | """ 59 | 3D rotated second order derivative in the direction z 60 | 61 | Parameters 62 | ---------- 63 | field: symbolic data whose derivative we are computing 64 | costheta: cosine of the tilt angle 65 | sintheta: sine of the tilt angle 66 | cosphi: cosine of the azymuth angle 67 | sinphi: sine of the azymuth angle 68 | space_order: discretization order 69 | 70 | Returns 71 | ------- 72 | rotated second order derivative wrt z 73 | """ 74 | if field.grid.dim == 2: 75 | return Gzz2d(field, costheta, sintheta, irho) 76 | 77 | order1 = field.space_order // 2 78 | x, y, z = field.grid.dimensions 79 | Gz = -(sintheta * cosphi * first_derivative(field, dim=x, side=centered, 80 | fd_order=order1) + 81 | sintheta * sinphi * first_derivative(field, dim=y, side=centered, 82 | fd_order=order1) + 83 | costheta * first_derivative(field, dim=z, side=centered, 84 | fd_order=order1)) 85 | Gzz = (first_derivative(Gz * sintheta * cosphi * irho, 86 | dim=x, side=centered, fd_order=order1, 87 | matvec=transpose) + 88 | first_derivative(Gz * sintheta * sinphi * irho, 89 | dim=y, side=centered, fd_order=order1, 90 | matvec=transpose) + 91 | first_derivative(Gz * costheta * irho, 92 | dim=z, side=centered, fd_order=order1, 93 | matvec=transpose)) 94 | return Gzz 95 | 96 | 97 | def Gzz2d(field, costheta, sintheta, irho): 98 | """ 99 | 3D rotated second order derivative in the direction z 100 | 101 | Parameters 102 | ---------- 103 | field: symbolic data whose derivative we are computing 104 | costheta: cosine of the tilt angle 105 | sintheta: sine of the tilt angle 106 | cosphi: cosine of the azymuth angle 107 | sinphi: sine of the azymuth angle 108 | space_order: discretization order 109 | 110 | Returns 111 | ------- 112 | rotated second order derivative wrt ztranspose 113 | """ 114 | if sintheta == 0: 115 | return getattr(field, 'd%s2'%field.grid.dimensions[-1]) 116 | 117 | order1 = field.space_order // 2 118 | x, z = field.grid.dimensions 119 | Gz = -(sintheta * first_derivative(field, dim=x, side=centered, fd_order=order1) + 120 | costheta * first_derivative(field, dim=z, side=centered, fd_order=order1)) 121 | Gzz = (first_derivative(Gz * sintheta * irho, dim=x, side=centered, 122 | fd_order=order1, matvec=transpose) + 123 | first_derivative(Gz * costheta * irho, dim=z, side=centered, 124 | fd_order=order1, matvec=transpose)) 125 | return Gzz 126 | 127 | 128 | # Centered case produces directly Gxx + Gyy 129 | def Gxxyy(field, costheta, sintheta, cosphi, sinphi, irho): 130 | """ 131 | Sum of the 3D rotated second order derivative in the direction x and y. 132 | As the Laplacian is rotation invariant, it is computed as the conventional 133 | Laplacian minus the second order rotated second order derivative in the direction z 134 | Gxx + Gyy = field.laplace - Gzz 135 | 136 | Parameters 137 | ---------- 138 | field: symbolic data whose derivative we are computing 139 | costheta: cosine of the tilt angle 140 | sintheta: sine of the tilt angle 141 | cosphi: cosine of the azymuth angle 142 | sinphi: sine of the azymuth angle 143 | space_order: discretization order 144 | 145 | Returns 146 | ------- 147 | Sum of the 3D rotated second order derivative in the direction x and y 148 | """ 149 | if sintheta == 0 and sinphi == 0: 150 | return sum([getattr(field, 'd%s2'%d) for d in field.grid.dimensions[:-1]]) 151 | 152 | lap = laplacian(field, irho) 153 | if field.grid.dim == 2: 154 | Gzzr = Gzz2d(field, costheta, sintheta, irho) 155 | else: 156 | Gzzr = Gzz(field, costheta, sintheta, cosphi, sinphi, irho) 157 | return lap - Gzzr 158 | -------------------------------------------------------------------------------- /seismic/tti/tti_example.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | try: 3 | import pytest 4 | except ImportError: 5 | pass 6 | 7 | from devito import Function, smooth, norm, info, Constant 8 | 9 | from examples.seismic import demo_model, setup_geometry, seismic_args 10 | from examples.seismic.tti import AnisotropicWaveSolver 11 | 12 | 13 | def tti_setup(shape=(50, 50, 50), spacing=(20.0, 20.0, 20.0), tn=250.0, 14 | kernel='centered', space_order=4, nbl=10, preset='layers-tti', 15 | **kwargs): 16 | 17 | # Two layer model for true velocity 18 | model = demo_model(preset, shape=shape, spacing=spacing, 19 | space_order=space_order, nbl=nbl, **kwargs) 20 | 21 | # Source and receiver geometries 22 | geometry = setup_geometry(model, tn) 23 | 24 | return AnisotropicWaveSolver(model, geometry, space_order=space_order, 25 | kernel=kernel, **kwargs) 26 | 27 | 28 | def run(shape=(50, 50, 50), spacing=(20.0, 20.0, 20.0), tn=250.0, 29 | autotune=False, space_order=4, nbl=10, preset='layers-tti', 30 | kernel='centered', full_run=False, checkpointing=False, **kwargs): 31 | 32 | solver = tti_setup(shape=shape, spacing=spacing, tn=tn, space_order=space_order, 33 | nbl=nbl, kernel=kernel, preset=preset, **kwargs) 34 | 35 | info("Applying Forward") 36 | # Whether or not we save the whole time history. We only need the full wavefield 37 | # with 'save=True' if we compute the gradient without checkpointing, if we use 38 | # checkpointing, PyRevolve will take care of the time history 39 | save = full_run and not checkpointing 40 | # Define receiver geometry (spread across `x, y`` just below surface) 41 | rec, u, v, summary = solver.forward(save=save, autotune=autotune) 42 | 43 | if preset in ['constant-tti', 'constant-tti-noazimuth']: 44 | # With new physical parameters as scalars (slightly higher from original values) 45 | vp = 2. 46 | epsilon = .35 47 | delta = .25 48 | theta = .75 49 | phi = None 50 | if len(shape) > 2 and preset not in ['constant-tti-noazimuth']: 51 | phi = .4 52 | solver.forward(save=save, vp=vp, epsilon=epsilon, delta=delta, 53 | theta=theta, phi=phi) 54 | # With new physical parameters as Constants 55 | d = {'vp': vp, 'epsilon': epsilon, 'delta': delta, 'theta': theta, 'phi': phi} 56 | d = {k: Constant(name=k, value=v, dtype=np.float32) for k, v in d.items()} 57 | solver.forward(save=save, **d) 58 | 59 | if not full_run: 60 | return summary.gflopss, summary.oi, summary.timings, [rec, u, v] 61 | 62 | # Smooth velocity 63 | initial_vp = Function(name='v0', grid=solver.model.grid, space_order=space_order) 64 | smooth(initial_vp, solver.model.vp) 65 | dm = np.float32(initial_vp.data**(-2) - solver.model.vp.data**(-2)) 66 | 67 | info("Applying Adjoint") 68 | solver.adjoint(rec, autotune=autotune) 69 | info("Applying Born") 70 | solver.jacobian(dm, autotune=autotune) 71 | info("Applying Gradient") 72 | solver.jacobian_adjoint(rec, u, v, autotune=autotune, checkpointing=checkpointing) 73 | 74 | return summary.gflopss, summary.oi, summary.timings, [rec, u, v] 75 | 76 | 77 | @pytest.mark.parametrize('shape', [(51, 51), (16, 16, 16)]) 78 | @pytest.mark.parametrize('kernel', ['centered', 'staggered']) 79 | def test_tti_stability(shape, kernel): 80 | spacing = tuple([20]*len(shape)) 81 | _, _, _, [rec, _, _] = run(shape=shape, spacing=spacing, kernel=kernel, 82 | tn=16000.0, nbl=0) 83 | assert np.isfinite(norm(rec)) 84 | 85 | 86 | if __name__ == "__main__": 87 | description = ("Example script to execute a TTI forward operator.") 88 | parser = seismic_args(description) 89 | parser.add_argument('--noazimuth', dest='azi', default=False, action='store_true', 90 | help="Whether or not to use an azimuth angle") 91 | parser.add_argument("-k", dest="kernel", default='centered', 92 | choices=['centered', 'staggered'], 93 | help="Choice of finite-difference kernel") 94 | args = parser.parse_args() 95 | 96 | if args.constant: 97 | if args.azi: 98 | preset = 'constant-tti-noazimuth' 99 | else: 100 | preset = 'constant-tti' 101 | else: 102 | if args.azi: 103 | preset = 'layers-tti-noazimuth' 104 | else: 105 | preset = 'layers-tti' 106 | 107 | # Preset parameters 108 | ndim = args.ndim 109 | shape = args.shape[:args.ndim] 110 | spacing = tuple(ndim * [20.0]) 111 | tn = args.tn if args.tn > 0 else (750. if ndim < 3 else 1250.) 112 | 113 | run(shape=shape, spacing=spacing, nbl=args.nbl, tn=tn, 114 | space_order=args.space_order, autotune=args.autotune, dtype=args.dtype, 115 | opt=args.opt, kernel=args.kernel, preset=preset, 116 | checkpointing=args.checkpointing, full_run=args.full) 117 | -------------------------------------------------------------------------------- /seismic/tti/wavesolver.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from devito import (Function, TimeFunction, warning, NODE, 3 | DevitoCheckpoint, CheckpointOperator, Revolver) 4 | from devito.tools import memoized_meth 5 | from examples.seismic.tti.operators import ForwardOperator, AdjointOperator 6 | from examples.seismic.tti.operators import JacobianOperator, JacobianAdjOperator 7 | from examples.seismic.tti.operators import particle_velocity_fields 8 | 9 | 10 | class AnisotropicWaveSolver: 11 | """ 12 | Solver object that provides operators for seismic inversion problems 13 | and encapsulates the time and space discretization for a given problem 14 | setup. 15 | 16 | Parameters 17 | ---------- 18 | model : Model 19 | Object containing the physical parameters. 20 | geometry : AcquisitionGeometry 21 | Geometry object that contains the source (SparseTimeFunction) and 22 | receivers (SparseTimeFunction) and their position. 23 | space_order : int, optional 24 | Order of the spatial stencil discretisation. Defaults to 4. 25 | 26 | Notes 27 | ----- 28 | space_order must be even and it is recommended to be a multiple of 4 29 | """ 30 | def __init__(self, model, geometry, space_order=4, kernel='centered', 31 | **kwargs): 32 | self.model = model 33 | self.model._initialize_bcs(bcs="damp") 34 | self.geometry = geometry 35 | self.kernel = kernel 36 | 37 | if model.fs and kernel == 'staggered': 38 | raise ValueError("Free surface only supported for centered TTI kernel") 39 | 40 | if space_order % 2 != 0: 41 | raise ValueError("space_order must be even but got %s" 42 | % space_order) 43 | 44 | if space_order % 4 != 0: 45 | warning("It is recommended for space_order to be a multiple of 4" + 46 | "but got %s" % space_order) 47 | 48 | self.space_order = space_order 49 | 50 | # Cache compiler options 51 | self._kwargs = kwargs 52 | 53 | @property 54 | def dt(self): 55 | return self.model.critical_dt 56 | 57 | @memoized_meth 58 | def op_fwd(self, save=False): 59 | """Cached operator for forward runs with buffered wavefield""" 60 | return ForwardOperator(self.model, save=save, geometry=self.geometry, 61 | space_order=self.space_order, kernel=self.kernel, 62 | **self._kwargs) 63 | 64 | @memoized_meth 65 | def op_adj(self): 66 | """Cached operator for adjoint runs""" 67 | return AdjointOperator(self.model, save=None, geometry=self.geometry, 68 | space_order=self.space_order, kernel=self.kernel, 69 | **self._kwargs) 70 | 71 | @memoized_meth 72 | def op_jac(self): 73 | """Cached operator for born runs""" 74 | return JacobianOperator(self.model, save=None, geometry=self.geometry, 75 | space_order=self.space_order, **self._kwargs) 76 | 77 | @memoized_meth 78 | def op_jacadj(self, save=True): 79 | """Cached operator for gradient runs""" 80 | return JacobianAdjOperator(self.model, save=save, geometry=self.geometry, 81 | space_order=self.space_order, **self._kwargs) 82 | 83 | def forward(self, src=None, rec=None, u=None, v=None, model=None, 84 | save=False, **kwargs): 85 | """ 86 | Forward modelling function that creates the necessary 87 | data objects for running a forward modelling operator. 88 | 89 | Parameters 90 | ---------- 91 | geometry : AcquisitionGeometry 92 | Geometry object that contains the source (SparseTimeFunction) and 93 | receivers (SparseTimeFunction) and their position. 94 | u : TimeFunction, optional 95 | The computed wavefield first component. 96 | v : TimeFunction, optional 97 | The computed wavefield second component. 98 | model : Model, optional 99 | Object containing the physical parameters. 100 | vp : Function or float, optional 101 | The time-constant velocity. 102 | epsilon : Function or float, optional 103 | The time-constant first Thomsen parameter. 104 | delta : Function or float, optional 105 | The time-constant second Thomsen parameter. 106 | theta : Function or float, optional 107 | The time-constant Dip angle (radians). 108 | phi : Function or float, optional 109 | The time-constant Azimuth angle (radians). 110 | save : bool, optional 111 | Whether or not to save the entire (unrolled) wavefield. 112 | kernel : str, optional 113 | Type of discretization, centered or shifted. 114 | 115 | Returns 116 | ------- 117 | Receiver, wavefield and performance summary. 118 | """ 119 | if self.kernel == 'staggered': 120 | time_order = 1 121 | stagg_u = stagg_v = NODE 122 | else: 123 | time_order = 2 124 | stagg_u = stagg_v = None 125 | # Source term is read-only, so re-use the default 126 | src = src or self.geometry.src 127 | # Create a new receiver object to store the result 128 | rec = rec or self.geometry.rec 129 | 130 | # Create the forward wavefield if not provided 131 | if u is None: 132 | u = TimeFunction(name='u', grid=self.model.grid, staggered=stagg_u, 133 | save=self.geometry.nt if save else None, 134 | time_order=time_order, 135 | space_order=self.space_order) 136 | # Create the forward wavefield if not provided 137 | if v is None: 138 | v = TimeFunction(name='v', grid=self.model.grid, staggered=stagg_v, 139 | save=self.geometry.nt if save else None, 140 | time_order=time_order, 141 | space_order=self.space_order) 142 | 143 | if self.kernel == 'staggered': 144 | vx, vz, vy = particle_velocity_fields(self.model, self.space_order) 145 | kwargs["vx"] = vx 146 | kwargs["vz"] = vz 147 | if vy is not None: 148 | kwargs["vy"] = vy 149 | 150 | model = model or self.model 151 | # Pick vp and Thomsen parameters from model unless explicitly provided 152 | kwargs.update(model.physical_params(**kwargs)) 153 | if self.model.dim < 3: 154 | kwargs.pop('phi', None) 155 | # Execute operator and return wavefield and receiver data 156 | summary = self.op_fwd(save).apply(src=src, rec=rec, u=u, v=v, 157 | dt=kwargs.pop('dt', self.dt), **kwargs) 158 | return rec, u, v, summary 159 | 160 | def adjoint(self, rec, srca=None, p=None, r=None, model=None, 161 | save=None, **kwargs): 162 | """ 163 | Adjoint modelling function that creates the necessary 164 | data objects for running an adjoint modelling operator. 165 | 166 | Parameters 167 | ---------- 168 | geometry : AcquisitionGeometry 169 | Geometry object that contains the source (SparseTimeFunction) and 170 | receivers (SparseTimeFunction) and their position. 171 | p : TimeFunction, optional 172 | The computed wavefield first component. 173 | r : TimeFunction, optional 174 | The computed wavefield second component. 175 | model : Model, optional 176 | Object containing the physical parameters. 177 | vp : Function or float, optional 178 | The time-constant velocity. 179 | epsilon : Function or float, optional 180 | The time-constant first Thomsen parameter. 181 | delta : Function or float, optional 182 | The time-constant second Thomsen parameter. 183 | theta : Function or float, optional 184 | The time-constant Dip angle (radians). 185 | phi : Function or float, optional 186 | The time-constant Azimuth angle (radians). 187 | 188 | Returns 189 | ------- 190 | Adjoint source, wavefield and performance summary. 191 | """ 192 | if self.kernel == 'staggered': 193 | time_order = 1 194 | stagg_p = stagg_r = NODE 195 | else: 196 | time_order = 2 197 | stagg_p = stagg_r = None 198 | 199 | # Source term is read-only, so re-use the default 200 | srca = srca or self.geometry.new_src(name='srca', src_type=None) 201 | 202 | # Create the wavefield if not provided 203 | if p is None: 204 | p = TimeFunction(name='p', grid=self.model.grid, staggered=stagg_p, 205 | time_order=time_order, 206 | space_order=self.space_order) 207 | # Create the wavefield if not provided 208 | if r is None: 209 | r = TimeFunction(name='r', grid=self.model.grid, staggered=stagg_r, 210 | time_order=time_order, 211 | space_order=self.space_order) 212 | 213 | if self.kernel == 'staggered': 214 | vx, vz, vy = particle_velocity_fields(self.model, self.space_order) 215 | kwargs["vx"] = vx 216 | kwargs["vz"] = vz 217 | if vy is not None: 218 | kwargs["vy"] = vy 219 | 220 | model = model or self.model 221 | # Pick vp and Thomsen parameters from model unless explicitly provided 222 | kwargs.update(model.physical_params(**kwargs)) 223 | if self.model.dim < 3: 224 | kwargs.pop('phi', None) 225 | # Execute operator and return wavefield and receiver data 226 | summary = self.op_adj().apply(srca=srca, rec=rec, p=p, r=r, 227 | dt=kwargs.pop('dt', self.dt), 228 | time_m=0 if time_order == 1 else None, 229 | **kwargs) 230 | return srca, p, r, summary 231 | 232 | def jacobian(self, dm, src=None, rec=None, u0=None, v0=None, du=None, dv=None, 233 | model=None, save=None, kernel='centered', **kwargs): 234 | """ 235 | Linearized Born modelling function that creates the necessary 236 | data objects for running an adjoint modelling operator. 237 | 238 | Parameters 239 | ---------- 240 | src : SparseTimeFunction or array_like, optional 241 | Time series data for the injected source term. 242 | rec : SparseTimeFunction or array_like, optional 243 | The interpolated receiver data. 244 | u : TimeFunction, optional 245 | The computed background wavefield first component. 246 | v : TimeFunction, optional 247 | The computed background wavefield second component. 248 | du : TimeFunction, optional 249 | The computed perturbed wavefield first component. 250 | dv : TimeFunction, optional 251 | The computed perturbed wavefield second component. 252 | model : Model, optional 253 | Object containing the physical parameters. 254 | vp : Function or float, optional 255 | The time-constant velocity. 256 | epsilon : Function or float, optional 257 | The time-constant first Thomsen parameter. 258 | delta : Function or float, optional 259 | The time-constant second Thomsen parameter. 260 | theta : Function or float, optional 261 | The time-constant Dip angle (radians). 262 | phi : Function or float, optional 263 | The time-constant Azimuth angle (radians). 264 | """ 265 | if kernel != 'centered': 266 | raise ValueError('Only centered kernel is supported for the jacobian') 267 | 268 | dt = kwargs.pop('dt', self.dt) 269 | # Source term is read-only, so re-use the default 270 | src = src or self.geometry.src 271 | # Create a new receiver object to store the result 272 | rec = rec or self.geometry.rec 273 | 274 | # Create the forward wavefields u, v du and dv if not provided 275 | u0 = u0 or TimeFunction(name='u0', grid=self.model.grid, 276 | time_order=2, space_order=self.space_order) 277 | v0 = v0 or TimeFunction(name='v0', grid=self.model.grid, 278 | time_order=2, space_order=self.space_order) 279 | du = du or TimeFunction(name='du', grid=self.model.grid, 280 | time_order=2, space_order=self.space_order) 281 | dv = dv or TimeFunction(name='dv', grid=self.model.grid, 282 | time_order=2, space_order=self.space_order) 283 | 284 | model = model or self.model 285 | # Pick vp and Thomsen parameters from model unless explicitly provided 286 | kwargs.update(model.physical_params(**kwargs)) 287 | if self.model.dim < 3: 288 | kwargs.pop('phi', None) 289 | 290 | # Execute operator and return wavefield and receiver data 291 | summary = self.op_jac().apply(dm=dm, u0=u0, v0=v0, du=du, dv=dv, src=src, 292 | rec=rec, dt=dt, **kwargs) 293 | return rec, u0, v0, du, dv, summary 294 | 295 | def jacobian_adjoint(self, rec, u0, v0, du=None, dv=None, dm=None, model=None, 296 | checkpointing=False, kernel='centered', **kwargs): 297 | """ 298 | Gradient modelling function for computing the adjoint of the 299 | Linearized Born modelling function, ie. the action of the 300 | Jacobian adjoint on an input data. 301 | 302 | Parameters 303 | ---------- 304 | rec : SparseTimeFunction 305 | Receiver data. 306 | u0 : TimeFunction 307 | The computed background wavefield. 308 | v0 : TimeFunction, optional 309 | The computed background wavefield. 310 | du : Function or float 311 | The computed perturbed wavefield. 312 | dv : Function or float 313 | The computed perturbed wavefield. 314 | dm : Function, optional 315 | Stores the gradient field. 316 | model : Model, optional 317 | Object containing the physical parameters. 318 | vp : Function or float, optional 319 | The time-constant velocity. 320 | epsilon : Function or float, optional 321 | The time-constant first Thomsen parameter. 322 | delta : Function or float, optional 323 | The time-constant second Thomsen parameter. 324 | theta : Function or float, optional 325 | The time-constant Dip angle (radians). 326 | phi : Function or float, optional 327 | The time-constant Azimuth angle (radians). 328 | 329 | Returns 330 | ------- 331 | Gradient field and performance summary. 332 | """ 333 | if kernel != 'centered': 334 | raise ValueError('Only centered kernel is supported for the jacobian_adj') 335 | 336 | dt = kwargs.pop('dt', self.dt) 337 | # Gradient symbol 338 | dm = dm or Function(name='dm', grid=self.model.grid) 339 | 340 | # Create the perturbation wavefields if not provided 341 | du = du or TimeFunction(name='du', grid=self.model.grid, 342 | time_order=2, space_order=self.space_order) 343 | dv = dv or TimeFunction(name='dv', grid=self.model.grid, 344 | time_order=2, space_order=self.space_order) 345 | 346 | model = model or self.model 347 | # Pick vp and Thomsen parameters from model unless explicitly provided 348 | kwargs.update(model.physical_params(**kwargs)) 349 | if self.model.dim < 3: 350 | kwargs.pop('phi', None) 351 | 352 | if checkpointing: 353 | u0 = TimeFunction(name='u0', grid=self.model.grid, 354 | time_order=2, space_order=self.space_order) 355 | v0 = TimeFunction(name='v0', grid=self.model.grid, 356 | time_order=2, space_order=self.space_order) 357 | cp = DevitoCheckpoint([u0, v0]) 358 | n_checkpoints = None 359 | wrap_fw = CheckpointOperator(self.op_fwd(save=False), src=self.geometry.src, 360 | u=u0, v=v0, dt=dt, **kwargs) 361 | wrap_rev = CheckpointOperator(self.op_jacadj(save=False), u0=u0, v0=v0, 362 | du=du, dv=dv, rec=rec, dm=dm, dt=dt, **kwargs) 363 | 364 | # Run forward 365 | wrp = Revolver(cp, wrap_fw, wrap_rev, n_checkpoints, rec.data.shape[0]-2) 366 | wrp.apply_forward() 367 | summary = wrp.apply_reverse() 368 | else: 369 | summary = self.op_jacadj().apply(rec=rec, dm=dm, u0=u0, v0=v0, du=du, dv=dv, 370 | dt=dt, **kwargs) 371 | return dm, summary 372 | -------------------------------------------------------------------------------- /seismic/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from argparse import Action, ArgumentError, ArgumentParser 3 | 4 | from devito import error, configuration, warning 5 | from devito.tools import Pickable 6 | from devito.types.sparse import _default_radius 7 | 8 | from .source import * 9 | 10 | __all__ = ['AcquisitionGeometry', 'setup_geometry', 'seismic_args'] 11 | 12 | 13 | def setup_geometry(model, tn, f0=0.010, interpolation='linear', **kwargs): 14 | # Source and receiver geometries 15 | src_coordinates = np.empty((1, model.dim)) 16 | if model.dim > 1: 17 | src_coordinates[0, :] = np.array(model.domain_size) * .5 18 | src_coordinates[0, -1] = model.origin[-1] + model.spacing[-1] 19 | else: 20 | src_coordinates[0, 0] = 2 * model.spacing[0] 21 | 22 | rec_coordinates = setup_rec_coords(model) 23 | 24 | r = kwargs.get('r', _default_radius[interpolation]) 25 | geometry = AcquisitionGeometry(model, rec_coordinates, src_coordinates, 26 | t0=0.0, tn=tn, src_type='Ricker', f0=f0, 27 | interpolation=interpolation, r=r) 28 | 29 | return geometry 30 | 31 | 32 | def setup_rec_coords(model): 33 | nrecx = model.shape[0] 34 | recx = np.linspace(model.origin[0], model.domain_size[0], nrecx) 35 | 36 | if model.dim == 1: 37 | return recx.reshape((nrecx, 1)) 38 | elif model.dim == 2: 39 | rec_coordinates = np.empty((nrecx, model.dim)) 40 | rec_coordinates[:, 0] = recx 41 | rec_coordinates[:, -1] = model.origin[-1] + 2 * model.spacing[-1] 42 | return rec_coordinates 43 | else: 44 | nrecy = model.shape[1] 45 | recy = np.linspace(model.origin[1], model.domain_size[1], nrecy) 46 | rec_coordinates = np.empty((nrecx*nrecy, model.dim)) 47 | rec_coordinates[:, 0] = np.array([recx[i] for i in range(nrecx) 48 | for j in range(nrecy)]) 49 | rec_coordinates[:, 1] = np.array([recy[j] for i in range(nrecx) 50 | for j in range(nrecy)]) 51 | rec_coordinates[:, -1] = model.origin[-1] + 2 * model.spacing[-1] 52 | return rec_coordinates 53 | 54 | 55 | class AcquisitionGeometry(Pickable): 56 | """ 57 | Encapsulate the geometry of an acquisition: 58 | - source positions and number 59 | - receiver positions and number 60 | 61 | In practice this would only point to a segy file with the 62 | necessary information 63 | """ 64 | 65 | __rargs__ = ('grid', 'rec_positions', 'src_positions', 't0', 'tn') 66 | __rkwargs__ = ('f0', 'src_type', 'interpolation', 'r') 67 | 68 | def __init__(self, model, rec_positions, src_positions, t0, tn, **kwargs): 69 | """ 70 | In practice would be __init__(segyfile) and all below parameters 71 | would come from a segy_read (at property call rather than at init) 72 | """ 73 | src_positions = np.reshape(src_positions, (-1, model.dim)) 74 | rec_positions = np.reshape(rec_positions, (-1, model.dim)) 75 | self.rec_positions = rec_positions 76 | self._nrec = rec_positions.shape[0] 77 | self.src_positions = src_positions 78 | self._nsrc = src_positions.shape[0] 79 | self._src_type = kwargs.get('src_type') 80 | assert (self.src_type in sources or self.src_type is None) 81 | self._f0 = kwargs.get('f0') 82 | self._a = kwargs.get('a', None) 83 | self._t0w = kwargs.get('t0w', None) 84 | if self._src_type is not None and self._f0 is None: 85 | error("Peak frequency must be provided in KHz" + 86 | " for source of type %s" % self._src_type) 87 | 88 | self._grid = model.grid 89 | self._model = model 90 | self._dt = model.critical_dt 91 | self._t0 = t0 92 | self._tn = tn 93 | self._interpolation = kwargs.get('interpolation', 'linear') 94 | self._r = kwargs.get('r', _default_radius[self.interpolation]) 95 | 96 | # Initialize to empty, created at new src/rec 97 | self._src_coordinates = None 98 | self._rec_coordinates = None 99 | 100 | def resample(self, dt): 101 | self._dt = dt 102 | return self 103 | 104 | @property 105 | def time_axis(self): 106 | return TimeAxis(start=self.t0, stop=self.tn, step=self.dt) 107 | 108 | @property 109 | def src_type(self): 110 | return self._src_type 111 | 112 | @property 113 | def grid(self): 114 | return self._grid 115 | 116 | @property 117 | def model(self): 118 | warning("Model is kept for backward compatibility but should not be" 119 | "obtained from the geometry") 120 | return self._model 121 | 122 | @property 123 | def f0(self): 124 | return self._f0 125 | 126 | @property 127 | def tn(self): 128 | return self._tn 129 | 130 | @property 131 | def t0(self): 132 | return self._t0 133 | 134 | @property 135 | def dt(self): 136 | return self._dt 137 | 138 | @property 139 | def nt(self): 140 | return self.time_axis.num 141 | 142 | @property 143 | def nrec(self): 144 | return self._nrec 145 | 146 | @property 147 | def nsrc(self): 148 | return self._nsrc 149 | 150 | @property 151 | def dtype(self): 152 | return self.grid.dtype 153 | 154 | @property 155 | def r(self): 156 | return self._r 157 | 158 | @property 159 | def interpolation(self): 160 | return self._interpolation 161 | 162 | @property 163 | def rec(self): 164 | return self.new_rec() 165 | 166 | def new_rec(self, name='rec', coordinates=None): 167 | coords = coordinates or self.rec_positions 168 | rec = Receiver(name=name, grid=self.grid, 169 | time_range=self.time_axis, npoint=self.nrec, 170 | interpolation=self.interpolation, r=self._r, 171 | coordinates=coords) 172 | 173 | return rec 174 | 175 | @property 176 | def adj_src(self): 177 | if self.src_type is None: 178 | return self.new_rec() 179 | coords = self.rec_positions 180 | adj_src = sources[self.src_type](name='rec', grid=self.grid, f0=self.f0, 181 | time_range=self.time_axis, npoint=self.nrec, 182 | interpolation=self.interpolation, r=self._r, 183 | coordinates=coords, t0=self._t0w, a=self._a) 184 | # Revert time axis to have a proper shot record and not compute on zeros 185 | for i in range(self.nrec): 186 | adj_src.data[:, i] = adj_src.wavelet[::-1] 187 | return adj_src 188 | 189 | @property 190 | def src(self): 191 | return self.new_src() 192 | 193 | def new_src(self, name='src', src_type='self', coordinates=None): 194 | coords = coordinates or self.src_positions 195 | if self.src_type is None or src_type is None: 196 | warning("No source type defined, returning uninitiallized (zero) source") 197 | src = PointSource(name=name, grid=self.grid, 198 | time_range=self.time_axis, npoint=self.nsrc, 199 | coordinates=coords, 200 | interpolation=self.interpolation, r=self._r) 201 | else: 202 | src = sources[self.src_type](name=name, grid=self.grid, f0=self.f0, 203 | time_range=self.time_axis, npoint=self.nsrc, 204 | coordinates=coords, 205 | t0=self._t0w, a=self._a, 206 | interpolation=self.interpolation, r=self._r) 207 | 208 | return src 209 | 210 | 211 | sources = {'Wavelet': WaveletSource, 'Ricker': RickerSource, 'Gabor': GaborSource} 212 | 213 | 214 | def seismic_args(description): 215 | """ 216 | Command line options for the seismic examples 217 | """ 218 | 219 | class _dtype_store(Action): 220 | def __call__(self, parser, args, values, option_string=None): 221 | values = {'float32': np.float32, 'float64': np.float64}[values] 222 | setattr(args, self.dest, values) 223 | 224 | class _opt_action(Action): 225 | def __call__(self, parser, args, values, option_string=None): 226 | try: 227 | # E.g., `('advanced', {'par-tile': True})` 228 | values = eval(values) 229 | if not isinstance(values, tuple) and len(values) >= 1: 230 | raise ArgumentError(self, ("Invalid choice `%s` (`opt` must be " 231 | "either str or tuple)" % str(values))) 232 | opt = values[0] 233 | except NameError: 234 | # E.g. `'advanced'` 235 | opt = values 236 | if opt not in configuration._accepted['opt']: 237 | raise ArgumentError(self, ("Invalid choice `%s` (choose from %s)" 238 | % (opt, str(configuration._accepted['opt'])))) 239 | setattr(args, self.dest, values) 240 | 241 | parser = ArgumentParser(description=description) 242 | parser.add_argument("-nd", dest="ndim", default=3, type=int, 243 | help="Number of dimensions") 244 | parser.add_argument("-d", "--shape", default=(51, 51, 51), type=int, nargs="+", 245 | help="Number of grid points along each axis") 246 | parser.add_argument('-f', '--full', default=False, action='store_true', 247 | help="Execute all operators and store forward wavefield") 248 | parser.add_argument("-so", "--space_order", default=4, 249 | type=int, help="Space order of the simulation") 250 | parser.add_argument("--nbl", default=40, 251 | type=int, help="Number of boundary layers around the domain") 252 | parser.add_argument("--constant", default=False, action='store_true', 253 | help="Constant velocity model, default is a two layer model") 254 | parser.add_argument("--checkpointing", default=False, action='store_true', 255 | help="Use checkpointing, default is False") 256 | parser.add_argument("-opt", default="advanced", action=_opt_action, 257 | help="Performance optimization level") 258 | parser.add_argument('-a', '--autotune', default='off', 259 | choices=(configuration._accepted['autotuning']), 260 | help="Operator auto-tuning mode") 261 | parser.add_argument("-tn", "--tn", default=0, 262 | type=float, help="Simulation time in millisecond") 263 | parser.add_argument("-dtype", action=_dtype_store, dest="dtype", default=np.float32, 264 | choices=['float32', 'float64']) 265 | parser.add_argument("-interp", dest="interp", default="linear", 266 | choices=['linear', 'sinc']) 267 | return parser 268 | -------------------------------------------------------------------------------- /seismic/viscoelastic/__init__.py: -------------------------------------------------------------------------------- 1 | from .operators import * # noqa 2 | from .wavesolver import * # noqa 3 | from .viscoelastic_example import * # noqa 4 | -------------------------------------------------------------------------------- /seismic/viscoelastic/operators.py: -------------------------------------------------------------------------------- 1 | import sympy as sp 2 | 3 | from devito import (Eq, Operator, VectorTimeFunction, TensorTimeFunction, 4 | div, grad, diag, solve) 5 | from examples.seismic.elastic import src_rec 6 | 7 | 8 | def ForwardOperator(model, geometry, space_order=4, save=False, **kwargs): 9 | """ 10 | Construct method for the forward modelling operator in an elastic media. 11 | 12 | Parameters 13 | ---------- 14 | model : Model 15 | Object containing the physical parameters. 16 | geometry : AcquisitionGeometry 17 | Geometry object that contains the source (SparseTimeFunction) and 18 | receivers (SparseTimeFunction) and their position. 19 | space_order : int, optional 20 | Space discretization order. 21 | save : int or Buffer 22 | Saving flag, True saves all time steps, False saves three buffered 23 | indices (last three time steps). Defaults to False. 24 | """ 25 | l, qp, mu, qs, b, damp = \ 26 | model.lam, model.qp, model.mu, model.qs, model.b, model.damp 27 | 28 | f0 = geometry._f0 29 | t_s = (sp.sqrt(1.+1./qp**2)-1./qp)/f0 30 | t_ep = 1./(f0**2*t_s) 31 | t_es = (1.+f0*qs*t_s)/(f0*qs-f0**2*t_s) 32 | 33 | # Create symbols for forward wavefield, source and receivers 34 | # Velocity: 35 | v = VectorTimeFunction(name="v", grid=model.grid, 36 | save=geometry.nt if save else None, 37 | time_order=1, space_order=space_order) 38 | # Stress: 39 | tau = TensorTimeFunction(name='t', grid=model.grid, 40 | save=geometry.nt if save else None, 41 | space_order=space_order, time_order=1) 42 | # Memory variable: 43 | r = TensorTimeFunction(name='r', grid=model.grid, 44 | save=geometry.nt if save else None, 45 | space_order=space_order, time_order=1) 46 | 47 | # Particle velocity 48 | pde_v = v.dt - b * div(tau) 49 | u_v = Eq(v.forward, model.damp * solve(pde_v, v.forward)) 50 | # Strain 51 | e = grad(v.forward) + grad(v.forward).transpose(inner=False) 52 | 53 | # Stress equations 54 | pde_tau = tau.dt - r.forward - l * t_ep / t_s * diag(div(v.forward)) - \ 55 | mu * t_es / t_s * e 56 | u_t = Eq(tau.forward, model.damp * solve(pde_tau, tau.forward)) 57 | 58 | # Memory variable equations: 59 | pde_r = r.dt + 1 / t_s * (r + l * (t_ep/t_s-1) * diag(div(v.forward)) + 60 | mu * (t_es / t_s - 1) * e) 61 | u_r = Eq(r.forward, damp * solve(pde_r, r.forward)) 62 | # Point source 63 | src_rec_expr = src_rec(v, tau, model, geometry) 64 | 65 | # Substitute spacing terms to reduce flops 66 | return Operator([u_v, u_r, u_t] + src_rec_expr, subs=model.spacing_map, 67 | name='ViscoIsoElasticForward', **kwargs) 68 | -------------------------------------------------------------------------------- /seismic/viscoelastic/viscoelastic_example.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | try: 3 | import pytest 4 | except ImportError: 5 | pass 6 | 7 | from devito import norm 8 | from devito.logger import info 9 | from examples.seismic.viscoelastic import ViscoelasticWaveSolver 10 | from examples.seismic import demo_model, setup_geometry, seismic_args 11 | 12 | 13 | def viscoelastic_setup(shape=(50, 50), spacing=(15.0, 15.0), tn=500., space_order=4, 14 | nbl=10, constant=True, **kwargs): 15 | 16 | preset = 'constant-viscoelastic' if constant else 'layers-viscoelastic' 17 | model = demo_model(preset, space_order=space_order, shape=shape, nbl=nbl, 18 | dtype=kwargs.pop('dtype', np.float32), spacing=spacing) 19 | 20 | # Source and receiver geometries 21 | geometry = setup_geometry(model, tn) 22 | 23 | # Create solver object to provide relevant operators 24 | solver = ViscoelasticWaveSolver(model, geometry, space_order=space_order, **kwargs) 25 | return solver 26 | 27 | 28 | def run(shape=(50, 50), spacing=(20.0, 20.0), tn=1000.0, 29 | space_order=4, nbl=40, autotune=False, constant=False, **kwargs): 30 | 31 | solver = viscoelastic_setup(shape=shape, spacing=spacing, nbl=nbl, tn=tn, 32 | space_order=space_order, constant=constant, **kwargs) 33 | info("Applying Forward") 34 | # Define receiver geometry (spread across x, just below surface) 35 | rec1, rec2, v, tau, summary = solver.forward(autotune=autotune) 36 | 37 | return (summary.gflopss, summary.oi, summary.timings, 38 | [rec1, rec2, v, tau]) 39 | 40 | 41 | @pytest.mark.parametrize("dtype", [np.float32, np.float64]) 42 | def test_viscoelastic(dtype): 43 | _, _, _, [rec1, rec2, v, tau] = run(dtype=dtype) 44 | assert np.isclose(norm(rec1), 12.62339, atol=1e-3, rtol=0) 45 | assert np.isclose(norm(rec2), 0.320817, atol=1e-3, rtol=0) 46 | 47 | 48 | @pytest.mark.parametrize('shape', [(51, 51), (16, 16, 16)]) 49 | def test_viscoelastic_stability(shape): 50 | spacing = tuple([20]*len(shape)) 51 | _, _, _, [rec1, rec2, v, tau] = run(shape=shape, spacing=spacing, tn=20000.0, nbl=0) 52 | assert np.isfinite(norm(rec1)) 53 | 54 | 55 | if __name__ == "__main__": 56 | description = ("Example script for a set of viscoelastic operators.") 57 | args = seismic_args(description).parse_args() 58 | 59 | # Preset parameters 60 | ndim = args.ndim 61 | shape = args.shape[:args.ndim] 62 | spacing = tuple(ndim * [10.0]) 63 | tn = args.tn if args.tn > 0 else (750. if ndim < 3 else 1250.) 64 | 65 | run(shape=shape, spacing=spacing, nbl=args.nbl, tn=tn, opt=args.opt, 66 | space_order=args.space_order, autotune=args.autotune, constant=args.constant, 67 | dtype=args.dtype) 68 | -------------------------------------------------------------------------------- /seismic/viscoelastic/wavesolver.py: -------------------------------------------------------------------------------- 1 | from devito import VectorTimeFunction, TensorTimeFunction 2 | from devito.tools import memoized_meth 3 | from examples.seismic.viscoelastic.operators import ForwardOperator 4 | 5 | 6 | class ViscoelasticWaveSolver: 7 | """ 8 | Solver object that provides operators for seismic inversion problems 9 | and encapsulates the time and space discretization for a given problem 10 | setup. 11 | 12 | Parameters 13 | ---------- 14 | model : Model 15 | Physical model with domain parameters. 16 | geometry : AcquisitionGeometry 17 | Geometry object that contains the source (SparseTimeFunction) and 18 | receivers (SparseTimeFunction) and their position. 19 | space_order : int, optional 20 | Order of the spatial stencil discretisation. Defaults to 4. 21 | 22 | Notes 23 | ----- 24 | This is an experimental staggered grid viscoelastic modeling kernel. 25 | """ 26 | def __init__(self, model, geometry, space_order=4, **kwargs): 27 | self.model = model 28 | self.model._initialize_bcs(bcs="mask") 29 | self.geometry = geometry 30 | 31 | self.space_order = space_order 32 | 33 | # The viscoelastic equation requires a smaller dt than the standard 34 | # elastic equation due to instability introduced by the viscosity. 35 | self.model.dt_scale = .9 36 | 37 | # Cache compiler options 38 | self._kwargs = kwargs 39 | 40 | @property 41 | def dt(self): 42 | return self.model.critical_dt 43 | 44 | @memoized_meth 45 | def op_fwd(self, save=None): 46 | """Cached operator for forward runs with buffered wavefield""" 47 | return ForwardOperator(self.model, save=save, geometry=self.geometry, 48 | space_order=self.space_order, **self._kwargs) 49 | 50 | def forward(self, src=None, rec1=None, rec2=None, v=None, tau=None, r=None, 51 | model=None, save=None, **kwargs): 52 | """ 53 | Forward modelling function that creates the necessary 54 | data objects for running a forward modelling operator. 55 | 56 | Parameters 57 | ---------- 58 | geometry : AcquisitionGeometry 59 | Geometry object that contains the source (src : SparseTimeFunction) and 60 | receivers (rec1(txx) : SparseTimeFunction, rec2(tzz) : SparseTimeFunction) 61 | and their position. 62 | v : VectorTimeFunction, optional 63 | The computed particle velocity. 64 | tau : TensorTimeFunction, optional 65 | The computed stress. 66 | r : TensorTimeFunction, optional 67 | The computed memory variable. 68 | model : Model, optional 69 | Object containing the physical parameters. 70 | lam : Function, optional 71 | The time-constant first Lame parameter rho * (vp**2 - 2 * vs **2). 72 | mu : Function, optional 73 | The Shear modulus (rho * vs*2). 74 | qp : Function, optional 75 | The P-wave quality factor (dimensionless). 76 | qs : Function, optional 77 | The S-wave quality factor (dimensionless). 78 | b : Function, optional 79 | The time-constant inverse density (1/rho=1 for water). 80 | save : bool, optional 81 | Whether or not to save the entire (unrolled) wavefield. 82 | 83 | Returns 84 | ------- 85 | Rec1 (txx), Rec2 (tzz), particle velocities vx and vz, stress txx, 86 | tzz and txz, memory variables rxx, rzz, rxz and performance summary. 87 | """ 88 | # Source term is read-only, so re-use the default 89 | src = src or self.geometry.src 90 | # Create a new receiver object to store the result 91 | rec1 = rec1 or self.geometry.new_rec(name='rec1') 92 | rec2 = rec2 or self.geometry.new_rec(name='rec2') 93 | 94 | # Create all the fields v, tau, r 95 | save_t = src.nt if save else None 96 | v = v or VectorTimeFunction(name="v", grid=self.model.grid, save=save_t, 97 | time_order=1, space_order=self.space_order) 98 | # Stress: 99 | tau = tau or TensorTimeFunction(name='t', grid=self.model.grid, save=save_t, 100 | space_order=self.space_order, time_order=1) 101 | # Memory variable: 102 | r = r or TensorTimeFunction(name='r', grid=self.model.grid, save=save_t, 103 | space_order=self.space_order, time_order=1) 104 | 105 | kwargs.update({k.name: k for k in v}) 106 | kwargs.update({k.name: k for k in tau}) 107 | kwargs.update({k.name: k for k in r}) 108 | 109 | model = model or self.model 110 | # Pick physical parameters from model unless explicitly provided 111 | kwargs.update(model.physical_params(**kwargs)) 112 | 113 | # Execute operator and return wavefield and receiver data 114 | summary = self.op_fwd(save).apply(src=src, rec1=rec1, rec2=rec2, 115 | dt=kwargs.pop('dt', self.dt), **kwargs) 116 | return rec1, rec2, v, tau, summary 117 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='Devito-Examples', 4 | version='1.0', 5 | description="Seismic modeling and inversion examples using Devito.", 6 | long_description="""This package of examples provides a detailed description 7 | and implementation of seismic modeling and inversion using Devito.""", 8 | url='https://slim.gatech.edu/', 9 | author="IGeorgai Institue of Technology", 10 | author_email='mlouboutin3@gatech.edu', 11 | license='MIT', 12 | packages=['seismic'], 13 | install_requires=['devito[extras,tests]']) 14 | --------------------------------------------------------------------------------