├── .DS_Store ├── .gitattributes ├── .github └── workflows │ ├── flake8.yml │ ├── pytest-cpu.yml │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── benchmark ├── marmousi_2D.py └── overthrust_3D.py ├── examples ├── .DS_Store ├── acoustic_2D.py ├── acoustic_2D_density.py ├── acoustic_3D.py ├── acoustic_3D_density.py └── marmousi_2D.py ├── miniwave ├── .gitignore ├── README.md ├── c-frontend │ ├── CMakeLists.txt │ ├── miniwave.c │ └── selected_kernel.h.in ├── kernels │ ├── sequential.c │ └── sequential.h ├── miniwave.py ├── selected_kernel.h └── utils │ ├── __init__.py │ ├── compiler.py │ ├── dataset_writer.py │ ├── kernel.py │ ├── model.py │ ├── plot.py │ └── properties.py ├── pytest.ini ├── requirements.txt ├── setup.cfg ├── setup.py ├── simwave ├── __init__.py ├── _version.py ├── io │ ├── __init__.py │ └── io.py ├── kernel │ ├── __init__.py │ ├── backend │ │ ├── __init__.py │ │ ├── c_code │ │ │ └── forward │ │ │ │ ├── constant_density │ │ │ │ ├── 2d │ │ │ │ │ ├── cuda │ │ │ │ │ │ └── wave.cu │ │ │ │ │ └── wave.c │ │ │ │ └── 3d │ │ │ │ │ ├── cuda │ │ │ │ │ └── wave.cu │ │ │ │ │ └── wave.c │ │ │ │ └── variable_density │ │ │ │ ├── 2d │ │ │ │ ├── cuda │ │ │ │ │ └── wave.cu │ │ │ │ └── wave.c │ │ │ │ └── 3d │ │ │ │ ├── cuda │ │ │ │ └── wave.cu │ │ │ │ └── wave.c │ │ ├── compiler.py │ │ └── middleware.py │ └── frontend │ │ ├── __init__.py │ │ ├── fd.py │ │ ├── kws.py │ │ ├── model.py │ │ ├── solver.py │ │ └── source.py └── plots │ ├── __init__.py │ └── plot.py ├── tests ├── reference_solution │ ├── generator.py │ ├── wavefield-2d-so-2.npy │ ├── wavefield-2d-so-8.npy │ ├── wavefield-3d-so-2.npy │ └── wavefield-3d-so-8.npy ├── test_compiler.py ├── test_convergence.py ├── test_gpu_convergence.py ├── test_gpu_solution.py ├── test_parallel_solution.py ├── test_solution.py ├── test_source.py ├── test_space_model.py ├── test_time_model.py └── test_u_saving.py ├── tutorial └── forward-2D.ipynb └── versioneer.py /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HPCSys-Lab/simwave/49e9c85d38020a4222aefa8c7224a103a40f9ba1/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | simwave/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.github/workflows/flake8.yml: -------------------------------------------------------------------------------- 1 | name: Flake8-CI 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the master branch 6 | push: 7 | branches: [ master ] 8 | pull_request: 9 | branches: [ master ] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # This workflow contains a single job called "build" 17 | flake8: 18 | # The type of runner that the job will run on 19 | runs-on: ubuntu-latest 20 | 21 | # Steps represent a sequence of tasks that will be executed as part of the job 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Set up Python 3.9 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: 3.9 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install flake8 32 | - name: Lint with flake8 33 | run: | 34 | flake8 . 35 | -------------------------------------------------------------------------------- /.github/workflows/pytest-cpu.yml: -------------------------------------------------------------------------------- 1 | name: Pytest-CPU-CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | #pull_request: 9 | # branches: 10 | # - master 11 | 12 | jobs: 13 | pytest: 14 | name: pytest-${{ matrix.os }}-${{ matrix.python }} 15 | runs-on: "${{ matrix.os }}" 16 | 17 | strategy: 18 | # Prevent all build to stop if a single one fails 19 | fail-fast: false 20 | 21 | matrix: 22 | os: [ubuntu-latest] 23 | python: [3.6, 3.7, 3.8, 3.9] 24 | 25 | steps: 26 | - name: Checkout Repo 27 | uses: actions/checkout@v2 28 | 29 | - name: Set up Python ${{ matrix.python }} 30 | uses: actions/setup-python@v2 31 | with: 32 | python-version: ${{ matrix.python }} 33 | 34 | - name: Install GCC 35 | run : | 36 | sudo apt install -y gcc 37 | 38 | - name: Install pywave 39 | run: | 40 | python -m pip install --upgrade pip 41 | pip install -e . 42 | 43 | - name: Test with pytest 44 | run: | 45 | pytest --codeblocks -m "not gpu" 46 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload to PyPI 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .cache 3 | *.so 4 | *.png 5 | dist/ 6 | build/ 7 | tmp/* 8 | /src/wavefield/* 9 | simwave.egg-info/* 10 | .ipynb_checkpoints 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include simwave/_version.py 3 | recursive-include simwave/kernel/backend/c_code *.c 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI version](https://badge.fury.io/py/simwave.svg)](https://badge.fury.io/py/simwave) 2 | [![DOI](https://zenodo.org/badge/285108327.svg)](https://zenodo.org/badge/latestdoi/285108327) 3 | 4 | # Simwave 5 | 6 | `Simwave` is a Python package to simulate the propagation of the constant or variable density acoustic wave in an isotropic 2D/3D medium using the finite difference method. Finite difference kernels of aribtrary spatial order (up to 20th order) are written in C for performance and compiled at run time. These kernels are called via a user-friendly Python interface for easy integration with several scientific and engineering libraries to, for example perform full-waveform inversion. 7 | 8 | For further information on the `simwave` design and implementation, please see the paper https://arxiv.org/abs/2201.05278 9 | 10 | ## Installation: 11 | 12 | For installation, `simwave` needs only scipy, numpy, and segyio. See `requirements.txt`. If you wish to plot, then `matplotlib` is additionally required. `simwave` compiles finite difference stencils at run time in C for performance and thus requires a working C compiler. 13 | 14 | `git clone https://github.com/HPCSys-Lab/simwave.git` 15 | 16 | `cd simwave` 17 | 18 | `pip3 install -e .` 19 | 20 | 21 | ## Contributing 22 | 23 | All contributions are welcome. 24 | 25 | To contribute to the software: 26 | 27 | 1. [Fork](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo) the repository. 28 | 2. Clone the forked repository, add your contributions and push the changes to your fork. 29 | 3. Create a [Pull request](https://github.com/HPCSys-Lab/simwave/pulls) 30 | 31 | Before creating the pull request, make sure that the tests pass by running 32 | ``` 33 | pytest 34 | ``` 35 | Some things that will increase the chance that your pull request is accepted: 36 | - Write tests. 37 | - Add Python docstrings that follow the [Sphinx](https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html). 38 | - Write good commit and pull request messages. 39 | 40 | 41 | [style]: https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html 42 | 43 | Problems? 44 | ========== 45 | 46 | If something isn't working as it should or you'd like to recommend a new addition/feature to the software, please let us know by starting an issue through the [issues](https://github.com/HPCSys-Lab/pywave/issues) tab. I'll try to get to it as soon as possible. 47 | 48 | Examples 49 | ======== 50 | 51 | Simulation with `simwave` is simple and can be accomplished in a dozen or so lines of Python! Jupyter notebooks with tutorials can be found here [here](https://github.com/HPCSys-Lab/simwave/tree/master/tutorial). 52 | 53 | Here we show how to simulate the constant density acoustic wave equation on a simple two layer velocity model. 54 | ```python 55 | from simwave import ( 56 | SpaceModel, TimeModel, RickerWavelet, Solver, Compiler, 57 | Receiver, Source, plot_wavefield, plot_shotrecord, plot_velocity_model 58 | ) 59 | import numpy as np 60 | 61 | 62 | # set compiler options 63 | # available language options: c (sequential) or cpu_openmp (parallel CPU) 64 | compiler = Compiler( 65 | cc='gcc', 66 | language='cpu_openmp', 67 | cflags='-O3 -fPIC -Wall -std=c99 -shared' 68 | ) 69 | 70 | # Velocity model 71 | vel = np.zeros(shape=(512, 512), dtype=np.float32) 72 | vel[:] = 1500.0 73 | vel[100:] = 2000.0 74 | 75 | # create the space model 76 | space_model = SpaceModel( 77 | bounding_box=(0, 5120, 0, 5120), 78 | grid_spacing=(10, 10), 79 | velocity_model=vel, 80 | space_order=4, 81 | dtype=np.float32 82 | ) 83 | 84 | # config boundary conditions 85 | # (none, null_dirichlet or null_neumann) 86 | space_model.config_boundary( 87 | damping_length=0, 88 | boundary_condition=( 89 | "null_neumann", "null_dirichlet", 90 | "none", "null_dirichlet" 91 | ), 92 | damping_polynomial_degree=3, 93 | damping_alpha=0.001 94 | ) 95 | 96 | # create the time model 97 | time_model = TimeModel( 98 | space_model=space_model, 99 | tf=1.0, 100 | saving_stride=0 101 | ) 102 | 103 | # create the set of sources 104 | source = Source( 105 | space_model, 106 | coordinates=[(2560, 2560)], 107 | window_radius=1 108 | ) 109 | 110 | # crete the set of receivers 111 | receiver = Receiver( 112 | space_model=space_model, 113 | coordinates=[(2560, i) for i in range(0, 5112, 10)], 114 | window_radius=1 115 | ) 116 | 117 | # create a ricker wavelet with 10hz of peak frequency 118 | ricker = RickerWavelet(10.0, time_model) 119 | 120 | # create the solver 121 | solver = Solver( 122 | space_model=space_model, 123 | time_model=time_model, 124 | sources=source, 125 | receivers=receiver, 126 | wavelet=ricker, 127 | compiler=compiler 128 | ) 129 | 130 | # run the forward 131 | u_full, recv = solver.forward() 132 | 133 | print("u_full shape:", u_full.shape) 134 | plot_velocity_model(space_model.velocity_model) 135 | plot_wavefield(u_full[-1]) 136 | plot_shotrecord(recv) 137 | ``` 138 | 139 | 140 | -------------------------------------------------------------------------------- /benchmark/marmousi_2D.py: -------------------------------------------------------------------------------- 1 | from simwave import ( 2 | SpaceModel, TimeModel, RickerWavelet, Solver, Compiler, 3 | Receiver, Source, read_2D_segy, 4 | plot_wavefield, plot_shotrecord, plot_velocity_model 5 | ) 6 | import numpy as np 7 | import sys 8 | import argparse 9 | 10 | 11 | ''' 12 | Use the Marmousi 2 model available in 13 | https://s3.amazonaws.com/open.source.geoscience/open_data/elastic-marmousi/elastic-marmousi-model.tar.gz 14 | http://mcee.ou.edu/aaspi/publications/2006/martin_etal_TLE2006.pdf 15 | ''' 16 | 17 | 18 | def get_args(args=sys.argv[1:]): 19 | 20 | parser = argparse.ArgumentParser(description='How to use this program') 21 | parser.add_argument("--model_file", type=str, 22 | help="Path to the velocity model file") 23 | parser.add_argument("--language", type=str, default="c", 24 | help="Language: c, cpu_openmp, gpu_openmp, cuda") 25 | parser.add_argument("--space_order", type=int, default=2, 26 | help="Space order") 27 | parser.add_argument("--stride", type=int, default=0, 28 | help="Saving stride") 29 | parsed_args = parser.parse_args(args) 30 | 31 | return parsed_args 32 | 33 | 34 | if __name__ == '__main__': 35 | 36 | args = get_args() 37 | 38 | # Velocity model 39 | # 3.5 km depth and 17 km width 40 | marmousi_model = read_2D_segy(args.model_file) 41 | 42 | language = args.language 43 | 44 | if language == 'c' or language == 'cpu_openmp': 45 | cc = 'gcc' 46 | cflags = '-O3 -fPIC -ffast-math -std=c99' 47 | elif language == 'gpu_openmp': 48 | cc = 'clang' 49 | cflags = '-O3 -fPIC -ffast-math -fopenmp \ 50 | -fopenmp-targets=nvptx64-nvidia-cuda \ 51 | -Xopenmp-target -march=sm_75' 52 | elif language == 'gpu_openacc': 53 | cc = 'pgcc' 54 | cflags = '-O3 -fPIC -acc:gpu -gpu=pinned' 55 | elif language == 'cuda': 56 | cc = 'nvcc' 57 | cflags = '-O3 -gencode arch=compute_75,code=sm_75 \ 58 | --compiler-options -fPIC,-Wall \ 59 | --use_fast_math -shared \ 60 | -DTX=32 -DTY=4 -DTZ=2' 61 | else: 62 | raise ValueError('Language not available') 63 | 64 | compiler = Compiler( 65 | cc=cc, 66 | language=language, 67 | cflags=cflags 68 | ) 69 | 70 | # create the space model 71 | space_model = SpaceModel( 72 | bounding_box=(0, 3500, 0, 17000), 73 | grid_spacing=(10.0, 10.0), 74 | velocity_model=marmousi_model, 75 | space_order=args.space_order, 76 | dtype=np.float64 77 | ) 78 | 79 | # config boundary conditions 80 | # (none, null_dirichlet or null_neumann) 81 | space_model.config_boundary( 82 | damping_length=(0, 700, 700, 700), 83 | boundary_condition=( 84 | "null_neumann", "null_dirichlet", 85 | "null_dirichlet", "null_dirichlet" 86 | ), 87 | damping_polynomial_degree=3, 88 | damping_alpha=0.001 89 | ) 90 | 91 | # create the time model 92 | time_model = TimeModel( 93 | space_model=space_model, 94 | tf=2, 95 | saving_stride=0 96 | ) 97 | 98 | # create the set of sources 99 | source = Source( 100 | space_model, 101 | coordinates=[(20, 8500)], 102 | window_radius=1 103 | ) 104 | 105 | # crete the set of receivers 106 | receiver = Receiver( 107 | space_model=space_model, 108 | coordinates=[(20, i) for i in range(0, 17000, 10)], 109 | window_radius=1 110 | ) 111 | 112 | # create a ricker wavelet with 10hz of peak frequency 113 | ricker = RickerWavelet(10.0, time_model) 114 | 115 | # create the solver 116 | solver = Solver( 117 | space_model=space_model, 118 | time_model=time_model, 119 | sources=source, 120 | receivers=receiver, 121 | wavelet=ricker, 122 | compiler=compiler 123 | ) 124 | 125 | # run the forward 126 | u_full, recv = solver.forward() 127 | 128 | print("u_full shape:", u_full.shape) 129 | print("timesteps:", time_model.timesteps) 130 | 131 | # remove damping extension from u_full 132 | u_full = space_model.remove_nbl(u_full) 133 | 134 | extent = [0, 17000, 3500, 0] 135 | 136 | plot_velocity_model(space_model.velocity_model, extent=extent) 137 | plot_wavefield(u_full[-1], extent=extent) 138 | plot_shotrecord(recv) 139 | -------------------------------------------------------------------------------- /benchmark/overthrust_3D.py: -------------------------------------------------------------------------------- 1 | from simwave import ( 2 | SpaceModel, TimeModel, RickerWavelet, Solver, 3 | Compiler, Receiver, Source 4 | ) 5 | import numpy as np 6 | import h5py 7 | import sys 8 | import argparse 9 | 10 | 11 | def get_args(args=sys.argv[1:]): 12 | 13 | parser = argparse.ArgumentParser(description='How to use this program') 14 | parser.add_argument("--model_file", type=str, 15 | help="Path to the velocity model file") 16 | parser.add_argument("--language", type=str, default="c", 17 | help="Language: c, cpu_openmp, gpu_openmp") 18 | parser.add_argument("--space_order", type=int, default=2, 19 | help="Space order") 20 | parser.add_argument("--stride", type=int, default=0, 21 | help="Saving stride") 22 | parsed_args = parser.parse_args(args) 23 | 24 | return parsed_args 25 | 26 | 27 | def read_model(filename): 28 | with h5py.File(filename, "r") as f: 29 | 30 | # Get the data 31 | data = list(f['m']) 32 | 33 | data = np.array(data) 34 | 35 | # convert to m/s 36 | data = (1 / (data ** (1 / 2))) * 1000.0 37 | 38 | return data 39 | 40 | 41 | if __name__ == '__main__': 42 | 43 | args = get_args() 44 | 45 | data = read_model(args.model_file) 46 | 47 | print("Overthrust original shape:", data.shape) 48 | 49 | language = args.language 50 | 51 | if language == 'c' or language == 'cpu_openmp': 52 | cc = 'gcc' 53 | cflags = '-O3 -fPIC -ffast-math -std=c99' 54 | elif language == 'gpu_openmp': 55 | cc = 'clang' 56 | cflags = '-O3 -fPIC -ffast-math -fopenmp \ 57 | -fopenmp-targets=nvptx64-nvidia-cuda \ 58 | -Xopenmp-target -march=sm_75' 59 | elif language == 'gpu_openacc': 60 | cc = 'pgcc' 61 | cflags = '-O3 -fPIC -acc:gpu -gpu=pinned' 62 | elif language == 'cuda': 63 | cc = 'nvcc' 64 | cflags = '-O3 -gencode arch=compute_75,code=sm_75 \ 65 | --compiler-options -fPIC,-Wall \ 66 | --use_fast_math -shared \ 67 | -DTX=32 -DTY=4 -DTZ=2' 68 | else: 69 | raise ValueError('Language not available') 70 | 71 | compiler = Compiler( 72 | cc=cc, 73 | language=language, 74 | cflags=cflags 75 | ) 76 | 77 | space_model = SpaceModel( 78 | bounding_box=(0, 4120, 0, 16000, 0, 16000), 79 | grid_spacing=(20., 20., 20.), 80 | velocity_model=data, 81 | space_order=args.space_order, 82 | dtype=np.float64 83 | ) 84 | 85 | space_model.config_boundary( 86 | damping_length=0, 87 | boundary_condition=( 88 | "null_neumann", "null_dirichlet", 89 | "null_dirichlet", "null_dirichlet", 90 | "null_dirichlet", "null_dirichlet" 91 | ), 92 | damping_polynomial_degree=3, 93 | damping_alpha=0.001 94 | ) 95 | 96 | time_model = TimeModel( 97 | space_model=space_model, 98 | tf=4, 99 | saving_stride=args.stride 100 | ) 101 | 102 | source = Source( 103 | space_model, 104 | coordinates=[(20, 8000, 8000)], 105 | window_radius=1 106 | ) 107 | 108 | receiver = Receiver( 109 | space_model=space_model, 110 | coordinates=[(20, 8000, i) for i in range(0, 16000, 20)], 111 | window_radius=1 112 | ) 113 | 114 | ricker = RickerWavelet(8.0, time_model) 115 | 116 | solver = Solver( 117 | space_model=space_model, 118 | time_model=time_model, 119 | sources=source, 120 | receivers=receiver, 121 | wavelet=ricker, 122 | compiler=compiler 123 | ) 124 | 125 | # run the forward 126 | u_full, recv = solver.forward() 127 | 128 | print("Timesteps:", time_model.timesteps) 129 | print("u_full shape:", u_full.shape) 130 | print("Receivers:", receiver.count) 131 | -------------------------------------------------------------------------------- /examples/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HPCSys-Lab/simwave/49e9c85d38020a4222aefa8c7224a103a40f9ba1/examples/.DS_Store -------------------------------------------------------------------------------- /examples/acoustic_2D.py: -------------------------------------------------------------------------------- 1 | from simwave import ( 2 | SpaceModel, TimeModel, RickerWavelet, Solver, Compiler, 3 | Receiver, Source, plot_wavefield, plot_shotrecord, plot_velocity_model 4 | ) 5 | import numpy as np 6 | 7 | 8 | # available language options: 9 | # c (sequential) 10 | # cpu_openmp (parallel CPU) 11 | # gpu_openmp (GPU) 12 | # gpu_openacc (GPU) 13 | compiler_options = { 14 | 'c': { 15 | 'cc': 'gcc', 16 | 'language': 'c', 17 | 'cflags': '-O3 -fPIC -ffast-math -Wall -std=c99 -shared' 18 | }, 19 | 'cpu_openmp': { 20 | 'cc': 'gcc', 21 | 'language': 'cpu_openmp', 22 | 'cflags': '-O3 -fPIC -ffast-math -Wall -std=c99 -shared -fopenmp' 23 | }, 24 | 'gpu_openmp': { 25 | 'cc': 'clang', 26 | 'language': 'gpu_openmp', 27 | 'cflags': '-O3 -fPIC -ffast-math -fopenmp \ 28 | -fopenmp-targets=nvptx64-nvidia-cuda \ 29 | -Xopenmp-target -march=sm_75' 30 | }, 31 | 'gpu_openacc': { 32 | 'cc': 'pgcc', 33 | 'language': 'gpu_openacc', 34 | 'cflags': '-O3 -fPIC -acc:gpu -gpu=pinned -mp' 35 | }, 36 | } 37 | 38 | selected_compiler = compiler_options['c'] 39 | 40 | # set compiler options 41 | compiler = Compiler( 42 | cc=selected_compiler['cc'], 43 | language=selected_compiler['language'], 44 | cflags=selected_compiler['cflags'] 45 | ) 46 | 47 | # Velocity model 48 | vel = np.zeros(shape=(512, 512), dtype=np.float32) 49 | vel[:] = 1500.0 50 | vel[100:] = 2000.0 51 | 52 | # create the space model 53 | space_model = SpaceModel( 54 | bounding_box=(0, 5120, 0, 5120), 55 | grid_spacing=(10, 10), 56 | velocity_model=vel, 57 | space_order=4, 58 | dtype=np.float32 59 | ) 60 | 61 | # config boundary conditions 62 | # (none, null_dirichlet or null_neumann) 63 | space_model.config_boundary( 64 | damping_length=(0, 510, 510, 510), 65 | boundary_condition=( 66 | "null_neumann", "null_dirichlet", 67 | "null_dirichlet", "null_dirichlet" 68 | ) 69 | ) 70 | 71 | print(' damping_alpha=',space_model.damping_alpha) 72 | 73 | # create the time model 74 | time_model = TimeModel( 75 | space_model=space_model, 76 | tf=2.0, 77 | saving_stride=0 78 | ) 79 | 80 | # create the set of sources 81 | source = Source( 82 | space_model, 83 | coordinates=[(2560, 2560)], 84 | window_radius=4 85 | ) 86 | 87 | # crete the set of receivers 88 | receiver = Receiver( 89 | space_model=space_model, 90 | coordinates=[(2560, i) for i in range(0, 5120, 10)], 91 | window_radius=4 92 | ) 93 | 94 | # create a ricker wavelet with 10hz of peak frequency 95 | ricker = RickerWavelet(10.0, time_model) 96 | 97 | # create the solver 98 | solver = Solver( 99 | space_model=space_model, 100 | time_model=time_model, 101 | sources=source, 102 | receivers=receiver, 103 | wavelet=ricker, 104 | compiler=compiler 105 | ) 106 | 107 | print("Timesteps:", time_model.timesteps) 108 | 109 | # run the forward 110 | u_full, recv = solver.forward() 111 | 112 | print("u_full shape:", u_full.shape) 113 | plot_velocity_model(space_model.velocity_model) 114 | plot_wavefield(u_full[-1]) 115 | plot_shotrecord(recv) 116 | -------------------------------------------------------------------------------- /examples/acoustic_2D_density.py: -------------------------------------------------------------------------------- 1 | from simwave import ( 2 | SpaceModel, TimeModel, RickerWavelet, Solver, Compiler, 3 | Receiver, Source, plot_wavefield, plot_shotrecord, plot_velocity_model 4 | ) 5 | import numpy as np 6 | 7 | 8 | # available language options: 9 | # c (sequential) 10 | # cpu_openmp (parallel CPU) 11 | # gpu_openmp (GPU) 12 | # gpu_openacc (GPU) 13 | compiler_options = { 14 | 'c': { 15 | 'cc': 'gcc', 16 | 'language': 'c', 17 | 'cflags': '-O3 -fPIC -ffast-math -Wall -std=c99 -shared' 18 | }, 19 | 'cpu_openmp': { 20 | 'cc': 'gcc', 21 | 'language': 'cpu_openmp', 22 | 'cflags': '-O3 -fPIC -ffast-math -Wall -std=c99 -shared -fopenmp' 23 | }, 24 | 'gpu_openmp': { 25 | 'cc': 'clang', 26 | 'language': 'gpu_openmp', 27 | 'cflags': '-O3 -fPIC -ffast-math -fopenmp \ 28 | -fopenmp-targets=nvptx64-nvidia-cuda \ 29 | -Xopenmp-target -march=sm_75' 30 | }, 31 | 'gpu_openacc': { 32 | 'cc': 'pgcc', 33 | 'language': 'gpu_openacc', 34 | 'cflags': '-O3 -fPIC -acc:gpu -gpu=pinned -mp' 35 | }, 36 | } 37 | 38 | selected_compiler = compiler_options['c'] 39 | 40 | # set compiler options 41 | compiler = Compiler( 42 | cc=selected_compiler['cc'], 43 | language=selected_compiler['language'], 44 | cflags=selected_compiler['cflags'] 45 | ) 46 | 47 | # Velocity model 48 | vel = np.zeros(shape=(512, 512), dtype=np.float32) 49 | vel[:] = 1500.0 50 | vel[100:] = 2000.0 51 | 52 | # Density model 53 | density = np.zeros(shape=(512, 512), dtype=np.float32) 54 | density[:] = 10 55 | 56 | # create the space model 57 | space_model = SpaceModel( 58 | bounding_box=(0, 5120, 0, 5120), 59 | grid_spacing=(10, 10), 60 | velocity_model=vel, 61 | density_model=density, 62 | space_order=2, 63 | dtype=np.float32 64 | ) 65 | 66 | # config boundary conditions 67 | # (none, null_dirichlet or null_neumann) 68 | space_model.config_boundary( 69 | damping_length=0, 70 | boundary_condition=( 71 | "null_neumann", "null_dirichlet", 72 | "none", "null_dirichlet" 73 | ), 74 | damping_polynomial_degree=3, 75 | damping_alpha=0.001 76 | ) 77 | 78 | # create the time model 79 | time_model = TimeModel( 80 | space_model=space_model, 81 | tf=1.0, 82 | saving_stride=0 83 | ) 84 | 85 | # create the set of sources 86 | source = Source( 87 | space_model, 88 | coordinates=[(2560, 2560)], 89 | window_radius=4 90 | ) 91 | 92 | # crete the set of receivers 93 | receiver = Receiver( 94 | space_model=space_model, 95 | coordinates=[(2560, i) for i in range(0, 5120, 10)], 96 | window_radius=4 97 | ) 98 | 99 | # create a ricker wavelet with 10hz of peak frequency 100 | ricker = RickerWavelet(10.0, time_model) 101 | 102 | # create the solver 103 | solver = Solver( 104 | space_model=space_model, 105 | time_model=time_model, 106 | sources=source, 107 | receivers=receiver, 108 | wavelet=ricker, 109 | compiler=compiler 110 | ) 111 | 112 | # run the forward 113 | u_full, recv = solver.forward() 114 | 115 | print("u_full shape:", u_full.shape) 116 | plot_velocity_model(space_model.velocity_model) 117 | plot_wavefield(u_full[-1]) 118 | plot_shotrecord(recv) 119 | -------------------------------------------------------------------------------- /examples/acoustic_3D.py: -------------------------------------------------------------------------------- 1 | from simwave import ( 2 | SpaceModel, TimeModel, RickerWavelet, Solver, Compiler, 3 | Receiver, Source, plot_wavefield, plot_shotrecord 4 | ) 5 | import numpy as np 6 | 7 | # available language options: 8 | # c (sequential) 9 | # cpu_openmp (parallel CPU) 10 | # gpu_openmp (GPU) 11 | # gpu_openacc (GPU) 12 | compiler_options = { 13 | 'c': { 14 | 'cc': 'gcc', 15 | 'language': 'c', 16 | 'cflags': '-O3 -fPIC -ffast-math -Wall -std=c99 -shared' 17 | }, 18 | 'cpu_openmp': { 19 | 'cc': 'gcc', 20 | 'language': 'cpu_openmp', 21 | 'cflags': '-O3 -fPIC -ffast-math -Wall -std=c99 -shared -fopenmp' 22 | }, 23 | 'gpu_openmp': { 24 | 'cc': 'clang', 25 | 'language': 'gpu_openmp', 26 | 'cflags': '-O3 -fPIC -ffast-math -fopenmp \ 27 | -fopenmp-targets=nvptx64-nvidia-cuda \ 28 | -Xopenmp-target -march=sm_75' 29 | }, 30 | 'gpu_openacc': { 31 | 'cc': 'pgcc', 32 | 'language': 'gpu_openacc', 33 | 'cflags': '-O3 -fPIC -acc:gpu -gpu=pinned -mp -DDEVICEID=2' 34 | }, 35 | } 36 | 37 | selected_compiler = compiler_options['c'] 38 | 39 | # set compiler options 40 | compiler = Compiler( 41 | cc=selected_compiler['cc'], 42 | language=selected_compiler['language'], 43 | cflags=selected_compiler['cflags'] 44 | ) 45 | 46 | # Velocity model 47 | vel = np.zeros(shape=(100, 100, 100), dtype=np.float32) 48 | vel[:] = 1500.0 49 | 50 | # create the space model 51 | space_model = SpaceModel( 52 | bounding_box=(0, 1000, 0, 1000, 0, 1000), 53 | grid_spacing=(10, 10, 10), 54 | velocity_model=vel, 55 | space_order=4, 56 | dtype=np.float32 57 | ) 58 | 59 | # config boundary conditions 60 | # (none, null_dirichlet or null_neumann) 61 | space_model.config_boundary( 62 | damping_length=0, 63 | boundary_condition=( 64 | "null_neumann", "null_dirichlet", 65 | "null_dirichlet", "null_dirichlet", 66 | "null_dirichlet", "null_dirichlet" 67 | ), 68 | damping_polynomial_degree=3, 69 | damping_alpha=0.001 70 | ) 71 | 72 | # create the time model 73 | time_model = TimeModel( 74 | space_model=space_model, 75 | tf=0.4, 76 | saving_stride=0 77 | ) 78 | 79 | # create the set of sources 80 | source = Source( 81 | space_model, 82 | coordinates=[(500, 500, 500)], 83 | window_radius=4 84 | ) 85 | 86 | # crete the set of receivers 87 | receiver = Receiver( 88 | space_model=space_model, 89 | coordinates=[(500, 500, i) for i in range(0, 1000, 10)], 90 | window_radius=4 91 | ) 92 | 93 | # create a ricker wavelet with 10hz of peak frequency 94 | ricker = RickerWavelet(15.0, time_model) 95 | 96 | # create the solver 97 | solver = Solver( 98 | space_model=space_model, 99 | time_model=time_model, 100 | sources=source, 101 | receivers=receiver, 102 | wavelet=ricker, 103 | compiler=compiler 104 | ) 105 | 106 | print("Timesteps:", time_model.timesteps) 107 | 108 | # run the forward 109 | u_full, recv = solver.forward() 110 | 111 | print("u_full shape:", u_full.shape) 112 | plot_wavefield(u_full[-1, 50, :, :]) 113 | plot_shotrecord(recv) 114 | -------------------------------------------------------------------------------- /examples/acoustic_3D_density.py: -------------------------------------------------------------------------------- 1 | from simwave import ( 2 | SpaceModel, TimeModel, RickerWavelet, Solver, Compiler, 3 | Receiver, Source, plot_wavefield, plot_shotrecord 4 | ) 5 | import numpy as np 6 | 7 | 8 | # available language options: 9 | # c (sequential) 10 | # cpu_openmp (parallel CPU) 11 | # gpu_openmp (GPU) 12 | # gpu_openacc (GPU) 13 | compiler_options = { 14 | 'c': { 15 | 'cc': 'gcc', 16 | 'language': 'c', 17 | 'cflags': '-O3 -fPIC -ffast-math -Wall -std=c99 -shared' 18 | }, 19 | 'cpu_openmp': { 20 | 'cc': 'gcc', 21 | 'language': 'cpu_openmp', 22 | 'cflags': '-O3 -fPIC -ffast-math -Wall -std=c99 -shared -fopenmp' 23 | }, 24 | 'gpu_openmp': { 25 | 'cc': 'clang', 26 | 'language': 'gpu_openmp', 27 | 'cflags': '-O3 -fPIC -ffast-math -fopenmp \ 28 | -fopenmp-targets=nvptx64-nvidia-cuda \ 29 | -Xopenmp-target -march=sm_75' 30 | }, 31 | 'gpu_openacc': { 32 | 'cc': 'pgcc', 33 | 'language': 'gpu_openacc', 34 | 'cflags': '-O3 -fPIC -acc:gpu -gpu=pinned -mp' 35 | }, 36 | } 37 | 38 | selected_compiler = compiler_options['c'] 39 | 40 | # set compiler options 41 | compiler = Compiler( 42 | cc=selected_compiler['cc'], 43 | language=selected_compiler['language'], 44 | cflags=selected_compiler['cflags'] 45 | ) 46 | 47 | # Velocity model 48 | vel = np.zeros(shape=(100, 100, 100), dtype=np.float32) 49 | vel[:] = 1500.0 50 | 51 | # Density model 52 | density = np.zeros(shape=(100, 100, 100), dtype=np.float32) 53 | density[:] = 10 54 | 55 | # create the space model 56 | space_model = SpaceModel( 57 | bounding_box=(0, 1000, 0, 1000, 0, 1000), 58 | grid_spacing=(10, 10, 10), 59 | velocity_model=vel, 60 | density_model=density, 61 | space_order=4, 62 | dtype=np.float32 63 | ) 64 | 65 | # config boundary conditions 66 | # (none, null_dirichlet or null_neumann) 67 | space_model.config_boundary( 68 | damping_length=0, 69 | boundary_condition=( 70 | "null_neumann", "null_dirichlet", 71 | "null_dirichlet", "null_dirichlet", 72 | "null_dirichlet", "null_dirichlet" 73 | ), 74 | damping_polynomial_degree=3, 75 | damping_alpha=0.001 76 | ) 77 | 78 | # create the time model 79 | time_model = TimeModel( 80 | space_model=space_model, 81 | tf=0.4, 82 | saving_stride=0 83 | ) 84 | 85 | # create the set of sources 86 | source = Source( 87 | space_model, 88 | coordinates=[(500, 500, 500)], 89 | window_radius=4 90 | ) 91 | 92 | # crete the set of receivers 93 | receiver = Receiver( 94 | space_model=space_model, 95 | coordinates=[(500, 500, i) for i in range(0, 1000, 10)], 96 | window_radius=4 97 | ) 98 | 99 | # create a ricker wavelet with 10hz of peak frequency 100 | ricker = RickerWavelet(15.0, time_model) 101 | 102 | # create the solver 103 | solver = Solver( 104 | space_model=space_model, 105 | time_model=time_model, 106 | sources=source, 107 | receivers=receiver, 108 | wavelet=ricker, 109 | compiler=compiler 110 | ) 111 | 112 | print("Timesteps:", time_model.timesteps) 113 | 114 | # run the forward 115 | u_full, recv = solver.forward() 116 | 117 | print("u_full shape:", u_full.shape) 118 | plot_wavefield(u_full[-1, 50, :, :]) 119 | plot_shotrecord(recv) 120 | -------------------------------------------------------------------------------- /examples/marmousi_2D.py: -------------------------------------------------------------------------------- 1 | from simwave import ( 2 | SpaceModel, TimeModel, RickerWavelet, Solver, Compiler, 3 | Receiver, Source, read_2D_segy, 4 | plot_wavefield, plot_shotrecord, plot_velocity_model 5 | ) 6 | import numpy as np 7 | 8 | ''' 9 | Use the Marmousi 2 model available in 10 | https://s3.amazonaws.com/open.source.geoscience/open_data/elastic-marmousi/elastic-marmousi-model.tar.gz 11 | http://mcee.ou.edu/aaspi/publications/2006/martin_etal_TLE2006.pdf 12 | ''' 13 | 14 | # Velocity model 15 | # 3.5 km depth and 17 km width 16 | marmousi_model = read_2D_segy('MODEL_P-WAVE_VELOCITY_1.25m.segy') 17 | 18 | compiler = Compiler( 19 | cc='gcc', 20 | language='c', 21 | cflags='-O3 -fPIC -ffast-math -Wall -std=c99 -shared' 22 | ) 23 | 24 | # create the space model 25 | space_model = SpaceModel( 26 | bounding_box=(0, 3500, 0, 17000), 27 | grid_spacing=(10.0, 10.0), 28 | velocity_model=marmousi_model, 29 | space_order=4, 30 | dtype=np.float64 31 | ) 32 | 33 | # config boundary conditions 34 | # (none, null_dirichlet or null_neumann) 35 | space_model.config_boundary( 36 | damping_length=(0, 700, 700, 700), 37 | boundary_condition=( 38 | "null_neumann", "null_dirichlet", 39 | "null_dirichlet", "null_dirichlet" 40 | ), 41 | damping_polynomial_degree=3, 42 | damping_alpha=0.001 43 | ) 44 | 45 | # create the time model 46 | time_model = TimeModel( 47 | space_model=space_model, 48 | tf=2, 49 | saving_stride=0 50 | ) 51 | 52 | # create the set of sources 53 | source = Source( 54 | space_model, 55 | coordinates=[(20, 8500)], 56 | window_radius=1 57 | ) 58 | 59 | # crete the set of receivers 60 | receiver = Receiver( 61 | space_model=space_model, 62 | coordinates=[(20, i) for i in range(0, 17000, 10)], 63 | window_radius=1 64 | ) 65 | 66 | # create a ricker wavelet with 10hz of peak frequency 67 | ricker = RickerWavelet(10.0, time_model) 68 | 69 | # create the solver 70 | solver = Solver( 71 | space_model=space_model, 72 | time_model=time_model, 73 | sources=source, 74 | receivers=receiver, 75 | wavelet=ricker, 76 | compiler=compiler 77 | ) 78 | 79 | # run the forward 80 | u_full, recv = solver.forward() 81 | 82 | # remove damping extension from u_full 83 | u_full = space_model.remove_nbl(u_full) 84 | 85 | extent = [0, 17000, 3500, 0] 86 | 87 | print("u_full shape:", u_full.shape) 88 | print("timesteps:", time_model.timesteps) 89 | plot_velocity_model(space_model.velocity_model, extent=extent) 90 | plot_wavefield(u_full[-1], extent=extent) 91 | plot_shotrecord(recv) 92 | -------------------------------------------------------------------------------- /miniwave/.gitignore: -------------------------------------------------------------------------------- 1 | plots/* 2 | R-miniwave* 3 | run.sh 4 | *.nvvp 5 | __pycache__ 6 | tmp/ 7 | *.sif 8 | data/ 9 | build/ -------------------------------------------------------------------------------- /miniwave/README.md: -------------------------------------------------------------------------------- 1 | # Miniwave - Minimum Simwave 2 | 3 | Miniwave is a streamlined version of Simwave, tailored mainly for educational use and benchmarking of computational systems. Its backend implements only the wave propagation kernel (no absorbing layers, boundary conditions, etc). miniwave.py is a Python wrapper for the forward wave propagation. 4 | 5 | # Dependencies 6 | 7 | `pip install numpy matplotlib findiff h5py` 8 | 9 | ``` 10 | mkdir /hdf5 && \ 11 | cd /hdf5 && \ 12 | wget https://support.hdfgroup.org/ftp/HDF5/releases/hdf5-1.14/hdf5-1.14.3/src/CMake-hdf5-1.14.3.tar.gz && \ 13 | tar xf CMake-hdf5-1.14.3.tar.gz && \ 14 | cd CMake-hdf5-1.14.3 && \ 15 | ./build-unix.sh && \ 16 | yes | ./HDF5-1.14.3-Linux.sh && \ 17 | cp -r HDF5-1.14.3-Linux/HDF_Group/HDF5/1.14.3/ ../build 18 | ``` 19 | 20 | # How to use 21 | 22 | `python3 miniwave.py --help` 23 | 24 | `python3 miniwave.py --file FILE --grid_size GRID_SIZE --num_timesteps NUM_TIMESTEPS --language {c,openmp,openacc,cuda,python} --space_order SPACE_ORDER` 25 | 26 | -------------------------------------------------------------------------------- /miniwave/c-frontend/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18) 2 | 3 | project(miniwave) 4 | 5 | # Set the kernel source file and header 6 | configure_file(${CMAKE_SOURCE_DIR}/selected_kernel.h.in 7 | ${CMAKE_BINARY_DIR}/selected_kernel.h) 8 | include_directories(${CMAKE_BINARY_DIR}) 9 | 10 | add_executable(miniwave ../${KERNEL_SOURCE} miniwave.c) 11 | 12 | # Include miniwave header files (sequential.h) 13 | target_include_directories(miniwave PUBLIC ${CMAKE_SOURCE_DIR}/../kernels) 14 | 15 | # HDF5 commands 16 | set(HDF5DIR "$ENV{HOME}/hdf5/build") 17 | target_include_directories(miniwave PRIVATE ${HDF5DIR}/include) 18 | target_link_directories(miniwave PRIVATE ${HDF5DIR}/lib) 19 | target_link_libraries(miniwave PRIVATE libhdf5.so libhdf5_cpp.so) 20 | -------------------------------------------------------------------------------- /miniwave/c-frontend/miniwave.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "hdf5.h" 6 | #ifdef CONTAINS_MPI 7 | #include "mpi.h" 8 | #endif 9 | #include "selected_kernel.h" 10 | 11 | hid_t open_hdf5_file(char *file) { 12 | return H5Fopen(file, H5F_ACC_RDONLY, H5P_DEFAULT); 13 | } 14 | hid_t open_hdf5_dataset(hid_t file_id, char *dataset) { 15 | return H5Dopen2(file_id, dataset, H5P_DEFAULT); 16 | } 17 | float *read_float_dataset(hid_t dataset_id) { 18 | hid_t dataspace; 19 | hsize_t dims_out[10]; 20 | int rank, total_size; 21 | 22 | dataspace = H5Dget_space(dataset_id); /* dataspace handle */ 23 | rank = H5Sget_simple_extent_ndims(dataspace); 24 | H5Sget_simple_extent_dims(dataspace, dims_out, NULL); 25 | 26 | total_size = 1; 27 | for (size_t i = 0; i < rank; i++) { 28 | total_size *= dims_out[i]; 29 | } 30 | 31 | float *dset_data = malloc(sizeof(float) * total_size); 32 | H5Dread(dataset_id, H5T_NATIVE_FLOAT, H5S_ALL, H5S_ALL, H5P_DEFAULT, 33 | dset_data); 34 | return dset_data; 35 | } 36 | float read_float_attribute(hid_t dataset_id, char *attribute_name) { 37 | // Gets attribute value from a dataset in a file 38 | // Ex: file example_data.h5 has a dataset named scalar_data 39 | // which has an attribute named 'attribute_X'. To get 40 | // the value of this attribute: 41 | // 42 | // float attribute_X = 43 | // read_float_attribute(scalar_data_dataset_id, "attribute_X"); 44 | 45 | 46 | const char *attribute_value; 47 | // Get attribute value as string 48 | hid_t attribute_id = H5Aopen(dataset_id, attribute_name, H5P_DEFAULT); 49 | hid_t attribute_type = H5Aget_type(attribute_id); 50 | H5Aread(attribute_id, attribute_type, &attribute_value); 51 | // Convert attribute value to float 52 | return atof(attribute_value); 53 | } 54 | void close_hdf5_dataset(hid_t dataset_id) { H5Dclose(dataset_id); } 55 | void close_hdf5_file(hid_t file_id) { H5Fclose(file_id); } 56 | 57 | int write_hdf5_result(int n1, int n2, int n3, double execution_time, f_type* next_u) { 58 | hid_t h5t_type = H5T_NATIVE_FLOAT; 59 | #if defined(DOUBLE) 60 | h5t_type = H5T_NATIVE_DOUBLE; 61 | #endif 62 | 63 | // Create a new HDF5 file using the default properties 64 | hid_t file_id = H5Fcreate("c-frontend/data/results.h5", H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT); 65 | if (file_id < 0) { 66 | printf("Error creating file.\n"); 67 | return 1; 68 | } 69 | 70 | // Define the dataspace for the vector dataset 71 | hsize_t vector_dims[3] = {n1,n2,n3}; // 3D vector 72 | hid_t vector_dataspace_id = H5Screate_simple(3, vector_dims, NULL); 73 | if (vector_dataspace_id < 0) { 74 | printf("Error creating vector dataspace.\n"); 75 | H5Fclose(file_id); 76 | return 1; 77 | } 78 | 79 | // Create the vector dataset with default properties 80 | hid_t vector_dataset_id = H5Dcreate(file_id, "vector", h5t_type, vector_dataspace_id, 81 | H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); 82 | if (vector_dataset_id < 0) { 83 | printf("Error creating vector dataset.\n"); 84 | H5Sclose(vector_dataspace_id); 85 | H5Fclose(file_id); 86 | return 1; 87 | } 88 | 89 | // Write the vector data to the dataset 90 | if (H5Dwrite(vector_dataset_id, h5t_type, H5S_ALL, H5S_ALL, H5P_DEFAULT, next_u) < 0) { 91 | printf("Error writing vector data.\n"); 92 | H5Dclose(vector_dataset_id); 93 | H5Sclose(vector_dataspace_id); 94 | H5Fclose(file_id); 95 | return 1; 96 | } 97 | 98 | // Define the dataspace for the execution time dataset 99 | hsize_t time_dims[1] = {1}; // Scalar 100 | hid_t time_dataspace_id = H5Screate_simple(1, time_dims, NULL); 101 | if (time_dataspace_id < 0) { 102 | printf("Error creating time dataspace.\n"); 103 | H5Dclose(vector_dataset_id); 104 | H5Sclose(vector_dataspace_id); 105 | H5Fclose(file_id); 106 | return 1; 107 | } 108 | 109 | // Create the execution time dataset with default properties 110 | hid_t time_dataset_id = H5Dcreate(file_id, "execution_time", H5T_NATIVE_DOUBLE, time_dataspace_id, 111 | H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); 112 | if (time_dataset_id < 0) { 113 | printf("Error creating time dataset.\n"); 114 | H5Dclose(vector_dataset_id); 115 | H5Sclose(vector_dataspace_id); 116 | H5Sclose(time_dataspace_id); 117 | H5Fclose(file_id); 118 | return 1; 119 | } 120 | 121 | // Write the execution time to the dataset 122 | if (H5Dwrite(time_dataset_id, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, &execution_time) < 0) { 123 | printf("Error writing time data.\n"); 124 | H5Dclose(vector_dataset_id); 125 | H5Sclose(vector_dataspace_id); 126 | H5Dclose(time_dataset_id); 127 | H5Sclose(time_dataspace_id); 128 | H5Fclose(file_id); 129 | return 1; 130 | } 131 | 132 | // Close the datasets, dataspaces, and file 133 | H5Dclose(vector_dataset_id); 134 | H5Sclose(vector_dataspace_id); 135 | H5Dclose(time_dataset_id); 136 | H5Sclose(time_dataspace_id); 137 | H5Fclose(file_id); 138 | return 0; 139 | } 140 | 141 | int main() { 142 | #ifdef CONTAINS_MPI 143 | MPI_Init(NULL, NULL); 144 | #endif 145 | 146 | int rank = 0; 147 | 148 | #ifdef CONTAINS_MPI 149 | MPI_Comm_rank(MPI_COMM_WORLD, &rank); 150 | #endif 151 | 152 | // Get the file id 153 | hid_t file_id = open_hdf5_file("c-frontend/data/miniwave_data.h5"); 154 | // Read arguments 155 | hid_t vel_model_id = open_hdf5_dataset(file_id, "vel_model"); 156 | hid_t next_u_id = open_hdf5_dataset(file_id, "next_u"); 157 | hid_t prev_u_id = open_hdf5_dataset(file_id, "prev_u"); 158 | hid_t coefficient_id = open_hdf5_dataset(file_id, "coefficient"); 159 | hid_t scalar_data_id = open_hdf5_dataset(file_id, "scalar_data"); 160 | float *vel_model = read_float_dataset(vel_model_id); 161 | float *next_u = read_float_dataset(next_u_id); 162 | float *prev_u = read_float_dataset(prev_u_id); 163 | float *coefficient = read_float_dataset(coefficient_id); 164 | float block_size_1 = read_float_attribute(scalar_data_id, "block_size_1"); 165 | float block_size_2 = read_float_attribute(scalar_data_id, "block_size_2"); 166 | float block_size_3 = read_float_attribute(scalar_data_id, "block_size_3"); 167 | float d1 = read_float_attribute(scalar_data_id, "d1"); 168 | float d2 = read_float_attribute(scalar_data_id, "d2"); 169 | float d3 = read_float_attribute(scalar_data_id, "d3"); 170 | float dt = read_float_attribute(scalar_data_id, "dt"); 171 | float iterations = read_float_attribute(scalar_data_id, "iterations"); 172 | float n1 = read_float_attribute(scalar_data_id, "n1"); 173 | float n2 = read_float_attribute(scalar_data_id, "n2"); 174 | float n3 = read_float_attribute(scalar_data_id, "n3"); 175 | float stencil_radius = read_float_attribute(scalar_data_id, "stencil_radius"); 176 | 177 | if (rank == 0) { 178 | printf("vel_model[0:50]:\n"); 179 | for (size_t i = 0; i < 50; i++) { 180 | printf("%f ", vel_model[i]); 181 | } 182 | printf("\n"); 183 | printf("next_u[0:50]:\n"); 184 | for (size_t i = 0; i < 50; i++) { 185 | printf("%lf ", next_u[i]); 186 | } 187 | printf("\n"); 188 | printf("prev_u[0:50]:\n"); 189 | for (size_t i = 0; i < 50; i++) { 190 | printf("%lf ", prev_u[i]); 191 | } 192 | printf("\n"); 193 | printf("coefficient:\n"); 194 | for (size_t i = 0; i < 2; i++) { 195 | printf("%lf ", coefficient[i]); 196 | } 197 | printf("\n"); 198 | printf("block_size_1: %f\n",block_size_1); 199 | printf("block_size_2: %f\n",block_size_2); 200 | printf("block_size_3: %f\n",block_size_3); 201 | printf("d1: %f\n",d1); 202 | printf("d2: %f\n",d2); 203 | printf("d3: %f\n",d3); 204 | printf("dt: %f\n",dt); 205 | printf("iterations: %f\n",iterations); 206 | printf("n1: %f\n",n1); 207 | printf("n2: %f\n",n2); 208 | printf("n3: %f\n",n3); 209 | printf("stencil_radius: %f\n",stencil_radius); 210 | } 211 | 212 | clock_t start_time = clock(); 213 | 214 | forward(prev_u, next_u, vel_model, coefficient, d1, d2, d3, dt, n1, n2, n3, iterations, stencil_radius, block_size_1, block_size_2, block_size_3); 215 | 216 | clock_t end_time = clock(); 217 | double execution_time = ((double)(end_time - start_time)) / CLOCKS_PER_SEC; 218 | 219 | if (rank == 0) { 220 | printf("\nprev_u[0:50]"); 221 | for (size_t i = 0; i < 50; i++) { 222 | printf("%lf ", prev_u[i]); 223 | } 224 | printf("\n"); 225 | 226 | write_hdf5_result(n1, n2, n3, execution_time, next_u); 227 | } 228 | 229 | close_hdf5_dataset(vel_model_id); 230 | close_hdf5_dataset(next_u_id); 231 | close_hdf5_dataset(prev_u_id); 232 | close_hdf5_dataset(coefficient_id); 233 | close_hdf5_file(file_id); 234 | 235 | #ifdef CONTAINS_MPI 236 | MPI_Finalize(); 237 | #endif 238 | } -------------------------------------------------------------------------------- /miniwave/c-frontend/selected_kernel.h.in: -------------------------------------------------------------------------------- 1 | #ifndef SELECTED_KERNEL_H 2 | #define SELECTED_KERNEL_H 3 | 4 | #include "../@KERNEL_HEADER@" 5 | 6 | #endif // SELECTED_KERNEL_H 7 | -------------------------------------------------------------------------------- /miniwave/kernels/sequential.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "sequential.h" 5 | 6 | int forward(f_type *prev_u, f_type *next_u, f_type *vel_model, f_type *coefficient, 7 | f_type d1, f_type d2, f_type d3, f_type dt, int n1, int n2, int n3, 8 | int iterations, int stencil_radius, 9 | int block_size_1, int block_size_2, int block_size_3 10 | ){ 11 | 12 | f_type d1Squared = d1 * d1; 13 | f_type d2Squared = d2 * d2; 14 | f_type d3Squared = d3 * d3; 15 | f_type dtSquared = dt * dt; 16 | 17 | for(int t = 0; t < iterations; t++) { 18 | 19 | for(int i = stencil_radius; i < n1 - stencil_radius; i++){ 20 | for(int j = stencil_radius; j < n2 - stencil_radius; j++){ 21 | for(int k = stencil_radius; k < n3 - stencil_radius; k++){ 22 | // index of the current point in the grid 23 | int current = (i * n2 + j) * n3 + k; 24 | 25 | // stencil code to update grid 26 | f_type value = coefficient[0] * (prev_u[current]/d1Squared + prev_u[current]/d2Squared + prev_u[current]/d3Squared); 27 | 28 | // radius of the stencil 29 | for(int ir = 1; ir <= stencil_radius; ir++){ 30 | value += coefficient[ir] * ( 31 | ( (prev_u[current + ir] + prev_u[current - ir]) / d3Squared ) + //neighbors in the third axis 32 | ( (prev_u[current + (ir * n3)] + prev_u[current - (ir * n3)]) / d2Squared ) + //neighbors in the second axis 33 | ( (prev_u[current + (ir * n2 * n3)] + prev_u[current - (ir * n2 * n3)]) / d1Squared )); //neighbors in the first axis 34 | } 35 | 36 | value *= dtSquared * vel_model[current] * vel_model[current]; 37 | next_u[current] = 2.0 * prev_u[current] - next_u[current] + value; 38 | } 39 | } 40 | } 41 | 42 | // swap arrays for next iteration 43 | f_type *swap = next_u; 44 | next_u = prev_u; 45 | prev_u = swap; 46 | } 47 | 48 | return 0; 49 | } 50 | -------------------------------------------------------------------------------- /miniwave/kernels/sequential.h: -------------------------------------------------------------------------------- 1 | 2 | // use single (float) or double precision 3 | // according to the value passed in the compilation cmd 4 | #if defined(FLOAT) 5 | typedef float f_type; 6 | #elif defined(DOUBLE) 7 | typedef double f_type; 8 | #else 9 | typedef float f_type; 10 | #endif 11 | 12 | int forward(f_type *prev_u, f_type *next_u, f_type *vel_model, f_type *coefficient, 13 | f_type d1, f_type d2, f_type d3, f_type dt, int n1, int n2, int n3, 14 | int iterations, int stencil_radius, 15 | int block_size_1, int block_size_2, int block_size_3 16 | ); -------------------------------------------------------------------------------- /miniwave/miniwave.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import argparse 3 | import sys 4 | from utils import Model, Compiler, Kernel, plot 5 | from utils.properties import Properties 6 | 7 | 8 | def get_args(args=sys.argv[1:]): 9 | 10 | parser = argparse.ArgumentParser(description='How to use this program') 11 | 12 | parser.add_argument("--file", type=str, default='kernels/sequential.c', 13 | help="Path to the Kernel file") 14 | 15 | parser.add_argument("--grid_size", type=int, default=256, 16 | help="Grid size") 17 | 18 | parser.add_argument("--num_timesteps", type=int, default=400, 19 | help="Number of timesteps") 20 | 21 | parser.add_argument( 22 | "--language", 23 | type=str, 24 | default="c", 25 | choices=[ 26 | 'c', 27 | 'openmp', 28 | 'openmp_cpu', 29 | 'openacc', 30 | 'cuda', 31 | 'python', 32 | 'mpi', 33 | 'mpi_cuda', 34 | 'ompc' 35 | ], 36 | help="Language: c, openmp, openacc, cuda, python, ompc, mpi, mpi_cuda" 37 | ) 38 | 39 | parser.add_argument("--space_order", type=int, default=2, 40 | help="Space order") 41 | 42 | parser.add_argument("--block_size_1", type=int, default=1, 43 | help="GPU block size in the first axis") 44 | 45 | parser.add_argument("--block_size_2", type=int, default=1, 46 | help="GPU block size in the second axis") 47 | 48 | parser.add_argument("--block_size_3", type=int, default=1, 49 | help="GPU block size in the third axis") 50 | 51 | parser.add_argument("--sm", type=int, default=75, 52 | help="Cuda capability") 53 | 54 | parser.add_argument( 55 | "--fast_math", 56 | default=False, 57 | action="store_true", 58 | help="Enable --fast-math flag" 59 | ) 60 | 61 | parser.add_argument( 62 | "--plot", 63 | default=False, 64 | action="store_true", 65 | help="Enable ploting" 66 | ) 67 | 68 | parser.add_argument( 69 | "--dtype", 70 | type=str, 71 | default="float64", 72 | help="Float Precision. float32 or float64 (default)" 73 | ) 74 | 75 | parsed_args = parser.parse_args(args) 76 | 77 | return parsed_args 78 | 79 | 80 | if __name__ == "__main__": 81 | 82 | args = get_args() 83 | 84 | # enable/disable fast math 85 | fast_math = args.fast_math 86 | 87 | # cuda capability 88 | sm = args.sm 89 | 90 | # language 91 | language = args.language 92 | 93 | # float precision 94 | dtype = args.dtype 95 | 96 | # create a compiler object 97 | compiler = Compiler(language=language, sm=sm, fast_math=fast_math) 98 | 99 | # define grid shape 100 | grid_size = (args.grid_size, args.grid_size, args.grid_size) 101 | 102 | vel_model = np.ones(shape=grid_size) * 1500.0 103 | 104 | model = Model( 105 | velocity_model=vel_model, 106 | grid_spacing=(10, 10, 10), 107 | dt=0.002, 108 | num_timesteps=args.num_timesteps, 109 | space_order=args.space_order, 110 | dtype=dtype 111 | ) 112 | 113 | # GPU block sizes 114 | properties = Properties( 115 | block_size_1=args.block_size_1, 116 | block_size_2=args.block_size_2, 117 | block_size_3=args.block_size_3 118 | ) 119 | 120 | solver = Kernel( 121 | file=args.file, 122 | model=model, 123 | compiler=compiler, 124 | properties=properties 125 | ) 126 | 127 | # run the kernel 128 | exec_time, u = solver.run() 129 | 130 | # plot a slice 131 | if args.plot: 132 | slice = vel_model.shape[1] // 2 133 | plot(u[:, slice, :]) 134 | 135 | print(f"Execution time: {exec_time} seconds") 136 | -------------------------------------------------------------------------------- /miniwave/selected_kernel.h: -------------------------------------------------------------------------------- 1 | #ifndef SELECTED_KERNEL_H 2 | #define SELECTED_KERNEL_H 3 | 4 | #include "{KERNEL_HEADER}" 5 | 6 | #endif // SELECTED_KERNEL_H 7 | -------------------------------------------------------------------------------- /miniwave/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .model import Model 3 | from .compiler import Compiler 4 | from .kernel import Kernel 5 | from .plot import plot 6 | from .properties import Properties 7 | -------------------------------------------------------------------------------- /miniwave/utils/compiler.py: -------------------------------------------------------------------------------- 1 | import os 2 | from hashlib import sha1 3 | from typing import Optional 4 | from utils.properties import Properties 5 | 6 | 7 | class Compiler: 8 | """ 9 | Base class to implement the runtime compiler. 10 | 11 | Parameters 12 | ---------- 13 | language : str 14 | Kernel language. 15 | sm : int 16 | Cuda capability. 17 | fast_math : bool 18 | Enable fast_math. Default is False. 19 | """ 20 | 21 | def __init__( 22 | self, 23 | language: str, 24 | sm: int, 25 | fast_math: Optional[bool] = False 26 | ): 27 | self._language = language 28 | self._sm = sm 29 | self._fast_math = fast_math 30 | 31 | self._define_default_flags() 32 | 33 | @property 34 | def language(self) -> str: 35 | return self._language 36 | 37 | @property 38 | def sm(self) -> int: 39 | return self._sm 40 | 41 | @property 42 | def fast_math(self) -> bool: 43 | return self._fast_math 44 | 45 | @property 46 | def cc(self) -> str: 47 | return self._cc 48 | 49 | @cc.setter 50 | def cc(self, value: str) -> None: 51 | self._cc = value 52 | 53 | @property 54 | def flags(self) -> str: 55 | return self._flags 56 | 57 | @flags.setter 58 | def flags(self, value: str) -> None: 59 | self._flags = value 60 | 61 | def _define_default_flags(self) -> None: 62 | 63 | # fast math for GNU and CLANG 64 | if self.fast_math: 65 | fast_math = "-ffast-math" 66 | else: 67 | fast_math = "" 68 | 69 | if self.language == 'c': 70 | self.cc = 'gcc' 71 | self.flags = f'-O3 -fPIC {fast_math} -shared' 72 | 73 | elif self.language == 'openmp': 74 | self.cc = 'clang' 75 | self.flags = f'-O3 -fPIC {fast_math} -fopenmp -fopenmp-version=51 \ 76 | -fopenmp-targets=nvptx64 -Xopenmp-target -march=sm_{self.sm} \ 77 | -shared -I/usr/local/cuda/include -L/usr/local/cuda/lib64 \ 78 | -lcudart -g' 79 | 80 | elif self.language == 'mpi': 81 | self.cc = 'mpicc' 82 | self.flags = f'-O3 -fPIC {fast_math} -shared' 83 | 84 | elif self.language == 'mpi_cuda': 85 | self.cc = 'mpicc' 86 | self.flags = f'-O3 -fPIC {fast_math} -L/usr/local/cuda/lib64 \ 87 | -lcudart -I/usr/local/cuda/include \ 88 | --offload-arch=sm_{self.sm} -shared' 89 | 90 | elif self.language == 'openmp_cpu': 91 | self.cc = 'gcc' 92 | self.flags = f'-O3 -fPIC {fast_math} -fopenmp -shared' 93 | 94 | elif self.language == 'openacc': 95 | 96 | if self.fast_math: 97 | fast_math = ",fastmath" 98 | 99 | self.cc = 'pgcc' 100 | self.flags = f'-O3 -fPIC -acc:gpu -gpu=pinned{fast_math} -shared' 101 | 102 | elif self.language == 'cuda': 103 | if self.fast_math: 104 | fast_math = "--use_fast_math" 105 | 106 | self.cc = 'nvcc' 107 | self.flags = f'-O3 -gencode \ 108 | arch=compute_{self.sm},code=sm_{self.sm} \ 109 | --compiler-options -fPIC,-Wall {fast_math} -shared' 110 | 111 | elif self.language == 'python': 112 | pass 113 | 114 | elif self.language == 'ompc': 115 | self.cc = 'clang' 116 | self.flags = f'-O3 -fPIC {fast_math} -fopenmp \ 117 | -fopenmp-targets=x86_64-pc-linux-gnu -shared' 118 | 119 | else: 120 | raise Exception("Language not supported") 121 | 122 | def compile( 123 | self, 124 | file: str, 125 | properties: Optional[Properties] = None 126 | ) -> str: 127 | 128 | object_dir = "/tmp/miniwave/" 129 | 130 | # create a dir to save the compiled shared object 131 | os.makedirs(object_dir, exist_ok=True) 132 | 133 | # get c file content 134 | with open(file, 'r', encoding='utf-8') as f: 135 | file_content = f.read() 136 | 137 | # float precision 138 | type = { 139 | 'float32': '-DFLOAT', 140 | 'float64': '-DDOUBLE' 141 | } 142 | 143 | float_precision = type[properties.dtype] 144 | 145 | b1, b2, b3 = properties.block3 146 | stencil_radius = properties.stencil_radius 147 | 148 | c_def = f" -DBLOCK1={b1} -DBLOCK2={b2} -DBLOCK3={b3} \ 149 | -DSTENCIL_RADIUS={stencil_radius} {float_precision}" 150 | 151 | # add defined properties to flags 152 | self.flags += c_def 153 | 154 | # compose the object string 155 | object_str = "{} {} {}".format(self.cc, self.flags, file_content) 156 | 157 | # apply sha1 hash to name the object 158 | hash = sha1() 159 | hash.update(object_str.encode()) 160 | object_name = hash.hexdigest() + ".so" 161 | 162 | # object complete path 163 | object_path = object_dir + object_name 164 | 165 | # check if object_file already exists 166 | if os.path.exists(object_path): 167 | print("Shared object already compiled in:", object_path) 168 | else: 169 | cmd = self.cc + " " + file + " " 170 | + self.flags + " -o " + object_path 171 | 172 | print("Compilation command:", cmd) 173 | 174 | # execute the command 175 | if os.system(cmd) != 0: 176 | raise Exception("Compilation failed") 177 | 178 | return object_path 179 | -------------------------------------------------------------------------------- /miniwave/utils/dataset_writer.py: -------------------------------------------------------------------------------- 1 | import h5py 2 | import numpy as np 3 | import os 4 | 5 | 6 | class DatasetWriter: 7 | def __init__(self) -> None: 8 | pass 9 | 10 | def write_dataset(data: dict, path: str): 11 | """Writes HDF5 file from data. 12 | Non array data written to "scalar_data" dataset as attributes. 13 | 14 | :param data: dictionary containing data to be written. 15 | Object format: 16 | 17 | data: { 18 | [dataset_name]: { 19 | dataset_data: np.ndarray | int | float, 20 | dataset_attributes: {[attribute_name]: str} 21 | } 22 | } 23 | 24 | Example: 25 | 26 | ```python 27 | data = { 28 | "my_dataset_1": { 29 | "dataset_data": np.array([1, 2, 3]), 30 | "dataset_attributes": { 31 | "description": "small numbers", 32 | "location": "collected at lab X", 33 | }, 34 | }, 35 | "my_dataset_2": { 36 | "dataset_data": np.array([3, 2, 1]), 37 | "dataset_attributes": { 38 | "my_attribute_1": "small numbers", 39 | "my_attribute_2": "collected at lab Y", 40 | }, 41 | }, 42 | } 43 | ``` 44 | :type data: dict[str, dict[str, np.ndarray | dict]] 45 | :param path: Where the file will be saved. 46 | :type path: str 47 | """ 48 | 49 | # Create file 50 | dir = os.path.dirname(path) 51 | if not os.path.exists(dir): 52 | os.makedirs(dir) 53 | file = h5py.File(path, "w") 54 | # Create datasets 55 | scalar_data_dataset = file.create_dataset( 56 | "scalar_data", 57 | (1,), 58 | dtype="f" 59 | ) 60 | for key, value in data.items(): 61 | 62 | dataset_name = key 63 | dataset_properties = value 64 | dataset_data = dataset_properties["dataset_data"] 65 | dataset_attributes = dataset_properties["dataset_attributes"] 66 | 67 | if isinstance(dataset_data, np.ndarray): 68 | dataset = file.create_dataset( 69 | name=dataset_name, 70 | data=dataset_data 71 | ) 72 | dataset.attrs.update(dataset_attributes) 73 | else: 74 | scalar_data_dataset.attrs[dataset_name] = str(dataset_data) 75 | 76 | file.close() 77 | -------------------------------------------------------------------------------- /miniwave/utils/kernel.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import numpy as np 3 | from numpy.ctypeslib import ndpointer 4 | import importlib.util 5 | import sys 6 | from typing import Tuple, Optional, Callable 7 | from utils.model import Model 8 | from utils.compiler import Compiler 9 | from utils.properties import Properties 10 | from utils.dataset_writer import DatasetWriter 11 | import subprocess 12 | import h5py 13 | 14 | 15 | class Kernel: 16 | 17 | def __init__( 18 | self, 19 | file: str, 20 | model: Model, 21 | compiler: Optional[Compiler] = None, 22 | properties: Optional[Properties] = Properties(1, 1, 1), 23 | ): 24 | 25 | self._file = file 26 | self._model = model 27 | self._compiler = compiler 28 | self._properties = properties 29 | 30 | if self.language != "python" and compiler is None: 31 | raise Exception("Compiler can not be None") 32 | 33 | @property 34 | def file(self) -> str: 35 | return self._file 36 | 37 | @property 38 | def language(self) -> str: 39 | return self.compiler.language 40 | 41 | @property 42 | def model(self) -> Model: 43 | return self._model 44 | 45 | @property 46 | def compiler(self) -> Compiler: 47 | return self._compiler 48 | 49 | @property 50 | def properties(self) -> Properties: 51 | return self._properties 52 | 53 | def _import_python_lib(self) -> Callable: 54 | 55 | spec = importlib.util.spec_from_file_location( 56 | "kernel.forward", 57 | self.file 58 | ) 59 | lib = importlib.util.module_from_spec(spec) 60 | sys.modules["kernel.forward"] = lib 61 | spec.loader.exec_module(lib) 62 | 63 | return lib.forward 64 | 65 | def _import_c_lib(self) -> Callable: 66 | 67 | dtype = "float32" if self.model.dtype == np.float32 else "float64" 68 | 69 | # add space order and dtype to properties 70 | self.properties.space_order = self.model.space_order 71 | self.properties.dtype = dtype 72 | 73 | shared_object = self.compiler.compile(self.file, self.properties) 74 | 75 | # load the library 76 | lib = ctypes.cdll.LoadLibrary(shared_object) 77 | 78 | if self.properties.dtype == "float32": 79 | 80 | argtypes = [ 81 | ndpointer(ctypes.c_float, flags="C_CONTIGUOUS"), # prev_u 82 | ndpointer(ctypes.c_float, flags="C_CONTIGUOUS"), # next_u 83 | ndpointer(ctypes.c_float, flags="C_CONTIGUOUS"), # vel_model 84 | ndpointer(ctypes.c_float, flags="C_CONTIGUOUS"), # coeffs 85 | ctypes.c_float, # d1 86 | ctypes.c_float, # d2 87 | ctypes.c_float, # d3 88 | ctypes.c_float, # dt 89 | ctypes.c_int, # n1 90 | ctypes.c_int, # n2 91 | ctypes.c_int, # n3 92 | ctypes.c_int, # number of timesteps 93 | ctypes.c_int, # radius of space order 94 | ctypes.c_int, # block_size_1 95 | ctypes.c_int, # block_size_2 96 | ctypes.c_int, # block_size_3 97 | ] 98 | 99 | elif self.properties.dtype == "float64": 100 | 101 | argtypes = [ 102 | ndpointer(ctypes.c_double, flags="C_CONTIGUOUS"), # prev_u 103 | ndpointer(ctypes.c_double, flags="C_CONTIGUOUS"), # next_u 104 | ndpointer(ctypes.c_double, flags="C_CONTIGUOUS"), # vel_model 105 | ndpointer(ctypes.c_double, flags="C_CONTIGUOUS"), # coeffs 106 | ctypes.c_double, # d1 107 | ctypes.c_double, # d2 108 | ctypes.c_double, # d3 109 | ctypes.c_double, # dt 110 | ctypes.c_int, # n1 111 | ctypes.c_int, # n2 112 | ctypes.c_int, # n3 113 | ctypes.c_int, # number of timesteps 114 | ctypes.c_int, # radius of space order 115 | ctypes.c_int, # block_size_1 116 | ctypes.c_int, # block_size_2 117 | ctypes.c_int, # block_size_3 118 | ] 119 | 120 | else: 121 | raise ValueError( 122 | "Unkown float precision. Must be float32 or float64" 123 | ) 124 | 125 | forward = lib.forward 126 | forward.restype = ctypes.c_int 127 | forward.argtypes = argtypes 128 | 129 | return forward 130 | 131 | def _load_lib(self) -> Callable: 132 | 133 | if self.language == "python": 134 | return self._import_python_lib() 135 | else: 136 | return self._import_c_lib() 137 | 138 | def read_data_from_hdf5(self, filename): 139 | with h5py.File(filename, "r") as file: 140 | exec_time = file["/execution_time"][()] 141 | vector = file["/vector"][:] 142 | return exec_time, vector 143 | 144 | def print_setup(self): 145 | print("----------") 146 | print("Language: ", self.language) 147 | print("File: ", self.file) 148 | print(f"Dtype: {self.model.dtype} ({self.properties.dtype})") 149 | print("Grid size: ", self.model.grid_shape) 150 | print("Grid spacing: ", self.model.grid_spacing) 151 | print("Space order: ", self.model.space_order) 152 | print("-DSTENCIL_RADIUS: ", self.properties.stencil_radius) 153 | print("Iterations: ", self.model.num_timesteps) 154 | print("Block sizes: ", self.properties.block3) 155 | print("SM: ", self.compiler.sm) 156 | print("Fast Math: ", self.compiler.fast_math) 157 | print("----------") 158 | 159 | def run(self) -> Tuple[float, np.ndarray]: 160 | # return execution time and last wavefield 161 | 162 | # load the lib 163 | self._load_lib() 164 | 165 | # get args 166 | prev_u, next_u = self.model.u_arrays 167 | n1, n2, n3 = self.model.grid_shape 168 | d1, d2, d3 = self.model.grid_spacing 169 | stencil_radius = self.model.space_order // 2 170 | 171 | # print setup 172 | self.print_setup() 173 | 174 | # Generate HDF5 file 175 | 176 | data = { 177 | "prev_u": {"dataset_data": prev_u, "dataset_attributes": {}}, 178 | "next_u": {"dataset_data": next_u, "dataset_attributes": {}}, 179 | "vel_model": { 180 | "dataset_data": self.model.velocity_model, 181 | "dataset_attributes": {}, 182 | }, 183 | "coefficient": { 184 | "dataset_data": self.model.stencil_coefficients, 185 | "dataset_attributes": {}, 186 | }, 187 | "d1": {"dataset_data": d1, "dataset_attributes": {}}, 188 | "d2": {"dataset_data": d2, "dataset_attributes": {}}, 189 | "d3": {"dataset_data": d3, "dataset_attributes": {}}, 190 | "dt": {"dataset_data": self.model.dt, "dataset_attributes": {}}, 191 | "n1": {"dataset_data": n1, "dataset_attributes": {}}, 192 | "n2": {"dataset_data": n2, "dataset_attributes": {}}, 193 | "n3": {"dataset_data": n3, "dataset_attributes": {}}, 194 | "iterations": { 195 | "dataset_data": self.model.num_timesteps, 196 | "dataset_attributes": {}, 197 | }, 198 | "stencil_radius": { 199 | "dataset_data": stencil_radius, 200 | "dataset_attributes": {}, 201 | }, 202 | "block_size_1": { 203 | "dataset_data": self.properties.block_size_1, 204 | "dataset_attributes": {}, 205 | }, 206 | "block_size_2": { 207 | "dataset_data": self.properties.block_size_2, 208 | "dataset_attributes": {}, 209 | }, 210 | "block_size_3": { 211 | "dataset_data": self.properties.block_size_3, 212 | "dataset_attributes": {}, 213 | }, 214 | } 215 | 216 | DatasetWriter.write_dataset(data, "c-frontend/data/miniwave_data.h5") 217 | 218 | KERNEL_SOURCE = self.file 219 | KERNEL_HEADER = self.file.split('.')[0] + ".h" 220 | CUDA_ARCH = self.compiler.sm 221 | KERNEL_TYPE = self.language 222 | 223 | # Compile C code 224 | subprocess.run("ls c-frontend", shell=True) 225 | subprocess.run( 226 | f"cmake -S c-frontend -B c-frontend/build/ \ 227 | -DKERNEL_SOURCE={KERNEL_SOURCE} -DKERNEL_HEADER={KERNEL_HEADER} \ 228 | -DKERNEL_TYPE={KERNEL_TYPE} -DCUDA_ARCHITECTURE={CUDA_ARCH}", 229 | shell=True 230 | ) 231 | subprocess.run("cmake --build c-frontend/build/", shell=True) 232 | 233 | # run the forward function 234 | if self.language == "ompc": 235 | subprocess.run( 236 | "mpirun -np 4 offload-mpi-worker : \ 237 | -np 1 ./c-frontend/build/miniwave", 238 | shell=True 239 | ) 240 | elif self.language == "mpi" or self.language == "mpi_cuda": 241 | subprocess.run( 242 | "mpirun -np 4 ./c-frontend/build/miniwave", 243 | shell=True 244 | ) 245 | else: 246 | subprocess.run("./c-frontend/build/miniwave", shell=True) 247 | 248 | exec_time, next_u = self.read_data_from_hdf5( 249 | "./c-frontend/data/results.h5" 250 | ) 251 | 252 | return exec_time[0], next_u 253 | -------------------------------------------------------------------------------- /miniwave/utils/model.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import Tuple, Optional 3 | import findiff 4 | 5 | 6 | class Model: 7 | """ 8 | Encapsulates the both spatial and time model of the simulation. 9 | 10 | Parameters 11 | ---------- 12 | velocity_model : ndarray 13 | Numpy 3-dimensional array with P wave velocity (m/s) profile. 14 | grid_spacing : tuple of float 15 | Grid spacing in meters in each axis (z, x, y). 16 | dt : float. 17 | Time step in seconds. 18 | num_timesteps : int 19 | Number of timesteps to run the simulation. 20 | space_order : int, optional 21 | Spatial order of the stencil. Accepts even orders. Default is 2. 22 | dtype : str, optional 23 | Float precision. Default is float64. 24 | """ 25 | def __init__( 26 | self, 27 | velocity_model: np.ndarray, 28 | grid_spacing: Tuple[float, float, float], 29 | dt: float, 30 | num_timesteps: int, 31 | space_order: Optional[int] = 2, 32 | dtype: Optional[str] = 'float64' 33 | ): 34 | 35 | if dtype == 'float64': 36 | self.dtype = np.float64 37 | elif dtype == 'float32': 38 | self.dtype = np.float32 39 | else: 40 | raise ValueError("Unknown dtype. It must be float32 or float64") 41 | 42 | self.velocity_model = velocity_model 43 | self.grid_spacing = grid_spacing 44 | self.num_timesteps = num_timesteps 45 | self.space_order = space_order 46 | self.dt = dt 47 | 48 | @property 49 | def dtype(self) -> np.dtype: 50 | return self._dtype 51 | 52 | @dtype.setter 53 | def dtype(self, value: np.dtype) -> None: 54 | self._dtype = value 55 | 56 | @property 57 | def velocity_model(self) -> np.ndarray: 58 | return self._velocity_model 59 | 60 | @velocity_model.setter 61 | def velocity_model(self, value: np.ndarray): 62 | 63 | if value is None or value.ndim != 3: 64 | raise ValueError("Velocity model must nd-array and 3-dimensional") 65 | 66 | self._velocity_model = self.dtype(value) 67 | 68 | @property 69 | def grid_spacing(self) -> Tuple[float, float, float]: 70 | return self._grid_spacing 71 | 72 | @grid_spacing.setter 73 | def grid_spacing(self, value: Tuple[float, float, float]): 74 | if len(value) != 3: 75 | raise ValueError("Grid spacing must be 3-dimensional") 76 | 77 | self._grid_spacing = ( 78 | self.dtype(value[0]), 79 | self.dtype(value[1]), 80 | self.dtype(value[2]) 81 | ) 82 | 83 | @property 84 | def dt(self) -> float: 85 | return self._dt 86 | 87 | @dt.setter 88 | def dt(self, value: float): 89 | if value < 0: 90 | raise ValueError("Time step cannot be negative.") 91 | elif value > self.critical_dt: 92 | raise ValueError("Time step value violates CFL condition.") 93 | else: 94 | self._dt = self.dtype(value) 95 | 96 | @property 97 | def num_timesteps(self) -> int: 98 | return self._num_timesteps 99 | 100 | @num_timesteps.setter 101 | def num_timesteps(self, value: int): 102 | self._num_timesteps = value 103 | 104 | @property 105 | def space_order(self) -> int: 106 | return self._space_order 107 | 108 | @space_order.setter 109 | def space_order(self, value: int): 110 | self._space_order = value 111 | 112 | @property 113 | def grid_shape(self) -> Tuple[int, int, int]: 114 | return self.velocity_model.shape 115 | 116 | @property 117 | def critical_dt(self) -> float: 118 | """ 119 | Calculate dt with CFL conditions 120 | Based on https://library.seg.org/doi/pdf/10.1190/1.1444605 121 | for the acoustic case. 122 | 123 | Returns 124 | ---------- 125 | float 126 | Critical dt in seconds. 127 | """ 128 | 129 | # 2nd order in time 130 | a1 = 4 131 | 132 | # fixed 2nd time derivative 133 | fd_coeffs = findiff.coefficients( 134 | deriv=2, 135 | acc=self.space_order 136 | )['center']['coefficients'] 137 | 138 | a2 = self.velocity_model.ndim * np.sum(np.abs(fd_coeffs)) 139 | coeff = np.sqrt(a1 / a2) 140 | dt = coeff * np.min(self.grid_spacing) / np.max(self.velocity_model) 141 | 142 | return self.dtype(dt) 143 | 144 | @property 145 | def stencil_coefficients(self) -> np.ndarray: 146 | # fixed second derivative 147 | coeffs = findiff.coefficients( 148 | deriv=2, 149 | acc=self.space_order 150 | )['center']['coefficients'] 151 | 152 | # calculate the center point index 153 | middle = len(coeffs) // 2 154 | 155 | # coefficients starting from the center 156 | coeffs = coeffs[middle:] 157 | 158 | return self.dtype(coeffs) 159 | 160 | @property 161 | def grid(self) -> np.ndarray: 162 | 163 | base_grid = np.zeros(shape=self.grid_shape, dtype=self.dtype) 164 | 165 | return self._add_initial_source(base_grid) 166 | 167 | @property 168 | def u_arrays(self) -> Tuple[np.ndarray, np.ndarray]: 169 | # return prev_u and next_u arrays 170 | 171 | prev_u = self.grid 172 | next_u = self.grid.copy() 173 | 174 | return prev_u, next_u 175 | 176 | def _add_initial_source(self, grid: np.ndarray) -> np.ndarray: 177 | 178 | n1, n2, n3 = grid.shape 179 | 180 | val = 50.0 181 | 182 | for s in range(4, -1, -1): 183 | grid[n1//2-s:n1//2+s, n2//2-s:n2//2+s, n3//2-s:n3//2+s] = val 184 | val *= 0.9 185 | 186 | return grid 187 | -------------------------------------------------------------------------------- /miniwave/utils/plot.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import os 3 | from mpl_toolkits.axes_grid1 import make_axes_locatable 4 | 5 | 6 | def plot( 7 | wavefield, 8 | file_name="wavefield", 9 | colorbar=True, 10 | cmap="gray", 11 | extent=None, 12 | show=False, 13 | clim=[-5, 5] 14 | ): 15 | """ 16 | Plot the wavefield. 17 | 18 | Parameters 19 | ---------- 20 | wavefield : ndarray 21 | Wavefield data. 22 | file_name : str, optional 23 | Name of the image to be saved. 24 | Default is wavefield. 25 | colorbar : bool, optional 26 | If True, show a colorbar. Default is True. 27 | cmap : str, optional 28 | The Colormap instance or registered colormap name 29 | used to map scalar data to colors. 30 | Default is gray. 31 | extent : floats(left, right, bottom, top), optional 32 | The bounding box in data coordinates that the image will fill. 33 | show : bool, optional 34 | If True, show the image on a pop up window. 35 | Default is False. 36 | clim : list 37 | Set the color limits of the current image. 38 | Default is (vmin=-5, vmax=5). 39 | """ 40 | 41 | # create the destination dir 42 | os.makedirs("plots", exist_ok=True) 43 | 44 | # process data and generate the plot 45 | plot = plt.imshow(wavefield, cmap=cmap, extent=extent) 46 | 47 | m_unit = 'm' if extent is not None else 'points' 48 | 49 | # labels 50 | plt.xlabel('Width ({})'.format(m_unit)) 51 | plt.ylabel('Depth ({})'.format(m_unit)) 52 | 53 | # Create aligned colorbar on the right 54 | if colorbar: 55 | ax = plt.gca() 56 | divider = make_axes_locatable(ax) 57 | cax = divider.append_axes("right", size="5%", pad=0.05) 58 | plt.colorbar(plot, cax=cax) 59 | plt.clim(clim) 60 | 61 | plt.savefig("plots/{}.png".format(file_name), format="png") 62 | 63 | if show: 64 | plt.show() 65 | 66 | plt.close() 67 | 68 | print("Wavefield saved in plots/{}.png".format(file_name)) 69 | -------------------------------------------------------------------------------- /miniwave/utils/properties.py: -------------------------------------------------------------------------------- 1 | class Properties: 2 | 3 | def __init__( 4 | self, 5 | block_size_1: int, 6 | block_size_2: int, 7 | block_size_3: int 8 | ): 9 | 10 | self.block_size_1 = block_size_1 11 | self.block_size_2 = block_size_2 12 | self.block_size_3 = block_size_3 13 | self._space_order = None 14 | self._dtype = None 15 | 16 | @property 17 | def block3(self): 18 | return (self.block_size_1, self.block_size_2, self.block_size_3) 19 | 20 | @property 21 | def dtype(self): 22 | return self._dtype 23 | 24 | @dtype.setter 25 | def dtype(self, value: str): 26 | self._dtype = value 27 | 28 | @property 29 | def space_order(self): 30 | return self._space_order 31 | 32 | @space_order.setter 33 | def space_order(self, value: str): 34 | self._space_order = value 35 | 36 | @property 37 | def stencil_radius(self): 38 | if self.space_order is not None: 39 | return self.space_order // 2 40 | 41 | return None 42 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | gpu: marks GPUs tests only (deselect with '-m "not gpu"') 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.18.1 2 | matplotlib>=3.2.1 3 | segyio>=1.9.1 4 | scipy>=1.4.1 5 | pytest>=6.2.2 6 | findiff>=0.8.9 7 | pytest-codeblocks>=0.10.4 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test = pytest 3 | 4 | [metadata] 5 | description-file = README.md 6 | 7 | [versioneer] 8 | VCS = git 9 | style = pep440 10 | versionfile_source = simwave/_version.py 11 | versionfile_build = simwave/_version.py 12 | tag_prefix = v 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import versioneer 2 | from setuptools import setup, find_packages 3 | 4 | with open("requirements.txt") as f: 5 | required = f.read().splitlines() 6 | 7 | with open("README.md") as f: 8 | readme = f.read() 9 | 10 | setup( 11 | name="simwave", 12 | version=versioneer.get_version(), 13 | cmdclass=versioneer.get_cmdclass(), 14 | description="Finite difference 2D/3D acoustic wave propagator.", 15 | long_description=readme, 16 | long_description_content_type="text/markdown", 17 | url='https://github.com/HPCSys-Lab/simwave', 18 | author="HPCSys-Lab", 19 | author_email="senger.hermes@gmail.com", 20 | license="GPL-3.0 License", 21 | packages=find_packages(), 22 | install_requires=required, 23 | include_package_data=True, 24 | classifiers=[ 25 | 'Development Status :: 4 - Beta', 26 | 'Intended Audience :: Developers', 27 | 'Intended Audience :: Science/Research', 28 | 'Topic :: Scientific/Engineering :: Mathematics', 29 | 'Topic :: Scientific/Engineering :: Physics', 30 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.6', 33 | 'Programming Language :: Python :: 3.7', 34 | 'Programming Language :: Python :: 3.8', 35 | 'Programming Language :: Python :: 3.9' 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /simwave/__init__.py: -------------------------------------------------------------------------------- 1 | from simwave.io import read_2D_segy 2 | 3 | from simwave.kernel import ( 4 | Compiler, 5 | SpaceModel, 6 | TimeModel, 7 | Source, 8 | Receiver, 9 | Wavelet, 10 | RickerWavelet, 11 | MultiWavelet, 12 | Solver 13 | ) 14 | 15 | from simwave.plots import ( 16 | plot_wavefield, 17 | plot_shotrecord, 18 | plot_velocity_model, 19 | plot_wavelet 20 | ) 21 | 22 | from ._version import get_versions 23 | __version__ = get_versions()['version'] 24 | del get_versions 25 | 26 | __all__ = [ 27 | "Compiler", 28 | "SpaceModel", 29 | "TimeModel", 30 | "Source", 31 | "Receiver", 32 | "Wavelet", 33 | "RickerWavelet", 34 | "MultiWavelet", 35 | "Solver", 36 | "plot_wavefield", 37 | "plot_shotrecord", 38 | "plot_velocity_model", 39 | "plot_wavelet", 40 | "read_2D_segy" 41 | ] 42 | -------------------------------------------------------------------------------- /simwave/_version.py: -------------------------------------------------------------------------------- 1 | 2 | # This file helps to compute a version number in source trees obtained from 3 | # git-archive tarball (such as those provided by githubs download-from-tag 4 | # feature). Distribution tarballs (built by setup.py sdist) and build 5 | # directories (produced by setup.py build) will contain a much shorter file 6 | # that just contains the computed version number. 7 | 8 | # This file is released into the public domain. Generated by 9 | # versioneer-0.18 (https://github.com/warner/python-versioneer) 10 | 11 | """Git implementation of _version.py.""" 12 | 13 | import errno 14 | import os 15 | import re 16 | import subprocess 17 | import sys 18 | 19 | 20 | def get_keywords(): 21 | """Get the keywords needed to look up the version information.""" 22 | # these strings will be replaced by git during git-archive. 23 | # setup.py/versioneer.py will grep for the variable names, so they must 24 | # each be defined on a line of their own. _version.py will just call 25 | # get_keywords(). 26 | git_refnames = " (HEAD -> master)" 27 | git_full = "49e9c85d38020a4222aefa8c7224a103a40f9ba1" 28 | git_date = "2025-05-27 19:36:49 -0300" 29 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 30 | return keywords 31 | 32 | 33 | class VersioneerConfig: 34 | """Container for Versioneer configuration parameters.""" 35 | 36 | 37 | def get_config(): 38 | """Create, populate and return the VersioneerConfig() object.""" 39 | # these strings are filled in when 'setup.py versioneer' creates 40 | # _version.py 41 | cfg = VersioneerConfig() 42 | cfg.VCS = "git" 43 | cfg.style = "pep440" 44 | cfg.tag_prefix = "v" 45 | cfg.parentdir_prefix = "None" 46 | cfg.versionfile_source = "simwave/_version.py" 47 | cfg.verbose = False 48 | return cfg 49 | 50 | 51 | class NotThisMethod(Exception): 52 | """Exception raised if a method is not valid for the current scenario.""" 53 | 54 | 55 | LONG_VERSION_PY = {} 56 | HANDLERS = {} 57 | 58 | 59 | def register_vcs_handler(vcs, method): # decorator 60 | """Decorator to mark a method as the handler for a particular VCS.""" 61 | def decorate(f): 62 | """Store f in HANDLERS[vcs][method].""" 63 | if vcs not in HANDLERS: 64 | HANDLERS[vcs] = {} 65 | HANDLERS[vcs][method] = f 66 | return f 67 | return decorate 68 | 69 | 70 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 71 | env=None): 72 | """Call the given command(s).""" 73 | assert isinstance(commands, list) 74 | p = None 75 | for c in commands: 76 | try: 77 | dispcmd = str([c] + args) 78 | # remember shell=False, so use git.cmd on windows, not just git 79 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 80 | stdout=subprocess.PIPE, 81 | stderr=(subprocess.PIPE if hide_stderr 82 | else None)) 83 | break 84 | except EnvironmentError: 85 | e = sys.exc_info()[1] 86 | if e.errno == errno.ENOENT: 87 | continue 88 | if verbose: 89 | print("unable to run %s" % dispcmd) 90 | print(e) 91 | return None, None 92 | else: 93 | if verbose: 94 | print("unable to find command, tried %s" % (commands,)) 95 | return None, None 96 | stdout = p.communicate()[0].strip() 97 | if sys.version_info[0] >= 3: 98 | stdout = stdout.decode() 99 | if p.returncode != 0: 100 | if verbose: 101 | print("unable to run %s (error)" % dispcmd) 102 | print("stdout was %s" % stdout) 103 | return None, p.returncode 104 | return stdout, p.returncode 105 | 106 | 107 | def versions_from_parentdir(parentdir_prefix, root, verbose): 108 | """Try to determine the version from the parent directory name. 109 | 110 | Source tarballs conventionally unpack into a directory that includes both 111 | the project name and a version string. We will also support searching up 112 | two directory levels for an appropriately named parent directory 113 | """ 114 | rootdirs = [] 115 | 116 | for i in range(3): 117 | dirname = os.path.basename(root) 118 | if dirname.startswith(parentdir_prefix): 119 | return {"version": dirname[len(parentdir_prefix):], 120 | "full-revisionid": None, 121 | "dirty": False, "error": None, "date": None} 122 | else: 123 | rootdirs.append(root) 124 | root = os.path.dirname(root) # up a level 125 | 126 | if verbose: 127 | print("Tried directories %s but none started with prefix %s" % 128 | (str(rootdirs), parentdir_prefix)) 129 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 130 | 131 | 132 | @register_vcs_handler("git", "get_keywords") 133 | def git_get_keywords(versionfile_abs): 134 | """Extract version information from the given file.""" 135 | # the code embedded in _version.py can just fetch the value of these 136 | # keywords. When used from setup.py, we don't want to import _version.py, 137 | # so we do it with a regexp instead. This function is not used from 138 | # _version.py. 139 | keywords = {} 140 | try: 141 | f = open(versionfile_abs, "r") 142 | for line in f.readlines(): 143 | if line.strip().startswith("git_refnames ="): 144 | mo = re.search(r'=\s*"(.*)"', line) 145 | if mo: 146 | keywords["refnames"] = mo.group(1) 147 | if line.strip().startswith("git_full ="): 148 | mo = re.search(r'=\s*"(.*)"', line) 149 | if mo: 150 | keywords["full"] = mo.group(1) 151 | if line.strip().startswith("git_date ="): 152 | mo = re.search(r'=\s*"(.*)"', line) 153 | if mo: 154 | keywords["date"] = mo.group(1) 155 | f.close() 156 | except EnvironmentError: 157 | pass 158 | return keywords 159 | 160 | 161 | @register_vcs_handler("git", "keywords") 162 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 163 | """Get version information from git keywords.""" 164 | if not keywords: 165 | raise NotThisMethod("no keywords at all, weird") 166 | date = keywords.get("date") 167 | if date is not None: 168 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 169 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 170 | # -like" string, which we must then edit to make compliant), because 171 | # it's been around since git-1.5.3, and it's too difficult to 172 | # discover which version we're using, or to work around using an 173 | # older one. 174 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 175 | refnames = keywords["refnames"].strip() 176 | if refnames.startswith("$Format"): 177 | if verbose: 178 | print("keywords are unexpanded, not using") 179 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 180 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 181 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 182 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 183 | TAG = "tag: " 184 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 185 | if not tags: 186 | # Either we're using git < 1.8.3, or there really are no tags. We use 187 | # a heuristic: assume all version tags have a digit. The old git %d 188 | # expansion behaves like git log --decorate=short and strips out the 189 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 190 | # between branches and tags. By ignoring refnames without digits, we 191 | # filter out many common branch names like "release" and 192 | # "stabilization", as well as "HEAD" and "master". 193 | tags = set([r for r in refs if re.search(r'\d', r)]) 194 | if verbose: 195 | print("discarding '%s', no digits" % ",".join(refs - tags)) 196 | if verbose: 197 | print("likely tags: %s" % ",".join(sorted(tags))) 198 | for ref in sorted(tags): 199 | # sorting will prefer e.g. "2.0" over "2.0rc1" 200 | if ref.startswith(tag_prefix): 201 | r = ref[len(tag_prefix):] 202 | if verbose: 203 | print("picking %s" % r) 204 | return {"version": r, 205 | "full-revisionid": keywords["full"].strip(), 206 | "dirty": False, "error": None, 207 | "date": date} 208 | # no suitable tags, so version is "0+unknown", but full hex is still there 209 | if verbose: 210 | print("no suitable tags, using unknown + full revision id") 211 | return {"version": "0+unknown", 212 | "full-revisionid": keywords["full"].strip(), 213 | "dirty": False, "error": "no suitable tags", "date": None} 214 | 215 | 216 | @register_vcs_handler("git", "pieces_from_vcs") 217 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 218 | """Get version from 'git describe' in the root of the source tree. 219 | 220 | This only gets called if the git-archive 'subst' keywords were *not* 221 | expanded, and _version.py hasn't already been rewritten with a short 222 | version string, meaning we're inside a checked out source tree. 223 | """ 224 | GITS = ["git"] 225 | if sys.platform == "win32": 226 | GITS = ["git.cmd", "git.exe"] 227 | 228 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 229 | hide_stderr=True) 230 | if rc != 0: 231 | if verbose: 232 | print("Directory %s not under git control" % root) 233 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 234 | 235 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 236 | # if there isn't one, this yields HEX[-dirty] (no NUM) 237 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 238 | "--always", "--long", 239 | "--match", "%s*" % tag_prefix], 240 | cwd=root) 241 | # --long was added in git-1.5.5 242 | if describe_out is None: 243 | raise NotThisMethod("'git describe' failed") 244 | describe_out = describe_out.strip() 245 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 246 | if full_out is None: 247 | raise NotThisMethod("'git rev-parse' failed") 248 | full_out = full_out.strip() 249 | 250 | pieces = {} 251 | pieces["long"] = full_out 252 | pieces["short"] = full_out[:7] # maybe improved later 253 | pieces["error"] = None 254 | 255 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 256 | # TAG might have hyphens. 257 | git_describe = describe_out 258 | 259 | # look for -dirty suffix 260 | dirty = git_describe.endswith("-dirty") 261 | pieces["dirty"] = dirty 262 | if dirty: 263 | git_describe = git_describe[:git_describe.rindex("-dirty")] 264 | 265 | # now we have TAG-NUM-gHEX or HEX 266 | 267 | if "-" in git_describe: 268 | # TAG-NUM-gHEX 269 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 270 | if not mo: 271 | # unparseable. Maybe git-describe is misbehaving? 272 | pieces["error"] = ("unable to parse git-describe output: '%s'" 273 | % describe_out) 274 | return pieces 275 | 276 | # tag 277 | full_tag = mo.group(1) 278 | if not full_tag.startswith(tag_prefix): 279 | if verbose: 280 | fmt = "tag '%s' doesn't start with prefix '%s'" 281 | print(fmt % (full_tag, tag_prefix)) 282 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 283 | % (full_tag, tag_prefix)) 284 | return pieces 285 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 286 | 287 | # distance: number of commits since tag 288 | pieces["distance"] = int(mo.group(2)) 289 | 290 | # commit: short hex revision ID 291 | pieces["short"] = mo.group(3) 292 | 293 | else: 294 | # HEX: no tags 295 | pieces["closest-tag"] = None 296 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 297 | cwd=root) 298 | pieces["distance"] = int(count_out) # total number of commits 299 | 300 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 301 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], 302 | cwd=root)[0].strip() 303 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 304 | 305 | return pieces 306 | 307 | 308 | def plus_or_dot(pieces): 309 | """Return a + if we don't already have one, else return a .""" 310 | if "+" in pieces.get("closest-tag", ""): 311 | return "." 312 | return "+" 313 | 314 | 315 | def render_pep440(pieces): 316 | """Build up version string, with post-release "local version identifier". 317 | 318 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 319 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 320 | 321 | Exceptions: 322 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 323 | """ 324 | if pieces["closest-tag"]: 325 | rendered = pieces["closest-tag"] 326 | if pieces["distance"] or pieces["dirty"]: 327 | rendered += plus_or_dot(pieces) 328 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 329 | if pieces["dirty"]: 330 | rendered += ".dirty" 331 | else: 332 | # exception #1 333 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 334 | pieces["short"]) 335 | if pieces["dirty"]: 336 | rendered += ".dirty" 337 | return rendered 338 | 339 | 340 | def render_pep440_pre(pieces): 341 | """TAG[.post.devDISTANCE] -- No -dirty. 342 | 343 | Exceptions: 344 | 1: no tags. 0.post.devDISTANCE 345 | """ 346 | if pieces["closest-tag"]: 347 | rendered = pieces["closest-tag"] 348 | if pieces["distance"]: 349 | rendered += ".post.dev%d" % pieces["distance"] 350 | else: 351 | # exception #1 352 | rendered = "0.post.dev%d" % pieces["distance"] 353 | return rendered 354 | 355 | 356 | def render_pep440_post(pieces): 357 | """TAG[.postDISTANCE[.dev0]+gHEX] . 358 | 359 | The ".dev0" means dirty. Note that .dev0 sorts backwards 360 | (a dirty tree will appear "older" than the corresponding clean one), 361 | but you shouldn't be releasing software with -dirty anyways. 362 | 363 | Exceptions: 364 | 1: no tags. 0.postDISTANCE[.dev0] 365 | """ 366 | if pieces["closest-tag"]: 367 | rendered = pieces["closest-tag"] 368 | if pieces["distance"] or pieces["dirty"]: 369 | rendered += ".post%d" % pieces["distance"] 370 | if pieces["dirty"]: 371 | rendered += ".dev0" 372 | rendered += plus_or_dot(pieces) 373 | rendered += "g%s" % pieces["short"] 374 | else: 375 | # exception #1 376 | rendered = "0.post%d" % pieces["distance"] 377 | if pieces["dirty"]: 378 | rendered += ".dev0" 379 | rendered += "+g%s" % pieces["short"] 380 | return rendered 381 | 382 | 383 | def render_pep440_old(pieces): 384 | """TAG[.postDISTANCE[.dev0]] . 385 | 386 | The ".dev0" means dirty. 387 | 388 | Eexceptions: 389 | 1: no tags. 0.postDISTANCE[.dev0] 390 | """ 391 | if pieces["closest-tag"]: 392 | rendered = pieces["closest-tag"] 393 | if pieces["distance"] or pieces["dirty"]: 394 | rendered += ".post%d" % pieces["distance"] 395 | if pieces["dirty"]: 396 | rendered += ".dev0" 397 | else: 398 | # exception #1 399 | rendered = "0.post%d" % pieces["distance"] 400 | if pieces["dirty"]: 401 | rendered += ".dev0" 402 | return rendered 403 | 404 | 405 | def render_git_describe(pieces): 406 | """TAG[-DISTANCE-gHEX][-dirty]. 407 | 408 | Like 'git describe --tags --dirty --always'. 409 | 410 | Exceptions: 411 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 412 | """ 413 | if pieces["closest-tag"]: 414 | rendered = pieces["closest-tag"] 415 | if pieces["distance"]: 416 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 417 | else: 418 | # exception #1 419 | rendered = pieces["short"] 420 | if pieces["dirty"]: 421 | rendered += "-dirty" 422 | return rendered 423 | 424 | 425 | def render_git_describe_long(pieces): 426 | """TAG-DISTANCE-gHEX[-dirty]. 427 | 428 | Like 'git describe --tags --dirty --always -long'. 429 | The distance/hash is unconditional. 430 | 431 | Exceptions: 432 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 433 | """ 434 | if pieces["closest-tag"]: 435 | rendered = pieces["closest-tag"] 436 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 437 | else: 438 | # exception #1 439 | rendered = pieces["short"] 440 | if pieces["dirty"]: 441 | rendered += "-dirty" 442 | return rendered 443 | 444 | 445 | def render(pieces, style): 446 | """Render the given version pieces into the requested style.""" 447 | if pieces["error"]: 448 | return {"version": "unknown", 449 | "full-revisionid": pieces.get("long"), 450 | "dirty": None, 451 | "error": pieces["error"], 452 | "date": None} 453 | 454 | if not style or style == "default": 455 | style = "pep440" # the default 456 | 457 | if style == "pep440": 458 | rendered = render_pep440(pieces) 459 | elif style == "pep440-pre": 460 | rendered = render_pep440_pre(pieces) 461 | elif style == "pep440-post": 462 | rendered = render_pep440_post(pieces) 463 | elif style == "pep440-old": 464 | rendered = render_pep440_old(pieces) 465 | elif style == "git-describe": 466 | rendered = render_git_describe(pieces) 467 | elif style == "git-describe-long": 468 | rendered = render_git_describe_long(pieces) 469 | else: 470 | raise ValueError("unknown style '%s'" % style) 471 | 472 | return {"version": rendered, "full-revisionid": pieces["long"], 473 | "dirty": pieces["dirty"], "error": None, 474 | "date": pieces.get("date")} 475 | 476 | 477 | def get_versions(): 478 | """Get version information or return default if unable to do so.""" 479 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 480 | # __file__, we can work backwards from there to the root. Some 481 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 482 | # case we can only use expanded keywords. 483 | 484 | cfg = get_config() 485 | verbose = cfg.verbose 486 | 487 | try: 488 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 489 | verbose) 490 | except NotThisMethod: 491 | pass 492 | 493 | try: 494 | root = os.path.realpath(__file__) 495 | # versionfile_source is the relative path from the top of the source 496 | # tree (where the .git directory might live) to this file. Invert 497 | # this to find the root from __file__. 498 | for i in cfg.versionfile_source.split('/'): 499 | root = os.path.dirname(root) 500 | except NameError: 501 | return {"version": "0+unknown", "full-revisionid": None, 502 | "dirty": None, 503 | "error": "unable to find root of source tree", 504 | "date": None} 505 | 506 | try: 507 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 508 | return render(pieces, cfg.style) 509 | except NotThisMethod: 510 | pass 511 | 512 | try: 513 | if cfg.parentdir_prefix: 514 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 515 | except NotThisMethod: 516 | pass 517 | 518 | return {"version": "0+unknown", "full-revisionid": None, 519 | "dirty": None, 520 | "error": "unable to compute version", "date": None} 521 | -------------------------------------------------------------------------------- /simwave/io/__init__.py: -------------------------------------------------------------------------------- 1 | from simwave.io.io import read_2D_segy 2 | 3 | __all__ = ["read_2D_segy"] 4 | -------------------------------------------------------------------------------- /simwave/io/io.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import segyio 3 | 4 | 5 | def read_2D_segy(file): 6 | """ 7 | Build a 2D velocity model from a SEG-Y format. 8 | It uses the 'segyio' from https://github.com/equinor/segyio 9 | 10 | Parameters 11 | ---------- 12 | file : str 13 | Path to the velocity/density model file. 14 | 15 | Returns 16 | ---------- 17 | ndarray 18 | 2D velocity model. 19 | """ 20 | with segyio.open(file, ignore_geometry=True) as f: 21 | n_samples = len(f.samples) 22 | n_traces = len(f.trace) 23 | data = np.zeros(shape=(n_samples, n_traces), dtype=np.float32) 24 | index = 0 25 | for trace in f.trace: 26 | data[:, index] = trace 27 | index += 1 28 | 29 | return data 30 | -------------------------------------------------------------------------------- /simwave/kernel/__init__.py: -------------------------------------------------------------------------------- 1 | from simwave.kernel.backend import Compiler 2 | 3 | from simwave.kernel.frontend import ( 4 | SpaceModel, 5 | TimeModel, 6 | Source, 7 | Receiver, 8 | Wavelet, 9 | RickerWavelet, 10 | MultiWavelet, 11 | Solver 12 | ) 13 | 14 | __all__ = [ 15 | "Compiler", 16 | "SpaceModel", 17 | "TimeModel", 18 | "Source", 19 | "Receiver", 20 | "Wavelet", 21 | "RickerWavelet", 22 | "MultiWavelet", 23 | "Solver", 24 | ] 25 | -------------------------------------------------------------------------------- /simwave/kernel/backend/__init__.py: -------------------------------------------------------------------------------- 1 | from simwave.kernel.backend.compiler import Compiler 2 | from simwave.kernel.backend.middleware import Middleware 3 | 4 | __all__ = [ 5 | "Compiler", 6 | "Middleware" 7 | ] 8 | -------------------------------------------------------------------------------- /simwave/kernel/backend/compiler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from hashlib import sha1 4 | 5 | 6 | class Compiler: 7 | """ 8 | Base class to implement the runtime compiler. 9 | 10 | Parameters 11 | ---------- 12 | cc : str, optional 13 | C compiler. Default is gcc. 14 | language: str, optional 15 | Define the code implementation language like: c (sequential), 16 | cpu_openmp (parallel CPU), gpu_openmp (GPU), gpu_openacc (GPU) 17 | and cuda (GPU). Default is c. 18 | cflags : str, optional 19 | C compiler flags. 20 | Default is '-O3 -fPIC -Wall -std=c99 -shared'. 21 | cfile : str, optional 22 | Path to the file with a custom C Kernel implementation. 23 | """ 24 | def __init__(self, cc='gcc', language='c', cflags=None, cfile=None): 25 | self.cc = cc 26 | self.language = language 27 | self.cflags = cflags 28 | self.cfile = cfile 29 | 30 | @property 31 | def cc(self): 32 | return self._cc 33 | 34 | @cc.setter 35 | def cc(self, value): 36 | if not isinstance(value, str): 37 | raise TypeError("Compiler.cc attribute must be str.") 38 | 39 | self._cc = value 40 | 41 | @property 42 | def cflags(self): 43 | return self._cflags 44 | 45 | @cflags.setter 46 | def cflags(self, value): 47 | # use default flags 48 | if value is None: 49 | value = '-O3 -fPIC -Wall -std=c99 -shared' 50 | 51 | if not isinstance(value, str): 52 | raise TypeError("Compiler.cflags attribute must be str.") 53 | 54 | # add -shared flag if not provided 55 | if '-shared' not in value: 56 | value += ' -shared' 57 | 58 | # add OpenMP flag if it is not provided and version is cpu_openmp 59 | if self.language in ('cpu_openmp', 'gpu_openmp'): 60 | omp_flag = self.get_openmp_flag() 61 | 62 | if omp_flag is None: 63 | print("WARNING: make sure OpenMP flag is provided in cflags.") 64 | else: 65 | if omp_flag not in value: 66 | value += ' {}'.format(omp_flag) 67 | 68 | self._cflags = value 69 | 70 | @property 71 | def language(self): 72 | return self._language 73 | 74 | @language.setter 75 | def language(self, value): 76 | if not isinstance(value, str): 77 | raise TypeError("Compiler.language attribute must be str.") 78 | 79 | options = ['c', 'cpu_openmp', 'gpu_openmp', 'gpu_openacc', 'cuda'] 80 | 81 | if value not in options: 82 | raise ValueError( 83 | "Compiler.language {} not implemented.".format(value) 84 | ) 85 | 86 | self._language = value 87 | 88 | @property 89 | def cfile(self): 90 | return self._cfile 91 | 92 | @cfile.setter 93 | def cfile(self, value): 94 | self._cfile = value 95 | 96 | def get_openmp_flag(self): 97 | """ 98 | Get the OpenMP flag according the compiler. 99 | Returns None if compiler is unknown. 100 | 101 | Returns 102 | ---------- 103 | str 104 | OpenMP flag's name for compilation command. 105 | """ 106 | omp_flag = { 107 | 'gcc': '-fopenmp', 108 | 'icc': '-openmp', 109 | 'pgcc': '-mp', 110 | 'clang': '-fopenmp' 111 | } 112 | 113 | return omp_flag.get(self.cc) 114 | 115 | def compile(self, dimension, density, float_precision, operator): 116 | """ 117 | Compile the program. 118 | 119 | Parameters 120 | ---------- 121 | dimension : int 122 | Grid dimension. 2D (2) or 3D (3). 123 | density : str 124 | Consider density or not. Options: constant (without density) 125 | or variable (consider density). Default is constant. 126 | float_precision : str 127 | Float single (C float) or double (C double) precision. 128 | operator : str 129 | Operator implementation. 130 | Only forward operator available at the moment. 131 | 132 | Returns 133 | ---------- 134 | str 135 | Path to the compiled shared object 136 | """ 137 | # get the working dir 138 | working_dir = os.getcwd() 139 | 140 | if self.cfile is None: 141 | # get the dir of the compiler.py file 142 | current_dir = os.path.dirname(os.path.realpath(__file__)) 143 | 144 | # c program root dir 145 | program_dir = current_dir + "/c_code/{}/{}/{}d/".format( 146 | operator, density, dimension 147 | ) 148 | 149 | # c pragram file name 150 | if self.language == 'cuda': 151 | program_dir += 'cuda/' 152 | c_code_name = "wave.cu" 153 | else: 154 | c_code_name = "wave.c" 155 | 156 | # c code complete path 157 | program_path = program_dir + c_code_name 158 | else: 159 | # c code complete path 160 | program_path = self.cfile 161 | 162 | # get c file content 163 | with open(program_path, 'r', encoding='utf-8') as f: 164 | c_file_content = f.read() 165 | 166 | # object root dir 167 | object_dir = working_dir + "/tmp/" 168 | 169 | # define the language 170 | if self.language == 'cpu_openmp': 171 | language_c = ' -DCPU_OPENMP' 172 | elif self.language == 'gpu_openmp': 173 | language_c = ' -DGPU_OPENMP' 174 | elif self.language == 'gpu_openacc': 175 | language_c = ' -DGPU_OPENACC' 176 | else: 177 | language_c = '' 178 | 179 | # compose the object string 180 | object_str = "wave {} {} {} {}d {} {} {} {} \n{}".format( 181 | self.language, 182 | self.cc, 183 | self.cflags, 184 | dimension, 185 | operator, 186 | density, 187 | float_precision, 188 | language_c, 189 | c_file_content 190 | ) 191 | 192 | # apply sha1 hash to name the object 193 | hash = sha1() 194 | hash.update(object_str.encode()) 195 | object_name = hash.hexdigest() + ".so" 196 | 197 | # object complete path 198 | object_path = object_dir + object_name 199 | 200 | # check if object_file already exists 201 | if os.path.exists(object_path): 202 | print("Shared object already compiled in:", object_path) 203 | else: 204 | # create arguments list for `subprocess.run`: pay attention to not 205 | # providing empty arguments, which the compiler will try to 206 | # interpret as source filenames and consequenty fail; moreover 207 | # split the compilation flags string to separate arguments to 208 | # ensure proper parsing 209 | args = [self.cc, program_path] 210 | args += [flag.strip() 211 | for flag in self.cflags.split(' ') 212 | if flag.strip() != ''] 213 | if float_precision.strip() != '': 214 | args.append("{}".format(float_precision).strip()) 215 | if language_c.strip() != '': 216 | args.append(language_c.strip()) 217 | args += ["-o", object_path] 218 | 219 | print("Compilation command:", ' '.join(args)) 220 | 221 | # create a dir to save the compiled shared object 222 | os.makedirs(object_dir, exist_ok=True) 223 | 224 | # execute the command 225 | result = subprocess.run(args) 226 | if result.returncode != 0: 227 | raise Exception("Compilation failed") 228 | 229 | return object_path 230 | -------------------------------------------------------------------------------- /simwave/kernel/backend/middleware.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import numpy as np 3 | from numpy.ctypeslib import ndpointer 4 | from simwave.kernel.backend.compiler import Compiler 5 | 6 | 7 | class Middleware: 8 | """ 9 | Communication interface between frontend and backend. 10 | 11 | Parameters 12 | ---------- 13 | compiler : Compiler 14 | Compiler object. 15 | """ 16 | def __init__(self, compiler): 17 | if compiler is None: 18 | self._compiler = Compiler() 19 | else: 20 | self._compiler = compiler 21 | 22 | @property 23 | def compiler(self): 24 | return self._compiler 25 | 26 | def library(self, dimension, density, dtype): 27 | """Load and return the C library.""" 28 | 29 | # convert dtype to C compiling value 30 | # that define float precision 31 | type = { 32 | 'float32': '-DFLOAT', 33 | 'float64': '-DDOUBLE' 34 | } 35 | 36 | # constant or variable density 37 | if density is not None: 38 | density = "variable_density" 39 | else: 40 | density = "constant_density" 41 | 42 | # compile the code 43 | shared_object = self.compiler.compile( 44 | dimension=dimension, 45 | density=density, 46 | float_precision=type[str(dtype)], 47 | operator="forward" 48 | ) 49 | 50 | # load the library 51 | return ctypes.cdll.LoadLibrary(shared_object) 52 | 53 | def exec(self, operator, **kwargs): 54 | """ 55 | Run an operator. 56 | 57 | Parameters 58 | ---------- 59 | operator : str 60 | operator to be executed. 61 | kwargs : dict 62 | List of keyword arguments. 63 | 64 | Returns 65 | ---------- 66 | tuple 67 | The operation results 68 | """ 69 | 70 | # convert the boundary condition to specification to int 71 | bc = self._convert_boundary_condition( 72 | kwargs.get('boundary_condition') 73 | ) 74 | kwargs.update({'boundary_condition': bc}) 75 | 76 | # if density model is None, remove it 77 | # and remove first_order_fd_coefficients 78 | if kwargs.get('density_model') is None: 79 | kwargs.pop('density_model') 80 | kwargs.pop('first_order_fd_coefficients') 81 | 82 | # get the grid shape 83 | grid_shape = kwargs.get('velocity_model').shape 84 | 85 | try: 86 | nz, nx = grid_shape 87 | kwargs.update({'nz': nz, 'nx': nx}) 88 | except ValueError: 89 | nz, nx, ny = grid_shape 90 | kwargs.update({'nz': nz, 'nx': nx, 'ny': ny}) 91 | 92 | # unpack the grid spacing 93 | grid_spacing = kwargs.get('grid_spacing') 94 | kwargs.pop('grid_spacing') 95 | 96 | try: 97 | dz, dx = grid_spacing 98 | kwargs.update({'dz': dz, 'dx': dx}) 99 | except ValueError: 100 | dz, dx, dy = grid_spacing 101 | kwargs.update({'dz': dz, 'dx': dx, 'dy': dy}) 102 | 103 | # run the forward operator 104 | if operator == 'forward': 105 | return self._exec_forward(**kwargs) 106 | 107 | def _exec_forward(self, **kwargs): 108 | """ 109 | Run the forward operator. 110 | 111 | Parameters 112 | ---------- 113 | kwargs : dict 114 | Dictonary of keyword arguments. 115 | 116 | Returns 117 | ---------- 118 | ndarray 119 | Full wavefield after timestepping. 120 | ndarray 121 | Shot record after timestepping. 122 | """ 123 | 124 | # load the C library 125 | lib = self.library( 126 | dimension=len(kwargs.get('velocity_model').shape), 127 | density=kwargs.get('density_model'), 128 | dtype=kwargs.get('velocity_model').dtype 129 | ) 130 | 131 | # get the argtype for each arg key 132 | types = self._argtypes(**kwargs) 133 | 134 | # get the all possible keys in order 135 | ordered_keys = self._keys_in_order 136 | 137 | # list of argtypes 138 | argtypes = [] 139 | 140 | # list of args to pass to C function 141 | args = [] 142 | 143 | # compose the list of args and arg types 144 | for key in ordered_keys: 145 | if kwargs.get(key) is not None: 146 | argtypes.append(types.get(key)) 147 | args.append(kwargs.get(key)) 148 | 149 | forward = lib.forward 150 | forward.restype = ctypes.c_double 151 | forward.argtypes = argtypes 152 | 153 | # run the C forward function 154 | exec_time = forward(*args) 155 | 156 | print('Run forward in %f seconds.' % exec_time) 157 | 158 | return kwargs.get('u_full'), kwargs.get('shot_record') 159 | 160 | @property 161 | def _keys_in_order(self): 162 | """ 163 | Return all possible arg keys in the expected order in the C function. 164 | """ 165 | 166 | key_order = [ 167 | 'u_full', 168 | 'velocity_model', 169 | 'density_model', 170 | 'damping_mask', 171 | 'wavelet', 172 | 'wavelet_size', 173 | 'wavelet_count', 174 | 'second_order_fd_coefficients', 175 | 'first_order_fd_coefficients', 176 | 'boundary_condition', 177 | 'src_points_interval', 178 | 'src_points_interval_size', 179 | 'src_points_values', 180 | 'src_points_values_size', 181 | 'src_points_values_offset', 182 | 'rec_points_interval', 183 | 'rec_points_interval_size', 184 | 'rec_points_values', 185 | 'rec_points_values_size', 186 | 'rec_points_values_offset', 187 | 'shot_record', 188 | 'num_sources', 189 | 'num_receivers', 190 | 'nz', 191 | 'nx', 192 | 'ny', 193 | 'dz', 194 | 'dx', 195 | 'dy', 196 | 'saving_stride', 197 | 'dt', 198 | 'begin_timestep', 199 | 'end_timestep', 200 | 'space_order', 201 | 'num_snapshots' 202 | ] 203 | 204 | return key_order 205 | 206 | def _argtypes(self, **kwargs): 207 | """ 208 | Get the ctypes argtypes of the keyword arguments. 209 | 210 | Parameters 211 | ---------- 212 | kwargs : dict 213 | Dictonary of keyword arguments. 214 | 215 | Returns 216 | ---------- 217 | dict 218 | Dictonary of argtypes with the same keys. 219 | """ 220 | types = {} 221 | 222 | for key, value in kwargs.items(): 223 | 224 | if isinstance(value, np.ndarray): 225 | types[key] = self._convert_type_to_ctypes( 226 | 'np({})'.format(str(value.dtype)) 227 | ) 228 | else: 229 | types[key] = self._convert_type_to_ctypes( 230 | type(value).__name__ 231 | ) 232 | 233 | return types 234 | 235 | def _convert_boundary_condition(self, boundary_condition): 236 | """ 237 | Convert a boundary condition str to int. 238 | (none : 0, null_dirichlet : 1, null_neumann : 2). 239 | 240 | Parameters 241 | ---------- 242 | boundary_condition : tuple of str 243 | Boundary conditions on the edges of each axis. 244 | 245 | Returns 246 | ---------- 247 | ndarray 248 | Boundary conditions sequence as a numpy array. 249 | """ 250 | 251 | bc = { 252 | 'none': 0, 253 | 'null_dirichlet': 1, 254 | 'null_neumann': 2 255 | } 256 | 257 | conv_bc = [bc[i] for i in boundary_condition] 258 | 259 | return np.uint(conv_bc) 260 | 261 | def _convert_type_to_ctypes(self, type): 262 | """ 263 | Convert a given type in python to a ctypes.argtypes format. 264 | 265 | Parameters 266 | ---------- 267 | type : str 268 | Native type in python or numpy dtype. 269 | 270 | Returns 271 | ---------- 272 | object 273 | Argtype in ctypes format. 274 | """ 275 | argtype = { 276 | 'int': ctypes.c_size_t, 277 | 'float': ctypes.c_float, 278 | 'float32': ctypes.c_float, 279 | 'float64': ctypes.c_double, 280 | 'np(uint64)': ndpointer(ctypes.c_size_t, flags="C_CONTIGUOUS"), 281 | 'np(float32)': ndpointer(ctypes.c_float, flags="C_CONTIGUOUS"), 282 | 'np(float64)': ndpointer(ctypes.c_double, flags="C_CONTIGUOUS") 283 | } 284 | 285 | return argtype[type] 286 | -------------------------------------------------------------------------------- /simwave/kernel/frontend/__init__.py: -------------------------------------------------------------------------------- 1 | from simwave.kernel.frontend.model import SpaceModel, TimeModel 2 | 3 | from simwave.kernel.frontend.source import ( 4 | Source, 5 | Receiver, 6 | Wavelet, 7 | RickerWavelet, 8 | MultiWavelet 9 | ) 10 | 11 | from simwave.kernel.frontend.solver import Solver 12 | 13 | __all__ = [ 14 | "SpaceModel", 15 | "TimeModel", 16 | "Source", 17 | "Receiver", 18 | "Wavelet", 19 | "RickerWavelet", 20 | "MultiWavelet", 21 | "Solver" 22 | ] 23 | -------------------------------------------------------------------------------- /simwave/kernel/frontend/fd.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import findiff 3 | 4 | 5 | def half_coefficients(derivative_order, space_order): 6 | """ 7 | Return a list of right side finite differences coefficients 8 | according to the space order and derivative order. 9 | 10 | Parameters 11 | ---------- 12 | space_order : int 13 | Spatial order. 14 | 15 | Returns 16 | ---------- 17 | ndarray 18 | List of FD coefficients. 19 | """ 20 | # all coefficients 21 | coeffs = coefficients(derivative_order, space_order) 22 | 23 | middle = len(coeffs) // 2 24 | 25 | # coefficients starting from the center 26 | coeffs = coeffs[middle:] 27 | 28 | return coeffs 29 | 30 | 31 | def coefficients(derivative_order, space_order): 32 | """ 33 | Return a list of finite differences coefficients according 34 | to the space order and derivative order. 35 | 36 | Parameters 37 | ---------- 38 | derivative_order : int 39 | Derivative order 40 | space_order : int 41 | Spatial order. 42 | 43 | Returns 44 | ---------- 45 | ndarray 46 | List of FD coefficients. 47 | """ 48 | 49 | # fixed second derivative 50 | coeffs = findiff.coefficients(deriv=derivative_order, acc=space_order) 51 | 52 | return coeffs['center']['coefficients'] 53 | 54 | 55 | def calculate_dt(dimension, space_order, grid_spacing, velocity_model): 56 | """ 57 | Calculate dt with CFL conditions 58 | Based on https://library.seg.org/doi/pdf/10.1190/1.1444605 59 | for the acoustic case. 60 | 61 | Parameters 62 | ---------- 63 | dimension : int 64 | Domain dimension. 2 (2D) or 3 (3D). 65 | space_order : int 66 | Spatial order 67 | grid_spacing : tuple(float, float, flsoat) 68 | Spacing between grid points. 69 | velocity_model : ndarray 70 | Velocity model. 71 | 72 | Returns 73 | ---------- 74 | float 75 | dt in seconds. 76 | """ 77 | 78 | # 2nd order in time 79 | a1 = 4 80 | 81 | # FD coeffs to the specific space order 82 | fd_coeffs = coefficients( 83 | derivative_order=2, 84 | space_order=space_order 85 | ) 86 | 87 | a2 = dimension * np.sum(np.abs(fd_coeffs)) 88 | 89 | coeff = np.sqrt(a1 / a2) 90 | 91 | # The CFL condtion is then given by 92 | # dt <= coeff * h / max(velocity) 93 | dt = coeff * np.min(grid_spacing) / np.max(velocity_model) 94 | 95 | return dt 96 | -------------------------------------------------------------------------------- /simwave/kernel/frontend/kws.py: -------------------------------------------------------------------------------- 1 | from scipy.special import i0 2 | import numpy as np 3 | import warnings 4 | 5 | 6 | def get_kaiser_half_width(half_width): 7 | """ 8 | Get the optimal b parameter of the kaiser windowing function 9 | according to the window half-width. 10 | 11 | Parameters 12 | ---------- 13 | half_width: int 14 | Window half-width of the kaiser windowing function. 15 | 16 | Returns 17 | ---------- 18 | float 19 | Value of b parameter for the window half-width. 20 | """ 21 | 22 | # half-width options are limited from 1 to 10 23 | if half_width not in list(range(1, 11)): 24 | raise Exception( 25 | "Kaiser windowing half-width {} not supported".format(half_width) 26 | ) 27 | 28 | kaiser_b = { 29 | 1: 1.24, 30 | 2: 2.94, 31 | 3: 4.53, 32 | 4: 6.31, 33 | 5: 7.91, 34 | 6: 9.42, 35 | 7: 10.95, 36 | 8: 12.53, 37 | 9: 14.09, 38 | 10: 14.18, 39 | } 40 | 41 | return kaiser_b[half_width] 42 | 43 | 44 | def kaiser_windowing_sinc(num_points, source_point, half_width): 45 | """ 46 | Calculate the Kaiser windowed sinc function of a 47 | source/receiver grid point position. 48 | 49 | Based on Hicks, Graham J. (2002) Arbitrary source and receiver positioning 50 | in finite-difference schemes using Kaiser windowed sinc functions. 51 | 52 | Parameters 53 | ---------- 54 | num_points: int 55 | Number of grid points along the axis. 56 | 57 | source_point: float 58 | Source position (in grid points) in the axis. 59 | 60 | half_width: int 61 | Window half-width of the kaiser windowing function. 62 | 63 | Returns 64 | ---------- 65 | ndarray 66 | 1D numpy array with the source kaiser windowed sinc value 67 | on each point. In the points outside the window, the value is NaN. 68 | """ 69 | 70 | x = np.linspace(start=0, stop=num_points-1, 71 | num=num_points, dtype=np.float32) 72 | 73 | # calculate the distance (in grid points) of each point of the axis 74 | # to the point of the source/receiver 75 | x = x - source_point 76 | 77 | # calculate the square root term of the kaiser windowing function 78 | with warnings.catch_warnings(): 79 | warnings.simplefilter("ignore") 80 | sqrt = np.sqrt(1 - (x / half_width) ** 2) 81 | 82 | # get the b term value 83 | b = get_kaiser_half_width(half_width) 84 | 85 | # calculate the kaiser window 86 | kaiser = i0(b * sqrt) / i0(b) 87 | 88 | # calculate the sinc function of x 89 | sinc = np.sinc(x) 90 | 91 | # calculate the kaiser windowed sinc function 92 | kws = kaiser * sinc 93 | 94 | return kws 95 | 96 | 97 | def get_kws_valid_points(kaiser_windowed_array): 98 | """ 99 | Given a kaiser windowed array, returns the valid (non-NaN) 100 | values and points of the source/receiver location. 101 | 102 | Parameters 103 | ---------- 104 | kaiser_windwed_array: ndarray 105 | 1D numpy array with the source kaiser windowed sinc values 106 | on all points of the axis. 107 | 108 | Returns 109 | ---------- 110 | int 111 | index of first valid point. 112 | int 113 | index of last valid point. 114 | ndarray 115 | Numpy array with valid (non-NaN) values. 116 | """ 117 | 118 | indexes = [] 119 | values = [] 120 | 121 | for index, value in enumerate(kaiser_windowed_array): 122 | if not np.isnan(value): 123 | indexes.append(index) 124 | values.append(value) 125 | 126 | if len(indexes) == 0: 127 | raise Exception( 128 | "There is no valid point in the source/receiver location" 129 | ) 130 | 131 | begin_index = indexes[0] 132 | end_index = indexes[-1] 133 | values = np.array(values, dtype=np.float32) 134 | 135 | return begin_index, end_index, values 136 | 137 | 138 | def get_source_points(grid_shape, source_location, half_width): 139 | """ 140 | Return the point interval of a source/receiver and ther values. 141 | 142 | Parameters 143 | ---------- 144 | grid_shape: tuple of int 145 | Number of grid points in each grid axis. 146 | source_location: tuple of float or list of float 147 | Source/receiver location (in grid points) in each axis. 148 | half_width: int 149 | Window half-width of the kaiser windowing function. 150 | 151 | Returns 152 | ---------- 153 | ndarray 154 | 1D Numpy array with [begin_point_axis1, end_point_axis1, .., 155 | begin_point_axisN, end_point_axisN]. 156 | ndarray 157 | 1D Numpy array with [source_values_axis1, .., source_values_axisN]. 158 | """ 159 | 160 | # check with grid and source location have the same dimension 161 | if len(grid_shape) != len(source_location): 162 | raise Exception( 163 | "Grid and source/receiver location must have the same dimension." 164 | ) 165 | 166 | source_points = [] 167 | source_values = np.array([], dtype=np.float32) 168 | 169 | # for each axis 170 | for axis in range(len(grid_shape)): 171 | num_points = grid_shape[axis] 172 | source_point = source_location[axis] 173 | 174 | kws = kaiser_windowing_sinc(num_points, source_point, half_width) 175 | begin_index, end_index, values = get_kws_valid_points(kws) 176 | 177 | source_points.append(begin_index) 178 | source_points.append(end_index) 179 | source_values = np.append(source_values, values) 180 | 181 | source_points = np.array(source_points, dtype=np.uint) 182 | 183 | return source_points, source_values 184 | -------------------------------------------------------------------------------- /simwave/kernel/frontend/solver.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from simwave.kernel.backend.middleware import Middleware 3 | 4 | 5 | class Solver: 6 | """ 7 | Acoustic solver for the simulation. 8 | 9 | Parameters 10 | ---------- 11 | space_model : SpaceModel 12 | Space model object. 13 | time_model: TimeModel 14 | Time model object. 15 | sources : Source 16 | Source object. 17 | receivers : Receiver 18 | Receiver object. 19 | wavelet : Wavelet 20 | Wavelet object. 21 | compiler : Compiler 22 | Backend compiler object. 23 | """ 24 | def __init__(self, space_model, time_model, sources, 25 | receivers, wavelet, compiler=None): 26 | 27 | self._space_model = space_model 28 | self._time_model = time_model 29 | self._sources = sources 30 | self._receivers = receivers 31 | self._wavelet = wavelet 32 | self._compiler = compiler 33 | 34 | # create a middleware to communicate with backend 35 | self._middleware = Middleware(compiler=self.compiler) 36 | 37 | @property 38 | def space_model(self): 39 | """Space model object.""" 40 | return self._space_model 41 | 42 | @property 43 | def time_model(self): 44 | """Time model object.""" 45 | return self._time_model 46 | 47 | @property 48 | def sources(self): 49 | """Source object.""" 50 | return self._sources 51 | 52 | @property 53 | def receivers(self): 54 | """Receiver object.""" 55 | return self._receivers 56 | 57 | @property 58 | def wavelet(self): 59 | """Wavelet object.""" 60 | return self._wavelet 61 | 62 | @property 63 | def compiler(self): 64 | """Compiler object.""" 65 | return self._compiler 66 | 67 | @property 68 | def snapshot_indexes(self): 69 | """List of snapshot indexes (wavefields to be saved).""" 70 | 71 | # if saving_stride is 0, only saves the last timestep 72 | if self.time_model.saving_stride == 0: 73 | return [self.time_model.time_indexes[-1]] 74 | 75 | snap_indexes = list( 76 | range( 77 | self.time_model.time_indexes[0], 78 | self.time_model.timesteps, 79 | self.time_model.saving_stride 80 | ) 81 | ) 82 | 83 | return snap_indexes 84 | 85 | @property 86 | def num_snapshots(self): 87 | """Number of snapshots (wavefields to be saved).""" 88 | return len(self.snapshot_indexes) 89 | 90 | @property 91 | def shot_record(self): 92 | """Return the shot record array.""" 93 | u_recv = np.zeros( 94 | shape=(self.time_model.timesteps, self.receivers.count), 95 | dtype=self.space_model.dtype 96 | ) 97 | 98 | return u_recv 99 | 100 | @property 101 | def u_full(self): 102 | """Return the complete grid (snapshots, nz. nz [, ny]).""" 103 | 104 | # add 2 halo snapshots (second order in time) 105 | snapshots = self.num_snapshots + 2 106 | 107 | # define the final shape (snapshots + domain) 108 | shape = (snapshots,) + self.space_model.extended_shape 109 | 110 | return np.zeros(shape, dtype=self.space_model.dtype) 111 | 112 | def forward(self): 113 | """ 114 | Run the forward propagator. 115 | 116 | Returns 117 | ---------- 118 | ndarray 119 | Full wavefield with snapshots. 120 | ndarray 121 | Shot record. 122 | """ 123 | 124 | src_points, src_values, src_offsets = \ 125 | self.sources.interpolated_points_and_values 126 | rec_points, rec_values, rec_offsets = \ 127 | self.receivers.interpolated_points_and_values 128 | 129 | u_full, recv = self._middleware.exec( 130 | operator='forward', 131 | u_full=self.u_full, 132 | velocity_model=self.space_model.extended_velocity_model, 133 | density_model=self.space_model.extended_density_model, 134 | damping_mask=self.space_model.damping_mask, 135 | wavelet=self.wavelet.values, 136 | wavelet_size=self.wavelet.timesteps, 137 | wavelet_count=self.wavelet.num_sources, 138 | second_order_fd_coefficients=self.space_model.fd_coefficients(2), 139 | first_order_fd_coefficients=self.space_model.fd_coefficients(1), 140 | boundary_condition=self.space_model.boundary_condition, 141 | src_points_interval=src_points, 142 | src_points_interval_size=len(src_points), 143 | src_points_values=src_values, 144 | src_points_values_offset=src_offsets, 145 | src_points_values_size=len(src_values), 146 | rec_points_interval=rec_points, 147 | rec_points_interval_size=len(rec_points), 148 | rec_points_values=rec_values, 149 | rec_points_values_offset=rec_offsets, 150 | rec_points_values_size=len(rec_values), 151 | shot_record=self.shot_record, 152 | num_sources=self.sources.count, 153 | num_receivers=self.receivers.count, 154 | grid_spacing=self.space_model.grid_spacing, 155 | saving_stride=self.time_model.saving_stride, 156 | dt=self.time_model.dt, 157 | begin_timestep=1, 158 | end_timestep=self.time_model.timesteps, 159 | space_order=self.space_model.space_order, 160 | num_snapshots=self.u_full.shape[0] 161 | ) 162 | 163 | # remove time halo region 164 | u_full = self.time_model.remove_time_halo_region(u_full) 165 | 166 | # remove spatial halo region 167 | u_full = self.space_model.remove_halo_region(u_full) 168 | 169 | return u_full, recv 170 | -------------------------------------------------------------------------------- /simwave/kernel/frontend/source.py: -------------------------------------------------------------------------------- 1 | from simwave.kernel.frontend import kws 2 | import numpy as np 3 | 4 | 5 | class Source: 6 | """ 7 | Implement the structure of a source/receiver. 8 | 9 | Parameters 10 | ---------- 11 | space_model : SpaceModel 12 | Space model object. 13 | coordinates : list of tuple, list of list or ndarray 14 | Physical coordinates (in meters) for this source. 15 | window_radius : int, optional 16 | Window half-width of the kaiser windowing function. Default is 4. 17 | """ 18 | def __init__(self, space_model, coordinates, window_radius=4): 19 | 20 | self._space_model = space_model 21 | self._window_radius = window_radius 22 | 23 | # coordinates can be lists, lists of tuples, tuples, 24 | # tuples of tuples, tuples of lists and ndarrays. 25 | if isinstance(coordinates, (list, tuple, np.ndarray)): 26 | self._coordinates = np.asarray(coordinates, 27 | dtype=self.space_model.dtype) 28 | 29 | # the shape must be (n,d) where 30 | # n is the number of sources/receivers 31 | # d is the dimension (2 or 3) 32 | if len(self.coordinates.shape) == 1: 33 | 34 | self._coordinates = np.reshape( 35 | self._coordinates, 36 | (1,) + self.coordinates.shape 37 | ) 38 | 39 | elif len(self.coordinates.shape) != 2 \ 40 | or self.coordinates.shape[1] not in [2, 3]: 41 | 42 | raise ValueError("Invalid source/receiver coordinates format.") 43 | else: 44 | raise ValueError("Source/Receiver coordinates must be " 45 | "represented as lists, tuples or ndarrays.") 46 | 47 | @property 48 | def space_model(self): 49 | """Corresponding space model.""" 50 | return self._space_model 51 | 52 | @property 53 | def coordinates(self): 54 | """Physical coordinates (in meters) for this source.""" 55 | return self._coordinates 56 | 57 | @property 58 | def window_radius(self): 59 | """Window half-width of the kaiser windowing function.""" 60 | return self._window_radius 61 | 62 | @property 63 | def grid_positions(self): 64 | """List of point positions (in grid points) for this source set.""" 65 | 66 | positions = [] 67 | 68 | # for each position (one source or receiver) 69 | for coord in self.coordinates: 70 | # 2D coordinates 71 | if len(coord) == 2: 72 | zmin, zmax, xmin, xmax = self.space_model.bounding_box 73 | z_spacing, x_spacing = self.space_model.grid_spacing 74 | 75 | if not (zmin <= coord[0] <= zmax) or \ 76 | not (xmin <= coord[1] <= xmax): 77 | raise Exception("Coordinates %s out of bounds." % coord) 78 | 79 | zpos = (coord[0] - zmin) / z_spacing 80 | xpos = (coord[1] - xmin) / x_spacing 81 | 82 | positions.append((zpos, xpos)) 83 | # 3D coordinates 84 | elif len(coord) == 3: 85 | zmin, zmax, xmin, xmax, ymin, ymax = \ 86 | self.space_model.bounding_box 87 | 88 | z_spacing, x_spacing, y_spacing = self.space_model.grid_spacing 89 | 90 | if not (zmin <= coord[0] <= zmax) or \ 91 | not (xmin <= coord[1] <= xmax) or \ 92 | not (ymin <= coord[2] <= ymax): 93 | raise Exception("Coordinates %s out of bounds." % coord) 94 | 95 | zpos = (coord[0] - zmin) / z_spacing 96 | xpos = (coord[1] - xmin) / x_spacing 97 | ypos = (coord[2] - ymin) / y_spacing 98 | 99 | positions.append((zpos, xpos, ypos)) 100 | else: 101 | raise Exception("Dimension %d not supported." % len(coord)) 102 | 103 | return np.asarray(positions, dtype=self.space_model.dtype) 104 | 105 | @property 106 | def adjusted_grid_positions(self): 107 | """ 108 | Adjusted source positions (in grid points) according to nbl extension 109 | and space order halo. In this case, the origin is shifted. 110 | """ 111 | # adjust the origin (nbl + halo size) 112 | nbl = list(self.space_model.nbl[::2]) 113 | halo = list(self.space_model.halo_size[::2]) 114 | origin = [nbl[i] + halo[i] for i in range(len(nbl))] 115 | 116 | adjusted_positions = [ 117 | tuple(src[i] + origin[i] for i in range(len(src))) 118 | for src in self.grid_positions 119 | ] 120 | 121 | return np.asarray(adjusted_positions, dtype=self.space_model.dtype) 122 | 123 | @property 124 | def interpolated_points_and_values(self): 125 | """ 126 | Calculate the point interval of a source/receiver and their values. 127 | Use Kaiser windowing sinc for the source positioning. 128 | 129 | Returns 130 | ---------- 131 | ndarray 132 | 1D Numpy array with [begin_point_axis1, end_point_axis1, 133 | .., begin_point_axisN, end_point_axisN]. 134 | ndarray 135 | 1D Numpy array with [source_values_axis1, .., source_values_axisN]. 136 | ndarray 137 | 1D Numpy array with [source_1_offset, .., source_N_offset]. 138 | """ 139 | 140 | points = np.array([], dtype=np.uint) 141 | values = np.array([], dtype=self.space_model.dtype) 142 | offsets = [0] 143 | 144 | for position in self.adjusted_grid_positions: 145 | # apply kasier window to interpolate the source/receiver 146 | # in a region of grid points 147 | p, v = kws.get_source_points( 148 | grid_shape=self.space_model.extended_shape, 149 | source_location=position, 150 | half_width=self.window_radius 151 | ) 152 | 153 | # values offset for each position 154 | offsets.append(offsets[-1] + v.size) 155 | 156 | points = np.append(points, p) 157 | values = np.append(values, v) 158 | 159 | return points, values, np.asarray(offsets, dtype=np.uint) 160 | 161 | @property 162 | def count(self): 163 | """Return the number of sources/receives.""" 164 | return self.coordinates.shape[0] 165 | 166 | 167 | # a receiver is also a type of source 168 | Receiver = Source 169 | 170 | 171 | class Wavelet: 172 | """ 173 | Implement a wavelet for the source. 174 | 175 | Parameters 176 | ---------- 177 | function : object 178 | Function (expression) that creates the wavelet. 179 | kwargs : dict 180 | key word arguments of the function. 181 | """ 182 | def __init__(self, function, **kwargs): 183 | self._function = function 184 | self._kwargs = kwargs 185 | 186 | @property 187 | def function(self): 188 | """Function (expression) that creates the wavelet.""" 189 | return self._function 190 | 191 | @property 192 | def kwargs(self): 193 | """key word arguments of the function.""" 194 | return self._kwargs 195 | 196 | @property 197 | def values(self): 198 | """Wavelet values.""" 199 | return self.function(**self.kwargs) 200 | 201 | @property 202 | def num_sources(self): 203 | """Number of sources.""" 204 | return 1 205 | 206 | @property 207 | def timesteps(self): 208 | """Number of timesteps.""" 209 | return len(self.values) 210 | 211 | 212 | class RickerWavelet(Wavelet): 213 | """ 214 | Implement a ricker wavelet. 215 | 216 | Parameters 217 | ---------- 218 | peak_frequency : float 219 | Peak frequency for the wavelet in Hz. 220 | time_model: TimeModel 221 | Time model object. 222 | amplitude : float, optional 223 | Amplitude of the wavelet. Default is 1.0. 224 | """ 225 | def __init__(self, peak_frequency, time_model, amplitude=1): 226 | 227 | self._peak_frequency = peak_frequency 228 | self._time_model = time_model 229 | self._amplitude = amplitude 230 | 231 | super().__init__( 232 | self._function, 233 | peak_frequency=self.peak_frequency, 234 | time_model=self.time_model, 235 | amplitude=self.amplitude 236 | ) 237 | 238 | @property 239 | def peak_frequency(self): 240 | """Peak frequency of the wavelet in Hz.""" 241 | return self._peak_frequency 242 | 243 | @property 244 | def time_model(self): 245 | """Corresponding time model.""" 246 | return self._time_model 247 | 248 | @property 249 | def amplitude(self): 250 | """Amplitude of the wavelet.""" 251 | return self._amplitude 252 | 253 | def _function(self, peak_frequency, time_model, amplitude): 254 | """Function that generates the ricker wavelet.""" 255 | t0 = 1 / peak_frequency 256 | r = np.pi * peak_frequency * (time_model.time_values - t0) 257 | return amplitude * (1 - 2.0 * r**2) * np.exp(-r**2) 258 | 259 | 260 | class MultiWavelet(Wavelet): 261 | """ 262 | Implement one wavelet for each source. 263 | 264 | Parameters 265 | ---------- 266 | values : ndarray 267 | Numpy array [timesteps][sources] 268 | time_model: TimeModel 269 | Time model object. 270 | """ 271 | def __init__(self, values, time_model): 272 | self._values = values 273 | self._time_model = time_model 274 | 275 | if self.timesteps != self.time_model.timesteps: 276 | raise ValueError("Wavelet must have {} timesteps.").format( 277 | self.time_model.timesteps 278 | ) 279 | 280 | @property 281 | def values(self): 282 | """Wavelet values.""" 283 | return np.ascontiguousarray(self._values, dtype=self.dtype) 284 | 285 | @property 286 | def num_sources(self): 287 | """Number of sources.""" 288 | return self.values.shape[1] 289 | 290 | @property 291 | def timesteps(self): 292 | """Number of timesteps.""" 293 | return self.values.shape[0] 294 | 295 | @property 296 | def time_model(self): 297 | """Corresponding time model.""" 298 | return self._time_model 299 | 300 | @property 301 | def dtype(self): 302 | return self.time_model.dtype 303 | -------------------------------------------------------------------------------- /simwave/plots/__init__.py: -------------------------------------------------------------------------------- 1 | from simwave.plots.plot import ( 2 | plot_wavefield, 3 | plot_shotrecord, 4 | plot_velocity_model, 5 | plot_wavelet 6 | ) 7 | 8 | __all__ = [ 9 | "plot_wavefield", 10 | "plot_shotrecord", 11 | "plot_velocity_model", 12 | "plot_wavelet" 13 | ] 14 | -------------------------------------------------------------------------------- /simwave/plots/plot.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import os 4 | from matplotlib import cm 5 | from mpl_toolkits.axes_grid1 import make_axes_locatable 6 | 7 | 8 | def plot_wavefield(wavefield, file_name="wavefield", colorbar=True, 9 | cmap="gray", extent=None, show=False, clim=[-5, 5]): 10 | """ 11 | Plot the wavefield. 12 | 13 | Parameters 14 | ---------- 15 | wavefield : ndarray 16 | Wavefield data. 17 | file_name : str, optional 18 | Name of the image to be saved. 19 | Default is wavefield. 20 | colorbar : bool, optional 21 | If True, show a colorbar. Default is True. 22 | cmap : str, optional 23 | The Colormap instance or registered colormap name 24 | used to map scalar data to colors. 25 | Default is gray. 26 | extent : floats(left, right, bottom, top), optional 27 | The bounding box in data coordinates that the image will fill. 28 | show : bool, optional 29 | If True, show the image on a pop up window. 30 | Default is False. 31 | clim : list 32 | Set the color limits of the current image. 33 | Default is (vmin=-5, vmax=5). 34 | """ 35 | 36 | # create the destination dir 37 | os.makedirs("plots", exist_ok=True) 38 | 39 | # process data and generate the plot 40 | plot = plt.imshow(wavefield, cmap=cmap, extent=extent) 41 | 42 | m_unit = 'm' if extent is not None else 'points' 43 | 44 | # labels 45 | plt.xlabel('Width ({})'.format(m_unit)) 46 | plt.ylabel('Depth ({})'.format(m_unit)) 47 | 48 | # Create aligned colorbar on the right 49 | if colorbar: 50 | ax = plt.gca() 51 | divider = make_axes_locatable(ax) 52 | cax = divider.append_axes("right", size="5%", pad=0.05) 53 | plt.colorbar(plot, cax=cax) 54 | plt.clim(clim) 55 | 56 | plt.savefig("plots/{}.png".format(file_name), format="png") 57 | 58 | if show: 59 | plt.show() 60 | 61 | plt.close() 62 | 63 | print("Wavefield saved in plots/{}.png".format(file_name)) 64 | 65 | 66 | def plot_shotrecord(rec, file_name="shotrecord", colorbar=True, show=False): 67 | """ 68 | Plot a shot record (receiver values over time). 69 | 70 | Parameters 71 | ---------- 72 | rec : ndarray 73 | Receiver data with shape (time, points). 74 | file_name : str, optional 75 | Name of the image to be saved. 76 | Default is wavelet. 77 | colorbar : bool, optional 78 | If True, show a colorbar. Default is True. 79 | show : bool, optional 80 | If True, show the image on a pop up window. 81 | Default is False. 82 | """ 83 | scale = np.max(rec) / 10.0 84 | plot = plt.imshow(rec, vmin=-scale, vmax=scale, cmap=cm.gray) 85 | plt.xlabel("Receivers") 86 | plt.ylabel("Time (ms)") 87 | 88 | # Create colorbar on the right 89 | if colorbar: 90 | ax = plt.gca() 91 | divider = make_axes_locatable(ax) 92 | cax = divider.append_axes("right", size="5%", pad=0.05) 93 | plt.colorbar(plot, cax=cax) 94 | 95 | # create the destination dir 96 | os.makedirs("plots", exist_ok=True) 97 | plt.savefig("plots/{}.png".format(file_name), format="png") 98 | 99 | if show: 100 | plt.show() 101 | 102 | plt.close() 103 | 104 | print("Shot record saved in plots/{}.png".format(file_name)) 105 | 106 | 107 | def plot_velocity_model(model, file_name="velocity_model", colorbar=True, 108 | extent=None, cmap="jet", show=False): 109 | """ 110 | Plot the velocity model. 111 | 112 | Parameters 113 | ---------- 114 | model : ndarray 115 | Velocity model data. 116 | file_name : str, optional 117 | Name of the image to be saved. 118 | Default is velocity_model. 119 | colorbar : bool, optional 120 | If True, show a colorbar. Default is True. 121 | cmap : str, optional 122 | The Colormap instance or registered colormap name 123 | used to map scalar data to colors. 124 | Default is jet. 125 | extent : floats(left, right, bottom, top), optional 126 | The bounding box in data coordinates that the image will fill. 127 | show : bool, optional 128 | If True, show the image on a pop up window. 129 | Default is False. 130 | """ 131 | 132 | # create the destination dir 133 | os.makedirs("plots", exist_ok=True) 134 | 135 | # process data and generate the plot 136 | plot = plt.imshow(model, cmap=cmap, extent=extent) 137 | 138 | m_unit = 'm' if extent is not None else 'points' 139 | 140 | # labels 141 | plt.xlabel('Width ({})'.format(m_unit)) 142 | plt.ylabel('Depth ({})'.format(m_unit)) 143 | 144 | # Create aligned colorbar on the right 145 | if colorbar: 146 | ax = plt.gca() 147 | divider = make_axes_locatable(ax) 148 | cax = divider.append_axes("right", size="5%", pad=0.05) 149 | plt.colorbar(plot, cax=cax) 150 | 151 | plt.savefig("plots/{}.png".format(file_name), format="png") 152 | 153 | if show: 154 | plt.show() 155 | 156 | plt.close() 157 | 158 | print("Velocity model saved in plots/{}.png".format(file_name)) 159 | 160 | 161 | def plot_wavelet(time_values, wavelet_values, 162 | file_name="wavelet", show=False): 163 | """ 164 | Show the wavelet in a graph. 165 | 166 | Parameters 167 | ---------- 168 | time_values : list 169 | Discretized values of time in seconds. 170 | wavelet_values : list 171 | Pulse of the wavelet. 172 | file_name : str, optional 173 | Name of the image to be saved. 174 | Default is wavelet. 175 | show : bool, optional 176 | If True, show the image on a pop up window. 177 | Default is False. 178 | """ 179 | plt.plot(time_values, wavelet_values) 180 | plt.xlabel("Time (s)") 181 | plt.ylabel("Amplitude") 182 | plt.tick_params() 183 | 184 | plt.savefig("plots/{}.png".format(file_name), format="png") 185 | 186 | if show: 187 | plt.show() 188 | 189 | print("Wavelet saved in plots/{}.png".format(file_name)) 190 | -------------------------------------------------------------------------------- /tests/reference_solution/generator.py: -------------------------------------------------------------------------------- 1 | from simwave import SpaceModel, TimeModel, RickerWavelet, Solver 2 | from simwave import Receiver, Source, Compiler 3 | import numpy as np 4 | 5 | 6 | def generate_one(dimension, space_order): 7 | 8 | if dimension == 2: 9 | shape = (500,)*dimension 10 | bbox = (0, 5000, 0, 5000) 11 | spacing = (10, 10) 12 | damping_length = 100 13 | boundary_condition = ( 14 | "null_neumann", "null_dirichlet", 15 | "none", "null_dirichlet" 16 | ) 17 | source_position = [(2500, 2500)] 18 | tf = 1.0 19 | f0 = 10.0 20 | else: 21 | shape = (100,)*dimension 22 | bbox = (0, 1000, 0, 1000, 0, 1000) 23 | spacing = (10, 10, 10) 24 | damping_length = 50 25 | boundary_condition = ( 26 | "null_neumann", "null_dirichlet", 27 | "none", "null_dirichlet", 28 | "null_neumann", "null_dirichlet", 29 | ) 30 | source_position = [(500, 495, 505)] 31 | tf = 0.4 32 | f0 = 15.0 33 | 34 | result_file = "wavefield-{}d-so-{}".format(dimension, space_order) 35 | 36 | # Velocity model 37 | vel = np.zeros(shape=shape, dtype=np.float32) 38 | vel[:] = 1500.0 39 | vel[shape[0]//2:] = 2000.0 40 | 41 | # create the space model 42 | space_model = SpaceModel( 43 | bounding_box=bbox, 44 | grid_spacing=spacing, 45 | velocity_model=vel, 46 | space_order=space_order, 47 | dtype=np.float32 48 | ) 49 | 50 | # config boundary conditions 51 | space_model.config_boundary( 52 | damping_length=damping_length, 53 | boundary_condition=boundary_condition, 54 | damping_polynomial_degree=3, 55 | damping_alpha=0.001 56 | ) 57 | 58 | # create the time model 59 | time_model = TimeModel( 60 | space_model=space_model, 61 | tf=tf, 62 | saving_stride=0 63 | ) 64 | 65 | # create the set of sources 66 | source = Source( 67 | space_model=space_model, 68 | coordinates=source_position, 69 | window_radius=1 70 | ) 71 | 72 | # crete the set of receivers 73 | receiver = Receiver( 74 | space_model=space_model, 75 | coordinates=source_position, 76 | window_radius=1 77 | ) 78 | 79 | # create a ricker wavelet with 10hz of peak frequency 80 | ricker = RickerWavelet(f0, time_model) 81 | 82 | # create a compiler 83 | compiler = Compiler(language='c') 84 | 85 | # create the solver 86 | solver = Solver( 87 | compiler=compiler, 88 | space_model=space_model, 89 | time_model=time_model, 90 | sources=source, 91 | receivers=receiver, 92 | wavelet=ricker 93 | ) 94 | 95 | # run the forward 96 | u, recv = solver.forward() 97 | 98 | np.save(result_file, u) 99 | 100 | 101 | def generate_all(): 102 | 103 | dimension = [2, 3] 104 | space_order = [2, 8] 105 | 106 | for dim in dimension: 107 | for so in space_order: 108 | generate_one(dim, so) 109 | 110 | 111 | if __name__ == "__main__": 112 | generate_all() 113 | -------------------------------------------------------------------------------- /tests/reference_solution/wavefield-2d-so-2.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HPCSys-Lab/simwave/49e9c85d38020a4222aefa8c7224a103a40f9ba1/tests/reference_solution/wavefield-2d-so-2.npy -------------------------------------------------------------------------------- /tests/reference_solution/wavefield-2d-so-8.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HPCSys-Lab/simwave/49e9c85d38020a4222aefa8c7224a103a40f9ba1/tests/reference_solution/wavefield-2d-so-8.npy -------------------------------------------------------------------------------- /tests/reference_solution/wavefield-3d-so-2.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HPCSys-Lab/simwave/49e9c85d38020a4222aefa8c7224a103a40f9ba1/tests/reference_solution/wavefield-3d-so-2.npy -------------------------------------------------------------------------------- /tests/reference_solution/wavefield-3d-so-8.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HPCSys-Lab/simwave/49e9c85d38020a4222aefa8c7224a103a40f9ba1/tests/reference_solution/wavefield-3d-so-8.npy -------------------------------------------------------------------------------- /tests/test_compiler.py: -------------------------------------------------------------------------------- 1 | from simwave import Compiler 2 | import pytest 3 | 4 | 5 | class TestCompiler: 6 | 7 | def test_cc(self): 8 | for cc in ['gcc', 'icc', 'clang', 'cc']: 9 | compiler = Compiler(cc=cc) 10 | assert compiler.cc == cc 11 | 12 | @pytest.mark.parametrize( 13 | 'cc, language, cflags, expected', [ 14 | ('gcc', 'c', None, '-O3 -fPIC -Wall -std=c99 -shared'), 15 | ('gcc', 'c', '-O3 -fPIC', '-O3 -fPIC -shared'), 16 | ('icc', 'c', '-O3 -shared', '-O3 -shared'), 17 | ('icc', 'c', '-O3', '-O3 -shared'), 18 | ('clang', 'c', None, '-O3 -fPIC -Wall -std=c99 -shared'), 19 | ('gcc', 'cpu_openmp', '-O3 -fPIC', '-O3 -fPIC -shared -fopenmp'), 20 | ('pgcc', 'cpu_openmp', '-shared', '-shared -mp'), 21 | ('icc', 'cpu_openmp', '-O3', '-O3 -shared -openmp'), 22 | ('clang', 'cpu_openmp', '-O3', '-O3 -shared -fopenmp'), 23 | ('foo', 'cpu_openmp', '-O3', '-O3 -shared') 24 | ] 25 | ) 26 | def test_cflags(self, cc, language, cflags, expected): 27 | compiler = Compiler(cc=cc, language=language, cflags=cflags) 28 | assert compiler.cflags == expected 29 | 30 | def test_language(self): 31 | for language in ['c', 'cpu_openmp']: 32 | compiler = Compiler(language=language) 33 | assert compiler.language == language 34 | 35 | def test_language_fail(self): 36 | with pytest.raises(ValueError): 37 | Compiler(language='cpu_mpi') 38 | 39 | @pytest.mark.parametrize( 40 | 'cc, omp_flag', [ 41 | ('gcc', '-fopenmp'), ('icc', '-openmp'), ('pgcc', '-mp'), 42 | ('clang', '-fopenmp'), ('foo', None) 43 | ] 44 | ) 45 | def test_get_openmp_flag(self, cc, omp_flag): 46 | 47 | compiler = Compiler(cc=cc) 48 | assert compiler.get_openmp_flag() == omp_flag 49 | -------------------------------------------------------------------------------- /tests/test_convergence.py: -------------------------------------------------------------------------------- 1 | # test quality of numerical solution 2 | from scipy.special import hankel2 3 | import numpy.linalg as la 4 | import numpy as np 5 | import simwave 6 | import pytest 7 | 8 | 9 | def _create_space_model(bbox, spacing, vel, order): 10 | space_model = simwave.SpaceModel( 11 | bounding_box=bbox, 12 | grid_spacing=spacing, 13 | velocity_model=vel * np.ones((100, 100), dtype=np.float64), 14 | space_order=order 15 | ) 16 | return space_model 17 | 18 | 19 | def _acquisition(space_model, time_model, source, receiver, freq): 20 | source = simwave.Source(space_model, coordinates=source) 21 | receiver = simwave.Receiver(space_model, coordinates=receiver) 22 | ricker = simwave.RickerWavelet(freq, time_model) 23 | 24 | return source, receiver, ricker 25 | 26 | 27 | def analytical_solution(space_model, freq, src, recs, dt): 28 | time_model = simwave.TimeModel(space_model=space_model, t0=0, tf=3000) 29 | time_model.dt = dt 30 | ricker = simwave.RickerWavelet(freq, time_model) 31 | # Ricker's FFT 32 | nf = int(time_model.timesteps / 2 + 1) 33 | df = 1 / time_model.tf 34 | frequencies = df * np.arange(nf) 35 | q = np.fft.fft(ricker.values)[:nf] 36 | # Coordinates 37 | xg = np.array([rec[0] for rec in recs]) 38 | zg = np.array([rec[1] for rec in recs]) 39 | r = np.sqrt((xg - src[0]) ** 2 + (zg - src[1]) ** 2) 40 | # Analytical solution 41 | k = 2 * np.pi * frequencies / np.unique(space_model.velocity_model) 42 | u = np.zeros((nf), dtype=complex) 43 | u[1:-1] = hankel2(0, k[1:-1][None, :] * r[:, None]) 44 | ui = np.fft.ifft(- 1j * np.pi * u * q, time_model.timesteps) 45 | # Scale correctly 46 | # ui = 1 / (2 * np.pi) * np.real(ui) * space_model.grid_spacing[0] ** 2 47 | ui = 1 / (2 * np.pi) * np.real(ui) 48 | 49 | return ui 50 | 51 | 52 | def accuracy(spacing, bbox, order, dt, t0, tf, c, f0, src, rec): 53 | space_model = _create_space_model(bbox, spacing, c, order) 54 | 55 | time_model = simwave.TimeModel(space_model=space_model, t0=t0, tf=tf) 56 | time_model.dt = dt 57 | 58 | source, receiver, wavelet = _acquisition( 59 | space_model, time_model, src, rec, f0 60 | ) 61 | 62 | solver = simwave.Solver(space_model, time_model, source, receiver, wavelet) 63 | 64 | # numerical solution 65 | # u_num = solver.forward()[-1].flatten() 66 | u_num = solver.forward()[-1].flatten() / spacing[0] ** 2 67 | 68 | # analytical solution 69 | u_exact = analytical_solution( 70 | space_model, f0, src[0], rec, dt 71 | ).flatten()[:time_model.timesteps] 72 | 73 | # compare solutions 74 | return la.norm(u_num - u_exact) / np.sqrt(u_num.size) 75 | 76 | 77 | @pytest.mark.parametrize( 78 | "spacing, bbox, order, dt, t0, tf, c, f0, src, rec, tol", 79 | [ 80 | ( 81 | (0.5, 0.5), (-40, 440, -40, 440), 8, 0.1, 0, 150.075, 82 | 1.5, 0.09, [(200, 200)], [(260, 260)], 1e-4 83 | ) 84 | ] 85 | ) 86 | def test_accuracy(spacing, bbox, order, dt, t0, tf, c, f0, src, rec, tol): 87 | assert accuracy(spacing, bbox, order, dt, t0, tf, c, f0, src, rec) < tol 88 | 89 | 90 | @pytest.mark.parametrize( 91 | "spacing, bbox, order, timesteps, t0, tf, c, \ 92 | f0, src, rec, time_convergence", 93 | [ 94 | ( 95 | (0.5, 0.5), (-40, 440, -40, 440), 8, [0.1, 0.075, 0.04, 0.025], 96 | 0, 150.075, 1.5, 0.09, [(200, 200)], [(260, 260)], 1.7 97 | ) 98 | ] 99 | ) 100 | def test_convergence_in_time(spacing, bbox, order, timesteps, t0, tf, c, f0, 101 | src, rec, time_convergence): 102 | accs = [] 103 | for dt in timesteps: 104 | accs.append( 105 | accuracy(spacing, bbox, order, dt, t0, tf, c, f0, src, rec) 106 | ) 107 | 108 | assert np.poly1d(np.polyfit(np.log(timesteps), 109 | np.log(accs), 1))[1] > time_convergence 110 | 111 | 112 | @pytest.mark.parametrize( 113 | "spacings, bbox, space_orders, dt, t0, tf, c, f0, src, rec, space_rates", 114 | [ 115 | ( 116 | [2.0, 2.5, 4.0], (-40, 440, -40, 440), [2, 4, 6, 8, 10], 0.025, 117 | 0, 150.075, 1.5, 0.09, [(200, 200)], [(260, 260)], 118 | [1.7, 4, 6, 7.7, 8.7] 119 | ) 120 | ] 121 | ) 122 | def test_convergence_in_space(spacings, bbox, space_orders, dt, t0, tf, c, 123 | f0, src, rec, space_rates): 124 | accs = {} 125 | conv_rates = [] 126 | for order in space_orders: 127 | accs[order] = {} 128 | for spacing in spacings: 129 | accs[order][spacing] = accuracy( 130 | tuple([spacing, spacing]), bbox, 131 | order, dt, t0, tf, c, f0, src, rec 132 | ) 133 | step = list(accs[order].keys()) 134 | error = list(accs[order].values()) 135 | conv_rates.append( 136 | np.poly1d(np.polyfit(np.log(step), np.log(error), 1))[1] 137 | ) 138 | for rate, min_rate in zip(conv_rates, space_rates): 139 | assert rate > min_rate 140 | -------------------------------------------------------------------------------- /tests/test_gpu_convergence.py: -------------------------------------------------------------------------------- 1 | # test quality of numerical solution 2 | from scipy.special import hankel2 3 | import numpy.linalg as la 4 | import numpy as np 5 | import simwave 6 | import pytest 7 | 8 | 9 | def _compiler(): 10 | 11 | compiler = simwave.Compiler( 12 | cc='clang', 13 | language='gpu_openmp', 14 | cflags='-O3 -fPIC -ffast-math -fopenmp \ 15 | -fopenmp-targets=nvptx64-nvidia-cuda -Xopenmp-target -march=sm_75' 16 | ) 17 | 18 | return compiler 19 | 20 | 21 | def _create_space_model(bbox, spacing, vel, order): 22 | space_model = simwave.SpaceModel( 23 | bounding_box=bbox, 24 | grid_spacing=spacing, 25 | velocity_model=vel * np.ones((100, 100), dtype=np.float64), 26 | space_order=order 27 | ) 28 | return space_model 29 | 30 | 31 | def _acquisition(space_model, time_model, source, receiver, freq): 32 | source = simwave.Source(space_model, coordinates=source) 33 | receiver = simwave.Receiver(space_model, coordinates=receiver) 34 | ricker = simwave.RickerWavelet(freq, time_model) 35 | 36 | return source, receiver, ricker 37 | 38 | 39 | def analytical_solution(space_model, freq, src, recs, dt): 40 | time_model = simwave.TimeModel(space_model=space_model, t0=0, tf=3000) 41 | time_model.dt = dt 42 | ricker = simwave.RickerWavelet(freq, time_model) 43 | # Ricker's FFT 44 | nf = int(time_model.timesteps / 2 + 1) 45 | df = 1 / time_model.tf 46 | frequencies = df * np.arange(nf) 47 | q = np.fft.fft(ricker.values)[:nf] 48 | # Coordinates 49 | xg = np.array([rec[0] for rec in recs]) 50 | zg = np.array([rec[1] for rec in recs]) 51 | r = np.sqrt((xg - src[0]) ** 2 + (zg - src[1]) ** 2) 52 | # Analytical solution 53 | k = 2 * np.pi * frequencies / np.unique(space_model.velocity_model) 54 | u = np.zeros((nf), dtype=complex) 55 | u[1:-1] = hankel2(0, k[1:-1][None, :] * r[:, None]) 56 | ui = np.fft.ifft(- 1j * np.pi * u * q, time_model.timesteps) 57 | # Scale correctly 58 | # ui = 1 / (2 * np.pi) * np.real(ui) * space_model.grid_spacing[0] ** 2 59 | ui = 1 / (2 * np.pi) * np.real(ui) 60 | 61 | return ui 62 | 63 | 64 | def accuracy(spacing, bbox, order, dt, t0, tf, c, f0, src, rec): 65 | space_model = _create_space_model(bbox, spacing, c, order) 66 | 67 | time_model = simwave.TimeModel(space_model=space_model, t0=t0, tf=tf) 68 | time_model.dt = dt 69 | 70 | source, receiver, wavelet = _acquisition( 71 | space_model, time_model, src, rec, f0 72 | ) 73 | 74 | compiler = _compiler() 75 | 76 | solver = simwave.Solver(space_model, time_model, source, 77 | receiver, wavelet, compiler=compiler) 78 | 79 | # numerical solution 80 | # u_num = solver.forward()[-1].flatten() 81 | u_num = solver.forward()[-1].flatten() / spacing[0] ** 2 82 | 83 | # analytical solution 84 | u_exact = analytical_solution( 85 | space_model, f0, src[0], rec, dt 86 | ).flatten()[:time_model.timesteps] 87 | 88 | # compare solutions 89 | return la.norm(u_num - u_exact) / np.sqrt(u_num.size) 90 | 91 | 92 | @pytest.mark.gpu 93 | @pytest.mark.parametrize( 94 | "spacing, bbox, order, dt, t0, tf, c, f0, src, rec, tol", 95 | [ 96 | ( 97 | (0.5, 0.5), (-40, 440, -40, 440), 8, 0.1, 0, 150.075, 98 | 1.5, 0.09, [(200, 200)], [(260, 260)], 1e-4 99 | ) 100 | ] 101 | ) 102 | def test_accuracy(spacing, bbox, order, dt, t0, tf, c, f0, src, rec, tol): 103 | assert accuracy(spacing, bbox, order, dt, t0, tf, c, f0, src, rec) < tol 104 | 105 | 106 | @pytest.mark.gpu 107 | @pytest.mark.parametrize( 108 | "spacing, bbox, order, timesteps, t0, tf, c, \ 109 | f0, src, rec, time_convergence", 110 | [ 111 | ( 112 | (0.5, 0.5), (-40, 440, -40, 440), 8, [0.1, 0.075, 0.04, 0.025], 113 | 0, 150.075, 1.5, 0.09, [(200, 200)], [(260, 260)], 1.7 114 | ) 115 | ] 116 | ) 117 | def test_convergence_in_time(spacing, bbox, order, timesteps, t0, tf, c, f0, 118 | src, rec, time_convergence): 119 | accs = [] 120 | for dt in timesteps: 121 | accs.append( 122 | accuracy(spacing, bbox, order, dt, t0, tf, c, f0, src, rec) 123 | ) 124 | 125 | assert np.poly1d(np.polyfit(np.log(timesteps), 126 | np.log(accs), 1))[1] > time_convergence 127 | 128 | 129 | @pytest.mark.gpu 130 | @pytest.mark.parametrize( 131 | "spacings, bbox, space_orders, dt, t0, tf, c, f0, src, rec, space_rates", 132 | [ 133 | ( 134 | [2.0, 2.5, 4.0], (-40, 440, -40, 440), [2, 4, 6, 8, 10], 0.025, 135 | 0, 150.075, 1.5, 0.09, [(200, 200)], [(260, 260)], 136 | [1.7, 4, 6, 7.7, 8.7] 137 | ) 138 | ] 139 | ) 140 | def test_convergence_in_space(spacings, bbox, space_orders, dt, t0, tf, c, 141 | f0, src, rec, space_rates): 142 | accs = {} 143 | conv_rates = [] 144 | for order in space_orders: 145 | accs[order] = {} 146 | for spacing in spacings: 147 | accs[order][spacing] = accuracy( 148 | tuple([spacing, spacing]), bbox, 149 | order, dt, t0, tf, c, f0, src, rec 150 | ) 151 | step = list(accs[order].keys()) 152 | error = list(accs[order].values()) 153 | conv_rates.append( 154 | np.poly1d(np.polyfit(np.log(step), np.log(error), 1))[1] 155 | ) 156 | for rate, min_rate in zip(conv_rates, space_rates): 157 | assert rate > min_rate 158 | -------------------------------------------------------------------------------- /tests/test_gpu_solution.py: -------------------------------------------------------------------------------- 1 | from simwave import SpaceModel, TimeModel, RickerWavelet, Solver 2 | from simwave import Receiver, Source, Compiler 3 | import numpy as np 4 | import pytest 5 | import os 6 | 7 | 8 | @pytest.mark.gpu 9 | class TestSolutionGPU: 10 | 11 | @pytest.mark.parametrize( 12 | 'dimension, space_order, language, density', [ 13 | (2, 2, 'gpu_openmp', False), 14 | (2, 2, 'gpu_openmp', True), 15 | (2, 8, 'gpu_openmp', False), 16 | (3, 2, 'gpu_openmp', False), 17 | (3, 2, 'gpu_openmp', True), 18 | (3, 8, 'gpu_openmp', False) 19 | ] 20 | ) 21 | def test_solution(self, dimension, space_order, language, density): 22 | 23 | compiler = Compiler( 24 | cc='clang', 25 | language=language, 26 | cflags='-O3 -fPIC -ffast-math -fopenmp \ 27 | -fopenmp-targets=nvptx64-nvidia-cuda -Xopenmp-target -march=sm_75' 28 | ) 29 | 30 | if dimension == 2: 31 | shape = (500,)*dimension 32 | bbox = (0, 5000, 0, 5000) 33 | spacing = (10, 10) 34 | damping_length = 100 35 | boundary_condition = ( 36 | "null_neumann", "null_dirichlet", 37 | "none", "null_dirichlet" 38 | ) 39 | source_position = [(2500, 2500)] 40 | tf = 1.0 41 | f0 = 10.0 42 | else: 43 | shape = (100,)*dimension 44 | bbox = (0, 1000, 0, 1000, 0, 1000) 45 | spacing = (10, 10, 10) 46 | damping_length = 50 47 | boundary_condition = ( 48 | "null_neumann", "null_dirichlet", 49 | "none", "null_dirichlet", 50 | "null_neumann", "null_dirichlet", 51 | ) 52 | source_position = [(500, 495, 505)] 53 | tf = 0.4 54 | f0 = 15.0 55 | 56 | ref_file = os.path.join( 57 | os.path.dirname(__file__), 58 | "reference_solution/wavefield-{}d-so-{}.npy".format( 59 | dimension, space_order 60 | ) 61 | ) 62 | 63 | # Velocity model 64 | vel = np.zeros(shape=shape, dtype=np.float32) 65 | vel[:] = 1500.0 66 | vel[shape[0]//2:] = 2000.0 67 | 68 | if density: 69 | den = np.zeros(shape=shape, dtype=np.float32) 70 | den[:] = 5.0 71 | 72 | # create the space model 73 | space_model = SpaceModel( 74 | bounding_box=bbox, 75 | grid_spacing=spacing, 76 | velocity_model=vel, 77 | density_model=den, 78 | space_order=space_order, 79 | dtype=np.float32 80 | ) 81 | else: 82 | # create the space model 83 | space_model = SpaceModel( 84 | bounding_box=bbox, 85 | grid_spacing=spacing, 86 | velocity_model=vel, 87 | space_order=space_order, 88 | dtype=np.float32 89 | ) 90 | 91 | # config boundary conditions 92 | space_model.config_boundary( 93 | damping_length=damping_length, 94 | boundary_condition=boundary_condition, 95 | damping_polynomial_degree=3, 96 | damping_alpha=0.001 97 | ) 98 | 99 | # create the time model 100 | time_model = TimeModel( 101 | space_model=space_model, 102 | tf=tf, 103 | saving_stride=0 104 | ) 105 | 106 | # create the set of sources 107 | source = Source( 108 | space_model=space_model, 109 | coordinates=source_position, 110 | window_radius=1 111 | ) 112 | 113 | # crete the set of receivers 114 | receiver = Receiver( 115 | space_model=space_model, 116 | coordinates=source_position, 117 | window_radius=1 118 | ) 119 | 120 | # create a ricker wavelet with 10hz of peak frequency 121 | ricker = RickerWavelet(f0, time_model) 122 | 123 | # create the solver 124 | solver = Solver( 125 | space_model=space_model, 126 | time_model=time_model, 127 | sources=source, 128 | receivers=receiver, 129 | wavelet=ricker, 130 | compiler=compiler 131 | ) 132 | 133 | # run the forward 134 | u, recv = solver.forward() 135 | 136 | # load the reference result 137 | u_ref = np.load(ref_file) 138 | 139 | assert np.allclose(u, u_ref, atol=1e-04) 140 | -------------------------------------------------------------------------------- /tests/test_parallel_solution.py: -------------------------------------------------------------------------------- 1 | from simwave import ( 2 | SpaceModel, TimeModel, RickerWavelet, Solver, Compiler, 3 | Receiver, Source 4 | ) 5 | import numpy as np 6 | import pytest 7 | 8 | 9 | def run_forward(dimension, density, language, dtype): 10 | 11 | if dimension == 2: 12 | shape = (128, 128) 13 | bounding_box = (0, 1280, 0, 1280) 14 | grid_spacing = (10., 10.) 15 | boundary_condition = ( 16 | "null_neumann", "null_dirichlet", 17 | "null_neumann", "null_dirichlet" 18 | ) 19 | source_coordinates = [(10, i) for i in range(128, 1280, 128)] 20 | receiver_coordinates = [(10, i) for i in range(0, 1280, 10)] 21 | damping_length = 128 22 | else: 23 | shape = (128, 128, 128) 24 | bounding_box = (0, 1280, 0, 1280, 0, 1280) 25 | grid_spacing = (10., 10., 10.) 26 | boundary_condition = ( 27 | "null_neumann", "null_dirichlet", 28 | "null_neumann", "null_dirichlet", 29 | "null_neumann", "null_dirichlet" 30 | ) 31 | source_coordinates = [(10, 640, i) for i in range(128, 1280, 128)] 32 | receiver_coordinates = [(10, 650, i) for i in range(0, 1280, 10)] 33 | damping_length = 128 34 | 35 | if density: 36 | density = np.zeros(shape=shape, dtype=dtype) 37 | density[:] = 5 38 | else: 39 | density = None 40 | 41 | vel = np.zeros(shape=shape, dtype=dtype) 42 | vel[:] = 1500.0 43 | 44 | if language == 'gpu_openmp': 45 | cflags = '-O3 -fPIC -ffast-math -fopenmp \ 46 | -fopenmp-targets=nvptx64-nvidia-cuda -Xopenmp-target -march=sm_75' 47 | 48 | cc = 'clang' 49 | else: 50 | cflags = '-O3 -fPIC' 51 | cc = 'gcc' 52 | 53 | compiler = Compiler( 54 | cc=cc, 55 | language=language, 56 | cflags=cflags 57 | ) 58 | 59 | space_model = SpaceModel( 60 | bounding_box=bounding_box, 61 | grid_spacing=grid_spacing, 62 | velocity_model=vel, 63 | density_model=density, 64 | space_order=4, 65 | dtype=dtype 66 | ) 67 | 68 | space_model.config_boundary( 69 | damping_length=damping_length, 70 | boundary_condition=boundary_condition 71 | ) 72 | 73 | time_model = TimeModel( 74 | space_model=space_model, 75 | tf=0.4, 76 | saving_stride=0 77 | ) 78 | 79 | source = Source( 80 | space_model, 81 | coordinates=source_coordinates, 82 | window_radius=8 83 | ) 84 | 85 | receiver = Receiver( 86 | space_model=space_model, 87 | coordinates=receiver_coordinates, 88 | window_radius=8 89 | ) 90 | 91 | ricker = RickerWavelet(10.0, time_model) 92 | 93 | solver = Solver( 94 | space_model=space_model, 95 | time_model=time_model, 96 | sources=source, 97 | receivers=receiver, 98 | wavelet=ricker, 99 | compiler=compiler 100 | ) 101 | 102 | u_full, recv = solver.forward() 103 | 104 | return u_full[-1], recv 105 | 106 | 107 | class TestParallelSolutionCPU: 108 | 109 | @pytest.mark.parametrize( 110 | 'dimension, density, language, dtype', [ 111 | (2, False, 'cpu_openmp', np.float64), 112 | (3, False, 'cpu_openmp', np.float64), 113 | (2, True, 'cpu_openmp', np.float64), 114 | (3, True, 'cpu_openmp', np.float64) 115 | ] 116 | 117 | ) 118 | def test_parallel_cpu(self, dimension, density, language, dtype): 119 | 120 | # baseline result 121 | u_base, rec_base = run_forward( 122 | dimension=dimension, 123 | density=density, 124 | language='c', 125 | dtype=dtype 126 | ) 127 | 128 | u, rec = run_forward( 129 | dimension=dimension, 130 | density=density, 131 | language=language, 132 | dtype=dtype 133 | ) 134 | 135 | # assert np.array_equal(u_base, u) 136 | # assert np.array_equal(rec_base, rec) 137 | 138 | assert np.allclose(u_base, u, atol=1e-08) 139 | assert np.allclose(rec_base, rec, atol=1e-08) 140 | 141 | 142 | @pytest.mark.gpu 143 | class TestParallelSolutionGPU: 144 | 145 | @pytest.mark.parametrize( 146 | 'dimension, density, language, dtype', [ 147 | (2, False, 'gpu_openmp', np.float64), 148 | (3, False, 'gpu_openmp', np.float64), 149 | (2, True, 'gpu_openmp', np.float64), 150 | (3, True, 'gpu_openmp', np.float64) 151 | ] 152 | 153 | ) 154 | def test_parallel_gpu(self, dimension, density, language, dtype): 155 | 156 | # baseline result 157 | u_base, rec_base = run_forward( 158 | dimension=dimension, 159 | density=density, 160 | language='c', 161 | dtype=dtype 162 | ) 163 | 164 | u, rec = run_forward( 165 | dimension=dimension, 166 | density=density, 167 | language=language, 168 | dtype=dtype 169 | ) 170 | 171 | assert np.allclose(u_base, u, atol=1e-08) 172 | assert np.allclose(rec_base, rec, atol=1e-08) 173 | -------------------------------------------------------------------------------- /tests/test_solution.py: -------------------------------------------------------------------------------- 1 | from simwave import SpaceModel, TimeModel, RickerWavelet, Solver 2 | from simwave import Receiver, Source, Compiler 3 | import numpy as np 4 | import pytest 5 | import os 6 | 7 | 8 | class TestSolution: 9 | 10 | @pytest.mark.parametrize( 11 | 'dimension, space_order, language, density', [ 12 | (2, 2, 'c', False), 13 | (2, 8, 'c', False), 14 | (3, 2, 'c', False), 15 | (3, 8, 'c', False), 16 | (2, 2, 'c', True), 17 | (3, 2, 'c', True), 18 | (2, 2, 'cpu_openmp', False), 19 | (2, 8, 'cpu_openmp', False), 20 | (3, 2, 'cpu_openmp', False), 21 | (3, 8, 'cpu_openmp', False), 22 | (2, 2, 'cpu_openmp', True), 23 | (3, 2, 'cpu_openmp', True), 24 | ] 25 | ) 26 | def test_solution(self, dimension, space_order, language, density): 27 | 28 | compiler = Compiler(language=language) 29 | 30 | if dimension == 2: 31 | shape = (500,)*dimension 32 | bbox = (0, 5000, 0, 5000) 33 | spacing = (10, 10) 34 | damping_length = 100 35 | boundary_condition = ( 36 | "null_neumann", "null_dirichlet", 37 | "none", "null_dirichlet" 38 | ) 39 | source_position = [(2500, 2500)] 40 | tf = 1.0 41 | f0 = 10.0 42 | else: 43 | shape = (100,)*dimension 44 | bbox = (0, 1000, 0, 1000, 0, 1000) 45 | spacing = (10, 10, 10) 46 | damping_length = 50 47 | boundary_condition = ( 48 | "null_neumann", "null_dirichlet", 49 | "none", "null_dirichlet", 50 | "null_neumann", "null_dirichlet", 51 | ) 52 | source_position = [(500, 495, 505)] 53 | tf = 0.4 54 | f0 = 15.0 55 | 56 | ref_file = os.path.join( 57 | os.path.dirname(__file__), 58 | "reference_solution/wavefield-{}d-so-{}.npy".format( 59 | dimension, space_order 60 | ) 61 | ) 62 | 63 | # Velocity model 64 | vel = np.zeros(shape=shape, dtype=np.float32) 65 | vel[:] = 1500.0 66 | vel[shape[0]//2:] = 2000.0 67 | 68 | if density: 69 | den = np.zeros(shape=shape, dtype=np.float32) 70 | den[:] = 1.0 71 | 72 | # create the space model 73 | space_model = SpaceModel( 74 | bounding_box=bbox, 75 | grid_spacing=spacing, 76 | velocity_model=vel, 77 | density_model=den, 78 | space_order=space_order, 79 | dtype=np.float32 80 | ) 81 | else: 82 | # create the space model 83 | space_model = SpaceModel( 84 | bounding_box=bbox, 85 | grid_spacing=spacing, 86 | velocity_model=vel, 87 | space_order=space_order, 88 | dtype=np.float32 89 | ) 90 | 91 | # config boundary conditions 92 | space_model.config_boundary( 93 | damping_length=damping_length, 94 | boundary_condition=boundary_condition, 95 | damping_polynomial_degree=3, 96 | damping_alpha=0.001 97 | ) 98 | 99 | # create the time model 100 | time_model = TimeModel( 101 | space_model=space_model, 102 | tf=tf, 103 | saving_stride=0 104 | ) 105 | 106 | # create the set of sources 107 | source = Source( 108 | space_model=space_model, 109 | coordinates=source_position, 110 | window_radius=1 111 | ) 112 | 113 | # crete the set of receivers 114 | receiver = Receiver( 115 | space_model=space_model, 116 | coordinates=source_position, 117 | window_radius=1 118 | ) 119 | 120 | # create a ricker wavelet with 10hz of peak frequency 121 | ricker = RickerWavelet(f0, time_model) 122 | 123 | # create the solver 124 | solver = Solver( 125 | space_model=space_model, 126 | time_model=time_model, 127 | sources=source, 128 | receivers=receiver, 129 | wavelet=ricker, 130 | compiler=compiler 131 | ) 132 | 133 | # run the forward 134 | u, recv = solver.forward() 135 | 136 | # load the reference result 137 | u_ref = np.load(ref_file) 138 | 139 | assert np.allclose(u, u_ref, atol=1e-05) 140 | -------------------------------------------------------------------------------- /tests/test_source.py: -------------------------------------------------------------------------------- 1 | from simwave import SpaceModel, Source 2 | import numpy as np 3 | import pytest 4 | 5 | 6 | class TestSource: 7 | 8 | @pytest.mark.parametrize( 9 | 'dimension, coords', [ 10 | (2, [(0, 25)]), 11 | (2, [(0, 25), [250, 250]]), 12 | (3, [(0.5, 50.8, 500)]) 13 | ] 14 | ) 15 | def test_properties(self, dimension, coords): 16 | shape = (50,) * dimension 17 | bbox = (0, 500) * dimension 18 | spacing = (10, ) * dimension 19 | 20 | # Velocity model 21 | vel = 1500 * np.ones(shape=shape) 22 | 23 | space_model = SpaceModel( 24 | bounding_box=bbox, 25 | grid_spacing=spacing, 26 | velocity_model=vel 27 | ) 28 | 29 | source = Source( 30 | space_model, 31 | coordinates=coords, 32 | window_radius=8 33 | ) 34 | 35 | assert source.space_model == space_model 36 | assert source.window_radius == 8 37 | assert source.count == len(coords) 38 | assert np.array_equal( 39 | source.coordinates, 40 | space_model.dtype(coords) 41 | ) 42 | 43 | @pytest.mark.parametrize( 44 | 'dimension, bbox, spacing, coords, grid_positions', [ 45 | # 2D tests 46 | (2, (0, 5120, 0, 5120), (10, 10), (0, 512), (0, 51.2)), 47 | (2, (0, 5120, 0, 5120), (10, 5), (120, 500), (12, 100)), 48 | # 3D tests 49 | (3, (0, 500, 20, 500, 0, 200), (20, 20, 20), 50 | (10, 20, 100), (0.5, 0.0, 5.0)) 51 | ] 52 | ) 53 | def test_conversion_coords_to_points(self, dimension, bbox, 54 | spacing, coords, grid_positions): 55 | 56 | shape = (50,)*dimension 57 | 58 | # Velocity model 59 | vel = np.zeros(shape=shape, dtype=np.float32) 60 | vel[:] = 1500.0 61 | 62 | space_model = SpaceModel( 63 | bounding_box=bbox, 64 | grid_spacing=spacing, 65 | velocity_model=vel 66 | ) 67 | 68 | source = Source(space_model, coordinates=coords, window_radius=4) 69 | 70 | assert np.array_equal( 71 | source.grid_positions, 72 | np.asarray([grid_positions], dtype=space_model.dtype) 73 | ) 74 | 75 | @pytest.mark.parametrize( 76 | 'dimension, damping_length, space_order, coords, expected_position', [ 77 | (2, 500, 2, (0, 0), [(51, 51)]), 78 | (2, 0, 2, (0, 0), [(1, 1)]), 79 | (2, 0, 4, (250, 100), [(27, 12)]), 80 | (2, 50, 16, (255, 255), [(38.5, 38.5)]), 81 | (3, (50, 0, 50, 0, 0, 0), 16, (255, 255, 0), [(38.5, 38.5, 8)]), 82 | (3, 2, 4, (250, 100, 100), [(27, 12, 12)]), 83 | ] 84 | ) 85 | def test_adjusted_grid_positions(self, dimension, damping_length, 86 | space_order, coords, expected_position): 87 | 88 | shape = (50,) * dimension 89 | bbox = (0, 500) * dimension 90 | spacing = (10, ) * dimension 91 | 92 | # Velocity model 93 | vel = 1500 * np.ones(shape=shape) 94 | 95 | space_model = SpaceModel( 96 | bounding_box=bbox, 97 | grid_spacing=spacing, 98 | velocity_model=vel, 99 | space_order=space_order 100 | ) 101 | 102 | space_model.config_boundary(damping_length=damping_length) 103 | 104 | source = Source(space_model, coordinates=coords) 105 | 106 | assert np.array_equal( 107 | source.adjusted_grid_positions, 108 | space_model.dtype(expected_position) 109 | ) 110 | 111 | @pytest.mark.parametrize( 112 | 'dimension, window_radius, damping_length, \ 113 | space_order, coords, points, values', [ 114 | ( 115 | 2, 1, 50, 2, (250, 250), [30, 32, 30, 32], 116 | [-1.9556131e-08, 1.0000001e+00, -1.9556131e-08, 117 | -1.9556131e-08, 1.0000001e+00, -1.9556131e-08] 118 | ), 119 | ( 120 | 3, 4, 0, 4, (255, 250, 100), [24, 31, 23, 31, 8, 16], 121 | [ 122 | -5.1983586e-03, 3.6357623e-02, -1.3933791e-01, 123 | 6.0838646e-01, 6.0838646e-01, -1.3933791e-01, 124 | 3.6357623e-02, -5.1983586e-03, 3.1170111e-10, 125 | -3.7239498e-10, 1.2889303e-08, -2.3163784e-08, 126 | 9.9999994e-01, -2.3163784e-08, 1.2889303e-08, 127 | -3.7239498e-10, 3.1170111e-10, 3.1170111e-10, 128 | -3.7239498e-10, 1.2889303e-08, -2.3163784e-08, 129 | 9.9999994e-01, -2.3163784e-08, 1.2889303e-08, 130 | -3.7239498e-10, 3.1170111e-10 131 | ] 132 | ) 133 | ] 134 | ) 135 | def test_interpolated_points_and_values(self, dimension, window_radius, 136 | damping_length, space_order, 137 | coords, points, values): 138 | 139 | shape = (50,) * dimension 140 | bbox = (0, 500) * dimension 141 | spacing = (10, ) * dimension 142 | 143 | # Velocity model 144 | vel = 1500 * np.ones(shape=shape) 145 | 146 | space_model = SpaceModel( 147 | bounding_box=bbox, 148 | grid_spacing=spacing, 149 | velocity_model=vel, 150 | space_order=space_order 151 | ) 152 | 153 | space_model.config_boundary(damping_length=damping_length) 154 | 155 | source = Source( 156 | space_model, 157 | coordinates=coords, 158 | window_radius=window_radius 159 | ) 160 | 161 | p, v, _ = source.interpolated_points_and_values 162 | 163 | assert np.array_equal(p, np.asarray(points)) 164 | assert np.array_equal(v, space_model.dtype(values)) 165 | -------------------------------------------------------------------------------- /tests/test_space_model.py: -------------------------------------------------------------------------------- 1 | from simwave import SpaceModel 2 | import numpy as np 3 | import pytest 4 | 5 | 6 | class TestSpaceModel: 7 | 8 | @pytest.mark.parametrize('dimension', [(2), (3)]) 9 | def test_properties(self, dimension): 10 | 11 | vel = 1500 * np.ones(shape=(101,)*dimension) 12 | den = 15 * np.ones(shape=(101,)*dimension) 13 | bbox = (0, 1000) * dimension 14 | spacing = (10, ) * dimension 15 | 16 | space_model = SpaceModel( 17 | bounding_box=bbox, 18 | grid_spacing=spacing, 19 | velocity_model=vel, 20 | density_model=den, 21 | space_order=4, 22 | dtype=np.float32 23 | ) 24 | 25 | assert space_model.bounding_box == bbox 26 | assert space_model.grid_spacing == spacing 27 | assert np.array_equal(space_model.velocity_model, vel) 28 | assert np.array_equal(space_model.density_model, den) 29 | assert space_model.space_order == 4 30 | assert space_model.dimension == dimension 31 | assert space_model.dtype == np.float32 32 | assert space_model.damping_length == (0.0,) * dimension * 2 33 | assert space_model.boundary_condition == ('none',) * dimension * 2 34 | assert space_model.damping_polynomial_degree == 3 35 | assert space_model.damping_alpha == 0.001 36 | 37 | @pytest.mark.parametrize( 38 | 'dimension, bbox, spacing, shape', [ 39 | (2, (0, 1000, 0, 1000), (10, 10), (101, 101)), 40 | (2, (100., 1000, 0, 1250.5), (10, 10), (91, 126)), 41 | (2, (-100, 1000, 500, 1000), (20, 10), (56, 51)), 42 | (3, (0, 1000, 0, 1000, 0, 1000.0), (5, 10, 20), (201, 101, 51)), 43 | (3, (10, 100, -10, 100, 0.0, 100.0), (5, 5, 5), (19, 23, 21)), 44 | ] 45 | ) 46 | def test_shape(self, dimension, bbox, spacing, shape): 47 | 48 | vel = 1500 * np.ones(shape=(50,)*dimension) 49 | 50 | space_model = SpaceModel( 51 | bounding_box=bbox, 52 | grid_spacing=spacing, 53 | velocity_model=vel 54 | ) 55 | 56 | assert space_model.shape == shape 57 | 58 | @pytest.mark.parametrize( 59 | 'dimension, damping_length, space_order, extended_shape', [ 60 | (2, 100, 2, (123, 123)), 61 | (2, 100, 4, (125, 125)), 62 | (2, 150, 16, (147, 147)), 63 | (2, (150, 100, 150, 100), 8, (134, 134)), 64 | (2, (150, 100, 0, 100), 8, (134, 119)), 65 | (3, 100, 2, (123, 123, 123)), 66 | (3, 100, 4, (125, 125, 125)), 67 | (3, 150, 16, (147, 147, 147)), 68 | (3, (150, 100, 150, 100, 150, 100), 8, (134, 134, 134)), 69 | (3, (150, 100, 0, 100, 0, 150), 8, (134, 119, 124)) 70 | ] 71 | ) 72 | def test_extended_shape(self, dimension, damping_length, 73 | space_order, extended_shape): 74 | 75 | vel = 1500 * np.ones(shape=(50,)*dimension) 76 | bbox = (0, 1000) * dimension 77 | spacing = (10, ) * dimension 78 | 79 | space_model = SpaceModel( 80 | bounding_box=bbox, 81 | grid_spacing=spacing, 82 | velocity_model=vel, 83 | space_order=space_order 84 | ) 85 | 86 | space_model.config_boundary(damping_length=damping_length) 87 | 88 | assert space_model.extended_shape == extended_shape 89 | 90 | @pytest.mark.parametrize('dimension', [(2), (3)]) 91 | def test_grid(self, dimension): 92 | 93 | vel = 1500 * np.ones(shape=(50,)*dimension) 94 | bbox = (0, 1000) * dimension 95 | spacing = (10, ) * dimension 96 | 97 | space_model = SpaceModel( 98 | bounding_box=bbox, 99 | grid_spacing=spacing, 100 | velocity_model=vel 101 | ) 102 | 103 | assert space_model.grid.shape == space_model.shape 104 | assert space_model.grid.dtype == space_model.dtype 105 | 106 | @pytest.mark.parametrize( 107 | 'dimension, damping_length, nbl', [ 108 | (2, None, (0, 0, 0, 0)), 109 | (3, None, (0, 0, 0, 0, 0, 0)), 110 | (2, 120, (12, 12, 12, 12)), 111 | (3, 90, (9, 9, 9, 9, 9, 9)), 112 | (2, (50, 60, 75, 80), (5, 6, 7, 8)), 113 | (3, (0, 10, 8, 50, 20, 30), (0, 1, 0, 5, 2, 3)) 114 | ] 115 | ) 116 | def test_nbl(self, dimension, damping_length, nbl): 117 | 118 | vel = 1500 * np.ones(shape=(50,)*dimension) 119 | bbox = (0, 1000) * dimension 120 | spacing = (10,) * dimension 121 | 122 | space_model = SpaceModel( 123 | bounding_box=bbox, 124 | grid_spacing=spacing, 125 | velocity_model=vel 126 | ) 127 | 128 | if damping_length is not None: 129 | space_model.config_boundary(damping_length=damping_length) 130 | 131 | assert space_model.nbl == nbl 132 | 133 | @pytest.mark.parametrize( 134 | 'time_order, space_order, coeffs', [ 135 | (2, 2, [-2., 1.]), 136 | (2, 4, [-2.5, 1.3333334, -0.08333334]), 137 | (2, 8, [-2.8472223e+00, 1.6e+00, -2.0e-01, 138 | 2.5396826e-02, -1.7857143e-03]) 139 | ] 140 | ) 141 | def test_fd_coefficients(self, time_order, space_order, coeffs): 142 | 143 | vel = 1500 * np.ones(shape=(50, 50)) 144 | 145 | space_model = SpaceModel( 146 | bounding_box=(0, 500, 0, 500), 147 | grid_spacing=(10, 10), 148 | velocity_model=vel, 149 | space_order=space_order 150 | ) 151 | 152 | assert np.allclose( 153 | space_model.fd_coefficients(2), 154 | np.asarray(coeffs, dtype=space_model.dtype) 155 | ) 156 | 157 | @pytest.mark.parametrize( 158 | 'dimension, space_order, halo_size', [ 159 | (2, 2, (1, 1, 1, 1)), 160 | (2, 4, (2, 2, 2, 2)), 161 | (2, 20, (10, 10, 10, 10)), 162 | (3, 2, (1, 1, 1, 1, 1, 1)), 163 | (3, 8, (4, 4, 4, 4, 4, 4)), 164 | (3, 10, (5, 5, 5, 5, 5, 5)) 165 | ] 166 | ) 167 | def test_halo_size(self, dimension, space_order, halo_size): 168 | 169 | vel = 1500 * np.ones(shape=(50,)*dimension) 170 | bbox = (0, 500) * dimension 171 | spacing = (10,) * dimension 172 | 173 | space_model = SpaceModel( 174 | bounding_box=bbox, 175 | grid_spacing=spacing, 176 | velocity_model=vel, 177 | space_order=space_order 178 | ) 179 | 180 | assert space_model.halo_size == halo_size 181 | 182 | @pytest.mark.parametrize( 183 | 'dimension, damping_length, boundary_condition, \ 184 | damping_polynomial_degree, damping_alpha', 185 | [ 186 | (2, 500, "none", 2, 0.1), 187 | (2, (50, 50, 40, 40), "null_neumann", 4, 0.001), 188 | (3, 100, "null_dirichlet", 4, 0.001), 189 | (2, 75, ("none", "null_neumann", "none", "null_dirichlet"), 190 | 4, 0.001), 191 | (3, (5, 5, 5, 5, 6, 6), "null_dirichlet", 1, 0.001) 192 | ] 193 | ) 194 | def test_config_boundary(self, dimension, damping_length, 195 | boundary_condition, damping_polynomial_degree, 196 | damping_alpha): 197 | 198 | vel = 1500 * np.ones(shape=(50,)*dimension) 199 | bbox = (0, 500) * dimension 200 | spacing = (10,) * dimension 201 | 202 | space_model = SpaceModel( 203 | bounding_box=bbox, 204 | grid_spacing=spacing, 205 | velocity_model=vel 206 | ) 207 | 208 | space_model.config_boundary( 209 | damping_length=damping_length, 210 | boundary_condition=boundary_condition, 211 | damping_polynomial_degree=damping_polynomial_degree, 212 | damping_alpha=damping_alpha 213 | ) 214 | 215 | if isinstance(damping_length, (float, int)): 216 | assert space_model.damping_length == (damping_length,) \ 217 | * dimension * 2 218 | else: 219 | assert space_model.damping_length == damping_length 220 | 221 | if isinstance(boundary_condition, str): 222 | assert space_model.boundary_condition == (boundary_condition,) \ 223 | * dimension * 2 224 | else: 225 | assert space_model.boundary_condition == boundary_condition 226 | 227 | assert space_model.damping_alpha == damping_alpha 228 | 229 | assert space_model.damping_polynomial_degree == \ 230 | damping_polynomial_degree 231 | 232 | @pytest.mark.parametrize( 233 | 'dimension, damping_length, space_order', [ 234 | (2, 100, 2), 235 | (2, 100, 4), 236 | (2, 150, 16), 237 | (2, (150, 100, 150, 100), 8), 238 | (2, (150, 100, 0, 100), 8), 239 | (3, 100, 2), 240 | (3, 100, 4), 241 | (3, 150, 16), 242 | (3, (150, 100, 150, 100, 150, 100), 8), 243 | (3, (150, 100, 0, 100, 0, 150), 8) 244 | ] 245 | ) 246 | def test_padding(self, dimension, damping_length, space_order): 247 | 248 | vel = 1500 * np.ones(shape=(50,)*dimension) 249 | den = 15 * np.ones(shape=(50,)*dimension) 250 | bbox = (0, 1000) * dimension 251 | spacing = (10, ) * dimension 252 | 253 | space_model = SpaceModel( 254 | bounding_box=bbox, 255 | grid_spacing=spacing, 256 | velocity_model=vel, 257 | density_model=den, 258 | space_order=space_order 259 | ) 260 | 261 | space_model.config_boundary(damping_length=damping_length) 262 | 263 | assert space_model.damping_mask.shape == space_model.extended_shape 264 | assert space_model.extended_grid.shape == space_model.extended_shape 265 | assert space_model.extended_velocity_model.shape == \ 266 | space_model.extended_shape 267 | assert space_model.extended_density_model.shape == \ 268 | space_model.extended_shape 269 | -------------------------------------------------------------------------------- /tests/test_time_model.py: -------------------------------------------------------------------------------- 1 | from simwave import SpaceModel, TimeModel 2 | import numpy as np 3 | import pytest 4 | 5 | 6 | class TestTimeModel: 7 | 8 | def test_properties(self): 9 | 10 | vel = 1500 * np.ones(shape=(10, 10)) 11 | 12 | space_model = SpaceModel( 13 | bounding_box=(0, 100, 0, 100), 14 | grid_spacing=(10, 10), 15 | velocity_model=vel, 16 | ) 17 | 18 | time_model = TimeModel( 19 | space_model=space_model, 20 | tf=1.0, 21 | t0=0.0, 22 | saving_stride=1 23 | ) 24 | 25 | time_model.dt = 0.001 26 | 27 | assert time_model.space_model == space_model 28 | assert time_model.tf == 1.0 29 | assert time_model.t0 == 0.0 30 | assert time_model.saving_stride == 1 31 | assert time_model.dt == space_model.dtype(0.001) 32 | 33 | @pytest.mark.parametrize( 34 | 'dt, tf, t0, saving_stride, timesteps', [ 35 | (0.001, 1.0, 0.0, 0, 1001), 36 | (0.001, 1.0, 0.5, 0, 501), 37 | (0.002, 2.0, 0.0, 0, 1001), 38 | (0.001, 1.0, 0.0, 1, 1001), 39 | (0.001, 1.0, 0.5, 1, 501), 40 | (0.002, 2.0, 0.0, 1, 1001), 41 | (0.001, 1.0, 0.0, 2, 1001), 42 | (0.001, 1.0, 0.5, 6, 505), 43 | (0.002, 2.0, 0.0, 3, 1003), 44 | ] 45 | ) 46 | def test_timesteps(self, dt, tf, t0, saving_stride, timesteps): 47 | 48 | vel = 1500 * np.ones(shape=(10, 10)) 49 | 50 | space_model = SpaceModel( 51 | bounding_box=(0, 100, 0, 100), 52 | grid_spacing=(10, 10), 53 | velocity_model=vel, 54 | ) 55 | 56 | time_model = TimeModel( 57 | space_model=space_model, 58 | tf=tf, 59 | t0=t0, 60 | saving_stride=saving_stride 61 | ) 62 | 63 | time_model.dt = dt 64 | 65 | assert time_model.timesteps == timesteps 66 | -------------------------------------------------------------------------------- /tests/test_u_saving.py: -------------------------------------------------------------------------------- 1 | from simwave import SpaceModel, TimeModel, RickerWavelet, Solver 2 | from simwave import Receiver, Source, Compiler 3 | import numpy as np 4 | import pytest 5 | 6 | 7 | def run_forward_2d(density, saving_stride): 8 | compiler = Compiler( 9 | cc='gcc', 10 | language='c', 11 | cflags='-O3' 12 | ) 13 | 14 | vel = np.zeros(shape=(512, 512), dtype=np.float32) 15 | vel[:] = 1500.0 16 | vel[250:] = 3000.0 17 | 18 | if density: 19 | density = np.zeros(shape=(512, 512), dtype=np.float32) 20 | density[:] = 5 21 | else: 22 | density = None 23 | 24 | # create the space model 25 | space_model = SpaceModel( 26 | bounding_box=(0, 5120, 0, 5120), 27 | grid_spacing=(10, 10), 28 | velocity_model=vel, 29 | density_model=density, 30 | space_order=4, 31 | dtype=np.float32 32 | ) 33 | 34 | space_model.config_boundary( 35 | damping_length=0, 36 | boundary_condition=( 37 | "null_neumann", "null_dirichlet", 38 | "none", "null_dirichlet" 39 | ), 40 | damping_polynomial_degree=3, 41 | damping_alpha=0.001 42 | ) 43 | 44 | time_model = TimeModel( 45 | space_model=space_model, 46 | tf=1.0, 47 | saving_stride=saving_stride 48 | ) 49 | 50 | source = Source( 51 | space_model, 52 | coordinates=[(2560, 2560)], 53 | window_radius=4 54 | ) 55 | 56 | receiver = Receiver( 57 | space_model=space_model, 58 | coordinates=[(2560, i) for i in range(0, 5120, 10)], 59 | window_radius=4 60 | ) 61 | 62 | ricker = RickerWavelet(10.0, time_model) 63 | 64 | solver = Solver( 65 | space_model=space_model, 66 | time_model=time_model, 67 | sources=source, 68 | receivers=receiver, 69 | wavelet=ricker, 70 | compiler=compiler 71 | ) 72 | 73 | u_full, _ = solver.forward() 74 | 75 | return u_full[-1] 76 | 77 | 78 | def run_forward_3d(density, saving_stride): 79 | compiler = Compiler( 80 | cc='gcc', 81 | language='c', 82 | cflags='-O3' 83 | ) 84 | 85 | vel = np.zeros(shape=(100, 100, 100), dtype=np.float32) 86 | vel[:] = 1500.0 87 | 88 | if density: 89 | density = np.zeros(shape=(100, 100, 100), dtype=np.float32) 90 | density[:] = 5 91 | else: 92 | density = None 93 | 94 | # create the space model 95 | space_model = SpaceModel( 96 | bounding_box=(0, 1000, 0, 1000, 0, 1000), 97 | grid_spacing=(10, 10, 10), 98 | velocity_model=vel, 99 | density_model=density, 100 | space_order=4, 101 | dtype=np.float32 102 | ) 103 | 104 | space_model.config_boundary( 105 | damping_length=0, 106 | boundary_condition=( 107 | "null_neumann", "null_dirichlet", 108 | "null_dirichlet", "null_dirichlet", 109 | "null_dirichlet", "null_dirichlet" 110 | ), 111 | damping_polynomial_degree=3, 112 | damping_alpha=0.001 113 | ) 114 | 115 | time_model = TimeModel( 116 | space_model=space_model, 117 | tf=0.4, 118 | saving_stride=saving_stride 119 | ) 120 | 121 | source = Source( 122 | space_model, 123 | coordinates=[(500, 500, 500)], 124 | window_radius=4 125 | ) 126 | 127 | receiver = Receiver( 128 | space_model=space_model, 129 | coordinates=[(500, 500, i) for i in range(0, 1000, 10)], 130 | window_radius=4 131 | ) 132 | 133 | ricker = RickerWavelet(15.0, time_model) 134 | 135 | solver = Solver( 136 | space_model=space_model, 137 | time_model=time_model, 138 | sources=source, 139 | receivers=receiver, 140 | wavelet=ricker, 141 | compiler=compiler 142 | ) 143 | 144 | u_full, _ = solver.forward() 145 | 146 | return u_full[-1] 147 | 148 | 149 | class TestUSaving: 150 | 151 | @pytest.mark.parametrize( 152 | 'saving_stride, density', [ 153 | (0, False), 154 | (1, False), 155 | (2, False), 156 | (5, False), 157 | (0, True), 158 | (1, True), 159 | (2, True), 160 | (5, True) 161 | ] 162 | 163 | ) 164 | def test_u_saving_2d(self, saving_stride, density): 165 | 166 | # baseline result 167 | u_base = run_forward_2d( 168 | density=density, 169 | saving_stride=0 170 | ) 171 | 172 | u_last = run_forward_2d( 173 | density=density, 174 | saving_stride=saving_stride 175 | ) 176 | 177 | assert np.array_equal(u_base, u_last) 178 | 179 | @pytest.mark.parametrize( 180 | 'saving_stride, density', [ 181 | (0, False), 182 | (1, False), 183 | (2, False), 184 | (3, False), 185 | (4, False), 186 | (5, False), 187 | (0, True), 188 | (1, True), 189 | (2, True), 190 | (3, True), 191 | (4, True), 192 | (5, True) 193 | ] 194 | 195 | ) 196 | def test_u_saving_3d(self, saving_stride, density): 197 | 198 | # baseline result 199 | u_base = run_forward_3d( 200 | density=density, 201 | saving_stride=0 202 | ) 203 | 204 | u_last = run_forward_3d( 205 | density=density, 206 | saving_stride=saving_stride 207 | ) 208 | 209 | assert np.array_equal(u_base, u_last) 210 | --------------------------------------------------------------------------------