diff --git a/.coverage b/.coverage new file mode 100644 index 00000000..e12b6832 Binary files /dev/null and b/.coverage differ diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index ab4856f7..09b2b928 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -15,40 +15,43 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Running serial tests in parallel (only one test per core) + - name: Running serial tests run: | - source /home/olender/Firedrakes/main/firedrake/bin/activate - pytest -n 10 --cov-report=xml --cov=spyro test/ - - name: Running serial tests for adjoint - run: | - source /home/olender/Firedrakes/main/firedrake/bin/activate - pytest -n 10 --cov-report=xml --cov-append --cov=spyro test_ad/ - - name: Running parallel tests - run: | - source /home/olender/Firedrakes/main/firedrake/bin/activate - cp /home/olender/Testing_files/velocity_models/* velocity_models/ - cp /home/olender/Testing_files/meshes/* meshes/ - mpiexec -n 10 pytest test_parallel/test_forward.py - - name: Covering parallel tests - continue-on-error: true - run: | - source /home/olender/Firedrakes/main/firedrake/bin/activate - cp /home/olender/Testing_files/velocity_models/* velocity_models/ - cp /home/olender/Testing_files/meshes/* meshes/ - mpiexec -n 10 pytest --cov-report=xml --cov-append --cov=spyro test_parallel/test_forward.py + source /home/olender/Firedrakes/newest3/firedrake/bin/activate + pytest --cov-report=xml --cov=spyro test/ - name: Running parallel 3D forward test run: | - source /home/olender/Firedrakes/main/firedrake/bin/activate - cp /home/olender/Testing_files/velocity_models/* velocity_models/ - cp /home/olender/Testing_files/meshes/* meshes/ - mpiexec -n 10 pytest test_3d/test_forward_3d.py + source /home/olender/Firedrakes/newest3/firedrake/bin/activate + mpiexec -n 6 pytest test_3d/test_hexahedral_convergence.py - name: Covering parallel 3D forward test continue-on-error: true run: | - source /home/olender/Firedrakes/main/firedrake/bin/activate - cp /home/olender/Testing_files/velocity_models/* velocity_models/ - cp /home/olender/Testing_files/meshes/* meshes/ - mpiexec -n 10 pytest --cov-report=xml --cov-append --cov=spyro test_3d/test_forward_3d.py + source /home/olender/Firedrakes/newest3/firedrake/bin/activate + mpiexec -n 6 pytest --cov-report=xml --cov-append --cov=spyro test_3d/test_hexahedral_convergence.py + # - name: Running serial tests for adjoint + # run: | + # source /home/olender/Firedrakes/main/firedrake/bin/activate + # pytest -n 10 --cov-report=xml --cov-append --cov=spyro test_ad/ + # - name: Running parallel tests + # run: | + # source /home/olender/Firedrakes/main/firedrake/bin/activate + # cp /home/olender/Testing_files/velocity_models/* velocity_models/ + # cp /home/olender/Testing_files/meshes/* meshes/ + # mpiexec -n 10 pytest test_parallel/test_forward.py + # - name: Covering parallel tests + # continue-on-error: true + # run: | + # source /home/olender/Firedrakes/main/firedrake/bin/activate + # cp /home/olender/Testing_files/velocity_models/* velocity_models/ + # cp /home/olender/Testing_files/meshes/* meshes/ + # mpiexec -n 10 pytest --cov-report=xml --cov-append --cov=spyro test_parallel/test_forward.py + # - name: Covering parallel 3D forward test + # continue-on-error: true + # run: | + # source /home/olender/Firedrakes/main/firedrake/bin/activate + # cp /home/olender/Testing_files/velocity_models/* velocity_models/ + # cp /home/olender/Testing_files/meshes/* meshes/ + # mpiexec -n 10 pytest --cov-report=xml --cov-append --cov=spyro test_3d/test_forward_3d.py - name: Uploading coverage to Codecov - run: export CODECOV_TOKEN="6cd21147-54f7-4b77-94ad-4b138053401d" && bash <(curl -s https://codecov.io/bash) + run: export CODECOV_TOKEN="057ec853-d7ea-4277-819b-0c5ea2f9ff57" && bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index 3a4f1288..216e51d1 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,19 @@ [![DOI](https://zenodo.org/badge/318542339.svg)](https://zenodo.org/badge/latestdoi/318542339) [![Python tests](https://github.com/NDF-Poli-USP/spyro/actions/workflows/python-tests.yml/badge.svg)](https://github.com/NDF-Poli-USP/spyro/actions/workflows/python-tests.yml) -[![codecov](https://codecov.io/gh/NDF-Poli-USP/spyro/branch/main/graph/badge.svg?token=8NM4N4N7YW)](https://codecov.io/gh/NDF-Poli-USP/spyro) +[![codecov](https://codecov.io/gh/Olender/spyro-1/graph/badge.svg?token=69M30UMRFD)](https://codecov.io/gh/Olender/spyro-1) +[![CodeScene Code Health](https://codescene.io/projects/42047/status-badges/code-health)](https://codescene.io/projects/42047) -spyro: Acoustic wave modeling in Firedrake +SPIRO: Seismic Parallel Inversion and Reconstruction Optimization framework ============================================ -spyro is a Python library for modeling acoustic waves. The main +Acoustic wave modeling in Firedrake + +SPIRO is a Python library for modeling acoustic waves. The main functionality is a set of forward and adjoint wave propagators for solving the acoustic wave equation in the time domain. These wave propagators can be used to form complete full waveform inversion (FWI) applications. See the [demos](https://github.com/krober10nd/spyro/tree/main/demos). -To implement these solvers, spyro uses the finite element package [Firedrake](https://www.firedrakeproject.org/index.html). +To implement these solvers, SPIRO uses the finite element package [Firedrake](https://www.firedrakeproject.org/index.html). -To use Spyro, you'll need to have some knowledge of Python and some basic concepts in inverse modeling relevant to active-sourcce seismology. +To use SPIRO, you'll need to have some knowledge of Python and some basic concepts in inverse modeling relevant to active-sourcce seismology. Discussions about development take place on our Slack channel. Everyone is invited to join using the link: https://join.slack.com/t/spyroworkspace/shared_invite/zt-u87ih28m-2h9JobfkdArs4ku3a1wLLQ @@ -51,137 +54,119 @@ See the demos folder for an FWI example (this requires some other dependencies p ![Above shows the simulation at two timesteps in ParaView that results from running the code below](https://user-images.githubusercontent.com/18619644/94087976-7e81df00-fde5-11ea-96c0-474348286091.png) ```python -from firedrake import ( - RectangleMesh, - FunctionSpace, - Function, - SpatialCoordinate, - conditional, - File, -) - import spyro -model = {} - -# Choose method and parameters -model["opts"] = { - "method": "KMV", # either CG or KMV - "quadratrue": "KMV", # Equi or KMV - "degree": 1, # p order - "dimension": 2, # dimension +dictionary = {} + +# Choose spatial discretization method and parameters +dictionary["options"] = { + # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "cell_type": "T", + # lumped, equispaced or DG, default is lumped "method":"MLT", + # (MLT/spectral_quadrilateral/DG_triangle/DG_quadrilateral) + # You can either specify a cell_type+variant or a method. + "variant": 'lumped', + # Polynomial order of the spatial discretion's basis functions. + # For MLT we recomend 4th order in 2D, 3rd order in 3D, for SEM 4th or 8th. + "degree": 4, + # Dimension (2 or 3) + "dimension": 2, } -# Number of cores for the shot. For simplicity, we keep things serial. -# spyro however supports both spatial parallelism and "shot" parallelism. -model["parallelism"] = { - "type": "spatial", # options: automatic (same number of cores for evey processor) or spatial +# Number of cores for the shot. For simplicity, we keep things automatic. +# SPIRO supports both spatial parallelism and "shot" parallelism. +dictionary["parallelism"] = { + # options: automatic (same number of cores for every shot) or spatial + "type": "automatic", } # Define the domain size without the PML. Here we'll assume a 0.75 x 1.50 km -# domain and reserve the remaining 250 m for the Perfectly Matched Layer (PML) to absorb -# outgoing waves on three sides (eg., -z, +-x sides) of the domain. -model["mesh"] = { - "Lz": 0.75, # depth in km - always positive - "Lx": 1.5, # width in km - always positive - "Ly": 0.0, # thickness in km - always positive - "meshfile": "not_used.msh", - "initmodel": "not_used.hdf5", - "truemodel": "not_used.hdf5", -} - -# Specify a 250-m PML on the three sides of the domain to damp outgoing waves. -model["BCs"] = { - "status": True, # True or false - "outer_bc": "non-reflective", # None or non-reflective (outer boundary condition) - "damping_type": "polynomial", # polynomial, hyperbolic, shifted_hyperbolic - "exponent": 2, # damping layer has a exponent variation - "cmax": 4.7, # maximum acoustic wave velocity in PML - km/s - "R": 1e-6, # theoretical reflection coefficient - "lz": 0.25, # thickness of the PML in the z-direction (km) - always positive - "lx": 0.25, # thickness of the PML in the x-direction (km) - always positive - "ly": 0.0, # thickness of the PML in the y-direction (km) - always positive +dictionary["mesh"] = { + # depth in km - always positive + "Lz": 0.75, + # width in km - always positive + "Lx": 1.50, + # thickness in km - always positive + "Ly": 0.0, + # If we are loading and external .msh mesh file + "mesh_file": None, + # options: None (default), firedrake_mesh, user_mesh, or SeismicMesh + # use this opion if your are not loading an external file + # 'firedrake_mesh' will create an automatic mesh using firedrake's built-in meshing tools + # 'user_mesh' gives the option to load other user generated meshes from unsuported formats + # 'SeismicMesh' automatically creates a waveform adapted unstructured mesh to reduce total + # DoFs using the SeismicMesh tool. + "mesh_type": "firedrake_mesh", } # Create a source injection operator. Here we use a single source with a # Ricker wavelet that has a peak frequency of 8 Hz injected at the center of the mesh. # We also specify to record the solution at 101 microphones near the top of the domain. # This transect of receivers is created with the helper function `create_transect`. -model["acquisition"] = { - "source_type": "Ricker", - "source_pos": [(-0.1, 0.75)], +dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-0.3, 0.75)], "frequency": 8.0, "delay": 1.0, "receiver_locations": spyro.create_transect( - (-0.10, 0.1), (-0.10, 1.4), 100 + (-0.5, 0.1), (-0.5, 1.4), 100 ), } # Simulate for 2.0 seconds. -model["timeaxis"] = { - "t0": 0.0, # Initial time for event - "tf": 2.00, # Final time for event - "dt": 0.0005, # timestep size - "amplitude": 1, # the Ricker has an amplitude of 1. - "nspool": 100, # how frequently to output solution to pvds - "fspool": 100, # how frequently to save solution to RAM +dictionary["time_axis"] = { + # Initial time for event + "initial_time": 0.0, + # Final time for event + "final_time": 0.50, + # timestep size + "dt": 0.0001, + # the Ricker has an amplitude of 1. + "amplitude": 1, + # how frequently to output solution to pvds + "output_frequency": 100, + # how frequently to save solution to RAM + "gradient_sampling_frequency": 100, } +dictionary["visualization"] = { + "forward_output" : True, + "output_filename": "results/forward_output.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": False, + "gradient_filename": None, +} -# Create a simple mesh of a rectangle ∈ [1 x 2] km with ~100 m sized elements -# and then create a function space for P=1 Continuous Galerkin FEM -mesh = RectangleMesh(100, 200, 1.0, 2.0) - -# We edit the coordinates of the mesh so that it's in the (z, x) plane -# and has a domain padding of 250 m on three sides, which will be used later to show -# the Perfectly Matched Layer (PML). More complex 2D/3D meshes can be automatically generated with -# SeismicMesh https://github.com/krober10nd/SeismicMesh -mesh.coordinates.dat.data[:, 0] -= 1.0 -mesh.coordinates.dat.data[:, 1] -= 0.25 +# Create an AcousticWave object with the above dictionary. +Wave_obj = spyro.AcousticWave(dictionary=dictionary) -# Create the computational environment -comm = spyro.utils.mpi_init(model) +# Defines the element size in the automatically generated firedrake mesh. +Wave_obj.set_mesh(dx=0.01) -element = spyro.domains.space.FE_method( - mesh, model["opts"]["method"], model["opts"]["degree"] -) -V = FunctionSpace(mesh, element) -# Manually create a simple two layer seismic velocity model `vp`. -# Note: the user can specify their own velocity model in a HDF5 file format -# in the above two lines using SeismicMesh. -# If so, the HDF5 file has to contain an array with +# Manually create a simple two layer seismic velocity model. +# Note: the user can specify their own velocity model in a HDF5 or SEG-Y file format. +# The HDF5 file has to contain an array with # the velocity data and it is linearly interpolated onto the mesh nodes at run-time. -x, y = SpatialCoordinate(mesh) -velocity = conditional(x > -0.35, 1.5, 3.0) -vp = Function(V, name="velocity").interpolate(velocity) -# These pvd files can be easily visualized in ParaView! -File("simple_velocity_model.pvd").write(vp) - - -# Now we instantiate both the receivers and source objects. -sources = spyro.Sources(model, mesh, V, comm) - -receivers = spyro.Receivers(model, mesh, V, comm) - -# Create a wavelet to force the simulation -wavelet = spyro.full_ricker_wavelet(dt=0.0005, tf=2.0, freq=8.0) +z = Wave_obj.mesh_z +import firedrake as fire +velocity_conditional = fire.conditional(z > -0.35, 1.5, 3.0) +Wave_obj.set_initial_velocity_model(conditional=velocity_conditional, output=True) # And now we simulate the shot using a 2nd order central time-stepping scheme # Note: simulation results are stored in the folder `~/results/` by default -p_field, p_at_recv = spyro.solvers.forward( - model, mesh, comm, vp, sources, wavelet, receivers -) +Wave_obj.forward_solve() # Visualize the shot record -spyro.plots.plot_shots(model, comm, p_at_recv) +spyro.plots.plot_shots(Wave_obj, show=True) # Save the shot (a Numpy array) as a pickle for other use. -spyro.io.save_shots(model, comm, p_at_recv) +spyro.io.save_shots(Wave_obj) # can be loaded back via -my_shot = spyro.io.load_shots(model, comm) +my_shot = spyro.io.load_shots(Wave_obj) ``` ### Testing diff --git a/results/receivers.txt b/results/receivers.txt new file mode 100644 index 00000000..d4ba1f15 --- /dev/null +++ b/results/receivers.txt @@ -0,0 +1,37 @@ +Z, X +-3.75, 3.75 +-3.45, 3.75 +-3.15, 3.75 +-2.85, 3.75 +-2.55, 3.75 +-2.25, 3.75 +-3.75, 4.05 +-3.45, 4.05 +-3.15, 4.05 +-2.85, 4.05 +-2.55, 4.05 +-2.25, 4.05 +-3.75, 4.35 +-3.45, 4.35 +-3.15, 4.35 +-2.85, 4.35 +-2.55, 4.35 +-2.25, 4.35 +-3.75, 4.65 +-3.45, 4.65 +-3.15, 4.65 +-2.85, 4.65 +-2.55, 4.65 +-2.25, 4.65 +-3.75, 4.95 +-3.45, 4.95 +-3.15, 4.95 +-2.85, 4.95 +-2.55, 4.95 +-2.25, 4.95 +-3.75, 5.25 +-3.45, 5.25 +-3.15, 5.25 +-2.85, 5.25 +-2.55, 5.25 +-2.25, 5.25 diff --git a/results/sources.txt b/results/sources.txt new file mode 100644 index 00000000..bebf4965 --- /dev/null +++ b/results/sources.txt @@ -0,0 +1,2 @@ +Z, X +-6.0, 4.5 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..d96cd6fe --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +[flake8] +ignore = + E501,F403,F405,E226,E402,E721,E731,E741,W503,F999, + N801,N802,N803,N806,N807,N811,N813,N814,N815,N816 +exclude = .git,__pycache__ +[coverage:run] +omit=*/site-packages/*,*/test/*,*/.eggs/*,/home/alexandre/firedrake/* diff --git a/shots/README.md b/shots/README.md index be3fd65d..785cf975 100644 --- a/shots/README.md +++ b/shots/README.md @@ -1 +1 @@ -Storage folder for pickled shots. Make sure to keep it clean after several runs! +* This directory contains the shot records \ No newline at end of file diff --git a/spyro/__init__.py b/spyro/__init__.py index 4c45979f..862021a3 100644 --- a/spyro/__init__.py +++ b/spyro/__init__.py @@ -3,13 +3,25 @@ from . import pml from .receivers.Receivers import Receivers from .sources.Sources import Sources, ricker_wavelet, full_ricker_wavelet -from .utils import utils +from .solvers.wave import Wave +from .solvers.acoustic_wave import AcousticWave + +# from .solvers.dg_wave import DG_Wave +from .solvers.mms_acoustic import AcousticWaveMMS from .utils.geometry_creation import create_transect, create_2d_grid from .utils.geometry_creation import insert_fixed_value, create_3d_grid from .utils.estimate_timestep import estimate_timestep -from .io import io +from . import utils +from . import io from . import solvers from . import tools +from . import examples +from . import sources +from .meshing import ( + RectangleMesh, + PeriodicRectangleMesh, + BoxMesh, +) __all__ = [ "io", @@ -28,4 +40,12 @@ "solvers", "plots", "tools", + "Wave", + "examples", + "sources", + "AcousticWave", + "AcousticWaveMMS", + "RectangleMesh", + "PeriodicRectangleMesh", + "BoxMesh", ] diff --git a/spyro/domains/quadrature.py b/spyro/domains/quadrature.py index 06b006f0..6c894ba6 100644 --- a/spyro/domains/quadrature.py +++ b/spyro/domains/quadrature.py @@ -1,24 +1,26 @@ import FIAT import finat -from firedrake import * +from firedrake import * # noqa:F403 def quadrature_rules(V): - """Returns quadrature rule - Gauss-Lobatto-Legendre, Gauss-Legendre and Equi-spaced, KMV - - Returns quadradure rule to use with UFL's dx object when integrating - - Parameters - ---------- - V : obj - UFL Function Space - - Returns - ------- - qr_x, qr_s, qr_k : obj - quadrature rules for Firedrake to use + """Quadrature rule - Gauss-Lobatto-Legendre, Gauss-Legendre and Equi + spaced, KMV + + Parameters: + ----------- + V: Firedrake FunctionSpace + Function space to be used in the quadrature rule. + + Returns: + -------- + qr_x: FIAT quadrature rule + Quadrature rule for the spatial domain. + qr_s: FIAT quadrature rule + Quadrature rule for the boundary of the spatial domain. + qr_k: FIAT quadrature rule + Quadrature rule for the spatial domain stiffness matrix. """ - degree = V.ufl_element().degree() dimension = V.mesh().geometric_dimension() cell_geometry = V.mesh().ufl_cell() @@ -34,9 +36,11 @@ def quadrature_rules(V): if ufl_method == "Mixed": ufl_method = V.sub(1).ufl_element().family() - if (cell_geometry == quadrilateral) and ufl_method == "Q": + if (cell_geometry == quadrilateral) and ufl_method == "Q": # noqa: F405 # In this case, for the spectral element method we use GLL quadrature - qr_x = gauss_lobatto_legendre_cube_rule(dimension=dimension, degree=degree) + qr_x = gauss_lobatto_legendre_cube_rule( + dimension=dimension, degree=degree + ) qr_k = qr_x qr_s = gauss_lobatto_legendre_cube_rule( dimension=(dimension - 1), degree=degree @@ -45,14 +49,16 @@ def quadrature_rules(V): # # In this case, we use GL quadrature # qr_x = gauss_legendre_cube_rule(dimension=dimension, degree=degree) # qr_k = qr_x - # qr_s = gauss_legendre_cube_rule(dimension=(dimension - 1), degree=degree) - elif (cell_geometry == triangle) and ( + # qr_s = gauss_legendre_cube_rule( + # dimension=(dimension - 1), degree=degree + # ) + elif (cell_geometry == triangle) and ( # noqa: F405 ufl_method == "Lagrange" or ufl_method == "Discontinuous Lagrange" ): qr_x = None qr_s = None qr_k = None - elif (cell_geometry == tetrahedron) and ( + elif (cell_geometry == tetrahedron) and ( # noqa: F405 ufl_method == "Lagrange" or ufl_method == "Discontinuous Lagrange" ): qr_x = None @@ -64,9 +70,22 @@ def quadrature_rules(V): ) qr_s = None qr_k = None + elif dimension == 3 and cell_geometry == TensorProductCell( # noqa: F405 + quadrilateral, # noqa: F405 + interval, # noqa: F405 + ): # noqa: F405 + # In this case, for the spectral element method we use GLL quadrature + degree, _ = degree + qr_x = gauss_lobatto_legendre_cube_rule( + dimension=dimension, degree=degree + ) + qr_k = qr_x + qr_s = gauss_lobatto_legendre_cube_rule( + dimension=(dimension - 1), degree=degree + ) else: raise ValueError("Unrecognized quadrature scheme") - return qr_x, qr_s, qr_k + return qr_x, qr_k, qr_s # -------------------------- # @@ -79,7 +98,7 @@ def gauss_lobatto_legendre_line_rule(degree): ---------- degree : int degree of the polynomial - + Returns ------- result : obj @@ -102,7 +121,7 @@ def gauss_lobatto_legendre_cube_rule(dimension, degree): dimension of the space degree : int degree of the polynomial - + Returns ------- result : obj @@ -124,7 +143,8 @@ def gauss_lobatto_legendre_cube_rule(dimension, degree): # fiat_rule = fiat_make_rule(FIAT.ufc_simplex(1), degree + 1) # finat_ps = finat.point_set.GaussLegendrePointSet # finat_qr = finat.quadrature.QuadratureRule -# return finat_qr(finat_ps(fiat_rule.get_points()), fiat_rule.get_weights()) +# return finat_qr(finat_ps(fiat_rule.get_points()), fiat_rule.get_weights() +# ) # # 3D diff --git a/spyro/domains/space.py b/spyro/domains/space.py index 6ba2640f..f42a4a0d 100644 --- a/spyro/domains/space.py +++ b/spyro/domains/space.py @@ -1,46 +1,40 @@ -from firedrake import * +from firedrake import * # noqa:F403 def FE_method(mesh, method, degree): - """Define the finite element method: - Space discretization - Continuous - or Discontinuous Galerkin methods + """Define the finite element space: - Parameters - ---------- - mesh : obj - Firedrake mesh - method : str - Finite element method - degree : int - Degree of the finite element method - - Returns - ------- - element : obj - Firedrake finite element + Parameters: + ----------- + mesh: Firedrake Mesh + Mesh to be used in the finite element space. + method: str + Method to be used for the finite element space. + degree: int + Degree of the finite element space. + + Returns: + -------- + function_space: Firedrake FunctionSpace + Function space. """ - cell_geometry = mesh.ufl_cell() - if method == "CG" or method == "spectral": - # CG - Continuous Galerkin - if cell_geometry == quadrilateral or cell_geometry == hexahedron: - element = FiniteElement( - method, mesh.ufl_cell(), degree=degree, variant="spectral" - ) - else: - element = FiniteElement( - method, mesh.ufl_cell(), degree=degree, variant="equispaced" - ) - elif method == "DG": - if cell_geometry == quadrilateral or cell_geometry == hexahedron: - element = FiniteElement( - method, mesh.ufl_cell(), degree=degree, variant="spectral" - ) - else: - element = FiniteElement( - method, mesh.ufl_cell(), degree=degree, variant="equispaced" - ) - elif method == "KMV": - # CG- with KMV elements - element = FiniteElement(method, mesh.ufl_cell(), degree=degree, variant="KMV") - return element + + if method == "mass_lumped_triangle": + element = FiniteElement( # noqa: F405 + "KMV", mesh.ufl_cell(), degree=degree, variant="KMV" + ) + elif method == "spectral_quadrilateral": + element = FiniteElement( # noqa: F405 + "CG", mesh.ufl_cell(), degree=degree, variant="spectral" + ) + elif method == "DG_triangle" or "DG_quadrilateral" or "DG": + element = FiniteElement( + "DG", mesh.ufl_cell(), degree=degree + ) # noqa: F405 + elif method == "CG_triangle" or "CG_quadrilateral" or "CG": + element = FiniteElement( + "CG", mesh.ufl_cell(), degree=degree + ) # noqa: F405 + + function_space = FunctionSpace(mesh, element) # noqa: F405 + return function_space diff --git a/spyro/examples/__init__.py b/spyro/examples/__init__.py new file mode 100644 index 00000000..33e8e4dc --- /dev/null +++ b/spyro/examples/__init__.py @@ -0,0 +1,13 @@ +from .camembert import Camembert_acoustic +from .marmousi import Marmousi_acoustic +from .cut_marmousi import Cut_marmousi_acoustic +from .example_model import Example_model_acoustic +from .rectangle import Rectangle_acoustic + +__all__ = [ + "Camembert_acoustic", + "Marmousi_acoustic", + "Example_model_acoustic", + "Rectangle_acoustic", + "Cut_marmousi_acoustic", +] diff --git a/spyro/examples/camembert.py b/spyro/examples/camembert.py new file mode 100644 index 00000000..48e96e21 --- /dev/null +++ b/spyro/examples/camembert.py @@ -0,0 +1,153 @@ +from spyro import create_transect +from spyro.examples.rectangle import Rectangle_acoustic +import firedrake as fire + +camembert_optimization_parameters = { + "General": { + "Secant": {"Type": "Limited-Memory BFGS", "Maximum Storage": 10} + }, + "Step": { + "Type": "Augmented Lagrangian", + "Augmented Lagrangian": { + "Subproblem Step Type": "Line Search", + "Subproblem Iteration Limit": 5.0, + }, + "Line Search": {"Descent Method": {"Type": "Quasi-Newton Step"}}, + }, + "Status Test": { + "Gradient Tolerance": 1e-16, + "Iteration Limit": None, + "Step Tolerance": 1.0e-16, + }, +} + +camembert_dictionary = {} +camembert_dictionary["options"] = { + "cell_type": "T", # simplexes such as triangles or tetrahedra (T) + # or quadrilaterals (Q) + "variant": "lumped", # lumped, equispaced or DG, default is lumped + "method": "MLT", # (MLT/spectral_quadrilateral/DG_triangle/ + # DG_quadrilateral) + # You can either specify a cell_type+variant or a method + "degree": 4, # p order + "dimension": 2, # dimension + "automatic_adjoint": False, +} + +# Number of cores for the shot. For simplicity, we keep things serial. +# spyro however supports both spatial parallelism and "shot" parallelism. +camembert_dictionary["parallelism"] = { + "type": "automatic", # options: automatic (same number of cores for + # every processor) or spatial +} + +# Define the domain size without the PML. Here we'll assume a 0.75 x 1.50 km +# domain and reserve the remaining 250 m for the Perfectly Matched Layer (PML) +# to absorb +# outgoing waves on three sides (eg., -z, +-x sides) of the domain. +camembert_dictionary["mesh"] = { + "Lz": 1.0, # depth in km - always positive + "Lx": 1.0, # width in km - always positive + "Ly": 0.0, # thickness in km - always positive + "h": 0.05, # mesh size in km + "mesh_file": None, + "mesh_type": "firedrake_mesh", # options: firedrake_mesh or user_mesh + "h": 0.05, +} +# For use only if you are using a synthetic test model +# or a forward only simulation +camembert_dictionary["synthetic_data"] = { + "real_mesh_file": None, + "real_velocity_file": None, +} +camembert_dictionary["inversion"] = { + "perform_fwi": False, # switch to true to make a FWI + "initial_guess_model_file": None, + "shot_record_file": None, + "optimization_parameters": camembert_optimization_parameters, +} + +# Specify a 250-m PML on the three sides of the domain to damp outgoing waves. +camembert_dictionary["absorving_boundary_conditions"] = { + "status": False, # True or false +} + +# Create a source injection operator. Here we use a single source with a +# Ricker wavelet that has a peak frequency of 8 Hz injected at the center +# of the mesh. +# We also specify to record the solution at 101 microphones near the top +# of the domain. +# This transect of receivers is created with the helper function +# `create_transect`. +camembert_dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-0.1, 0.5)], + "frequency": 5.0, + "delay": 1.0, + "receiver_locations": create_transect((-0.90, 0.1), (-0.90, 0.9), 30), +} + +# Simulate for 2.0 seconds. +camembert_dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": 1.0, # Final time for event + "dt": 0.0005, # timestep size + "amplitude": 1, # the Ricker has an amplitude of 1. + "output_frequency": 100, # how frequently to output solution to pvds + # how frequently to save solution to RAM + "gradient_sampling_frequency": 100, +} + +camembert_dictionary["visualization"] = { + "forward_output": True, + "forward_output_filename": "results/camembert_forward_output.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": False, + "gradient_filename": None, + "adjoint_output": False, + "adjoint_filename": None, + "debug_output": False, +} + + +class Camembert_acoustic(Rectangle_acoustic): + """Camembert model. + This class is a child of the Example_model class. + It is used to create a dictionary with the parameters of the + Camembert model. + + Parameters + ---------- + dictionary : dict, optional + Dictionary with the parameters of the model that are different from + the default Camembert model. The default is None. + + """ + + def __init__( + self, + dictionary=None, + example_dictionary=camembert_dictionary, + comm=None, + ): + super().__init__( + dictionary=dictionary, + example_dictionary=example_dictionary, + comm=comm, + ) + self._camembert_velocity_model() + + def _camembert_velocity_model(self): + z = self.mesh_z + x = self.mesh_x + zc = -0.5 + xc = 0.5 + rc = 0.2 + c_salt = 4.6 + c_not_salt = 1.6 + cond = fire.conditional( + (z - zc) ** 2 + (x - xc) ** 2 < rc**2, c_salt, c_not_salt + ) + self.set_initial_velocity_model(conditional=cond) + return None diff --git a/spyro/examples/cut_marmousi.py b/spyro/examples/cut_marmousi.py new file mode 100644 index 00000000..2264c55c --- /dev/null +++ b/spyro/examples/cut_marmousi.py @@ -0,0 +1,118 @@ +from spyro import create_transect +from spyro.examples.example_model import Example_model_acoustic + +cut_marmousi_optimization_parameters = { + "General": { + "Secant": {"Type": "Limited-Memory BFGS", "Maximum Storage": 10} + }, + "Step": { + "Type": "Augmented Lagrangian", + "Augmented Lagrangian": { + "Subproblem Step Type": "Line Search", + "Subproblem Iteration Limit": 5.0, + }, + "Line Search": {"Descent Method": {"Type": "Quasi-Newton Step"}}, + }, + "Status Test": { + "Gradient Tolerance": 1e-16, + "Iteration Limit": None, + "Step Tolerance": 1.0e-16, + }, +} + +cut_marmousi_dictionary = {} +cut_marmousi_dictionary["options"] = { + # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "cell_type": "T", + "variant": "lumped", # lumped, equispaced or DG, default is lumped + "degree": 4, # p order + "dimension": 2, # dimension + "automatic_adjoint": False, +} + +# Number of cores for the shot. For simplicity, we keep things serial. +# spyro however supports both spatial parallelism and "shot" parallelism. +cut_marmousi_dictionary["parallelism"] = { + # options: automatic (same number of cores for evey processor) or spatial + "type": "automatic", +} + +# Define the domain size without the PML. Here we'll assume a 0.75 x 1.50 km +# domain and reserve the remaining 250 m for the Perfectly Matched Layer (PML) +# to absorb +# outgoing waves on three sides (eg., -z, +-x sides) of the domain. +cut_marmousi_dictionary["mesh"] = { + "Lz": 2.0, # depth in km - always positive + "Lx": 4.0, # width in km - always positive + "Ly": 0.0, # thickness in km - always positive + "mesh_file": "meshes/cut_marmousi_small_p=2_M=7.02.msh", +} +cut_marmousi_dictionary[ + "synthetic_data" + # For use only if you are using a synthetic test model or + # a forward only simulation -adicionar discrição para modelo direto +] = { + "real_velocity_file": "velocity_models/MODEL_P-WAVE_VELOCITY_1.25m_small_domain.hdf5", # noqa: E501 +} +cut_marmousi_dictionary["inversion"] = { + "perform_fwi": False, # switch to true to make a FWI + "initial_guess_model_file": None, + "shot_record_file": None, + "optimization_parameters": cut_marmousi_optimization_parameters, +} + +# Specify a 250-m PML on the three sides of the domain to damp outgoing waves. +cut_marmousi_dictionary["absorving_boundary_conditions"] = { + "status": False, # True or false + "outer_bc": False, # None or non-reflective (outer boundary condition) + "damping_type": "polynomial", # polynomial, hyperbolic, shifted_hyperbolic + "exponent": 2, # damping layer has a exponent variation + "cmax": 4.7, # maximum acoustic wave velocity in PML - km/s + "R": 1e-6, # theoretical reflection coefficient + # thickness of the PML in the z-direction (km) - always positive + "lz": 0.25, + # thickness of the PML in the x-direction (km) - always positive + "lx": 0.25, + # thickness of the PML in the y-direction (km) - always positive + "ly": 0.0, +} + +# Create a source injection operator. Here we use a single source with a +# Ricker wavelet that has a peak frequency of 8 Hz injected at the center +# of the mesh. +# We also specify to record the solution at 101 microphones near the top +# of the domain. +# This transect of receivers is created with the helper function +# `create_transect`. +cut_marmousi_dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(2.0, -0.01)], + "frequency": 3.0, + "amplitude": 1.0, + "delay": 1.0, + "receiver_locations": create_transect((0.1, -0.10), (3.9, -0.10), 100), +} + +# Simulate for 2.0 seconds. +cut_marmousi_dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": 2.5, # Final time for event + "dt": 0.00025, # timestep size + "output_frequency": 20, # how frequently to output solution to pvds + # how frequently to save solution to RAM + "gradient_sampling_frequency": 10, +} + + +class Cut_marmousi_acoustic(Example_model_acoustic): + def __init__( + self, + dictionary=None, + example_dictionary=cut_marmousi_dictionary, + comm=None, + ): + super().__init__( + dictionary=dictionary, + default_dictionary=example_dictionary, + comm=comm, + ) diff --git a/spyro/examples/example_model.py b/spyro/examples/example_model.py new file mode 100644 index 00000000..905a769b --- /dev/null +++ b/spyro/examples/example_model.py @@ -0,0 +1,49 @@ +from ..solvers.acoustic_wave import AcousticWave +from copy import deepcopy + + +def get_list(dictionary): + list = [] + for key in dictionary.keys(): + list.append(key) + + return list + + +def recursive_dictionary_substitution(dictionary, default): + keys = get_list(default) + for key in keys: + if key not in dictionary: + dictionary[key] = default[key] + elif isinstance(default[key], dict): + recursive_dictionary_substitution(dictionary[key], default[key]) + + +class Example_model_acoustic(AcousticWave): + """Sets up a basic model parameter class for examples and test case models. + It has the option of reading a dictionary, and if any parameter is missing + from + this dictioanry it calls on a default value, that should be defined in the + relevant + example file. + + Parameters: + ----------- + dictionary: 'python dictionary' (optional): dictionary with changes to the + default parameters + + default_dictionary: python 'dictionary': default parameters + + Returns: + -------- + Example_model + """ + + def __init__(self, dictionary=None, default_dictionary=None, comm=None): + self.optional_dictionary = deepcopy(dictionary) + self.default_dictionary = default_dictionary + if dictionary is None: + dictionary = {} + recursive_dictionary_substitution(dictionary, default_dictionary) + self.input_dictionary = dictionary + super().__init__(dictionary=dictionary, comm=comm) diff --git a/spyro/examples/marmousi.py b/spyro/examples/marmousi.py new file mode 100644 index 00000000..c3b3af0e --- /dev/null +++ b/spyro/examples/marmousi.py @@ -0,0 +1,123 @@ +from spyro import create_transect +from spyro.examples.example_model import Example_model_acoustic + +marmousi_optimization_parameters = { + "General": { + "Secant": {"Type": "Limited-Memory BFGS", "Maximum Storage": 10} + }, + "Step": { + "Type": "Augmented Lagrangian", + "Augmented Lagrangian": { + "Subproblem Step Type": "Line Search", + "Subproblem Iteration Limit": 5.0, + }, + "Line Search": {"Descent Method": {"Type": "Quasi-Newton Step"}}, + }, + "Status Test": { + "Gradient Tolerance": 1e-16, + "Iteration Limit": None, + "Step Tolerance": 1.0e-16, + }, +} + +marmousi_dictionary = {} +marmousi_dictionary["options"] = { + # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "cell_type": "T", + "variant": "lumped", # lumped, equispaced or DG, default is lumped + # (MLT/spectral_quadrilateral/DG_triangle/DG_quadrilateral) + # You can either specify a cell_type+variant or a method + "method": "MLT", + "degree": 4, # p order + "dimension": 2, # dimension + "automatic_adjoint": False, +} + +# Number of cores for the shot. For simplicity, we keep things serial. +# spyro however supports both spatial parallelism and "shot" parallelism. +marmousi_dictionary["parallelism"] = { + # options: automatic (same number of cores for evey processor) or spatial + "type": "automatic", +} + +# Define the domain size without the PML. Here we'll assume a 0.75 x 1.50 km +# domain and reserve the remaining 250 m for the Perfectly Matched Layer (PML) +# to absorb +# outgoing waves on three sides (eg., -z, +-x sides) of the domain. +marmousi_dictionary["mesh"] = { + "Lz": 1.0, # depth in km - always positive + "Lx": 1.0, # width in km - always positive + "Ly": 0.0, # thickness in km - always positive + "mesh_file": None, +} +marmousi_dictionary[ + "synthetic_data" + # For use only if you are using a synthetic test model or + # a forward only simulation +] = { + "real_mesh_file": None, + "real_velocity_file": None, +} +marmousi_dictionary["inversion"] = { + "perform_fwi": False, # switch to true to make a FWI + "initial_guess_model_file": None, + "shot_record_file": None, + "optimization_parameters": marmousi_optimization_parameters, +} + +# Specify a 250-m PML on the three sides of the domain to damp outgoing waves. +marmousi_dictionary["absorving_boundary_conditions"] = { + "status": False, # True or false + # None or non-reflective (outer boundary condition) + "outer_bc": "non-reflective", + "damping_type": "polynomial", # polynomial, hyperbolic, shifted_hyperbolic + "exponent": 2, # damping layer has a exponent variation + "cmax": 4.7, # maximum acoustic wave velocity in PML - km/s + "R": 1e-6, # theoretical reflection coefficient + # thickness of the PML in the z-direction (km) - always positive + "lz": 0.25, + # thickness of the PML in the x-direction (km) - always positive + "lx": 0.25, + # thickness of the PML in the y-direction (km) - always positive + "ly": 0.0, +} + +# Create a source injection operator. Here we use a single source with a +# Ricker wavelet that has a peak frequency of 8 Hz injected at the center +# of the mesh. +# We also specify to record the solution at 101 microphones near the top +# of the domain. +# This transect of receivers is created with the helper function +# `create_transect`. +marmousi_dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-0.1, 0.5)], + "frequency": 5.0, + "delay": 1.0, + "receiver_locations": create_transect((-0.10, 0.1), (-0.10, 0.9), 20), +} + +# Simulate for 2.0 seconds. +marmousi_dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": 2.00, # Final time for event + "dt": 0.001, # timestep size + "amplitude": 1, # the Ricker has an amplitude of 1. + "output_frequency": 100, # how frequently to output solution to pvds + # how frequently to save solution to RAM + "gradient_sampling_frequency": 100, +} + + +class Marmousi_acoustic(Example_model_acoustic): + def __init__( + self, + dictionary=None, + example_dictionary=marmousi_dictionary, + comm=None, + ): + super().__init__( + dictionary=dictionary, + default_dictionary=example_dictionary, + comm=comm, + ) diff --git a/spyro/examples/rectangle.py b/spyro/examples/rectangle.py new file mode 100644 index 00000000..3d2b3d34 --- /dev/null +++ b/spyro/examples/rectangle.py @@ -0,0 +1,188 @@ +from spyro import create_transect +from spyro.examples.example_model import Example_model_acoustic +import firedrake as fire + +rectangle_optimization_parameters = { + "General": { + "Secant": {"Type": "Limited-Memory BFGS", "Maximum Storage": 10} + }, + "Step": { + "Type": "Augmented Lagrangian", + "Augmented Lagrangian": { + "Subproblem Step Type": "Line Search", + "Subproblem Iteration Limit": 5.0, + }, + "Line Search": {"Descent Method": {"Type": "Quasi-Newton Step"}}, + }, + "Status Test": { + "Gradient Tolerance": 1e-16, + "Iteration Limit": None, + "Step Tolerance": 1.0e-16, + }, +} + +rectangle_dictionary = {} +rectangle_dictionary["options"] = { + # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "cell_type": "Q", + "variant": "lumped", # lumped, equispaced or DG, default is lumped + "degree": 4, # p order + "dimension": 2, # dimension + "automatic_adjoint": False, +} + +# Number of cores for the shot. For simplicity, we keep things serial. +# spyro however supports both spatial parallelism and "shot" parallelism. +rectangle_dictionary["parallelism"] = { + # options: automatic (same number of cores for evey processor) or spatial + "type": "automatic", +} + +# Define the domain size without the PML. Here we'll assume a 0.75 x 1.50 km +# domain and reserve the remaining 250 m for the Perfectly Matched Layer (PML) +# to absorb +# outgoing waves on three sides (eg., -z, +-x sides) of the domain. +rectangle_dictionary["mesh"] = { + "Lz": 1.0, # depth in km - always positive + "Lx": 1.0, # width in km - always positive + "Ly": 0.0, # thickness in km - always positive + "h": 0.05, # mesh size in km + "mesh_file": None, + "mesh_type": "firedrake_mesh", # options: firedrake_mesh or user_mesh +} +rectangle_dictionary[ + "synthetic_data" + # For use only if you are using a synthetic test model or a forward only + # simulation -adicionar discrição para modelo direto +] = { + "real_mesh_file": None, + "real_velocity_file": None, + "velocity_conditional": None, +} +rectangle_dictionary["inversion"] = { + "perform_fwi": False, # switch to true to make a FWI + "initial_guess_model_file": None, + "shot_record_file": None, + "optimization_parameters": rectangle_optimization_parameters, +} +# Specify a 250-m PML on the three sides of the domain to damp outgoing waves. +rectangle_dictionary["absorving_boundary_conditions"] = { + "status": True, + "damping_type": "PML", + "exponent": 2, + "cmax": 4.5, + "R": 1e-6, + "pad_length": 0.25, + "status": True, + "damping_type": "PML", + "exponent": 2, + "cmax": 4.5, + "R": 1e-6, + "pad_length": 0.25, +} + +rectangle_dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-0.1, 0.3)], + "frequency": 5.0, + "delay": 1.0, + "receiver_locations": create_transect((-0.10, 0.1), (-0.10, 0.9), 20), +} + +# Simulate for 2.0 seconds. +rectangle_dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": 1.0, # Final time for event + "dt": 0.0005, # timestep size + "amplitude": 1, # the Ricker has an amplitude of 1. + "output_frequency": 100, # how frequently to output solution to pvds + # how frequently to save solution to RAM + "gradient_sampling_frequency": 100, +} + +rectangle_dictionary["visualization"] = { + "forward_output": True, + "forward_output_filename": "results/rectangle_forward_output.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": False, + "gradient_filename": None, +} + + +class Rectangle_acoustic(Example_model_acoustic): + def __init__( + self, + dictionary=None, + example_dictionary=rectangle_dictionary, + comm=None, + periodic=False, + ): + super().__init__( + dictionary=dictionary, + default_dictionary=example_dictionary, + comm=comm, + ) + self.periodic = periodic + + self._rectangle_mesh() + + def _rectangle_mesh(self): + mesh_dict = self.input_dictionary["mesh"] + h = mesh_dict["h"] + super().set_mesh(dx=h, periodic=self.periodic) + + def multiple_layer_velocity_model(self, z_switch, layers): + """ + Sets the heterogeneous velocity model to be split into horizontal layers. + Each layer's velocity value is defined by the corresponding value in the + layers list. The layers are separated by the values in the z_switch list. + + Parameters + ---------- + z_switch : list of floats + List of z values that separate the layers. + layers : list of floats + List of velocity values for each layer. + """ + if len(z_switch) != (len(layers) - 1): + raise ValueError( + "Float list of z_switch has to have length exactly one less \ + than list of layer values" + ) + if len(z_switch) == 0: + raise ValueError("Float list of z_switch cannot be empty") + for i in range(len(z_switch)): + if i == 0: + cond = fire.conditional( + self.mesh_z > z_switch[i], layers[i], layers[i + 1] + ) + else: + cond = fire.conditional( + self.mesh_z > z_switch[i], cond, layers[i + 1] + ) + # cond = fire.conditional(self.mesh_z > z_switch, layer1, layer2) + self.set_initial_velocity_model(conditional=cond) + + +# class Rectangle(AcousticWave): +# def __init__(self, model_dictionary=None, comm=None): +# model_parameters = Rectangle_parameters( +# dictionary=model_dictionary, comm=comm +# ) +# super().__init__( +# model_parameters=model_parameters, comm=model_parameters.comm +# ) +# comm = self.comm +# num_sources = self.number_of_sources +# if comm.comm.rank == 0 and comm.ensemble_comm.rank == 0: +# print( +# "INFO: Distributing %d shot(s) across %d core(s). \ +# Each shot is using %d cores" +# % ( +# num_sources, +# fire.COMM_WORLD.size, +# fire.COMM_WORLD.size / comm.ensemble_comm.size, +# ), +# flush=True, +# ) diff --git a/spyro/io/__init__.py b/spyro/io/__init__.py index add90fb3..31599e6d 100644 --- a/spyro/io/__init__.py +++ b/spyro/io/__init__.py @@ -1,12 +1,10 @@ -from .io import ( +from .basicio import ( write_function_to_grid, create_segy, is_owner, save_shots, load_shots, read_mesh, -) -from .io import ( interpolate, ensemble_forward, ensemble_forward_ad, @@ -14,8 +12,13 @@ ensemble_gradient, ensemble_gradient_elastic_waves, ensemble_plot, + parallel_print, + saving_source_and_receiver_location_in_csv, ) - +from .model_parameters import Model_parameters +from .backwards_compatibility_io import Dictionary_conversion +from . import dictionaryio +from . import boundary_layer_io __all__ = [ "write_function_to_grid", @@ -31,4 +34,11 @@ "ensemble_gradient", "ensemble_gradient_elastic_waves", "ensemble_plot", + "parallel_print", + "Model_parameters", + "convert_old_dictionary", + "Dictionary_conversion", + "dictionaryio", + "boundary_layer_io", + "saving_source_and_receiver_location_in_csv", ] diff --git a/spyro/io/backwards_compatibility_io.py b/spyro/io/backwards_compatibility_io.py new file mode 100644 index 00000000..52c61e6b --- /dev/null +++ b/spyro/io/backwards_compatibility_io.py @@ -0,0 +1,257 @@ +from genericpath import exists +import warnings + + +class Dictionary_conversion: + """ + Convert the old dictionary to the new one + + Attributes + ---------- + old_dictionary : dict + Old dictionary + new_dictionary : dict + New dictionary + fwi_running : bool + True if fwi is running, False if not + + Methods + ------- + convert_options() + Convert the options section of dictionary + convert_parallelism() + Convert the parallelism section of dictionary + convert_mesh() + Convert the mesh section of dictionary + check_if_fwi() + Check if fwi is running + convert_synthetic_data() + Convert the synthetic_data section of dictionary + set_optimization_parameters() + Set the optimization_parameters section of dictionary + set_no_inversion() + Set the no_inversion section of dictionary + """ + + def __init__(self, old_dictionary): + """ + Convert the old dictionary to the new one + + Parameters + ---------- + old_dictionary : dict + Old dictionary + + Returns + ------- + new_dictionary : dict + New dictionary + """ + self.new_dictionary = {} + self.old_dictionary = old_dictionary + self.fwi_running = False + + self.convert_options() + self.convert_parallelism() + self.convert_mesh() + self.check_if_fwi() + self.convert_synthetic_data() + if self.fwi_running: + self.set_optimization_parameters() + else: + self.set_no_inversion() + self.convert_absorving_boundary_conditions() + self.convert_acquisition() + self.convert_time_axis() + + def convert_options(self): + """ + Convert the options section of dictionary + """ + self.new_dictionary["options"] = { + "method": self.old_dictionary["opts"]["method"], + "variant": self.old_dictionary["opts"]["quadrature"], + "degree": self.old_dictionary["opts"]["degree"], + "dimension": self.old_dictionary["opts"]["dimension"], + } + + def convert_parallelism(self): + """ + Convert the parallelism section of dictionary + """ + self.new_dictionary["parallelism"] = { + "type": self.old_dictionary["parallelism"][ + # options: automatic (same number of cores for evey processor) + # or spatial + "type" + ], + } + + def convert_mesh(self): + """ + Convert the mesh section of dictionary + """ + self.new_dictionary["mesh"] = { + "Lz": self.old_dictionary["mesh"]["Lz"], + "Lx": self.old_dictionary["mesh"]["Lx"], + "Ly": self.old_dictionary["mesh"]["Ly"], + "mesh_file": self.old_dictionary["mesh"]["meshfile"], + } + + def check_if_fwi(self): + """ + Check if fwi is running + """ + if ( + self.old_dictionary["mesh"]["initmodel"] is not None + and self.old_dictionary["mesh"]["truemodel"] is not None + ) and ( + self.old_dictionary["mesh"]["initmodel"] != "not_used.hdf5" + and self.old_dictionary["mesh"]["truemodel"] != "not_used.hdf5" + ): + warnings.warn("Assuming parameters set for fwi.") + self.fwi_running = True + + if self.fwi_running is False: + warnings.warn( + "Assuming parameters set for forward only propagation, will \ + use velocity model from old_dictionary truemodel." + ) + + def convert_synthetic_data(self): + """ + Convert the synthetic_data section of dictionary + """ + if self.fwi_running: + self.new_dictionary["synthetic_data"] = { + "real_velocity_file": self.old_dictionary["mesh"]["truemodel"], + "real_mesh_file": None, + } + else: + model_file = None + if ( + self.old_dictionary["mesh"]["initmodel"] is not None + and self.old_dictionary["mesh"]["initmodel"] != "not_used.hdf5" + ): + model_file = self.old_dictionary["mesh"]["initmodel"] + else: + model_file = self.old_dictionary["mesh"]["truemodel"] + self.new_dictionary["synthetic_data"] = { + "real_velocity_file": model_file, + "real_mesh_file": None, + } + + def set_optimization_parameters(self): + """ + Set the optimization_parameters section of dictionary + """ + if self.fwi_running is False: + pass + + warnings.warn("Using default optimization parameters.") + default_optimization_parameters = { + "General": { + "Secant": { + "Type": "Limited-Memory BFGS", + "Maximum Storage": 10, + } + }, + "Step": { + "Type": "Augmented Lagrangian", + "Augmented Lagrangian": { + "Subproblem Step Type": "Line Search", + "Subproblem Iteration Limit": 5.0, + }, + "Line Search": { + "Descent Method": {"Type": "Quasi-Newton Step"} + }, + }, + "Status Test": { + "Gradient Tolerance": 1e-16, + "Iteration Limit": None, + "Step Tolerance": 1.0e-16, + }, + } + old_default_shot_record_file = "shots/shot_record_1.dat" + shot_record_file = None + if exists(old_default_shot_record_file): + shot_record_file = old_default_shot_record_file + self.new_dictionary["inversion"] = { + "perform_fwi": True, # switch to true to make a FWI + "initial_guess_model_file": self.old_dictionary["mesh"][ + "initmodel" + ], + "shot_record_file": shot_record_file, + "optimization_parameters": default_optimization_parameters, + } + + def set_no_inversion(self): + """ + Set the no_inversion section of dictionary + """ + self.new_dictionary["inversion"] = { + "perform_fwi": False, # switch to true to make a FWI + "initial_guess_model_file": None, + "shot_record_file": None, + "optimization_parameters": None, + } + + # default_dictionary["absorving_boundary_conditions"] = { + # # thickness of the PML in the z-direction (km) - always positive + # "lz": 0.25, + # # thickness of the PML in the x-direction (km) - always positive + # "lx": 0.25, + # # thickness of the PML in the y-direction (km) - always positive + # "ly": 0.0, + # } + + def convert_absorving_boundary_conditions(self): + """ + convert the absorving_boundary_conditions section of dictionary + """ + old_dictionary = self.old_dictionary["BCs"] + self.new_dictionary["absorving_boundary_conditions"] = { + "status": old_dictionary["status"], + "damping_type": "PML", + "exponent": old_dictionary["exponent"], + "cmax": old_dictionary["cmax"], + "R": old_dictionary["R"], + "pad_length": old_dictionary["lz"], + } + + def convert_acquisition(self): + """ + Convert the acquisition section of dictionary + """ + self.new_dictionary["acquisition"] = { + "source_type": self.old_dictionary["acquisition"]["source_type"], + "source_locations": self.old_dictionary["acquisition"][ + "source_pos" + ], + "frequency": self.old_dictionary["acquisition"]["frequency"], + "delay": self.old_dictionary["acquisition"]["delay"], + "amplitude": self.old_dictionary["timeaxis"]["amplitude"], + "receiver_locations": self.old_dictionary["acquisition"][ + "receiver_locations" + ], + } + + def convert_time_axis(self): + """ + Convert the time_axis section of dictionary + """ + self.new_dictionary["time_axis"] = { + "initial_time": self.old_dictionary["timeaxis"][ + "t0" + ], # Initial time for event + "final_time": self.old_dictionary["timeaxis"][ + "tf" + ], # Final time for event + "dt": self.old_dictionary["timeaxis"]["dt"], # timestep size + "output_frequency": self.old_dictionary["timeaxis"][ + "nspool" + ], # how frequently to output solution to pvds + "gradient_sampling_frequency": self.old_dictionary["timeaxis"][ + "fspool" + ], # how frequently to save solution to RAM + } diff --git a/spyro/io/io.py b/spyro/io/basicio.py similarity index 66% rename from spyro/io/io.py rename to spyro/io/basicio.py index 166e6f17..e6435f9f 100644 --- a/spyro/io/io.py +++ b/spyro/io/basicio.py @@ -1,4 +1,5 @@ from __future__ import with_statement + import pickle import firedrake as fire @@ -8,36 +9,25 @@ from scipy.interpolate import griddata import segyio -from .. import domains - -def ensemble_save(func): - """Decorator for read and write shots for ensemble parallelism - - Parameters - ---------- - func : function - Function to be decorated - - Returns - ------- - wrapper : function - Decorated function - """ +def ensemble_save_or_load(func): + """Decorator for read and write shots for ensemble parallelism""" def wrapper(*args, **kwargs): - acq = args[0].get("acquisition") - num = len(acq["source_pos"]) - _comm = args[1] + num = args[0].number_of_sources + comm = args[0].comm custom_file_name = kwargs.get("file_name") for snum in range(num): - if is_owner(_comm, snum) and _comm.comm.rank == 0: + if is_owner(comm, snum) and comm.comm.rank == 0: if custom_file_name is None: func( *args, **dict( kwargs, - file_name="shots/shot_record_" + str(snum + 1) + ".dat", + source_id=snum, + file_name="shots/shot_record_" + + str(snum + 1) + + ".dat", ) ) else: @@ -45,68 +35,52 @@ def wrapper(*args, **kwargs): *args, **dict( kwargs, - file_name=custom_file_name+"shot_record_" + str(snum + 1) + ".dat" - ) + source_id=snum, + file_name="shots/" + + custom_file_name + + str(snum + 1) + + ".dat", ) + ) + return wrapper -def ensemble_load(func): - """Decorator for loading shots for ensemble parallelism""" +def ensemble_plot(func): + """Decorator for `plot_shots` to distribute shots for + ensemble parallelism""" def wrapper(*args, **kwargs): - acq = args[0].get("acquisition") - num = len(acq["source_pos"]) - _comm = args[1] - custom_file_name = kwargs.get("file_name") + num = args[0].number_of_sources + _comm = args[0].comm for snum in range(num): - if is_owner(_comm, snum): - if custom_file_name is None: - values = func( - *args, - **dict( - kwargs, - file_name="shots/shot_record_" + str(snum + 1) + ".dat", - ) - ) - else: - values = func( - *args, - **dict( - kwargs, - file_name=custom_file_name+"shot_record_" + str(snum + 1) + ".dat" - ) - ) - return values + if is_owner(_comm, snum) and _comm.comm.rank == 0: + func(*args, **dict(kwargs, file_name=str(snum + 1))) return wrapper -def ensemble_plot(func): - """Decorator for `plot_shots` to distribute shots for ensemble parallelism""" +def ensemble_forward(func): + """Decorator for forward to distribute shots for ensemble parallelism""" def wrapper(*args, **kwargs): acq = args[0].get("acquisition") num = len(acq["source_pos"]) - _comm = args[1] - custom_file_name = kwargs.get("file_name") + _comm = args[2] for snum in range(num): - if is_owner(_comm, snum) and _comm.comm.rank == 0: - if custom_file_name is None: - func(*args, **dict(kwargs, file_name="shot_number_" + str(snum + 1))) - else: - func(*args, **dict(kwargs, file_name=custom_file_name + str(snum + 1))) + if is_owner(_comm, snum): + u, u_r = func(*args, **dict(kwargs, source_num=snum)) + return u, u_r return wrapper -def ensemble_forward(func): +def ensemble_propagator(func): """Decorator for forward to distribute shots for ensemble parallelism""" def wrapper(*args, **kwargs): - acq = args[0].get("acquisition") - num = len(acq["source_pos"]) - _comm = args[2] + num = args[0].number_of_sources + _comm = args[0].comm for snum in range(num): if is_owner(_comm, snum): u, u_r = func(*args, **dict(kwargs, source_num=snum)) @@ -116,7 +90,7 @@ def wrapper(*args, **kwargs): def ensemble_forward_ad(func): - """Decorator for forward_ad to distribute shots for ensemble parallelism""" + """Decorator for forward to distribute shots for ensemble parallelism""" def wrapper(*args, **kwargs): acq = args[0].get("acquisition") @@ -135,7 +109,8 @@ def wrapper(*args, **kwargs): def ensemble_forward_elastic_waves(func): - """Decorator for forward elastic waves to distribute shots for ensemble parallelism""" + """Decorator for forward elastic waves to distribute shots for + ensemble parallelism""" def wrapper(*args, **kwargs): acq = args[0].get("acquisition") @@ -143,7 +118,9 @@ def wrapper(*args, **kwargs): _comm = args[2] for snum in range(num): if is_owner(_comm, snum): - u, uz_r, ux_r, uy_r = func(*args, **dict(kwargs, source_num=snum)) + u, uz_r, ux_r, uy_r = func( + *args, **dict(kwargs, source_num=snum) + ) return u, uz_r, ux_r, uy_r return wrapper @@ -170,7 +147,8 @@ def wrapper(*args, **kwargs): def ensemble_gradient_elastic_waves(func): - """Decorator for gradient (elastic waves) to distribute shots for ensemble parallelism""" + """Decorator for gradient (elastic waves) to distribute shots + for ensemble parallelism""" def wrapper(*args, **kwargs): acq = args[0].get("acquisition") @@ -191,7 +169,7 @@ def wrapper(*args, **kwargs): def write_function_to_grid(function, V, grid_spacing): """Interpolate a Firedrake function to a structured grid - + Parameters ---------- function : firedrake.Function @@ -203,12 +181,12 @@ def write_function_to_grid(function, V, grid_spacing): Returns ------- - x : numpy.ndarray + xi : numpy.ndarray x coordinates of grid points - y : numpy.ndarray + yi : numpy.ndarray y coordinates of grid points - z : numpy.ndarray - z coordinates of grid points + zi : numpy.ndarray + Interpolated values on grid points """ # get DoF coordinates m = V.ufl_domain() @@ -237,18 +215,18 @@ def write_function_to_grid(function, V, grid_spacing): def create_segy(velocity, filename): """Write the velocity data into a segy file named filename - + Parameters ---------- - velocity : firedrake.Function - Velocity in a firedrake function - filename : str - Name of the segy file to write + velocity: + Firedrake function representing the values of the velocity + model to save + filename: str + Name of the segy file to save Returns ------- None - """ spec = segyio.spec() @@ -266,18 +244,19 @@ def create_segy(velocity, filename): for tr, il in enumerate(spec.ilines): f.trace[tr] = velocity[:, tr] - return None -@ensemble_save -def save_shots(model, comm, array, file_name=None): - """Save a `numpy.ndarray` to a `pickle`. +@ensemble_save_or_load +def save_shots(Wave_obj, source_id=0, file_name=None): + """Save a the shot record from last forward solve to a `pickle`. Parameters ---------- - file_name: str, optional by default shot_record_#.dat + Wave_obj: `spyro.Wave` object + A `spyro.Wave` object + source_id: int, optional by default 0 + The source number + file_name: str, optional by default shot_number_#.dat The filename to save the data as a `pickle` - array: `numpy.ndarray` - The data to save a pickle (e.g., a shot) Returns ------- @@ -285,12 +264,12 @@ def save_shots(model, comm, array, file_name=None): """ with open(file_name, "wb") as f: - pickle.dump(array, f) + pickle.dump(Wave_obj.forward_solution_receivers[:, source_id], f) return None -@ensemble_load -def load_shots(model, comm, file_name=None): +@ensemble_save_or_load +def load_shots(Wave_obj, source_id=0, file_name=None): """Load a `pickle` to a `numpy.ndarray`. Parameters @@ -304,10 +283,12 @@ def load_shots(model, comm, file_name=None): The data """ + array = np.zeros(()) with open(file_name, "rb") as f: array = np.asarray(pickle.load(f), dtype=float) - return array + Wave_obj.forward_solution_receivers = array + return None def is_owner(ens_comm, rank): @@ -330,7 +311,6 @@ def is_owner(ens_comm, rank): def _check_units(c): - """Checks if velocity is in m/s or km/s""" if min(c.dat.data[:]) > 100.0: # data is in m/s but must be in km/s if fire.COMM_WORLD.rank == 0: @@ -339,43 +319,42 @@ def _check_units(c): return c -def interpolate(model, mesh, V, guess=False): +def interpolate(Model, fname, V): """Read and interpolate a seismic velocity model stored in a HDF5 file onto the nodes of a finite element space. Parameters ---------- - model: `dictionary` + Model: spyro object Model options and parameters. - mesh: Firedrake.mesh object - A mesh object read in by Firedrake. + fname: str + The name of the HDF5 file containing the seismic velocity model. V: Firedrake.FunctionSpace object The space of the finite elements. - guess: boolean, optinal - Is it a guess model or a `exact` model? Returns ------- c: Firedrake.Function - P-wave seismic velocity interpolated onto the nodes of the finite elements. + P-wave seismic velocity interpolated onto the nodes + of the finite elements. """ sd = V.mesh().geometric_dimension() m = V.ufl_domain() - if model["BCs"]["status"]: - minz = -model["mesh"]["Lz"] - model["BCs"]["lz"] + if Model.abc_status: + minz = -Model.length_z - Model.abc_lz maxz = 0.0 - minx = 0.0 - model["BCs"]["lx"] - maxx = model["mesh"]["Lx"] + model["BCs"]["lx"] - miny = 0.0 - model["BCs"]["ly"] - maxy = model["mesh"]["Ly"] + model["BCs"]["ly"] + minx = 0.0 - Model.abc_lx + maxx = Model.length_x + Model.abc_lx + miny = 0.0 - Model.abc_ly + maxy = Model.length_y + Model.abc_ly else: - minz = -model["mesh"]["Lz"] + minz = -Model.length_z maxz = 0.0 minx = 0.0 - maxx = model["mesh"]["Lx"] + maxx = Model.length_x miny = 0.0 - maxy = model["mesh"]["Ly"] + maxy = Model.length_y W = fire.VectorFunctionSpace(m, V.ufl_element()) coords = fire.interpolate(m.coordinates, W) @@ -391,11 +370,6 @@ def interpolate(model, mesh, V, guess=False): else: raise NotImplementedError - if guess: - fname = model["mesh"]["initmodel"] - else: - fname = model["mesh"]["truemodel"] - with h5py.File(fname, "r") as f: Z = np.asarray(f.get("velocity_model")[()]) @@ -405,8 +379,12 @@ def interpolate(model, mesh, V, guess=False): x = np.linspace(minx, maxx, ncol) # make sure no out-of-bounds - qp_z2 = [minz if z < minz else maxz if z > maxz else z for z in qp_z] - qp_x2 = [minx if x < minx else maxx if x > maxx else x for x in qp_x] + qp_z2 = [ + minz if z < minz else maxz if z > maxz else z for z in qp_z + ] + qp_x2 = [ + minx if x < minx else maxx if x > maxx else x for x in qp_x + ] interpolant = RegularGridInterpolator((z, x), Z) tmp = interpolant((qp_z2, qp_x2)) @@ -417,9 +395,15 @@ def interpolate(model, mesh, V, guess=False): y = np.linspace(miny, maxy, ncol2) # make sure no out-of-bounds - qp_z2 = [minz if z < minz else maxz if z > maxz else z for z in qp_z] - qp_x2 = [minx if x < minx else maxx if x > maxx else x for x in qp_x] - qp_y2 = [miny if y < miny else maxy if y > maxy else y for y in qp_y] + qp_z2 = [ + minz if z < minz else maxz if z > maxz else z for z in qp_z + ] + qp_x2 = [ + minx if x < minx else maxx if x > maxx else x for x in qp_x + ] + qp_y2 = [ + miny if y < miny else maxy if y > maxy else y for y in qp_y + ] interpolant = RegularGridInterpolator((z, x, y), Z) tmp = interpolant((qp_z2, qp_x2, qp_y2)) @@ -430,32 +414,27 @@ def interpolate(model, mesh, V, guess=False): return c -def read_mesh(model, ens_comm): +def read_mesh(model_parameters): """Reads in an external mesh and scatters it between cores. Parameters ---------- - model: `dictionary` + model_parameters: spyro object Model options and parameters. - ens_comm: Firedrake.ensemble_communicator - An ensemble communicator Returns ------- mesh: Firedrake.Mesh object The distributed mesh across `ens_comm`. - V: Firedrake.FunctionSpace object - The space of the finite elements - """ - method = model["opts"]["method"] - degree = model["opts"]["degree"] + method = model_parameters.method + ens_comm = model_parameters.comm - num_sources = len(model["acquisition"]["source_pos"]) - mshname = model["mesh"]["meshfile"] + num_sources = model_parameters.number_of_sources + mshname = model_parameters.mesh_file - if method == "CG" or method == "KMV": + if method == "CG_triangle" or method == "mass_lumped_triangle": mesh = fire.Mesh( mshname, comm=ens_comm.comm, @@ -467,7 +446,8 @@ def read_mesh(model, ens_comm): mesh = fire.Mesh(mshname, comm=ens_comm.comm) if ens_comm.comm.rank == 0 and ens_comm.ensemble_comm.rank == 0: print( - "INFO: Distributing %d shot(s) across %d core(s). Each shot is using %d cores" + "INFO: Distributing %d shot(s) across %d core(s). \ + Each shot is using %d cores" % ( num_sources, fire.COMM_WORLD.size, @@ -485,7 +465,38 @@ def read_mesh(model, ens_comm): ), flush=True, ) - # Element type - element = domains.space.FE_method(mesh, method, degree) - # Space of problem - return mesh, fire.FunctionSpace(mesh, element) + + return mesh + + +def parallel_print(string, comm): + """ + Just prints a string in comm 0 + """ + if comm.ensemble_comm.rank == 0 and comm.comm.rank == 0: + print(string, flush=True) + + +def saving_source_and_receiver_location_in_csv(model, folder_name=None): + if folder_name is None: + folder_name = "results/" + + file_name = folder_name + "sources.txt" + file_obj = open(file_name, "w") + file_obj.write("Z,\tX \n") + for source in model["acquisition"]["source_locations"]: + z, x = source + string = str(z) + ",\t" + str(x) + " \n" + file_obj.write(string) + file_obj.close() + + file_name = folder_name + "receivers.txt" + file_obj = open(file_name, "w") + file_obj.write("Z,\tX \n") + for receiver in model["acquisition"]["receiver_locations"]: + z, x = receiver + string = str(z) + ",\t" + str(x) + " \n" + file_obj.write(string) + file_obj.close() + + return None diff --git a/spyro/io/boundary_layer_io.py b/spyro/io/boundary_layer_io.py new file mode 100644 index 00000000..2e28ff86 --- /dev/null +++ b/spyro/io/boundary_layer_io.py @@ -0,0 +1,68 @@ +# # Specify a 250-m PML on the three sides of the +# # domain to damp outgoing waves. +# default_dictionary["absorving_boundary_conditions"] = { +# "status": False, # True or false +# # None or non-reflective (outer boundary condition) +# "outer_bc": "non-reflective", +# # polynomial, hyperbolic, shifted_hyperbolic +# "damping_type": "polynomial", +# "exponent": 2, # damping layer has a exponent variation +# "cmax": 4.7, # maximum acoustic wave velocity in PML - km/s +# "R": 1e-6, # theoretical reflection coefficient +# # thickness of the PML in the z-direction (km) - always positive +# "lz": 0.25, +# # thickness of the PML in the x-direction (km) - always positive +# "lx": 0.25, +# # thickness of the PML in the y-direction (km) - always positive +# "ly": 0.0, +# } + + +class read_boundary_layer: + """ + Read the boundary layer dictionary + + Attributes + ---------- + dictionary : dict + Dictionary containing the boundary layer information + abc_exponent : float + Exponent of the polynomial damping + abc_cmax : float + Maximum acoustic wave velocity in PML - km/s + abc_R : float + Theoretical reflection coefficient + abc_pad_length : float + Thickness of the PML in the z-direction (km) - always positive + abc_boundary_layer_type : str + Type of the boundary layer + + Methods + ------- + read_PML_dictionary() + Read the PML dictionary for a perfectly matched layer + """ + + def __init__(self, abc_dictionary): + self.dictionary = abc_dictionary + if self.dictionary["status"] is False: + self.abc_exponent = None + self.abc_cmax = None + self.abc_R = None + self.abc_pad_length = 0.0 + self.abc_boundary_layer_type = None + pass + elif self.dictionary["damping_type"] == "PML": + self.abc_boundary_layer_type = self.dictionary["damping_type"] + self.read_PML_dictionary() + else: + abc_type = self.dictionary["damping_type"] + raise ValueError( + f"Boundary layer type of {abc_type} not recognized" + ) + + def read_PML_dictionary(self): + self.abc_exponent = self.dictionary["exponent"] + self.abc_cmax = self.dictionary["cmax"] + self.abc_R = self.dictionary["R"] + self.abc_pad_length = self.dictionary["pad_length"] diff --git a/spyro/io/dictionaryio.py b/spyro/io/dictionaryio.py new file mode 100644 index 00000000..166a3574 --- /dev/null +++ b/spyro/io/dictionaryio.py @@ -0,0 +1,474 @@ +import warnings +import os + + +def parse_cg(dictionary): + """ + Parse the CG method from the dictionary. + + Parameters + ---------- + dictionary : dict + Dictionary containing the options information. + + Returns + ------- + method : str + The method to be used. + cell_type : str + The cell type to be used. + variant : str + The variant to be used. + """ + if "variant" not in dictionary: + raise ValueError("variant must be specified for CG method.") + if dictionary["variant"] == "KMV": + dictionary["cell_type"] = "T" + if "cell_type" not in dictionary: + raise ValueError("cell_type must be specified for CG method.") + + cell_type = None + triangle_equivalents = ["T", "triangle", "triangles", "tetrahedra"] + quadrilateral_equivalents = [ + "Q", + "quadrilateral", + "quadrilaterals, hexahedra", + ] + if dictionary["cell_type"] in triangle_equivalents: + cell_type = "triangle" + elif dictionary["cell_type"] in quadrilateral_equivalents: + cell_type = "quadrilateral" + elif dictionary["variant"] == "GLL": + cell_type = "quadrilateral" + warnings.warn( + "GLL variant only supported for quadrilateral meshes. Assuming quadrilateral." + ) + else: + raise ValueError( + f"cell_type of {dictionary['cell_type']} is not valid." + ) + + if dictionary["variant"] is None: + warnings.warn("variant not specified for CG method. Assuming lumped.") + dictionary["variant"] = "lumped" + + accepted_variants = ["lumped", "equispaced", "DG", "GLL", "KMV"] + if dictionary["variant"] not in accepted_variants: + raise ValueError(f"variant of {dictionary['variant']} is not valid.") + + variant = dictionary["variant"] + + if variant == "GLL": + variant = "lumped" + + if variant == "KMV": + variant = "lumped" + + if cell_type == "triangle" and variant == "lumped": + method = "mass_lumped_triangle" + elif cell_type == "triangle" and variant == "equispaced": + method = "CG_triangle" + elif cell_type == "triangle" and variant == "DG": + method = "DG_triangle" + elif cell_type == "quadrilateral" and variant == "lumped": + method = "spectral_quadrilateral" + elif cell_type == "quadrilateral" and variant == "equispaced": + method = "CG_quadrilateral" + elif cell_type == "quadrilateral" and variant == "DG": + method = "DG_quadrilateral" + return method, cell_type, variant + + +def check_if_mesh_file_exists(file_name): + if file_name is None: + return + if os.path.isfile(file_name): + return + else: + raise ValueError(f"Mesh file {file_name} does not exist.") + + +class read_options: + """ + Read the options section of the dictionary. + + Attributes + ---------- + options_dictionary : dict + Dictionary containing the options information. + cell_type : str + The cell type to be used. + method : str + The FEM method to be used. + variant : str + The quadrature variant to be used. + degree : int + The polynomial degree of the FEM method. + dimension : int + The spatial dimension of the problem. + automatic_adjoint : bool + Whether to automatically compute the adjoint. + + Methods + ------- + check_valid_degree() + Check that the degree is valid for the method. + _check_valid_degree_for_mlt() + Check that the degree is valid for the MLT method. + check_mismatch_cell_type_variant_method() + Check that the user has not specified both the method and the cell type. + get_from_method() + Get the method, cell type and variant from the method. + get_from_cell_type_variant() + Get the method, cell type and variant from the cell type and variant. + """ + + def __init__(self, options_dictionary=None): + default_dictionary = { + # simplexes such as triangles or tetrahedra (T) + # or quadrilaterals (Q) + "cell_type": "T", + # lumped, equispaced or DG, default is lumped + "variant": "lumped", + # (MLT/spectral_quadrilateral/DG_triangle/ + # DG_quadrilateral) You can either specify a cell_type+variant or a method + "method": "MLT", + # p order + "degree": 4, + # dimension + "dimension": 2, + "automatic_adjoint": False, + } + + if options_dictionary is None: + self.options_dictionary = default_dictionary + else: + self.options_dictionary = options_dictionary + + self.cell_type = None + self.method = None + self.overdefined_method = False + self.variant = None + self.degree = None + self.dimension = None + self.overdefined_method = self.check_mismatch_cell_type_variant_method() + + if "method" not in self.options_dictionary: + self.options_dictionary["method"] = None + + if self.overdefined_method is True: + self.method, self.cell_type, self.variant = self.get_from_method() + elif self.options_dictionary["method"] is not None: + self.method, self.cell_type, self.variant = self.get_from_method() + else: + ( + self.method, + self.cell_type, + self.variant, + ) = self.get_from_cell_type_variant() + + if "degree" in self.options_dictionary: + self.degree = self.options_dictionary["degree"] + else: + self.degree = default_dictionary["degree"] + warnings.warn("Degree not specified, using default of 4.") + + if "dimension" in self.options_dictionary: + self.dimension = self.options_dictionary["dimension"] + else: + self.dimension = default_dictionary["dimension"] + warnings.warn("Dimension not specified, using default of 2.") + + if "automatic_adjoint" in self.options_dictionary: + self.automatic_adjoint = self.options_dictionary[ + "automatic_adjoint" + ] + else: + self.automatic_adjoint = default_dictionary["automatic_adjoint"] + + self.check_valid_degree() + + def check_valid_degree(self): + if self.degree < 1: + raise ValueError("Degree must be greater than 0.") + if self.method == "mass_lumped_triangle": + self._check_valid_degree_for_mlt() + + def _check_valid_degree_for_mlt(self): + degree = self.degree + dimension = self.dimension + if dimension == 2 and degree > 5: + raise ValueError( + "Degree must be less than or equal to 5 for MLT in 2D." + ) + elif dimension == 3 and degree > 4: + raise ValueError( + "Degree must be less than or equal to 4 for MLT in 3D." + ) + elif dimension == 3 and degree == 4: + warnings.warn( + f"Degree of {self.degree} not supported by \ + {self.dimension}D {self.method} in main firedrake." + ) + + def check_mismatch_cell_type_variant_method(self): + dictionary = self.options_dictionary + overdefined = False + if "method" in dictionary and ( + "cell_type" in dictionary or "variant" in dictionary + ): + overdefined = True + else: + pass + + if overdefined: + if dictionary["method"] is None: + overdefined = False + + if overdefined: + warnings.warn( + "Both methods of specifying method and cell_type with \ + variant used. Method specification taking priority." + ) + return overdefined + + def get_from_method(self): + dictionary = self.options_dictionary + if dictionary["method"] is None: + raise ValueError("Method input of None is invalid.") + + mlt_equivalents = [ + "KMV", + "MLT", + "mass_lumped_triangle", + "mass_lumped_tetrahedra", + ] + sem_equivalents = ["spectral", "SEM", "spectral_quadrilateral"] + dg_t_equivalents = [ + "DG_triangle", + "DGT", + "discontinuous_galerkin_triangle", + ] + dg_q_equivalents = [ + "DG_quadrilateral", + "DGQ", + "discontinuous_galerkin_quadrilateral", + ] + if dictionary["method"] in mlt_equivalents: + method = "mass_lumped_triangle" + cell_type = "triangle" + variant = "lumped" + elif dictionary["method"] in sem_equivalents: + method = "spectral_quadrilateral" + cell_type = "quadrilateral" + variant = "lumped" + elif dictionary["method"] in dg_t_equivalents: + method = "DG_triangle" + cell_type = "triangle" + variant = "DG" + elif dictionary["method"] in dg_q_equivalents: + method = "DG_quadrilateral" + cell_type = "quadrilateral" + variant = "DG" + elif dictionary["method"] == "DG": + raise ValueError( + "DG is not a valid method. Please specify \ + either DG_triangle or DG_quadrilateral." + ) + elif dictionary["method"] == "CG": + method, cell_type, variant = parse_cg(dictionary) + else: + raise ValueError(f"Method of {dictionary['method']} is not valid.") + return method, cell_type, variant + + def get_from_cell_type_variant(self): + triangle_equivalents = [ + "T", + "triangle", + "triangles", + "tetrahedra", + "tetrahedron", + ] + quadrilateral_equivalents = [ + "Q", + "quadrilateral", + "quadrilaterals", + "hexahedra", + "hexahedron", + ] + cell_type = self.options_dictionary["cell_type"] + if cell_type in triangle_equivalents: + cell_type = "triangle" + elif cell_type in quadrilateral_equivalents: + cell_type = "quadrilateral" + else: + raise ValueError(f"cell_type of {cell_type} is not valid.") + + variant = self.options_dictionary["variant"] + accepted_variants = ["lumped", "equispaced", "DG"] + if variant not in accepted_variants: + raise ValueError(f"variant of {variant} is not valid.") + + if cell_type == "triangle" and variant == "lumped": + method = "mass_lumped_triangle" + elif cell_type == "triangle" and variant == "equispaced": + method = "CG_triangle" + elif cell_type == "triangle" and variant == "DG": + method = "DG_triangle" + elif cell_type == "quadrilateral" and variant == "lumped": + method = "spectral_quadrilateral" + elif cell_type == "quadrilateral" and variant == "equispaced": + method = "CG_quadrilateral" + elif cell_type == "quadrilateral" and variant == "DG": + method = "DG_quadrilateral" + else: + raise ValueError( + f"cell_type of {cell_type} with variant of {variant} is not valid." + ) + return method, cell_type, variant + + +class read_mesh: + """ + Read the mesh section of the dictionary. + + Attributes + ---------- + mesh_dictionary : dict + Dictionary containing the mesh information. + dimension : int + The spatial dimension of the problem. + mesh_file : str + The mesh file name. + mesh_type : str + The type of mesh. + user_mesh : bool + Whether the user has provided a mesh. + firedrake_mesh : bool + Whether the user requires a firedrake mesh. + length_z : float + The length in the z direction. + length_x : float + The length in the x direction. + length_y : float + The length in the y direction. + + Methods + ------- + get_mesh_file_info() + Get the mesh file name. + get_mesh_type() + Get the mesh type. + _derive_mesh_type() + Derive the mesh type. + get_user_mesh() + Get the user mesh. + """ + + def __init__(self, dimension=2, mesh_dictionary=None): + default_dictionary = { + # depth in km - always positive + "Lz": 0.0, + # width in km - always positive + "Lx": 0.0, + # thickness in km - always positive + "Ly": 0.0, + # mesh file name with .msh extension + "mesh_file": None, + } + if mesh_dictionary is None: + self.mesh_dictionary = default_dictionary + else: + self.mesh_dictionary = mesh_dictionary + + self.dimension = dimension + self.mesh_file = self.get_mesh_file_info() + + check_if_mesh_file_exists(self.mesh_file) + + self.mesh_type = self.get_mesh_type() + self.user_mesh = self.get_user_mesh() + if self.mesh_type == "firedrake_mesh": + self.firedrake_mesh = True + else: + self.firedrake_mesh = False + + if "Lz" in self.mesh_dictionary: + self.length_z = self.mesh_dictionary["Lz"] + else: + self.length_z = default_dictionary["Lz"] + warnings.warn("Lz not specified, using default of 0.0.") + + if "Lx" in self.mesh_dictionary: + self.length_x = self.mesh_dictionary["Lx"] + else: + self.length_x = default_dictionary["Lx"] + warnings.warn("Lx not specified, using default of 0.0.") + + if "Ly" in self.mesh_dictionary: + self.length_y = self.mesh_dictionary["Ly"] + elif dimension == 2: + self.length_y = 0.0 + else: + self.length_y = default_dictionary["Ly"] + warnings.warn("Ly not specified, using default of 0.0.") + + def get_mesh_file_info(self): + dictionary = self.mesh_dictionary + if "mesh_file" not in dictionary: + mesh_file = None + return None + + mesh_file = dictionary["mesh_file"] + + if mesh_file is None: + return None + + if mesh_file == "not_used.msh": + mesh_file = None + else: + return mesh_file + + def get_mesh_type(self): + valid_mesh_types = [ + "file", + "firedrake_mesh", + "user_mesh", + "SeismicMesh", + None, + ] + dictionary = self.mesh_dictionary + if "mesh_type" not in dictionary: + mesh_type = self._derive_mesh_type() + elif dictionary["mesh_type"] in valid_mesh_types: + mesh_type = dictionary["mesh_type"] + else: + raise ValueError( + f"mesh_type of {dictionary['mesh_type']} is not valid." + ) + + if mesh_type is None: + warnings.warn("No mesh yet provided.") + + return mesh_type + + def _derive_mesh_type(self): + dictionary = self.mesh_dictionary + user_mesh_in_dictionary = False + if "user_mesh" not in dictionary: + dictionary["user_mesh"] = None + + if self.mesh_file is not None: + mesh_type = "file" + return mesh_type + elif dictionary["user_mesh"] is not None: + mesh_type = "user_mesh" + return mesh_type + else: + return None + + def get_user_mesh(self): + if self.mesh_type == "user_mesh": + return self.mesh_dictionary["user_mesh"] + else: + return False diff --git a/spyro/io/model_parameters.py b/spyro/io/model_parameters.py new file mode 100644 index 00000000..8922450f --- /dev/null +++ b/spyro/io/model_parameters.py @@ -0,0 +1,811 @@ +import warnings +from .. import io +from .. import utils +from .. import meshing +from ..sources import full_ricker_wavelet + +# default_optimization_parameters = { +# "General": {"Secant": {"Type": "Limited-Memory BFGS", +# "Maximum Storage": 10}}, +# "Step": { +# "Type": "Augmented Lagrangian", +# "Augmented Lagrangian": { +# "Subproblem Step Type": "Line Search", +# "Subproblem Iteration Limit": 5.0, +# }, +# "Line Search": {"Descent Method": {"Type": "Quasi-Newton Step"}}, +# }, +# "Status Test": { +# "Gradient Tolerance": 1e-16, +# "Iteration Limit": None, +# "Step Tolerance": 1.0e-16, +# }, +# } + +# default_dictionary = {} +# default_dictionary["options"] = { +# "cell_type": "T", # simplexes such as triangles or tetrahedra (T) +# or quadrilaterals (Q) +# "variant": 'lumped', # lumped, equispaced or DG, default is lumped +# "method": "MLT", # (MLT/spectral_quadrilateral/DG_triangle/ +# DG_quadrilateral) You can either specify a cell_type+variant or a method +# "degree": 4, # p order +# "dimension": 2, # dimension +# "automatic_adjoint": False, +# OPTIONAL PARAMETERS +# "time_integration_scheme": "central_difference", +# "equation_type": "second_order_in_pressure", +# } + +# # Number of cores for the shot. For simplicity, we keep things serial. +# # spyro however supports both spatial parallelism and "shot" parallelism. +# default_dictionary["parallelism"] = { +# # options: automatic (same number of cores for evey processor) or spatial +# "type": "automatic", +# } + +# # Define the domain size without the PML. Here we'll assume a 0.75 x 1.50 km +# # domain and reserve the remaining 250 m for the Perfectly Matched Layer +# # (PML) to absorb +# # outgoing waves on three sides (eg., -z, +-x sides) of the domain. +# default_dictionary["mesh"] = { +# "Lz": 1.0, # depth in km - always positive +# "Lx": 1.0, # width in km - always positive +# "Ly": 0.0, # thickness in km - always positive +# "mesh_file": None, +# } +# #For use only if you are using a synthetic test model +# #or a forward only simulation -adicionar discrição para modelo direto +# default_dictionary["synthetic_data"] = { +# "real_mesh_file": None, +# "real_velocity_file": None, +# } +# default_dictionary["inversion"] = { +# "perform_fwi": False, # switch to true to make a FWI +# "initial_guess_model_file": None, +# "shot_record_file": None, +# "optimization_parameters": default_optimization_parameters, +# } + +# # Specify a 250-m PML on the three sides of the +# # domain to damp outgoing waves. +# default_dictionary["absorving_boundary_conditions"] = { +# "status": False, # True or false +# # None or non-reflective (outer boundary condition) +# "outer_bc": "non-reflective", +# # polynomial, hyperbolic, shifted_hyperbolic +# "damping_type": "polynomial", +# "exponent": 2, # damping layer has a exponent variation +# "cmax": 4.7, # maximum acoustic wave velocity in PML - km/s +# "R": 1e-6, # theoretical reflection coefficient +# # thickness of the PML in the z-direction (km) - always positive +# "lz": 0.25, +# # thickness of the PML in the x-direction (km) - always positive +# "lx": 0.25, +# # thickness of the PML in the y-direction (km) - always positive +# "ly": 0.0, +# } + +# # Create a source injection operator. Here we use a single source with a +# # Ricker wavelet that has a peak frequency of 8 Hz injected at the +# # center of the mesh. +# # We also specify to record the solution at 101 microphones near the +# # top of the domain. +# # This transect of receivers is created with the helper function +# # `create_transect`. +# default_dictionary["acquisition"] = { +# "source_type": "ricker", +# "source_locations": [(-0.1, 0.5)], +# "frequency": 5.0, +# "delay": 1.0, +# "receiver_locations": spyro.create_transect( +# (-0.10, 0.1), (-0.10, 0.9), 20 +# ), +# } + +# # Simulate for 2.0 seconds. +# default_dictionary["time_axis"] = { +# "initial_time": 0.0, # Initial time for event +# "final_time": 2.00, # Final time for event +# "dt": 0.001, # timestep size +# "amplitude": 1, # the Ricker has an amplitude of 1. +# # how frequently to output solution to pvds +# "output_frequency": 100, +# # how frequently to save solution to RAM +# "gradient_sampling_frequency": 100, +# } +# default_dictionary["visualization"] = { +# "forward_output" : True, +# "output_filename": "results/forward_output.pvd", +# "fwi_velocity_model_output": False, +# "velocity_model_filename": None, +# "gradient_output": False, +# "gradient_filename": None, +# "adjoint_output": False, +# "adjoint_filename": None, +# "debug_output": False, +# } + + +class Model_parameters: + """ + Class that reads and sanitizes input parameters. + + Attributes + ---------- + input_dictionary: dictionary + Contains all input parameters already organized based on examples + from github. + cell_type: str + Type of cell used in meshing. Can be "T" for triangles or "Q" for + quadrilaterals. + method: str + Method used in meshing. Can be "MLT" for mass lumped triangles, + "spectral_quadrilateral" for spectral quadrilaterals, "DG_triangle" + for discontinuous Galerkin triangles, or "DG_quadrilateral" for + discontinuous Galerkin quadrilaterals. + variant: str + Variant used in meshing. Can be "lumped" for lumped mass matrices, + "equispaced" for equispaced nodes, or "DG" for discontinuous Galerkin + nodes. + degree: int + Degree of the basis functions used in the FEM. + dimension: int + Dimension of the mesh. + final_time: float + Final time of the simulation. + dt: float + Time step of the simulation. + initial_time: float + Initial time of the simulation. + output_frequency: int + Frequency of outputting the solution to pvd files. + gradient_sampling_frequency: int + Frequency of saving the solution to RAM. + number_of_sources: int + Number of sources used in the simulation. + source_locations: list + List of source locations. + frequency: float + Frequency of the source. + amplitude: float + Amplitude of the source. + delay: float + Delay of the source. + number_of_receivers: int + Number of receivers used in the simulation. + receiver_locations: list + List of receiver locations. + parallelism_type: str + Type of parallelism used in the simulation. Can be "automatic" for + automatic parallelism or "spatial" for spatial parallelism. + mesh_file: str + Path to the mesh file. + length_z: float + Length of the domain in the z-direction. + length_x: float + Length of the domain in the x-direction. + length_y: float + Length of the domain in the y-direction. + user_mesh: spyro.Mesh + User defined mesh. + firedrake_mesh: firedrake.Mesh + Firedrake mesh. + abc_status: bool + Whether or not the absorbing boundary conditions are used. + abc_exponent: int + Exponent of the absorbing boundary conditions. + abc_cmax: float + Maximum acoustic wave velocity in the absorbing boundary conditions. + abc_R: float + Theoretical reflection coefficient of the absorbing boundary + conditions. + abc_pad_length: float + Thickness of the absorbing boundary conditions. + source_type: str + Type of source used in the simulation. Can be "ricker" for a Ricker + wavelet or "MMS" for a manufactured solution. + running_fwi: bool + Whether or not the simulation is a FWI. + initial_velocity_model_file: str + Path to the initial velocity model file. + fwi_output_folder: str + Path to the FWI output folder. + control_output_file: str + Path to the control output file. + gradient_output_file: str + Path to the gradient output file. + optimization_parameters: dict + Dictionary of the optimization parameters. + automatic_adjoint: bool + Whether or not the adjoint is calculated automatically. + forward_output: bool + Whether or not the forward output is saved. + fwi_velocity_model_output: bool + Whether or not the FWI velocity model output is saved. + gradient_output: bool + Whether or not the gradient output is saved. + adjoint_output: bool + Whether or not the adjoint output is saved. + forward_output_file: str + Path to the forward output file. + fwi_velocity_model_output_file: str + Path to the FWI velocity model output file. + gradient_output_file: str + Path to the gradient output file. + adjoint_output_file: str + Path to the adjoint output file. + comm: MPI communicator + MPI communicator. + velocity_model_type: str + Type of velocity model used in the simulation. Can be "file" for a + file, "conditional" for a conditional, or None for no velocity model. + velocity_conditional: str + Conditional used for the velocity model. + equation_type: str + Type of equation used in the simulation. Can be "second_order_in_pressure". + time_integrator: str + Type of time integrator used in the simulation. Can be "central_difference". + + Methods + ------- + get_wavelet() + Returns a wavelet based on the source type. + set_mesh() + Sets the mesh. + get_mesh() + Reads in a mesh and scatters it between cores. + """ + + def __init__(self, dictionary=None, comm=None): + """Initializes class that reads and sanitizes input parameters. + A dictionary can be used. + + Parameters + ---------- + dictionary: 'dictionary' (optional) + Contains all input parameters already organized based on examples + from github. + comm: MPI communicator (optional) + MPI comunicator. If None is given model_parameters creates one. + + Returns + ------- + model_parameters: :class: 'model_parameters' object + """ + # Converts old dictionary to new one. Deprecated feature + if "opts" in dictionary: + warnings.warn("Old deprecated dictionary style in usage.") + dictionary = io.Dictionary_conversion(dictionary).new_dictionary + # Saves inout_dictionary internally + self.input_dictionary = dictionary + + # Sanitizes method or cell_type+variant inputs + Options = io.dictionaryio.read_options(self.input_dictionary["options"]) + self.cell_type = Options.cell_type + self.method = Options.method + self.variant = Options.variant + self.degree = Options.degree + self.dimension = Options.dimension + self.time_integrator = self._check_time_integrator() + self.equation_type = self._check_equation_type() + + # Checks time inputs + self._sanitize_time_inputs() + + # Checks inversion variables, FWI and velocity model inputs and outputs + self._sanitize_optimization_and_velocity() + + # Checking mesh_parameters + # self._sanitize_mesh() + Mesh_parameters = io.dictionaryio.read_mesh( + mesh_dictionary=self.input_dictionary["mesh"], + dimension=self.dimension, + ) + self.mesh_file = Mesh_parameters.mesh_file + self.mesh_type = Mesh_parameters.mesh_type + self.length_z = Mesh_parameters.length_z + self.length_x = Mesh_parameters.length_x + self.length_y = Mesh_parameters.length_y + self.user_mesh = Mesh_parameters.user_mesh + self.firedrake_mesh = Mesh_parameters.firedrake_mesh + + # Checking absorving boundary condition parameters + self._sanitize_absorving_boundary_condition() + + # Checking source and receiver inputs + self._sanitize_acquisition() + + # Setting up MPI communicator and checking parallelism: + self._sanitize_comm(comm) + + # Check automatic adjoint + self._sanitize_automatic_adjoint() + + # Sanitize output files + self._sanitize_output() + + # default_dictionary["absorving_boundary_conditions"] = { + # "status": False, # True or false + # # None or non-reflective (outer boundary condition) + # "outer_bc": "non-reflective", + # # polynomial, hyperbolic, shifted_hyperbolic + # "damping_type": "polynomial", + # "exponent": 2, # damping layer has a exponent variation + # "cmax": 4.7, # maximum acoustic wave velocity in PML - km/s + # "R": 1e-6, # theoretical reflection coefficient + # # thickness of the PML in the z-direction (km) - always positive + # "lz": 0.25, + # # thickness of the PML in the x-direction (km) - always positive + # "lx": 0.25, + # # thickness of the PML in the y-direction (km) - always positive + # "ly": 0.0, + # } + def _check_time_integrator(self): + if "time_integration_scheme" in self.input_dictionary: + time_integrator = self.input_dictionary["time_integration_scheme"] + else: + time_integrator = "central_difference" + + if time_integrator != "central_difference": + raise ValueError( + "The time integrator specified is not implemented yet" + ) + + return time_integrator + + def _check_equation_type(self): + if "equation_type" in self.input_dictionary: + equation_type = self.input_dictionary["equation_type"] + else: + equation_type = "second_order_in_pressure" + + if equation_type != "second_order_in_pressure": + raise ValueError( + "The equation type specified is not implemented yet" + ) + + return equation_type + + def _sanitize_absorving_boundary_condition(self): + if "absorving_boundary_conditions" not in self.input_dictionary: + self.input_dictionary["absorving_boundary_conditions"] = { + "status": False + } + dictionary = self.input_dictionary["absorving_boundary_conditions"] + self.abc_status = dictionary["status"] + + BL_obj = io.boundary_layer_io.read_boundary_layer(dictionary) + self.abc_exponent = BL_obj.abc_exponent + self.abc_cmax = BL_obj.abc_cmax + self.abc_R = BL_obj.abc_R + self.abc_pad_length = BL_obj.abc_pad_length + self.abc_boundary_layer_type = BL_obj.abc_boundary_layer_type + if self.abc_status: + self._correct_time_integrator_for_abc() + + def _correct_time_integrator_for_abc(self): + if self.time_integrator == "central_difference": + self.time_integrator = "mixed_space_central_difference" + + def _sanitize_output(self): + # default_dictionary["visualization"] = { + # "forward_output" : True, + # "forward_output_filename": "results/forward.pvd", + # "fwi_velocity_model_output": False, + # "velocity_model_filename": None, + # "gradient_output": False, + # "gradient_filename": None, + # "adjoint_output": False, + # "adjoint_filename": None, + # } + # Checking if any output should be saved + if "visualization" in self.input_dictionary: + dictionary = self.input_dictionary["visualization"] + else: + dictionary = { + "forward_output": False, + "fwi_velocity_model_output": False, + "gradient_output": False, + "adjoint_output": False, + } + self.input_dictionary["visualization"] = dictionary + + self.forward_output = dictionary["forward_output"] + + if "fwi_velocity_model_output" in dictionary: + self.fwi_velocity_model_output = dictionary[ + "fwi_velocity_model_output" + ] + else: + self.fwi_velocity_model_output = False + + if "gradient_output" in dictionary: + self.gradient_output = dictionary["gradient_output"] + else: + self.gradient_output = False + + if "adjoint_output" in dictionary: + self.adjoint_output = dictionary["adjoint_output"] + else: + self.adjoint_output = False + + # Getting output file names + self._sanitize_output_files() + + def _sanitize_output_files(self): + self._sanitize_forward_output_files() + dictionary = self.input_dictionary["visualization"] + + # Estabilishing velocity model file and setting a default + if "velocity_model_filename" not in dictionary: + self.fwi_velocity_model_output_file = ( + "results/fwi_velocity_model.pvd" + ) + elif dictionary["velocity_model_filename"] is None: + self.fwi_velocity_model_output_file = dictionary[ + "velocity_model_filename" + ] + else: + self.fwi_velocity_model_output_file = ( + "results/fwi_velocity_model.pvd" + ) + + self._check_debug_output() + + def _sanitize_forward_output_files(self): + dictionary = self.input_dictionary["visualization"] + if "forward_output_filename" not in dictionary: + self.forward_output_file = "results/forward_propogation.pvd" + elif dictionary["forward_output_filename"] is not None: + self.forward_output_file = dictionary["forward_output_filename"] + else: + self.forward_output_file = "results/forward_propagation.pvd" + + def _sanitize_adjoint_and_gradient_output_files(self): + dictionary = self.input_dictionary["visualization"] + # Estabilishing gradient file and setting a default + if "gradient_filename" not in dictionary: + self.gradient_output_file = "results/gradient.pvd" + elif dictionary["gradient_filename"] is None: + self.gradient_output_file = dictionary["gradient_filename"] + else: + self.gradient_output_file = "results/gradient.pvd" + + # Estabilishing adjoint file and setting a default + if "adjoint_filename" not in dictionary: + self.adjoint_output_file = "results/adjoint.pvd" + elif dictionary["adjoint_filename"] is None: + self.adjoint_output_file = dictionary["adjoint_filename"] + else: + self.adjoint_output_file = "results/adjoint.pvd" + + def _check_debug_output(self): + dictionary = self.input_dictionary["visualization"] + # Estabilishing debug output + if "debug_output" not in dictionary: + self.debug_output = False + elif dictionary["debug_output"] is None: + self.debug_output = False + elif dictionary["debug_output"] is False: + self.debug_output = False + elif dictionary["debug_output"] is True: + self.debug_output = True + else: + raise ValueError("Debug output not understood") + + def get_wavelet(self): + """Returns a wavelet based on the source type. + + Returns + ------- + wavelet : numpy.ndarray + Wavelet values in each time step to be used in the simulation. + """ + if self.source_type == "ricker": + if "delay_type" in self.input_dictionary["acquisition"]: + delay_type = self.input_dictionary["acquisition"]["delay_type"] + self.delay_type = delay_type + else: + delay_type = "multiples_of_minimun" + self.delay_type = delay_type + wavelet = full_ricker_wavelet( + dt=self.dt, + final_time=self.final_time, + frequency=self.frequency, + delay=self.delay, + amplitude=self.amplitude, + delay_type=delay_type, + ) + elif self.source_type == "MMS": + wavelet = None + else: + raise ValueError( + f"Source type of {self.source_type} not yet implemented." + ) + + return wavelet + + def _sanitize_automatic_adjoint(self): + dictionary = self.input_dictionary + if "automatic_adjoint" in dictionary: + self.automatic_adjoint = True + else: + self.automatic_adjoint = False + + def _sanitize_comm(self, comm): + dictionary = self.input_dictionary + if "parallelism" in dictionary: + self.parallelism_type = dictionary["parallelism"]["type"] + else: + warnings.warn("No paralellism type listed. Assuming automatic") + self.parallelism_type = "automatic" + + if self.source_type == "MMS": + self.parallelism_type = "spatial" + + if comm is None: + self.comm = utils.mpi_init(self) + self.comm.comm.barrier() + else: + self.comm = comm + + def _sanitize_acquisition(self): + dictionary = self.input_dictionary["acquisition"] + self.number_of_receivers = len(dictionary["receiver_locations"]) + self.receiver_locations = dictionary["receiver_locations"] + + # Check ricker source: + self.source_type = dictionary["source_type"] + if self.source_type == "Ricker": + self.source_type = "ricker" + elif self.source_type == "MMS": + self.number_of_sources = 1 + self.source_locations = [] + self.frequency = None + self.amplitude = None + self.delay = None + return + + self.number_of_sources = len(dictionary["source_locations"]) + self.source_locations = dictionary["source_locations"] + self.frequency = dictionary["frequency"] + if "amplitude" in dictionary: + self.amplitude = dictionary["amplitude"] + else: + self.amplitude = 1.0 + if "delay" in dictionary: + self.delay = dictionary["delay"] + else: + self.delay = 1.5 + self.__check_acquisition() + + def _sanitize_optimization_and_velocity(self): + """ + Checks if we are doing a FWI and sorts velocity model types, inputs, + and outputs + """ + dictionary = self.input_dictionary + self.velocity_model_type = "file" + + # Check if we are doing a FWI and sorting output locations and + # velocity model inputs + self.running_fwi = False + if "inversion" not in dictionary: + dictionary["inversion"] = {"perform_fwi": False} + + if dictionary["inversion"]["perform_fwi"]: + self.running_fwi = True + + if self.running_fwi: + self._sanitize_optimization_and_velocity_for_fwi() + else: + self._sanitize_optimization_and_velocity_without_fwi() + + if self.initial_velocity_model_file is None: + if "velocity_conditional" not in dictionary["synthetic_data"]: + self.velocity_model_type = None + warnings.warn( + "No velocity model set initially. If using \ + user defined conditional or expression, please \ + input it in the Wave object." + ) + + if "velocity_conditional" in dictionary["synthetic_data"]: + self.velocity_model_type = "conditional" + self.velocity_conditional = dictionary["synthetic_data"][ + "velocity_conditional" + ] + + self.forward_output_file = "results/forward_output.pvd" + + def _sanitize_optimization_and_velocity_for_fwi(self): + dictionary = self.input_dictionary + self.initial_velocity_model_file = dictionary["inversion"][ + "initial_guess_model_file" + ] + self.fwi_output_folder = "fwi/" + self.control_output_file = self.fwi_output_folder + "control" + self.gradient_output_file = self.fwi_output_folder + "gradient" + self.optimization_parameters = dictionary["inversion"][ + "optimization_parameters" + ] + + def _sanitize_optimization_and_velocity_without_fwi(self): + dictionary = self.input_dictionary + if "synthetic_data" in dictionary: + self.initial_velocity_model_file = dictionary["synthetic_data"][ + "real_velocity_file" + ] + else: + dictionary["synthetic_data"] = {"real_velocity_file": None} + self.initial_velocity_model_file = None + + def _sanitize_time_inputs(self): + dictionary = self.input_dictionary["time_axis"] + self.final_time = dictionary["final_time"] + self.dt = dictionary["dt"] + if "initial_time" in dictionary: + self.initial_time = dictionary["initial_time"] + else: + self.initial_time = 0.0 + self.output_frequency = dictionary["output_frequency"] + self.gradient_sampling_frequency = dictionary[ + "gradient_sampling_frequency" + ] + + self.__check_time() + + def __check_acquisition(self): + for source in self.source_locations: + if self.dimension == 2: + source_z, source_x = source + source_y = 0.0 + elif self.dimension == 3: + source_z, source_x, source_y = source + else: + raise ValueError("Source input type not supported") + + def __check_time(self): + if self.final_time < 0.0: + raise ValueError(f"Negative time of {self.final_time} not valid.") + if self.dt > 1.0: + warnings.warn(f"Time step of {self.dt} too big.") + if self.dt is None: + warnings.warn( + "Timestep not given. Will calculate internally when user \ + attemps to propagate wave." + ) + + def set_mesh( + self, + dx=None, + user_mesh=None, + mesh_file=None, + length_z=None, + length_x=None, + length_y=None, + periodic=False, + edge_length=None, + ): + """ + + Parameters + ---------- + dx : float, optional + The desired mesh spacing. The default is None. + user_mesh : spyro.Mesh, optional + The desired mesh. The default is None. + mesh_file : str, optional + The path to the desired mesh file. The default is None. + length_z : float, optional + The length of the domain in the z-direction. The default is None. + length_x : float, optional + The length of the domain in the x-direction. The default is None. + length_y : float, optional + The length of the domain in the y-direction. The default is None. + periodic : bool, optional + Whether the domain is periodic. The default is False. + """ + + self._set_mesh_length( + length_z=length_z, + length_x=length_x, + length_y=length_y, + ) + + if self.mesh_type == "firedrake_mesh": + automatic_mesh = True + elif self.mesh_type == "SeismicMesh": + automatic_mesh = True + else: + automatic_mesh = False + + if user_mesh is not None: + self.user_mesh = user_mesh + self.mesh_type = "user_mesh" + elif mesh_file is not None: + self.mesh_file = mesh_file + self.mesh_type = "file" + elif automatic_mesh: + self.user_mesh = self.creating_automatic_mesh( + periodic=periodic, edge_length=edge_length, dx=dx + ) + + if ( + length_z is None + or length_x is None + or (length_y is None and self.dimension == 2) + ) and self.mesh_type != "firedrake_mesh": + warnings.warn( + "Mesh dimensions not completely reset from initial dictionary" + ) + + def creating_automatic_mesh( + self, periodic=False, edge_length=None, dx=None + ): + if self.mesh_type == "firedrake_mesh": + AutoMeshing = meshing.AutomaticMesh( + dimension=self.dimension, + comm=self.comm, + abc_pad=self.abc_pad_length, + mesh_type=self.mesh_type, + ) + AutoMeshing.set_mesh_size( + length_z=self.length_z, + length_x=self.length_x, + length_y=self.length_y, + ) + if dx is None: + dx = edge_length + AutoMeshing.set_meshing_parameters( + dx=dx, cell_type=self.cell_type, mesh_type=self.mesh_type + ) + elif self.mesh_type == "SeismicMesh": + AutoMeshing = meshing.AutomaticMesh( + dimension=self.dimension, + comm=self.comm, + abc_pad=self.abc_pad_length, + mesh_type=self.mesh_type, + ) + AutoMeshing.set_mesh_size( + length_z=self.length_z, + length_x=self.length_x, + length_y=self.length_y, + ) + AutoMeshing.set_seismicmesh_parameters(edge_length=edge_length) + + if periodic: + AutoMeshing.make_periodic() + + return AutoMeshing.create_mesh() + + def _set_mesh_length( + self, + length_z=None, + length_x=None, + length_y=None, + ): + if length_z is not None: + self.length_z = length_z + if length_x is not None: + self.length_x = length_x + if length_y is not None: + self.length_y = length_y + + def get_mesh(self): + """Reads in an external mesh and scatters it between cores. + + Returns + ------- + mesh: Firedrake.Mesh object + The distributed mesh across `ens_comm` + """ + if self.mesh_file is not None: + return io.read_mesh(self) + elif ( + self.mesh_type == "user_mesh" or self.mesh_type == "firedrake_mesh" + ): + return self.user_mesh + elif self.mesh_type == "SeismicMesh": + return self.user_mesh diff --git a/spyro/meshing/__init__.py b/spyro/meshing/__init__.py new file mode 100644 index 00000000..2e8ebf57 --- /dev/null +++ b/spyro/meshing/__init__.py @@ -0,0 +1,10 @@ +from .meshing_functions import RectangleMesh +from .meshing_functions import PeriodicRectangleMesh, BoxMesh +from .meshing_functions import AutomaticMesh + +all = [ + "RectangleMesh", + "PeriodicRectangleMesh", + "BoxMesh", + "AutomaticMesh", +] diff --git a/spyro/meshing/meshing_functions.py b/spyro/meshing/meshing_functions.py new file mode 100644 index 00000000..7c393649 --- /dev/null +++ b/spyro/meshing/meshing_functions.py @@ -0,0 +1,435 @@ +import firedrake as fire +import SeismicMesh +import meshio + + +class AutomaticMesh: + """ + Class for automatic meshing. + + Attributes + ---------- + dimension : int + Spatial dimension of the mesh. + length_z : float + Length of the domain in the z direction. + length_x : float + Length of the domain in the x direction. + length_y : float + Length of the domain in the y direction. + dx : float + Mesh size. + quadrilateral : bool + If True, the mesh is quadrilateral. + periodic : bool + If True, the mesh is periodic. + comm : MPI communicator + MPI communicator. + mesh_type : str + Type of the mesh. + abc_pad : float + Padding to be added to the domain. + + Methods + ------- + set_mesh_size(length_z=None, length_x=None, length_y=None) + Sets the mesh size. + set_meshing_parameters(dx=None, cell_type=None, mesh_type=None) + Sets the meshing parameters. + make_periodic() + Sets the mesh boundaries periodic. + create_mesh() + Creates the mesh. + create_firedrake_mesh() + Creates a 2D mesh based on Firedrake meshing utilities. + create_firedrake_2D_mesh() + Creates a 2D mesh based on Firedrake meshing utilities. + create_firedrake_3D_mesh() + Creates a 3D mesh based on Firedrake meshing utilities. + """ + + def __init__( + self, dimension=2, comm=None, abc_pad=None, mesh_type="firedrake_mesh" + ): + """ + Parameters + ---------- + dimension : int, optional + Dimension of the mesh. The default is 2. + comm : MPI communicator, optional + MPI communicator. The default is None. + """ + self.dimension = dimension + self.length_z = None + self.length_x = None + self.length_y = None + self.comm = comm + if abc_pad is None: + self.abc_pad = 0.0 + elif abc_pad >= 0.0: + self.abc_pad = abc_pad + else: + raise ValueError("abc_pad must be positive") + self.mesh_type = mesh_type + + # Firedrake mesh only parameters + self.dx = None + self.quadrilateral = False + self.periodic = False + + # SeismicMesh only parameters + self.cpw = None + self.velocity_model = None + self.edge_length = None + + def set_mesh_size(self, length_z=None, length_x=None, length_y=None): + """ + Parameters + ---------- + length_z : float, optional + Length of the domain in the z direction. The default is None. + length_x : float, optional + Length of the domain in the x direction. The default is None. + length_y : float, optional + Length of the domain in the y direction. The default is None. + + Returns + ------- + None + """ + if length_z is not None: + self.length_z = length_z + if length_x is not None: + self.length_x = length_x + if length_y is not None: + self.length_y = length_y + + def set_meshing_parameters(self, dx=None, cell_type=None, mesh_type=None): + """ + Parameters + ---------- + dx : float, optional + Mesh size. The default is None. + cell_type : str, optional + Type of the cell. The default is None. + mesh_type : str, optional + Type of the mesh. The default is None. + + Returns + ------- + None + """ + if cell_type is not None: + self.cell_type = cell_type + if self.cell_type == "quadrilateral": + self.quadrilateral = True + if dx is not None: + self.dx = dx + if mesh_type is not None: + self.mesh_type = mesh_type + + if self.mesh_type != "firedrake_mesh": + raise ValueError("mesh_type is not supported") + + def set_seismicmesh_parameters( + self, + cpw=None, + velocity_model=None, + edge_length=None, + output_file_name=None, + ): + """ + Parameters + ---------- + cpw : float, optional + Cells per wavelength parameter. The default is None. + velocity_model : str, optional + Velocity model. The default is None. + edge_length : float, optional + Edge length. The default is None. + + Returns + ------- + None + """ + if cpw is not None: + self.cpw = cpw + if velocity_model is not None: + # self.velocity_model = velocity_model + raise NotImplementedError( + "Reading from velocity file not yet implemented" + ) + if edge_length is not None: + self.edge_length = edge_length + if output_file_name is not None: + self.output_file_name = output_file_name + else: + self.output_file_name = "automatically_generated_mesh.msh" + + if self.mesh_type != "SeismicMesh": + raise ValueError("mesh_type is not supported") + + def make_periodic(self): + """ + Sets the mesh boundaries periodic. + """ + self.periodic = True + if self.mesh_type != "firedrake_mesh": + raise ValueError( + "periodic mesh is only supported for firedrake_mesh" + ) + + def create_mesh(self): + """ + Creates the mesh. + + Returns + ------- + mesh : Firedrake Mesh + Mesh + """ + if self.mesh_type == "firedrake_mesh": + return self.create_firedrake_mesh() + elif self.mesh_type == "SeismicMesh": + return self.create_seismicmesh_mesh() + else: + raise ValueError("mesh_type is not supported") + + def create_firedrake_mesh(self): + if self.dx is None: + raise ValueError("dx is not set") + elif self.dimension == 2: + return self.create_firedrake_2D_mesh() + elif self.dimension == 3: + return self.create_firedrake_3D_mesh() + else: + raise ValueError("dimension is not supported") + + def create_firedrake_2D_mesh(self): + """ + Creates a 2D mesh based on Firedrake meshing utilities. + """ + nx = int(self.length_x / self.dx) + nz = int(self.length_z / self.dx) + comm = self.comm + if self.cell_type == "quadrilateral": + quadrilateral = True + else: + quadrilateral = False + + if self.periodic: + return PeriodicRectangleMesh( + nz, + nx, + self.length_z, + self.length_x, + quadrilateral=quadrilateral, + comm=comm.comm, + pad=self.abc_pad, + ) + else: + return RectangleMesh( + nz, + nx, + self.length_z, + self.length_x, + quadrilateral=quadrilateral, + comm=comm.comm, + pad=self.abc_pad, + ) + + def create_firedrake_3D_mesh(self): + dx = self.dx + nx = int(self.length_x / dx) + nz = int(self.length_z / dx) + ny = int(self.length_y / dx) + if self.cell_type == "quadrilateral": + quadrilateral = True + else: + quadrilateral = False + + return BoxMesh( + nz, + nx, + ny, + self.length_z, + self.length_x, + self.length_y, + quadrilateral=quadrilateral, + ) + + def create_seismicmesh_mesh(self): + if self.dimension == 2: + return self.create_seimicmesh_2d_mesh() + elif self.dimension == 3: + raise NotImplementedError("Not implemented yet") + # return self.create_seismicmesh_3D_mesh() + else: + raise ValueError("dimension is not supported") + + def create_seimicmesh_2d_mesh(self): + if self.edge_length is not None: + return self.create_seismicmesh_2D_mesh_homogeneous() + else: + raise NotImplementedError("Not yet implemented") + + def create_seismicmesh_2D_mesh_homogeneous(self): + """ + Creates a 2D mesh based on SeismicMesh meshing utilities. + """ + Lz = self.length_z + Lx = self.length_x + pad = self.abc_pad + + real_lz = Lz + pad + real_lx = Lx + 2 * pad + + edge_length = self.edge_length + bbox = (-real_lz, 0.0, -pad, real_lx - pad) + rectangle = SeismicMesh.Rectangle(bbox) + + points, cells = SeismicMesh.generate_mesh( + domain=rectangle, + edge_length=edge_length, + verbose=0, + ) + + points, cells = SeismicMesh.geometry.delete_boundary_entities( + points, cells, min_qual=0.6 + ) + + meshio.write_points_cells( + self.output_file_name, + points, + [("triangle", cells)], + file_format="gmsh22", + binary=False, + ) + meshio.write_points_cells( + self.output_file_name + ".vtk", + points, + [("triangle", cells)], + file_format="vtk", + ) + + return fire.Mesh(self.output_file_name) + # raise NotImplementedError("Not implemented yet") + + +# def create_firedrake_3D_mesh_based_on_parameters(dx, cell_type): +# nx = int(self.length_x / dx) +# nz = int(self.length_z / dx) +# ny = int(self.length_y / dx) +# if self.cell_type == "quadrilateral": +# quadrilateral = True +# else: +# quadrilateral = False + +# return spyro.BoxMesh( +# nz, +# nx, +# ny, +# self.length_z, +# self.length_x, +# self.length_y, +# quadrilateral=quadrilateral, +# ) + + +def RectangleMesh(nx, ny, Lx, Ly, pad=None, comm=None, quadrilateral=False): + """Create a rectangle mesh based on the Firedrake mesh. + First axis is negative, second axis is positive. If there is a pad, both + axis are dislocated by the pad. + + Parameters + ---------- + Lx : float + Length of the domain in the x direction. + Ly : float + Length of the domain in the y direction. + nx : int + Number of elements in the x direction. + ny : int + Number of elements in the y direction. + pad : float, optional + Padding to be added to the domain. The default is None. + comm : MPI communicator, optional + MPI communicator. The default is None. + quadrilateral : bool, optional + If True, the mesh is quadrilateral. The default is False. + + Returns + ------- + mesh : Firedrake Mesh + Mesh + """ + if pad is not None: + Lx += pad + Ly += 2 * pad + else: + pad = 0 + mesh = fire.RectangleMesh(nx, ny, Lx, Ly, quadrilateral=quadrilateral) + mesh.coordinates.dat.data[:, 0] *= -1.0 + mesh.coordinates.dat.data[:, 1] -= pad + + return mesh + + +def PeriodicRectangleMesh( + nx, ny, Lx, Ly, pad=None, comm=None, quadrilateral=False +): + """Create a periodic rectangle mesh based on the Firedrake mesh. + First axis is negative, second axis is positive. If there is a pad, both + axis are dislocated by the pad. + + Parameters + ---------- + Lx : float + Length of the domain in the x direction. + Ly : float + Length of the domain in the y direction. + nx : int + Number of elements in the x direction. + ny : int + Number of elements in the y direction. + pad : float, optional + Padding to be added to the domain. The default is None. + comm : MPI communicator, optional + MPI communicator. The default is None. + quadrilateral : bool, optional + If True, the mesh is quadrilateral. The default is False. + + Returns + ------- + mesh : Firedrake Mesh + Mesh + + """ + if pad is not None: + Lx += pad + Ly += 2 * pad + else: + pad = 0 + mesh = fire.PeriodicRectangleMesh( + nx, ny, Lx, Ly, quadrilateral=quadrilateral, comm=comm + ) + mesh.coordinates.dat.data[:, 0] *= -1.0 + mesh.coordinates.dat.data[:, 1] -= pad + + return mesh + + +def BoxMesh(nx, ny, nz, Lx, Ly, Lz, pad=None, quadrilateral=False): + if pad is not None: + Lx += pad + Ly += 2 * pad + Lz += 2 * pad + else: + pad = 0 + quad_mesh = fire.RectangleMesh(nx, ny, Lx, Ly, quadrilateral=quadrilateral) + quad_mesh.coordinates.dat.data[:, 0] *= -1.0 + quad_mesh.coordinates.dat.data[:, 1] -= pad + layer_height = Lz / nz + mesh = fire.ExtrudedMesh(quad_mesh, nz, layer_height=layer_height) + + return mesh diff --git a/spyro/plots/plots.py b/spyro/plots/plots.py index 7215fc8b..7394dfe2 100644 --- a/spyro/plots/plots.py +++ b/spyro/plots/plots.py @@ -8,9 +8,7 @@ @ensemble_plot def plot_shots( - model, - comm, - arr, + Wave_object, show=False, file_name="1", vmin=-1e-5, @@ -49,12 +47,14 @@ def plot_shots( None """ - num_recvs = len(model["acquisition"]["receiver_locations"]) + num_recvs = Wave_object.number_of_receivers - dt = model["timeaxis"]["dt"] - tf = model["timeaxis"]["tf"] + dt = Wave_object.dt + tf = Wave_object.final_time - nt = int(tf / dt) # number of timesteps + arr = Wave_object.receivers_output + + nt = int(tf / dt) + 1 # number of timesteps if end_index == 0: end_index = num_recvs @@ -64,7 +64,7 @@ def plot_shots( X, Y = np.meshgrid(x_rec, t_rec) cmap = plt.get_cmap("gray") - plt.contourf(X, Y, arr, cmap=cmap, vmin=vmin, vmax=vmax) + plt.contourf(X, Y, arr, 700, cmap=cmap, vmin=vmin, vmax=vmax) # savemat("test.mat", {"mydata": arr}) plt.xlabel("receiver number", fontsize=18) plt.ylabel("time (s)", fontsize=18) diff --git a/spyro/pml/__init__.py b/spyro/pml/__init__.py index 85515f35..3f5168a0 100644 --- a/spyro/pml/__init__.py +++ b/spyro/pml/__init__.py @@ -1,3 +1,3 @@ -from . import damping +# from . import damping -__all__ = ["damping"] +# __all__ = ["damping"] diff --git a/spyro/pml/damping.py b/spyro/pml/damping.py index 630c7594..34519169 100644 --- a/spyro/pml/damping.py +++ b/spyro/pml/damping.py @@ -1,60 +1,16 @@ import math import warnings -from firedrake import * - - -def functions( - model, - V, - dimension, - x, - x1, - x2, - a_pml, - z, - z1, - z2, - c_pml, - y=None, - y1=None, - y2=None, - b_pml=None, -): +from firedrake import * # noqa: F403 + + +def functions(Wave_obj): """Damping functions for the perfect matched layer for 2D and 3D - + Parameters ---------- - model : dict - Dictionary with the model parameters - V : obj - Firedrake function space - dimension : int - Dimension of the problem - x : obj - Firedrake spatial coordinate - x1 : float - x coordinate of the left boundary of the PML - x2 : float - x coordinate of the right boundary of the PML - a_pml : float - Width of the PML in the x direction - z : obj - Firedrake spatial coordinate - z1 : float - z coordinate of the bottom boundary of the PML - z2 : float - z coordinate of the top boundary of the PML - c_pml : float - Width of the PML in the z direction - y : obj, optional - Firedrake spatial coordinate, by default None - y1 : float, optional - y coordinate of the back boundary of the PML, by default None - y2 : float, optional - y coordinate of the front boundary of the PML, by default None - b_pml : float, optional - Width of the PML in the y direction, by default None + Wave_obj : obj + Wave object with the parameters of the problem Returns ------- @@ -64,35 +20,39 @@ def functions( Firedrake function with the damping function in the z direction sigma_y : obj Firedrake function with the damping function in the y direction - - """ - damping_type = model["BCs"]["damping_type"] - if damping_type == "polynomial": - ps = model["BCs"]["exponent"] # polynomial scaling - cmax = model["BCs"]["cmax"] # maximum acoustic wave velocity - R = model["BCs"]["R"] # theoretical reclection coefficient + """ - bar_sigma = ((3.0 * cmax) / (2.0 * a_pml)) * math.log10(1.0 / R) + ps = Wave_obj.abc_exponent + cmax = Wave_obj.abc_cmax # maximum acoustic wave velocity + R = Wave_obj.abc_R # theoretical reclection coefficient + pad_length = Wave_obj.abc_pad_length # length of the padding + V = Wave_obj.function_space + dimension = Wave_obj.dimension + z = Wave_obj.mesh_z + x = Wave_obj.mesh_x + x1 = 0.0 + x2 = Wave_obj.length_x + z1 = 0.0 + z2 = -Wave_obj.length_z + + bar_sigma = ((3.0 * cmax) / (2.0 * pad_length)) * math.log10(1.0 / R) aux1 = Function(V) aux2 = Function(V) - if damping_type != "polynomial": - warnings.warn("Warning: only polynomial damping functions supported!") - # Sigma X sigma_max_x = bar_sigma # Max damping aux1.interpolate( conditional( - And((x >= x1 - a_pml), x < x1), - ((abs(x - x1) ** (ps)) / (a_pml ** (ps))) * sigma_max_x, + And((x >= x1 - pad_length), x < x1), + ((abs(x - x1) ** (ps)) / (pad_length ** (ps))) * sigma_max_x, 0.0, ) ) aux2.interpolate( conditional( - And(x > x2, (x <= x2 + a_pml)), - ((abs(x - x2) ** (ps)) / (a_pml ** (ps))) * sigma_max_x, + And(x > x2, (x <= x2 + pad_length)), + ((abs(x - x2) ** (ps)) / (pad_length ** (ps))) * sigma_max_x, 0.0, ) ) @@ -103,37 +63,34 @@ def functions( sigma_max_z = bar_sigma # Max damping aux1.interpolate( conditional( - And(z < z2, (z >= z2 - tol_z * c_pml)), - ((abs(z - z2) ** (ps)) / (c_pml ** (ps))) * sigma_max_z, + And(z < z2, (z >= z2 - tol_z * pad_length)), + ((abs(z - z2) ** (ps)) / (pad_length ** (ps))) * sigma_max_z, 0.0, ) ) sigma_z = Function(V, name="sigma_z").interpolate(aux1) - # sgm_x = File("pmlField/sigma_x.pvd") # , target_degree=1, target_continuity=H1 - # sgm_x.write(sigma_x) - # sgm_z = File("pmlField/sigma_z.pvd") - # sgm_z.write(sigma_z) - if dimension == 2: - return (sigma_x, sigma_z) elif dimension == 3: # Sigma Y sigma_max_y = bar_sigma # Max damping + y = Wave_obj.mesh_y + y1 = 0.0 + y2 = Wave_obj.length_y aux1.interpolate( conditional( - And((y >= y1 - b_pml), y < y1), - ((abs(y - y1) ** (ps)) / (b_pml ** (ps))) * sigma_max_y, + And((y >= y1 - pad_length), y < y1), + ((abs(y - y1) ** (ps)) / (pad_length ** (ps))) * sigma_max_y, 0.0, ) ) aux2.interpolate( conditional( - And(y > y2, (y <= y2 + b_pml)), - ((abs(y - y2) ** (ps)) / (b_pml ** (ps))) * sigma_max_y, + And(y > y2, (y <= y2 + pad_length)), + ((abs(y - y2) ** (ps)) / (pad_length ** (ps))) * sigma_max_y, 0.0, ) ) @@ -154,7 +111,9 @@ def matrices_2D(sigma_x, sigma_y): def matrices_3D(sigma_x, sigma_y, sigma_z): """Damping matrices for a three-dimensional problem""" - Gamma_1 = as_tensor([[sigma_x, 0.0, 0.0], [0.0, sigma_y, 0.0], [0.0, 0.0, sigma_z]]) + Gamma_1 = as_tensor( + [[sigma_x, 0.0, 0.0], [0.0, sigma_y, 0.0], [0.0, 0.0, sigma_z]] + ) Gamma_2 = as_tensor( [ [sigma_x - sigma_y - sigma_z, 0.0, 0.0], diff --git a/spyro/receivers/Receivers.py b/spyro/receivers/Receivers.py index 56362b27..8ef596df 100644 --- a/spyro/receivers/Receivers.py +++ b/spyro/receivers/Receivers.py @@ -1,14 +1,11 @@ -from firedrake import * -from FIAT.reference_element import UFCTriangle, UFCTetrahedron, UFCQuadrilateral -from FIAT.kong_mulder_veldhuizen import KongMulderVeldhuizen as KMV -from FIAT.lagrange import Lagrange as CG -from FIAT.discontinuous_lagrange import DiscontinuousLagrange as DG +from firedrake import * # noqa: F403 +from spyro.receivers.dirac_delta_projector import Delta_projector import numpy as np -class Receivers: - """Interpolate data defined on a triangular mesh to a +class Receivers(Delta_projector): + """Project data defined on a triangular mesh to a set of 2D/3D coordinates for variable spatial order using Lagrange interpolation. @@ -50,7 +47,7 @@ class Receivers: in timestep IT, for usage with adjoint propagation """ - def __init__(self, model, mesh, V, my_ensemble): + def __init__(self, wave_object): """Initializes class and gets all receiver parameters from input file. Parameters @@ -67,107 +64,22 @@ def __init__(self, model, mesh, V, my_ensemble): ------- Receivers: :class: 'Receiver' object """ - - if "Aut_Dif" in model: - self.automatic_adjoint = True - else: - self.automatic_adjoint = False - - self.mesh = mesh - self.space = V - self.my_ensemble = my_ensemble - self.dimension = model["opts"]["dimension"] - self.degree = model["opts"]["degree"] - self.receiver_locations = model["acquisition"]["receiver_locations"] - - if self.dimension == 3 and model["aut_dif"]["status"]: - self.column_x = model["acquisition"]["num_rec_x_columns"] - self.column_y = model["acquisition"]["num_rec_y_columns"] - self.column_z = model["acquisition"]["num_rec_z_columns"] - self.num_receivers = self.column_x * self.column_y - + super().__init__(wave_object) + self.point_locations = wave_object.receiver_locations + + if self.dimension == 3 and wave_object.automatic_adjoint: + # self.column_x = model["acquisition"]["num_rec_x_columns"] + # self.column_y = model["acquisition"]["num_rec_y_columns"] + # self.column_z = model["acquisition"]["num_rec_z_columns"] + # self.number_of_points = self.column_x*self.column_y + raise ValueError("Implement this later") else: - self.num_receivers = len(self.receiver_locations) + self.number_of_points = wave_object.number_of_receivers - self.cellIDs = None - self.cellVertices = None - self.cell_tabulations = None - self.cellNodeMaps = None - self.nodes_per_cell = None - self.quadrilateral = model["opts"]["quadrature"] == "GLL" - self.is_local = [0] * self.num_receivers + self.is_local = [0] * self.number_of_points if not self.automatic_adjoint: self.build_maps() - @property - def num_receivers(self): - return self.__num_receivers - - @num_receivers.setter - def num_receivers(self, value): - if value <= 0: - raise ValueError("No receivers specified") - self.__num_receivers = value - - def build_maps(self): - """Calculates and stores tabulations for interpolation - - Is always automatticaly called when initializing the class, - therefore should only be called again if a mesh related attribute - changes. - - Returns - ------- - cellIDs: list - List of cell IDs for each receiver - cellVertices: list - List of vertices for each receiver - cellNodeMaps: list - List of node maps for each receiver - cell_tabulations: list - List of tabulations for each receiver - """ - - for rid in range(self.num_receivers): - tolerance = 1e-6 - if self.dimension == 2: - receiver_z, receiver_x = self.receiver_locations[rid] - cell_id = self.mesh.locate_cell( - [receiver_z, receiver_x], tolerance=tolerance - ) - elif self.dimension == 3: - receiver_z, receiver_x, receiver_y = self.receiver_locations[rid] - cell_id = self.mesh.locate_cell( - [receiver_z, receiver_x, receiver_y], tolerance=tolerance - ) - self.is_local[rid] = cell_id - - ( - self.cellIDs, - self.cellVertices, - self.cellNodeMaps, - ) = self.__func_receiver_locator() - self.cell_tabulations = self.__func_build_cell_tabulations() - - self.num_receivers = len(self.receiver_locations) - - def interpolate(self, field): - """Interpolate the solution to the receiver coordinates for - one simulation timestep. - - Parameters - ---------- - field: array-like - An array of the solution at a given timestep at all nodes - - Returns - ------- - solution_at_receivers: list - Solution interpolated to the list of receiver coordinates - for the given timestep. - """ - return [self.__new_at(field, rn) for rn in range(self.num_receivers)] - def apply_receivers_as_source(self, rhs_forcing, residual, IT): """The adjoint operation of interpolation (injection) @@ -184,13 +96,13 @@ def apply_receivers_as_source(self, rhs_forcing, residual, IT): and timesteps IT: int Desired time step number to get residual value from - + Returns ------- rhs_forcing: object Firedrake assembled right hand side operator with injected values """ - for rid in range(self.num_receivers): + for rid in range(self.number_of_points): value = residual[IT][rid] if self.is_local[rid]: idx = np.int_(self.cellNodeMaps[rid]) @@ -203,289 +115,14 @@ def apply_receivers_as_source(self, rhs_forcing, residual, IT): return rhs_forcing - def __func_receiver_locator(self): - """Function that returns a list of tuples and a matrix - the list of tuples has in line n the receiver position - and the position of the nodes in the element that contains - the receiver. - The matrix has the deegres of freedom of the nodes inside - same element as the receiver. - """ - if self.dimension == 2: - return self.__func_receiver_locator_2D() - elif self.dimension == 3: - return self.__func_receiver_locator_3D() - else: - raise ValueError - - def __build_local_nodes(self): - """Builds local element nodes, locations and I,J,K numbering""" - if self.dimension == 2: - return self.__build_local_nodes_2D() - elif self.dimension == 3: - return self.__build_local_nodes_3D() - else: - raise ValueError - - def __func_node_locations(self): - """Function that returns a list which includes a numpy matrix - where line n has the x and y values of the nth degree of freedom, - and a numpy matrix of the vertex coordinates. - """ - if self.dimension == 2: - return self.__func_node_locations_2D() - elif self.dimension == 3: - return self.__func_node_locations_3D() - else: - raise ValueError - - def __func_receiver_locator_2D(self): - """Function that returns a list of tuples and a matrix - the list of tuples has in line n the receiver position - and the position of the nodes in the element that contains - the receiver. - The matrix has the deegres of freedom of the nodes inside - same element as the receiver. - """ - num_recv = self.num_receivers - - fdrake_cell_node_map = self.space.cell_node_map() - cell_node_map = fdrake_cell_node_map.values_with_halo - (num_cells, nodes_per_cell) = cell_node_map.shape - node_locations = self.__func_node_locations() - self.nodes_per_cell = nodes_per_cell - - cellId_maps = np.zeros((num_recv, 1)) - cellNodeMaps = np.zeros((num_recv, nodes_per_cell)) - cellVertices = [] - - if self.quadrilateral is True: - end_vertex_id = 4 - degree = self.degree - cell_ends = [ - 0, - (degree + 1) * (degree + 1) - degree - 1, - (degree + 1) * (degree + 1) - 1, - degree, - ] - else: - end_vertex_id = 3 - cell_ends = [0, 1, 2] - - for receiver_id in range(num_recv): - cell_id = self.is_local[receiver_id] - - cellVertices.append([]) - - if cell_id is not None: - cellId_maps[receiver_id] = cell_id - cellNodeMaps[receiver_id, :] = cell_node_map[cell_id, :] - for vertex_number in range(0, end_vertex_id): - cellVertices[receiver_id].append([]) - z = node_locations[ - cell_node_map[cell_id, cell_ends[vertex_number]], 0 - ] - x = node_locations[ - cell_node_map[cell_id, cell_ends[vertex_number]], 1 - ] - cellVertices[receiver_id][vertex_number] = (z, x) - - return cellId_maps, cellVertices, cellNodeMaps - - def __new_at(self, udat, receiver_id): - """Function that evaluates the receiver value given its id. - For 2D simplices only. - Parameters - ---------- - udat: array-like - An array of the solution at a given timestep at all nodes - receiver_id: a list of integers - A list of receiver ids, ranging from 0 to total receivers - minus one. - - Returns - ------- - at: Function value at given receiver - """ - - if self.is_local is not None: - # Getting relevant receiver points - u = udat[np.int_(self.cellNodeMaps[receiver_id, :])] - else: - return udat[0] # junk receiver isn't local - - phis = self.cell_tabulations[receiver_id, :] - - at = phis.T @ u - - return at - - def __func_node_locations_2D(self): - """Function that returns a list which includes a numpy matrix - where line n has the x and y values of the nth degree of freedom, - and a numpy matrix of the vertex coordinates. - """ - z, x = SpatialCoordinate(self.mesh) - ux = Function(self.space).interpolate(x) - uz = Function(self.space).interpolate(z) - datax = ux.dat.data_ro_with_halos[:] - dataz = uz.dat.data_ro_with_halos[:] - node_locations = np.zeros((len(datax), 2)) - node_locations[:, 0] = dataz - node_locations[:, 1] = datax - - return node_locations - - def __func_receiver_locator_3D(self): - """Function that returns a list of tuples and a matrix - the list of tuples has in line n the receiver position - and the position of the nodes in the element that contains - the receiver. - The matrix has the deegres of freedom of the nodes inside - same element as the receiver. - - """ - num_recv = self.num_receivers - - fdrake_cell_node_map = self.space.cell_node_map() - cell_node_map = fdrake_cell_node_map.values_with_halo - (num_cells, nodes_per_cell) = cell_node_map.shape - node_locations = self.__func_node_locations() - self.nodes_per_cell = nodes_per_cell - - cellId_maps = np.zeros((num_recv, 1)) - cellNodeMaps = np.zeros((num_recv, nodes_per_cell)) - cellVertices = [] - - for receiver_id in range(num_recv): - cell_id = self.is_local[receiver_id] - cellVertices.append([]) - if cell_id is not None: - cellId_maps[receiver_id] = cell_id - cellNodeMaps[receiver_id, :] = cell_node_map[cell_id, :] - for vertex_number in range(0, 4): - cellVertices[receiver_id].append([]) - z = node_locations[cell_node_map[cell_id, vertex_number], 0] - x = node_locations[cell_node_map[cell_id, vertex_number], 1] - y = node_locations[cell_node_map[cell_id, vertex_number], 2] - cellVertices[receiver_id][vertex_number] = (z, x, y) - - return cellId_maps, cellVertices, cellNodeMaps - - def __func_node_locations_3D(self): - """Function that returns a list which includes a numpy matrix - where line n has the x and y values of the nth degree of freedom, - and a numpy matrix of the vertex coordinates. - - """ - x, y, z = SpatialCoordinate(self.mesh) - ux = Function(self.space).interpolate(x) - uy = Function(self.space).interpolate(y) - uz = Function(self.space).interpolate(z) - datax = ux.dat.data_ro_with_halos[:] - datay = uy.dat.data_ro_with_halos[:] - dataz = uz.dat.data_ro_with_halos[:] - node_locations = np.zeros((len(datax), 3)) - node_locations[:, 0] = datax - node_locations[:, 1] = datay - node_locations[:, 2] = dataz - return node_locations - - def __func_build_cell_tabulations(self): - if self.dimension == 2 and self.quadrilateral is False: - return self.__func_build_cell_tabulations_2D() - elif self.dimension == 3 and self.quadrilateral is False: - return self.__func_build_cell_tabulations_3D() - elif self.dimension == 2 and self.quadrilateral is True: - return self.__func_build_cell_tabulations_2D_quad() - elif self.dimension == 3 and self.quadrilateral is True: - raise ValueError("3D GLL hexas not yet supported.") - else: - raise ValueError - - def __func_build_cell_tabulations_2D(self): - - element = choosing_element(self.space, self.degree) - - cell_tabulations = np.zeros((self.num_receivers, self.nodes_per_cell)) - - for receiver_id in range(self.num_receivers): - cell_id = self.is_local[receiver_id] - if cell_id is not None: - # getting coordinates to change to reference element - p = self.receiver_locations[receiver_id] - v0 = self.cellVertices[receiver_id][0] - v1 = self.cellVertices[receiver_id][1] - v2 = self.cellVertices[receiver_id][2] - - p_reference = change_to_reference_triangle(p, v0, v1, v2) - initial_tab = element.tabulate(0, [p_reference]) - phi_tab = initial_tab[(0, 0)] - - cell_tabulations[receiver_id, :] = phi_tab.transpose() - - return cell_tabulations - - def __func_build_cell_tabulations_3D(self): - element = choosing_element(self.space, self.degree) - - cell_tabulations = np.zeros((self.num_receivers, self.nodes_per_cell)) - - for receiver_id in range(self.num_receivers): - cell_id = self.is_local[receiver_id] - if cell_id is not None: - # getting coordinates to change to reference element - p = self.receiver_locations[receiver_id] - v0 = self.cellVertices[receiver_id][0] - v1 = self.cellVertices[receiver_id][1] - v2 = self.cellVertices[receiver_id][2] - v3 = self.cellVertices[receiver_id][3] - - p_reference = change_to_reference_tetrahedron(p, v0, v1, v2, v3) - initial_tab = element.tabulate(0, [p_reference]) - phi_tab = initial_tab[(0, 0, 0)] - - cell_tabulations[receiver_id, :] = phi_tab.transpose() - - return cell_tabulations - - def __func_build_cell_tabulations_2D_quad(self): - finatelement = FiniteElement( - "CG", self.mesh.ufl_cell(), degree=self.degree, variant="spectral" - ) - V = FunctionSpace(self.mesh, finatelement) - u = TrialFunction(V) - Q = u.function_space() - element = Q.finat_element.fiat_equivalent - - cell_tabulations = np.zeros((self.num_receivers, self.nodes_per_cell)) - - for receiver_id in range(self.num_receivers): - cell_id = self.is_local[receiver_id] - if cell_id is not None: - # getting coordinates to change to reference element - p = self.receiver_locations[receiver_id] - v0 = self.cellVertices[receiver_id][0] - v1 = self.cellVertices[receiver_id][1] - v2 = self.cellVertices[receiver_id][2] - v3 = self.cellVertices[receiver_id][3] - - p_reference = change_to_reference_quad(p, v0, v1, v2, v3) - initial_tab = element.tabulate(0, [p_reference]) - phi_tab = initial_tab[(0, 0)] - - cell_tabulations[receiver_id, :] = phi_tab.transpose() - - return cell_tabulations - def set_point_cloud(self, comm): # Receivers always parallel to z-axis - rec_pos = self.receiver_locations + rec_pos = self.point_locations # 2D -- if self.dimension == 2: - num_rec = self.num_receivers + num_rec = self.number_of_points δz = np.linspace(rec_pos[0, 0], rec_pos[num_rec - 1, 0], 1) δx = np.linspace(rec_pos[0, 1], rec_pos[num_rec - 1, 1], num_rec) @@ -504,418 +141,9 @@ def set_point_cloud(self, comm): print("This dimension is not accepted.") quit() - point_cloud = VertexOnlyMesh(self.mesh, xs) + point_cloud = VertexOnlyMesh(self.mesh, xs) # noqa: F405 return point_cloud - -# Some helper functions - - -def choosing_element(V, degree): - """Chooses UFL element based on desired function space - and degree of interpolation. - - Parameters - ---------- - V : firedrake.FunctionSpace - Function space to be used. - degree : int - Degree of interpolation. - - Returns - ------- - element : UFL element - UFL element to be used in the interpolation. - """ - cell_geometry = V.mesh().ufl_cell() - if cell_geometry == quadrilateral: - T = UFCQuadrilateral() - raise ValueError("Point interpolation for quads implemented somewhere else.") - - elif cell_geometry == triangle: - T = UFCTriangle() - - elif cell_geometry == tetrahedron: - T = UFCTetrahedron() - - else: - raise ValueError("Unrecognized cell geometry.") - - if V.ufl_element().family() == "Kong-Mulder-Veldhuizen": - element = KMV(T, degree) - elif V.ufl_element().family() == "Lagrange": - element = CG(T, degree) - elif V.ufl_element().family() == "Discontinuous Lagrange": - element = DG(T, degree) - else: - raise ValueError("Function space not yet supported.") - - return element - - -def change_to_reference_triangle(p, a, b, c): - """Changes variables to reference triangle""" - (xa, ya) = a - (xb, yb) = b - (xc, yc) = c - (px, py) = p - - xna = 0.0 - yna = 0.0 - xnb = 1.0 - ynb = 0.0 - xnc = 0.0 - ync = 1.0 - - div = xa * yb - xb * ya - xa * yc + xc * ya + xb * yc - xc * yb - a11 = -(xnb * ya - xnc * ya - xna * yb + xnc * yb + xna * yc - xnb * yc) / div - a12 = (xa * xnb - xa * xnc - xb * xna + xb * xnc + xc * xna - xc * xnb) / div - a13 = ( - xa * xnc * yb - - xb * xnc * ya - - xa * xnb * yc - + xc * xnb * ya - + xb * xna * yc - - xc * xna * yb - ) / div - a21 = -(ya * ynb - ya * ync - yb * yna + yb * ync + yc * yna - yc * ynb) / div - a22 = (xa * ynb - xa * ync - xb * yna + xb * ync + xc * yna - xc * ynb) / div - a23 = ( - xa * yb * ync - - xb * ya * ync - - xa * yc * ynb - + xc * ya * ynb - + xb * yc * yna - - xc * yb * yna - ) / div - - pnx = px * a11 + py * a12 + a13 - pny = px * a21 + py * a22 + a23 - - return (pnx, pny) - - -def change_to_reference_tetrahedron(p, a, b, c, d): - """Changes variables to reference tetrahedron""" - (xa, ya, za) = a - (xb, yb, zb) = b - (xc, yc, zc) = c - (xd, yd, zd) = d - (px, py, pz) = p - - xna = 0.0 - yna = 0.0 - zna = 0.0 - - xnb = 1.0 - ynb = 0.0 - znb = 0.0 - - xnc = 0.0 - ync = 1.0 - znc = 0.0 - - xnd = 0.0 - ynd = 0.0 - znd = 1.0 - - det = ( - xa * yb * zc - - xa * yc * zb - - xb * ya * zc - + xb * yc * za - + xc * ya * zb - - xc * yb * za - - xa * yb * zd - + xa * yd * zb - + xb * ya * zd - - xb * yd * za - - xd * ya * zb - + xd * yb * za - + xa * yc * zd - - xa * yd * zc - - xc * ya * zd - + xc * yd * za - + xd * ya * zc - - xd * yc * za - - xb * yc * zd - + xb * yd * zc - + xc * yb * zd - - xc * yd * zb - - xd * yb * zc - + xd * yc * zb - ) - a11 = ( - (xnc * (ya * zb - yb * za - ya * zd + yd * za + yb * zd - yd * zb)) / det - - (xnd * (ya * zb - yb * za - ya * zc + yc * za + yb * zc - yc * zb)) / det - - (xnb * (ya * zc - yc * za - ya * zd + yd * za + yc * zd - yd * zc)) / det - + (xna * (yb * zc - yc * zb - yb * zd + yd * zb + yc * zd - yd * zc)) / det - ) - a12 = ( - (xnd * (xa * zb - xb * za - xa * zc + xc * za + xb * zc - xc * zb)) / det - - (xnc * (xa * zb - xb * za - xa * zd + xd * za + xb * zd - xd * zb)) / det - + (xnb * (xa * zc - xc * za - xa * zd + xd * za + xc * zd - xd * zc)) / det - - (xna * (xb * zc - xc * zb - xb * zd + xd * zb + xc * zd - xd * zc)) / det - ) - a13 = ( - (xnc * (xa * yb - xb * ya - xa * yd + xd * ya + xb * yd - xd * yb)) / det - - (xnd * (xa * yb - xb * ya - xa * yc + xc * ya + xb * yc - xc * yb)) / det - - (xnb * (xa * yc - xc * ya - xa * yd + xd * ya + xc * yd - xd * yc)) / det - + (xna * (xb * yc - xc * yb - xb * yd + xd * yb + xc * yd - xd * yc)) / det - ) - a14 = ( - ( - xnd - * ( - xa * yb * zc - - xa * yc * zb - - xb * ya * zc - + xb * yc * za - + xc * ya * zb - - xc * yb * za - ) - ) - / det - - ( - xnc - * ( - xa * yb * zd - - xa * yd * zb - - xb * ya * zd - + xb * yd * za - + xd * ya * zb - - xd * yb * za - ) - ) - / det - + ( - xnb - * ( - xa * yc * zd - - xa * yd * zc - - xc * ya * zd - + xc * yd * za - + xd * ya * zc - - xd * yc * za - ) - ) - / det - - ( - xna - * ( - xb * yc * zd - - xb * yd * zc - - xc * yb * zd - + xc * yd * zb - + xd * yb * zc - - xd * yc * zb - ) - ) - / det - ) - a21 = ( - (ync * (ya * zb - yb * za - ya * zd + yd * za + yb * zd - yd * zb)) / det - - (ynd * (ya * zb - yb * za - ya * zc + yc * za + yb * zc - yc * zb)) / det - - (ynb * (ya * zc - yc * za - ya * zd + yd * za + yc * zd - yd * zc)) / det - + (yna * (yb * zc - yc * zb - yb * zd + yd * zb + yc * zd - yd * zc)) / det - ) - a22 = ( - (ynd * (xa * zb - xb * za - xa * zc + xc * za + xb * zc - xc * zb)) / det - - (ync * (xa * zb - xb * za - xa * zd + xd * za + xb * zd - xd * zb)) / det - + (ynb * (xa * zc - xc * za - xa * zd + xd * za + xc * zd - xd * zc)) / det - - (yna * (xb * zc - xc * zb - xb * zd + xd * zb + xc * zd - xd * zc)) / det - ) - a23 = ( - (ync * (xa * yb - xb * ya - xa * yd + xd * ya + xb * yd - xd * yb)) / det - - (ynd * (xa * yb - xb * ya - xa * yc + xc * ya + xb * yc - xc * yb)) / det - - (ynb * (xa * yc - xc * ya - xa * yd + xd * ya + xc * yd - xd * yc)) / det - + (yna * (xb * yc - xc * yb - xb * yd + xd * yb + xc * yd - xd * yc)) / det - ) - a24 = ( - ( - ynd - * ( - xa * yb * zc - - xa * yc * zb - - xb * ya * zc - + xb * yc * za - + xc * ya * zb - - xc * yb * za - ) - ) - / det - - ( - ync - * ( - xa * yb * zd - - xa * yd * zb - - xb * ya * zd - + xb * yd * za - + xd * ya * zb - - xd * yb * za - ) - ) - / det - + ( - ynb - * ( - xa * yc * zd - - xa * yd * zc - - xc * ya * zd - + xc * yd * za - + xd * ya * zc - - xd * yc * za - ) - ) - / det - - ( - yna - * ( - xb * yc * zd - - xb * yd * zc - - xc * yb * zd - + xc * yd * zb - + xd * yb * zc - - xd * yc * zb - ) - ) - / det - ) - a31 = ( - (znc * (ya * zb - yb * za - ya * zd + yd * za + yb * zd - yd * zb)) / det - - (znd * (ya * zb - yb * za - ya * zc + yc * za + yb * zc - yc * zb)) / det - - (znb * (ya * zc - yc * za - ya * zd + yd * za + yc * zd - yd * zc)) / det - + (zna * (yb * zc - yc * zb - yb * zd + yd * zb + yc * zd - yd * zc)) / det - ) - a32 = ( - (znd * (xa * zb - xb * za - xa * zc + xc * za + xb * zc - xc * zb)) / det - - (znc * (xa * zb - xb * za - xa * zd + xd * za + xb * zd - xd * zb)) / det - + (znb * (xa * zc - xc * za - xa * zd + xd * za + xc * zd - xd * zc)) / det - - (zna * (xb * zc - xc * zb - xb * zd + xd * zb + xc * zd - xd * zc)) / det - ) - a33 = ( - (znc * (xa * yb - xb * ya - xa * yd + xd * ya + xb * yd - xd * yb)) / det - - (znd * (xa * yb - xb * ya - xa * yc + xc * ya + xb * yc - xc * yb)) / det - - (znb * (xa * yc - xc * ya - xa * yd + xd * ya + xc * yd - xd * yc)) / det - + (zna * (xb * yc - xc * yb - xb * yd + xd * yb + xc * yd - xd * yc)) / det - ) - a34 = ( - ( - znd - * ( - xa * yb * zc - - xa * yc * zb - - xb * ya * zc - + xb * yc * za - + xc * ya * zb - - xc * yb * za - ) - ) - / det - - ( - znc - * ( - xa * yb * zd - - xa * yd * zb - - xb * ya * zd - + xb * yd * za - + xd * ya * zb - - xd * yb * za - ) - ) - / det - + ( - znb - * ( - xa * yc * zd - - xa * yd * zc - - xc * ya * zd - + xc * yd * za - + xd * ya * zc - - xd * yc * za - ) - ) - / det - - ( - zna - * ( - xb * yc * zd - - xb * yd * zc - - xc * yb * zd - + xc * yd * zb - + xd * yb * zc - - xd * yc * zb - ) - ) - / det - ) - - pnx = px * a11 + py * a12 + pz * a13 + a14 - pny = px * a21 + py * a22 + pz * a23 + a24 - pnz = px * a31 + py * a32 + pz * a33 + a34 - - return (pnx, pny, pnz) - - -def change_to_reference_quad(p, v0, v1, v2, v3): - """Changes varibales to reference quadrilateral""" - (px, py) = p - # Irregular quad - (x0, y0) = v0 - (x1, y1) = v1 - (x2, y2) = v2 - (x3, y3) = v3 - - # Reference quad - # xn0 = 0.0 - # yn0 = 0.0 - # xn1 = 1.0 - # yn1 = 0.0 - # xn2 = 1.0 - # yn2 = 1.0 - # xn3 = 0.0 - # yn3 = 1.0 - - dx1 = x1 - x2 - dx2 = x3 - x2 - dy1 = y1 - y2 - dy2 = y3 - y2 - sumx = x0 - x1 + x2 - x3 - sumy = y0 - y1 + y2 - y3 - - gover = np.array([[sumx, dx2], [sumy, dy2]]) - - g_under = np.array([[dx1, dx2], [dy1, dy2]]) - - gunder = np.linalg.det(g_under) - - hover = np.array([[dx1, sumx], [dy1, sumy]]) - - hunder = gunder - - g = np.linalg.det(gover) / gunder - h = np.linalg.det(hover) / hunder - i = 1.0 - - a = x1 - x0 + g * x1 - b = x3 - x0 + h * x3 - c = x0 - d = y1 - y0 + g * y1 - e = y3 - y0 + h * y3 - f = y0 - - A = e * i - f * h - B = c * h - b * i - C = b * f - c * e - D = f * g - d * i - E = a * i - c * g - F = c * d - a * f - G = d * h - e * g - H = b * g - a * h - I = a * e - b * d - - pnx = (A * px + B * py + C) / (G * px + H * py + I) - pny = (D * px + E * py + F) / (G * px + H * py + I) - - return (pnx, pny) + def new_at(self, udat, receiver_id): + return super().new_at(udat, receiver_id) diff --git a/spyro/receivers/__init__.py b/spyro/receivers/__init__.py index 4cb1b004..8b137891 100644 --- a/spyro/receivers/__init__.py +++ b/spyro/receivers/__init__.py @@ -1,3 +1 @@ -from . import Receivers -__all__ = ["Receivers"] diff --git a/spyro/receivers/changing_coordinates.py b/spyro/receivers/changing_coordinates.py new file mode 100644 index 00000000..f4861e84 --- /dev/null +++ b/spyro/receivers/changing_coordinates.py @@ -0,0 +1,707 @@ +import numpy as np + + +def change_to_reference_triangle(p, cell_vertices): + """Changes variables to reference triangle""" + (xa, ya) = cell_vertices[0] + (xb, yb) = cell_vertices[1] + (xc, yc) = cell_vertices[2] + (px, py) = p + + xna = 0.0 + yna = 0.0 + xnb = 1.0 + ynb = 0.0 + xnc = 0.0 + ync = 1.0 + + div = xa * yb - xb * ya - xa * yc + xc * ya + xb * yc - xc * yb + a11 = ( + -(xnb * ya - xnc * ya - xna * yb + xnc * yb + xna * yc - xnb * yc) / div + ) + a12 = ( + xa * xnb - xa * xnc - xb * xna + xb * xnc + xc * xna - xc * xnb + ) / div + a13 = ( + xa * xnc * yb + - xb * xnc * ya + - xa * xnb * yc + + xc * xnb * ya + + xb * xna * yc + - xc * xna * yb + ) / div + a21 = ( + -(ya * ynb - ya * ync - yb * yna + yb * ync + yc * yna - yc * ynb) / div + ) + a22 = ( + xa * ynb - xa * ync - xb * yna + xb * ync + xc * yna - xc * ynb + ) / div + a23 = ( + xa * yb * ync + - xb * ya * ync + - xa * yc * ynb + + xc * ya * ynb + + xb * yc * yna + - xc * yb * yna + ) / div + + pnx = px * a11 + py * a12 + a13 + pny = px * a21 + py * a22 + a23 + + return (pnx, pny) + + +def change_to_reference_tetrahedron( + p, cell_vertices, reference_coordinates=None +): + """Changes variables to reference tetrahedron""" + (xa, ya, za) = cell_vertices[0] + (xb, yb, zb) = cell_vertices[1] + (xc, yc, zc) = cell_vertices[2] + (xd, yd, zd) = cell_vertices[3] + (px, py, pz) = p + + if reference_coordinates is None: + ra = (0.0, 0.0, 0.0) + rb = (1.0, 0.0, 0.0) + rc = (0.0, 1.0, 0.0) + rd = (0.0, 0.0, 1.0) + reference_coordinates = [ + ra, + rb, + rc, + rd, + ] + + xna, yna, zna = reference_coordinates[0] + xnb, ynb, znb = reference_coordinates[1] + xnc, ync, znc = reference_coordinates[2] + xnd, ynd, znd = reference_coordinates[3] + + det = ( + xa * yb * zc + - xa * yc * zb + - xb * ya * zc + + xb * yc * za + + xc * ya * zb + - xc * yb * za + - xa * yb * zd + + xa * yd * zb + + xb * ya * zd + - xb * yd * za + - xd * ya * zb + + xd * yb * za + + xa * yc * zd + - xa * yd * zc + - xc * ya * zd + + xc * yd * za + + xd * ya * zc + - xd * yc * za + - xb * yc * zd + + xb * yd * zc + + xc * yb * zd + - xc * yd * zb + - xd * yb * zc + + xd * yc * zb + ) + a11 = ( + (xnc * (ya * zb - yb * za - ya * zd + yd * za + yb * zd - yd * zb)) + / det + - (xnd * (ya * zb - yb * za - ya * zc + yc * za + yb * zc - yc * zb)) + / det + - (xnb * (ya * zc - yc * za - ya * zd + yd * za + yc * zd - yd * zc)) + / det + + (xna * (yb * zc - yc * zb - yb * zd + yd * zb + yc * zd - yd * zc)) + / det + ) + a12 = ( + (xnd * (xa * zb - xb * za - xa * zc + xc * za + xb * zc - xc * zb)) + / det + - (xnc * (xa * zb - xb * za - xa * zd + xd * za + xb * zd - xd * zb)) + / det + + (xnb * (xa * zc - xc * za - xa * zd + xd * za + xc * zd - xd * zc)) + / det + - (xna * (xb * zc - xc * zb - xb * zd + xd * zb + xc * zd - xd * zc)) + / det + ) + a13 = ( + (xnc * (xa * yb - xb * ya - xa * yd + xd * ya + xb * yd - xd * yb)) + / det + - (xnd * (xa * yb - xb * ya - xa * yc + xc * ya + xb * yc - xc * yb)) + / det + - (xnb * (xa * yc - xc * ya - xa * yd + xd * ya + xc * yd - xd * yc)) + / det + + (xna * (xb * yc - xc * yb - xb * yd + xd * yb + xc * yd - xd * yc)) + / det + ) + a14 = ( + ( + xnd + * ( + xa * yb * zc + - xa * yc * zb + - xb * ya * zc + + xb * yc * za + + xc * ya * zb + - xc * yb * za + ) + ) + / det + - ( + xnc + * ( + xa * yb * zd + - xa * yd * zb + - xb * ya * zd + + xb * yd * za + + xd * ya * zb + - xd * yb * za + ) + ) + / det + + ( + xnb + * ( + xa * yc * zd + - xa * yd * zc + - xc * ya * zd + + xc * yd * za + + xd * ya * zc + - xd * yc * za + ) + ) + / det + - ( + xna + * ( + xb * yc * zd + - xb * yd * zc + - xc * yb * zd + + xc * yd * zb + + xd * yb * zc + - xd * yc * zb + ) + ) + / det + ) + a21 = ( + (ync * (ya * zb - yb * za - ya * zd + yd * za + yb * zd - yd * zb)) + / det + - (ynd * (ya * zb - yb * za - ya * zc + yc * za + yb * zc - yc * zb)) + / det + - (ynb * (ya * zc - yc * za - ya * zd + yd * za + yc * zd - yd * zc)) + / det + + (yna * (yb * zc - yc * zb - yb * zd + yd * zb + yc * zd - yd * zc)) + / det + ) + a22 = ( + (ynd * (xa * zb - xb * za - xa * zc + xc * za + xb * zc - xc * zb)) + / det + - (ync * (xa * zb - xb * za - xa * zd + xd * za + xb * zd - xd * zb)) + / det + + (ynb * (xa * zc - xc * za - xa * zd + xd * za + xc * zd - xd * zc)) + / det + - (yna * (xb * zc - xc * zb - xb * zd + xd * zb + xc * zd - xd * zc)) + / det + ) + a23 = ( + (ync * (xa * yb - xb * ya - xa * yd + xd * ya + xb * yd - xd * yb)) + / det + - (ynd * (xa * yb - xb * ya - xa * yc + xc * ya + xb * yc - xc * yb)) + / det + - (ynb * (xa * yc - xc * ya - xa * yd + xd * ya + xc * yd - xd * yc)) + / det + + (yna * (xb * yc - xc * yb - xb * yd + xd * yb + xc * yd - xd * yc)) + / det + ) + a24 = ( + ( + ynd + * ( + xa * yb * zc + - xa * yc * zb + - xb * ya * zc + + xb * yc * za + + xc * ya * zb + - xc * yb * za + ) + ) + / det + - ( + ync + * ( + xa * yb * zd + - xa * yd * zb + - xb * ya * zd + + xb * yd * za + + xd * ya * zb + - xd * yb * za + ) + ) + / det + + ( + ynb + * ( + xa * yc * zd + - xa * yd * zc + - xc * ya * zd + + xc * yd * za + + xd * ya * zc + - xd * yc * za + ) + ) + / det + - ( + yna + * ( + xb * yc * zd + - xb * yd * zc + - xc * yb * zd + + xc * yd * zb + + xd * yb * zc + - xd * yc * zb + ) + ) + / det + ) + a31 = ( + (znc * (ya * zb - yb * za - ya * zd + yd * za + yb * zd - yd * zb)) + / det + - (znd * (ya * zb - yb * za - ya * zc + yc * za + yb * zc - yc * zb)) + / det + - (znb * (ya * zc - yc * za - ya * zd + yd * za + yc * zd - yd * zc)) + / det + + (zna * (yb * zc - yc * zb - yb * zd + yd * zb + yc * zd - yd * zc)) + / det + ) + a32 = ( + (znd * (xa * zb - xb * za - xa * zc + xc * za + xb * zc - xc * zb)) + / det + - (znc * (xa * zb - xb * za - xa * zd + xd * za + xb * zd - xd * zb)) + / det + + (znb * (xa * zc - xc * za - xa * zd + xd * za + xc * zd - xd * zc)) + / det + - (zna * (xb * zc - xc * zb - xb * zd + xd * zb + xc * zd - xd * zc)) + / det + ) + a33 = ( + (znc * (xa * yb - xb * ya - xa * yd + xd * ya + xb * yd - xd * yb)) + / det + - (znd * (xa * yb - xb * ya - xa * yc + xc * ya + xb * yc - xc * yb)) + / det + - (znb * (xa * yc - xc * ya - xa * yd + xd * ya + xc * yd - xd * yc)) + / det + + (zna * (xb * yc - xc * yb - xb * yd + xd * yb + xc * yd - xd * yc)) + / det + ) + a34 = ( + ( + znd + * ( + xa * yb * zc + - xa * yc * zb + - xb * ya * zc + + xb * yc * za + + xc * ya * zb + - xc * yb * za + ) + ) + / det + - ( + znc + * ( + xa * yb * zd + - xa * yd * zb + - xb * ya * zd + + xb * yd * za + + xd * ya * zb + - xd * yb * za + ) + ) + / det + + ( + znb + * ( + xa * yc * zd + - xa * yd * zc + - xc * ya * zd + + xc * yd * za + + xd * ya * zc + - xd * yc * za + ) + ) + / det + - ( + zna + * ( + xb * yc * zd + - xb * yd * zc + - xc * yb * zd + + xc * yd * zb + + xd * yb * zc + - xd * yc * zb + ) + ) + / det + ) + + pnx = px * a11 + py * a12 + pz * a13 + a14 + pny = px * a21 + py * a22 + pz * a23 + a24 + pnz = px * a31 + py * a32 + pz * a33 + a34 + + return (pnx, pny, pnz) + + +def change_to_reference_quad(p, cell_vertices): + """Changes varibales to reference quadrilateral""" + (px, py) = p + # Irregular quad + (x0, y0) = cell_vertices[0] + (x1, y1) = cell_vertices[1] + (x2, y2) = cell_vertices[2] + (x3, y3) = cell_vertices[3] + + # Reference quad + # xn0 = 0.0 + # yn0 = 0.0 + # xn1 = 1.0 + # yn1 = 0.0 + # xn2 = 1.0 + # yn2 = 1.0 + # xn3 = 0.0 + # yn3 = 1.0 + + dx1 = x1 - x2 + dx2 = x3 - x2 + dy1 = y1 - y2 + dy2 = y3 - y2 + sumx = x0 - x1 + x2 - x3 + sumy = y0 - y1 + y2 - y3 + + gover = np.array([[sumx, dx2], [sumy, dy2]]) + + g_under = np.array([[dx1, dx2], [dy1, dy2]]) + + gunder = np.linalg.det(g_under) + + hover = np.array([[dx1, sumx], [dy1, sumy]]) + + hunder = gunder + + g = np.linalg.det(gover) / gunder + h = np.linalg.det(hover) / hunder + i = 1.0 + + a = x1 - x0 + g * x1 + b = x3 - x0 + h * x3 + c = x0 + d = y1 - y0 + g * y1 + e = y3 - y0 + h * y3 + f = y0 + + A = e * i - f * h + B = c * h - b * i + C = b * f - c * e + D = f * g - d * i + E = a * i - c * g + F = c * d - a * f + G = d * h - e * g + H = b * g - a * h + Ij = a * e - b * d + + pnx = (A * px + B * py + C) / (G * px + H * py + Ij) + pny = (D * px + E * py + F) / (G * px + H * py + Ij) + + return (pnx, pny) + + +def change_to_reference_hexa(p, cell_vertices): + a = cell_vertices[0] + b = cell_vertices[1] + c = cell_vertices[2] + d = cell_vertices[4] + + ra = (0.0, 0.0, 0.0) + rb = (0.0, 0.0, 1.0) + rc = (0.0, 1.0, 0.0) + rd = (1.0, 0.0, 0.0) + + reference_coordinates = [ra, rb, rc, rd] + tet_cell_vertices = [a, b, c, d] + + return change_to_reference_tetrahedron( + p, tet_cell_vertices, reference_coordinates=reference_coordinates + ) + + # det = ( + # xa * yb * zc + # - xa * yc * zb + # - xb * ya * zc + # + xb * yc * za + # + xc * ya * zb + # - xc * yb * za + # - xa * yb * zd + # + xa * yd * zb + # + xb * ya * zd + # - xb * yd * za + # - xd * ya * zb + # + xd * yb * za + # + xa * yc * zd + # - xa * yd * zc + # - xc * ya * zd + # + xc * yd * za + # + xd * ya * zc + # - xd * yc * za + # - xb * yc * zd + # + xb * yd * zc + # + xc * yb * zd + # - xc * yd * zb + # - xd * yb * zc + # + xd * yc * zb + # ) + # a11 = ( + # (xnc * (ya * zb - yb * za - ya * zd + yd * za + yb * zd - yd * zb)) + # / det + # - (xnd * (ya * zb - yb * za - ya * zc + yc * za + yb * zc - yc * zb)) + # / det + # - (xnb * (ya * zc - yc * za - ya * zd + yd * za + yc * zd - yd * zc)) + # / det + # + (xna * (yb * zc - yc * zb - yb * zd + yd * zb + yc * zd - yd * zc)) + # / det + # ) + # a12 = ( + # (xnd * (xa * zb - xb * za - xa * zc + xc * za + xb * zc - xc * zb)) + # / det + # - (xnc * (xa * zb - xb * za - xa * zd + xd * za + xb * zd - xd * zb)) + # / det + # + (xnb * (xa * zc - xc * za - xa * zd + xd * za + xc * zd - xd * zc)) + # / det + # - (xna * (xb * zc - xc * zb - xb * zd + xd * zb + xc * zd - xd * zc)) + # / det + # ) + # a13 = ( + # (xnc * (xa * yb - xb * ya - xa * yd + xd * ya + xb * yd - xd * yb)) + # / det + # - (xnd * (xa * yb - xb * ya - xa * yc + xc * ya + xb * yc - xc * yb)) + # / det + # - (xnb * (xa * yc - xc * ya - xa * yd + xd * ya + xc * yd - xd * yc)) + # / det + # + (xna * (xb * yc - xc * yb - xb * yd + xd * yb + xc * yd - xd * yc)) + # / det + # ) + # a14 = ( + # ( + # xnd + # * ( + # xa * yb * zc + # - xa * yc * zb + # - xb * ya * zc + # + xb * yc * za + # + xc * ya * zb + # - xc * yb * za + # ) + # ) + # / det + # - ( + # xnc + # * ( + # xa * yb * zd + # - xa * yd * zb + # - xb * ya * zd + # + xb * yd * za + # + xd * ya * zb + # - xd * yb * za + # ) + # ) + # / det + # + ( + # xnb + # * ( + # xa * yc * zd + # - xa * yd * zc + # - xc * ya * zd + # + xc * yd * za + # + xd * ya * zc + # - xd * yc * za + # ) + # ) + # / det + # - ( + # xna + # * ( + # xb * yc * zd + # - xb * yd * zc + # - xc * yb * zd + # + xc * yd * zb + # + xd * yb * zc + # - xd * yc * zb + # ) + # ) + # / det + # ) + # a21 = ( + # (ync * (ya * zb - yb * za - ya * zd + yd * za + yb * zd - yd * zb)) + # / det + # - (ynd * (ya * zb - yb * za - ya * zc + yc * za + yb * zc - yc * zb)) + # / det + # - (ynb * (ya * zc - yc * za - ya * zd + yd * za + yc * zd - yd * zc)) + # / det + # + (yna * (yb * zc - yc * zb - yb * zd + yd * zb + yc * zd - yd * zc)) + # / det + # ) + # a22 = ( + # (ynd * (xa * zb - xb * za - xa * zc + xc * za + xb * zc - xc * zb)) + # / det + # - (ync * (xa * zb - xb * za - xa * zd + xd * za + xb * zd - xd * zb)) + # / det + # + (ynb * (xa * zc - xc * za - xa * zd + xd * za + xc * zd - xd * zc)) + # / det + # - (yna * (xb * zc - xc * zb - xb * zd + xd * zb + xc * zd - xd * zc)) + # / det + # ) + # a23 = ( + # (ync * (xa * yb - xb * ya - xa * yd + xd * ya + xb * yd - xd * yb)) + # / det + # - (ynd * (xa * yb - xb * ya - xa * yc + xc * ya + xb * yc - xc * yb)) + # / det + # - (ynb * (xa * yc - xc * ya - xa * yd + xd * ya + xc * yd - xd * yc)) + # / det + # + (yna * (xb * yc - xc * yb - xb * yd + xd * yb + xc * yd - xd * yc)) + # / det + # ) + # a24 = ( + # ( + # ynd + # * ( + # xa * yb * zc + # - xa * yc * zb + # - xb * ya * zc + # + xb * yc * za + # + xc * ya * zb + # - xc * yb * za + # ) + # ) + # / det + # - ( + # ync + # * ( + # xa * yb * zd + # - xa * yd * zb + # - xb * ya * zd + # + xb * yd * za + # + xd * ya * zb + # - xd * yb * za + # ) + # ) + # / det + # + ( + # ynb + # * ( + # xa * yc * zd + # - xa * yd * zc + # - xc * ya * zd + # + xc * yd * za + # + xd * ya * zc + # - xd * yc * za + # ) + # ) + # / det + # - ( + # yna + # * ( + # xb * yc * zd + # - xb * yd * zc + # - xc * yb * zd + # + xc * yd * zb + # + xd * yb * zc + # - xd * yc * zb + # ) + # ) + # / det + # ) + # a31 = ( + # (znc * (ya * zb - yb * za - ya * zd + yd * za + yb * zd - yd * zb)) + # / det + # - (znd * (ya * zb - yb * za - ya * zc + yc * za + yb * zc - yc * zb)) + # / det + # - (znb * (ya * zc - yc * za - ya * zd + yd * za + yc * zd - yd * zc)) + # / det + # + (zna * (yb * zc - yc * zb - yb * zd + yd * zb + yc * zd - yd * zc)) + # / det + # ) + # a32 = ( + # (znd * (xa * zb - xb * za - xa * zc + xc * za + xb * zc - xc * zb)) + # / det + # - (znc * (xa * zb - xb * za - xa * zd + xd * za + xb * zd - xd * zb)) + # / det + # + (znb * (xa * zc - xc * za - xa * zd + xd * za + xc * zd - xd * zc)) + # / det + # - (zna * (xb * zc - xc * zb - xb * zd + xd * zb + xc * zd - xd * zc)) + # / det + # ) + # a33 = ( + # (znc * (xa * yb - xb * ya - xa * yd + xd * ya + xb * yd - xd * yb)) + # / det + # - (znd * (xa * yb - xb * ya - xa * yc + xc * ya + xb * yc - xc * yb)) + # / det + # - (znb * (xa * yc - xc * ya - xa * yd + xd * ya + xc * yd - xd * yc)) + # / det + # + (zna * (xb * yc - xc * yb - xb * yd + xd * yb + xc * yd - xd * yc)) + # / det + # ) + # a34 = ( + # ( + # znd + # * ( + # xa * yb * zc + # - xa * yc * zb + # - xb * ya * zc + # + xb * yc * za + # + xc * ya * zb + # - xc * yb * za + # ) + # ) + # / det + # - ( + # znc + # * ( + # xa * yb * zd + # - xa * yd * zb + # - xb * ya * zd + # + xb * yd * za + # + xd * ya * zb + # - xd * yb * za + # ) + # ) + # / det + # + ( + # znb + # * ( + # xa * yc * zd + # - xa * yd * zc + # - xc * ya * zd + # + xc * yd * za + # + xd * ya * zc + # - xd * yc * za + # ) + # ) + # / det + # - ( + # zna + # * ( + # xb * yc * zd + # - xb * yd * zc + # - xc * yb * zd + # + xc * yd * zb + # + xd * yb * zc + # - xd * yc * zb + # ) + # ) + # / det + # ) + + # pnx = px * a11 + py * a12 + pz * a13 + a14 + # pny = px * a21 + py * a22 + pz * a23 + a24 + # pnz = px * a31 + py * a32 + pz * a33 + a34 + + return (pnx, pny, pnz) diff --git a/spyro/receivers/dirac_delta_projector.py b/spyro/receivers/dirac_delta_projector.py new file mode 100644 index 00000000..5b4327bc --- /dev/null +++ b/spyro/receivers/dirac_delta_projector.py @@ -0,0 +1,518 @@ +from firedrake import * # noqa: F403 +from FIAT.reference_element import ( + UFCTriangle, + UFCTetrahedron, + UFCQuadrilateral, +) +from FIAT.reference_element import UFCInterval +from FIAT import GaussLobattoLegendre as GLLelement +from FIAT.tensor_product import TensorProductElement +from FIAT.kong_mulder_veldhuizen import KongMulderVeldhuizen as KMV +from FIAT.lagrange import Lagrange as CG +from FIAT.discontinuous_lagrange import DiscontinuousLagrange as DG +from .changing_coordinates import ( + change_to_reference_triangle, + change_to_reference_tetrahedron, + change_to_reference_quad, + change_to_reference_hexa, +) + +import numpy as np + + +class Delta_projector: + def __init__(self, wave_object): + my_ensemble = wave_object.comm + if wave_object.automatic_adjoint: + self.automatic_adjoint = True + else: + self.automatic_adjoint = False + + self.mesh = wave_object.mesh + self.space = wave_object.function_space.sub(0) + self.my_ensemble = my_ensemble + self.dimension = wave_object.dimension + self.degree = wave_object.degree + + self.point_locations = None + self.number_of_points = None + + self.cellIDs = None + self.cellVertices = None + self.cell_tabulations = None + self.cellNodeMaps = None + self.nodes_per_cell = None + if wave_object.cell_type == "quadrilateral": + self.quadrilateral = True + else: + self.quadrilateral = False + self.is_local = None + + def build_maps(self): + """Calculates and stores tabulations for interpolation + + Is always automatticaly called when initializing the class, + therefore should only be called again if a mesh related attribute + changes. + + Returns + ------- + cellIDs: list + List of cell IDs for each receiver + cellVertices: list + List of vertices for each receiver + cellNodeMaps: list + List of node maps for each receiver + cell_tabulations: list + List of tabulations for each receiver + """ + + for rid in range(self.number_of_points): + tolerance = 1e-6 + if self.dimension == 2: + receiver_z, receiver_x = self.point_locations[rid] + cell_id = self.mesh.locate_cell( + [receiver_z, receiver_x], tolerance=tolerance + ) + elif self.dimension == 3: + receiver_z, receiver_x, receiver_y = self.point_locations[rid] + cell_id = self.mesh.locate_cell( + [receiver_z, receiver_x, receiver_y], tolerance=tolerance + ) + self.is_local[rid] = cell_id + + ( + self.cellIDs, + self.cellVertices, + self.cellNodeMaps, + ) = self.__point_locator() + self.cell_tabulations = self.__func_build_cell_tabulations() + + self.number_of_points = len(self.point_locations) + + def interpolate(self, field): + """Interpolate the solution to the receiver coordinates for + one simulation timestep. + + Parameters + ---------- + field: array-like + An array of the solution at a given timestep at all nodes + + Returns + ------- + solution_at_receivers: list + Solution interpolated to the list of receiver coordinates + for the given timestep. + """ + return [self.new_at(field, rn) for rn in range(self.number_of_points)] + + def new_at(self, udat, receiver_id): + """Function that evaluates the receiver value given its id. + For 2D simplices only. + Parameters + ---------- + udat: array-like + An array of the solution at a given timestep at all nodes + receiver_id: a list of integers + A list of receiver ids, ranging from 0 to total receivers + minus one. + + Returns + ------- + at: Function value at given receiver + """ + + if self.is_local is not None: + # Getting relevant receiver points + u = udat[np.int_(self.cellNodeMaps[receiver_id, :])] + else: + return udat[0] # junk receiver isn't local + + phis = self.cell_tabulations[receiver_id, :] + + at = phis.T @ u + + return at + + def __func_build_cell_tabulations(self): + if self.dimension == 2 and self.quadrilateral is False: + return self.__func_build_cell_tabulations_2D() + elif self.dimension == 3 and self.quadrilateral is False: + return self.__func_build_cell_tabulations_3D() + elif self.dimension == 2 and self.quadrilateral is True: + return self.__func_build_cell_tabulations_2D_quad() + elif self.dimension == 3 and self.quadrilateral is True: + return self.__func_build_cell_tabulations_3D_quad() + else: + raise ValueError + + def __func_build_cell_tabulations_2D(self): + element = choosing_element(self.space, self.degree) + + cell_tabulations = np.zeros( + (self.number_of_points, self.nodes_per_cell) + ) + + for receiver_id in range(self.number_of_points): + cell_id = self.is_local[receiver_id] + if cell_id is not None: + # getting coordinates to change to reference element + p = self.point_locations[receiver_id] + v0 = self.cellVertices[receiver_id][0] + v1 = self.cellVertices[receiver_id][1] + v2 = self.cellVertices[receiver_id][2] + cell_vertices = [v0, v1, v2] + + p_reference = change_to_reference_triangle(p, cell_vertices) + initial_tab = element.tabulate(0, [p_reference]) + phi_tab = initial_tab[(0, 0)] + + cell_tabulations[receiver_id, :] = phi_tab.transpose() + + return cell_tabulations + + def __func_build_cell_tabulations_3D(self): + element = choosing_element(self.space, self.degree) + + cell_tabulations = np.zeros( + (self.number_of_points, self.nodes_per_cell) + ) + + for receiver_id in range(self.number_of_points): + cell_id = self.is_local[receiver_id] + if cell_id is not None: + # getting coordinates to change to reference element + p = self.point_locations[receiver_id] + v0 = self.cellVertices[receiver_id][0] + v1 = self.cellVertices[receiver_id][1] + v2 = self.cellVertices[receiver_id][2] + v3 = self.cellVertices[receiver_id][3] + cell_vertices = [v0, v1, v2, v3] + + p_reference = change_to_reference_tetrahedron(p, cell_vertices) + initial_tab = element.tabulate(0, [p_reference]) + phi_tab = initial_tab[(0, 0, 0)] + + cell_tabulations[receiver_id, :] = phi_tab.transpose() + + return cell_tabulations + + def __func_build_cell_tabulations_2D_quad(self): + # finatelement = FiniteElement('CG', self.mesh.ufl_cell(), + # degree=self.degree, variant='spectral') + V = self.space + + element = V.finat_element.fiat_equivalent + + cell_tabulations = np.zeros( + (self.number_of_points, self.nodes_per_cell) + ) + + for receiver_id in range(self.number_of_points): + cell_id = self.is_local[receiver_id] + if cell_id is not None: + # getting coordinates to change to reference element + p = self.point_locations[receiver_id] + v0 = self.cellVertices[receiver_id][0] + v1 = self.cellVertices[receiver_id][1] + v2 = self.cellVertices[receiver_id][2] + v3 = self.cellVertices[receiver_id][3] + cell_vertices = [v0, v1, v2, v3] + + p_reference = change_to_reference_quad(p, cell_vertices) + initial_tab = element.tabulate(0, [p_reference]) + phi_tab = initial_tab[(0, 0)] + + cell_tabulations[receiver_id, :] = phi_tab.transpose() + + return cell_tabulations + + def __func_build_cell_tabulations_3D_quad(self): + Inter = UFCInterval() + An = GLLelement(Inter, self.degree) + Bn = GLLelement(Inter, self.degree) + Cn = GLLelement(Inter, self.degree) + Dn = TensorProductElement(An, Bn) + element = TensorProductElement(Dn, Cn) + + cell_tabulations = np.zeros( + (self.number_of_points, self.nodes_per_cell) + ) + + for receiver_id in range(self.number_of_points): + cell_id = self.is_local[receiver_id] + if cell_id is not None: + # getting coordinates to change to reference element + p = self.point_locations[receiver_id] + v0 = self.cellVertices[receiver_id][0] + v1 = self.cellVertices[receiver_id][1] + v2 = self.cellVertices[receiver_id][2] + v3 = self.cellVertices[receiver_id][3] + v4 = self.cellVertices[receiver_id][4] + v5 = self.cellVertices[receiver_id][5] + v6 = self.cellVertices[receiver_id][6] + v7 = self.cellVertices[receiver_id][7] + cell_vertices = [v0, v1, v2, v3, v4, v5, v6, v7] + + p_reference = change_to_reference_hexa(p, cell_vertices) + initial_tab = element.tabulate(0, [p_reference]) + phi_tab = initial_tab[(0, 0, 0)] + + cell_tabulations[receiver_id, :] = phi_tab.transpose() + + return cell_tabulations + + def __build_local_nodes(self): + """Builds local element nodes, locations and I,J,K numbering""" + if self.dimension == 2: + return self.__build_local_nodes_2D() + elif self.dimension == 3: + return self.__build_local_nodes_3D() + else: + raise ValueError + + def __func_node_locations(self): + """Function that returns a list which includes a numpy matrix + where line n has the x and y values of the nth degree of freedom, + and a numpy matrix of the vertex coordinates. + """ + if self.dimension == 2: + return self.__func_node_locations_2D() + elif self.dimension == 3: + return self.__func_node_locations_3D() + else: + raise ValueError + + def __func_node_locations_2D(self): + """Function that returns a list which includes a numpy matrix + where line n has the x and y values of the nth degree of freedom, + and a numpy matrix of the vertex coordinates. + """ + z, x = SpatialCoordinate(self.mesh) # noqa: F405 + ux = Function(self.space).interpolate(x) # noqa: F405 + uz = Function(self.space).interpolate(z) # noqa: F405 + datax = ux.dat.data_ro_with_halos[:] + dataz = uz.dat.data_ro_with_halos[:] + node_locations = np.zeros((len(datax), 2)) + node_locations[:, 0] = dataz + node_locations[:, 1] = datax + + return node_locations + + def __func_node_locations_3D(self): + """Function that returns a list which includes a numpy matrix + where line n has the x and y values of the nth degree of freedom, + and a numpy matrix of the vertex coordinates. + + """ + x, y, z = SpatialCoordinate(self.mesh) # noqa: F405 + ux = Function(self.space).interpolate(x) # noqa: F405 + uy = Function(self.space).interpolate(y) # noqa: F405 + uz = Function(self.space).interpolate(z) # noqa: F405 + datax = ux.dat.data_ro_with_halos[:] + datay = uy.dat.data_ro_with_halos[:] + dataz = uz.dat.data_ro_with_halos[:] + node_locations = np.zeros((len(datax), 3)) + node_locations[:, 0] = datax + node_locations[:, 1] = datay + node_locations[:, 2] = dataz + return node_locations + + def __point_locator(self): + """Function that returns a list of tuples and a matrix + the list of tuples has in line n the receiver position + and the position of the nodes in the element that contains + the receiver. + The matrix has the deegres of freedom of the nodes inside + same element as the receiver. + """ + if self.dimension == 2: + return self.__point_locator_2D() + elif self.dimension == 3: + return self.__point_locator_3D() + else: + raise ValueError + + def __point_locator_2D(self): + """Function that returns a list of tuples and a matrix + the list of tuples has in line n the receiver position + and the position of the nodes in the element that contains + the receiver. + The matrix has the deegres of freedom of the nodes inside + same element as the receiver. + """ + num_recv = self.number_of_points + + fdrake_cell_node_map = self.space.cell_node_map() + cell_node_map = fdrake_cell_node_map.values_with_halo + (num_cells, nodes_per_cell) = cell_node_map.shape + node_locations = self.__func_node_locations() + self.nodes_per_cell = nodes_per_cell + + cellId_maps = np.zeros((num_recv, 1)) + cellNodeMaps = np.zeros((num_recv, nodes_per_cell)) + cellVertices = [] + + if self.quadrilateral is True: + end_vertex_id = 4 + degree = self.degree + cell_ends = [ + 0, + (degree + 1) * (degree + 1) - degree - 1, + (degree + 1) * (degree + 1) - 1, + degree, + ] + else: + end_vertex_id = 3 + cell_ends = [0, 1, 2] + + for receiver_id in range(num_recv): + cell_id = self.is_local[receiver_id] + + cellVertices.append([]) + + if cell_id is not None: + cellId_maps[receiver_id] = cell_id + cellNodeMaps[receiver_id, :] = cell_node_map[cell_id, :] + for vertex_number in range(0, end_vertex_id): + cellVertices[receiver_id].append([]) + z = node_locations[ + cell_node_map[cell_id, cell_ends[vertex_number]], 0 + ] + x = node_locations[ + cell_node_map[cell_id, cell_ends[vertex_number]], 1 + ] + cellVertices[receiver_id][vertex_number] = (z, x) + + return cellId_maps, cellVertices, cellNodeMaps + + def __point_locator_3D(self): + """Function that returns a list of tuples and a matrix + the list of tuples has in line n the receiver position + and the position of the nodes in the element that contains + the receiver. + The matrix has the deegres of freedom of the nodes inside + same element as the receiver. + + """ + print("start func_receiver_locator", flush=True) + num_recv = self.number_of_points + + fdrake_cell_node_map = self.space.cell_node_map() + cell_node_map = fdrake_cell_node_map.values_with_halo + if self.quadrilateral is True: + cell_node_map = get_hexa_real_cell_node_map(self.space, self.mesh) + (num_cells, nodes_per_cell) = cell_node_map.shape + node_locations = self.__func_node_locations() + self.nodes_per_cell = nodes_per_cell + + cellId_maps = np.zeros((num_recv, 1)) + cellNodeMaps = np.zeros((num_recv, nodes_per_cell)) + cellVertices = [] + + if self.quadrilateral is True: + end_vertex = 8 + p = self.degree + vertex_ids = [ + 0, + p, + (p + 1) * p, + (p + 1) * p + p, + (p + 1) * (p + 1) * p, + (p + 1) * (p + 1) * p + p, + (p + 1) * (p + 1) * p + (p + 1) * p, + (p + 1) ** 3 - 1, + ] + else: + end_vertex = 4 + vertex_ids = [0, 1, 2, 3] + + for receiver_id in range(num_recv): + cell_id = self.is_local[receiver_id] + cellVertices.append([]) + if cell_id is not None: + cellId_maps[receiver_id] = cell_id + cellNodeMaps[receiver_id, :] = cell_node_map[cell_id, :] + for vertex_number in range(0, end_vertex): + vertex_id = vertex_ids[vertex_number] + cellVertices[receiver_id].append([]) + z = node_locations[cell_node_map[cell_id, vertex_id], 0] + x = node_locations[cell_node_map[cell_id, vertex_id], 1] + y = node_locations[cell_node_map[cell_id, vertex_id], 2] + cellVertices[receiver_id][vertex_number] = (z, x, y) + + print("end func_receiver_locator", flush=True) + return cellId_maps, cellVertices, cellNodeMaps + + +def choosing_geometry(cell_geometry): + if cell_geometry == quadrilateral: # noqa: F405 + T = UFCQuadrilateral() + raise ValueError( + "Point interpolation for quads implemented somewhere else." + ) + + elif cell_geometry == triangle: # noqa: F405 + T = UFCTriangle() + + elif cell_geometry == tetrahedron: # noqa: F405 + T = UFCTetrahedron() + + else: + raise ValueError("Unrecognized cell geometry.") + + return T + + +def choosing_element(V, degree): + """Chooses UFL element based on desired function space + and degree of interpolation. + + Parameters + ---------- + V : firedrake.FunctionSpace + Function space to be used. + degree : int + Degree of interpolation. + + Returns + ------- + element : UFL element + UFL element to be used in the interpolation. + """ + T = choosing_geometry(V.mesh().ufl_cell()) + + if V.ufl_element().family() == "Kong-Mulder-Veldhuizen": + element = KMV(T, degree) + elif V.ufl_element().family() == "Lagrange": + element = CG(T, degree) + elif V.ufl_element().family() == "Discontinuous Lagrange": + element = DG(T, degree) + else: + raise ValueError("Function space not yet supported.") + + return element + + +def get_hexa_real_cell_node_map(V, mesh): + weird_cnm_func = V.cell_node_map() + weird_cnm = weird_cnm_func.values_with_halo + cells_per_layer, nodes_per_cell = np.shape(weird_cnm) + layers = mesh.cell_set.layers - 1 + ufl_element = V.ufl_element() + _, p = ufl_element.degree() + + cell_node_map = np.zeros( + (layers * cells_per_layer, nodes_per_cell), dtype=int + ) + print(f"cnm size : {np.shape(cell_node_map)}", flush=True) + + for layer in range(layers): + print(f"layer : {layer} of {layers}", flush=True) + for cell in range(cells_per_layer): + cnm_base = weird_cnm[cell] + cell_id = layer + layers * cell + cell_node_map[cell_id] = [item + layer * (p) for item in cnm_base] + + return cell_node_map diff --git a/spyro/solvers/__init__.py b/spyro/solvers/__init__.py index d7c534b4..e7a9e48b 100644 --- a/spyro/solvers/__init__.py +++ b/spyro/solvers/__init__.py @@ -1,9 +1,9 @@ -from .forward import forward -from .forward_AD import forward as forward_AD -from .gradient import gradient +from .wave import Wave +from .acoustic_wave import AcousticWave +from .mms_acoustic import AcousticWaveMMS __all__ = [ - "forward", # forward solver adapted for discrete adjoint - "forward_AD", # forward solver adapted for Automatic Differentiation - "gradient", + "Wave", + "AcousticWave", + "AcousticWaveMMS", ] diff --git a/spyro/solvers/acoustic_solver_construction_no_pml.py b/spyro/solvers/acoustic_solver_construction_no_pml.py new file mode 100644 index 00000000..0e0ab796 --- /dev/null +++ b/spyro/solvers/acoustic_solver_construction_no_pml.py @@ -0,0 +1,47 @@ +import firedrake as fire +from firedrake import dx, Constant, dot, grad + + +def construct_solver_or_matrix_no_pml(Wave_object): + """Builds solver operators. Doesn't create mass matrices if + matrix_free option is on, + which it is by default. + """ + V = Wave_object.function_space + quad_rule = Wave_object.quadrature_rule + + # typical CG FEM in 2d/3d + u = fire.TrialFunction(V) + v = fire.TestFunction(V) + + u_nm1 = fire.Function(V, name="pressure t-dt") + u_n = fire.Function(V, name="pressure") + Wave_object.u_nm1 = u_nm1 + Wave_object.u_n = u_n + + Wave_object.current_time = 0.0 + dt = Wave_object.dt + + # ------------------------------------------------------- + m1 = ( + (1 / (Wave_object.c * Wave_object.c)) + * ((u - 2.0 * u_n + u_nm1) / Constant(dt**2)) + * v + * dx(scheme=quad_rule) + ) + a = dot(grad(u_n), grad(v)) * dx(scheme=quad_rule) # explicit + + B = fire.Function(V) + + form = m1 + a + lhs = fire.lhs(form) + rhs = fire.rhs(form) + Wave_object.lhs = lhs + + A = fire.assemble(lhs, mat_type="matfree") + Wave_object.solver = fire.LinearSolver( + A, solver_parameters=Wave_object.solver_parameters + ) + + Wave_object.rhs = rhs + Wave_object.B = B diff --git a/spyro/solvers/acoustic_solver_construction_with_pml.py b/spyro/solvers/acoustic_solver_construction_with_pml.py new file mode 100644 index 00000000..b6519b2f --- /dev/null +++ b/spyro/solvers/acoustic_solver_construction_with_pml.py @@ -0,0 +1,160 @@ +import firedrake as fire +from firedrake import dx, ds, Constant, dot, grad, inner + +from ..pml import damping + + +def construct_solver_or_matrix_with_pml(Wave_object): + if Wave_object.dimension == 2: + return construct_solver_or_matrix_with_pml_2d(Wave_object) + elif Wave_object.dimension == 3: + return construct_solver_or_matrix_with_pml_3d(Wave_object) + + +def construct_solver_or_matrix_with_pml_2d(Wave_object): + """Builds solver operators. Doesn't create mass matrices if + matrix_free option is on, + which it is by default. + """ + dt = Wave_object.dt + c = Wave_object.c + + V = Wave_object.function_space + Z = Wave_object.vector_function_space + W = V * Z + dxlump = dx(scheme=Wave_object.quadrature_rule) + dslump = ds(scheme=Wave_object.surface_quadrature_rule) + + u, pp = fire.TrialFunctions(W) + v, qq = fire.TestFunctions(W) + + X = fire.Function(W) + X_n = fire.Function(W) + X_nm1 = fire.Function(W) + + u_n, pp_n = X_n.split() + u_nm1, _ = X_nm1.split() + + Wave_object.u_n = u_n + Wave_object.X = X + Wave_object.X_n = X_n + Wave_object.X_nm1 = X_nm1 + + sigma_x, sigma_z = damping.functions(Wave_object) + Gamma_1, Gamma_2 = damping.matrices_2D(sigma_z, sigma_x) + pml1 = (sigma_x + sigma_z) * ((u - u_nm1) / Constant(2.0 * dt)) * v * dxlump + + # typical CG FEM in 2d/3d + + # ------------------------------------------------------- + m1 = ((u - 2.0 * u_n + u_nm1) / Constant(dt**2)) * v * dxlump + a = c * c * dot(grad(u_n), grad(v)) * dxlump # explicit + + nf = c * ((u_n - u_nm1) / dt) * v * dslump + + FF = m1 + a + nf + + B = fire.Function(W) + + pml2 = sigma_x * sigma_z * u_n * v * dxlump + pml3 = inner(pp_n, grad(v)) * dxlump + FF += pml1 + pml2 + pml3 + # ------------------------------------------------------- + mm1 = (dot((pp - pp_n), qq) / Constant(dt)) * dxlump + mm2 = inner(dot(Gamma_1, pp_n), qq) * dxlump + dd = c * c * inner(grad(u_n), dot(Gamma_2, qq)) * dxlump + FF += mm1 + mm2 + dd + + lhs_ = fire.lhs(FF) + rhs_ = fire.rhs(FF) + + A = fire.assemble(lhs_, mat_type="matfree") + solver = fire.LinearSolver( + A, solver_parameters=Wave_object.solver_parameters + ) + Wave_object.solver = solver + Wave_object.rhs = rhs_ + Wave_object.B = B + + +def construct_solver_or_matrix_with_pml_3d(Wave_object): + dt = Wave_object.dt + c = Wave_object.c + + V = Wave_object.function_space + Z = Wave_object.vector_function_space + W = V * V * Z + dxlump = dx(scheme=Wave_object.quadrature_rule) + dslump = ds(scheme=Wave_object.surface_quadrature_rule) + + u, psi, pp = fire.TrialFunctions(W) + v, phi, qq = fire.TestFunctions(W) + + X = fire.Function(W) + X_n = fire.Function(W) + X_nm1 = fire.Function(W) + + u_n, psi_n, pp_n = X_n.split() + u_nm1, psi_nm1, _ = X_nm1.split() + + Wave_object.u_n = u_n + Wave_object.X = X + Wave_object.X_n = X_n + Wave_object.X_nm1 = X_nm1 + + sigma_x, sigma_y, sigma_z = damping.functions(Wave_object) + Gamma_1, Gamma_2, Gamma_3 = damping.matrices_3D(sigma_x, sigma_y, sigma_z) + + pml1 = ( + (sigma_x + sigma_y + sigma_z) + * ((u - u_nm1) / Constant(2.0 * dt)) + * v + * dxlump + ) + + pml2 = ( + (sigma_x * sigma_y + sigma_x * sigma_z + sigma_y * sigma_z) + * u_n + * v + * dxlump + ) + + pml3 = (sigma_x * sigma_y * sigma_z) * psi_n * v * dxlump + pml4 = inner(pp_n, grad(v)) * dxlump + + # typical CG FEM in 2d/3d + + # ------------------------------------------------------- + m1 = ((u - 2.0 * u_n + u_nm1) / Constant(dt**2)) * v * dxlump + a = c * c * dot(grad(u_n), grad(v)) * dxlump # explicit + + nf = c * ((u_n - u_nm1) / dt) * v * dslump + + FF = m1 + a + nf + + B = fire.Function(W) + + FF += pml1 + pml2 + pml3 + pml4 + # ------------------------------------------------------- + mm1 = (dot((pp - pp_n), qq) / Constant(dt)) * dxlump + mm2 = inner(dot(Gamma_1, pp_n), qq) * dxlump + dd1 = c * c * inner(grad(u_n), dot(Gamma_2, qq)) * dxlump + dd2 = -c * c * inner(grad(psi_n), dot(Gamma_3, qq)) * dxlump + FF += mm1 + mm2 + dd1 + dd2 + + mmm1 = (dot((psi - psi_n), phi) / Constant(dt)) * dxlump + uuu1 = (-u_n * phi) * dxlump + FF += mmm1 + uuu1 + + lhs_ = fire.lhs(FF) + rhs_ = fire.rhs(FF) + + A = fire.assemble(lhs_, mat_type="matfree") + solver = fire.LinearSolver( + A, solver_parameters=Wave_object.solver_parameters + ) + Wave_object.solver = solver + Wave_object.rhs = rhs_ + Wave_object.B = B + + return diff --git a/spyro/solvers/acoustic_wave.py b/spyro/solvers/acoustic_wave.py new file mode 100644 index 00000000..2416f3be --- /dev/null +++ b/spyro/solvers/acoustic_wave.py @@ -0,0 +1,210 @@ +import firedrake as fire + +from .wave import Wave +from .time_integration import time_integrator +from ..io.basicio import ensemble_propagator +from ..domains.quadrature import quadrature_rules +from .acoustic_solver_construction_no_pml import ( + construct_solver_or_matrix_no_pml, +) +from .acoustic_solver_construction_with_pml import ( + construct_solver_or_matrix_with_pml, +) + + +class AcousticWave(Wave): + def forward_solve(self): + """Solves the forward problem. + + Parameters: + ----------- + None + + Returns: + -------- + None + """ + self._get_initial_velocity_model() + self.c = self.initial_velocity_model + self.matrix_building() + self.wave_propagator() + + def matrix_building(self): + """Builds solver operators. Doesn't create mass matrices if + matrix_free option is on, + which it is by default. + """ + self.current_time = 0.0 + quad_rule, k_rule, s_rule = quadrature_rules(self.function_space) + self.quadrature_rule = quad_rule + self.stiffness_quadrature_rule = k_rule + self.surface_quadrature_rule = s_rule + + abc_type = self.abc_boundary_layer_type + + # Just to document variables that will be overwritten + self.trial_function = None + self.u_nm1 = None + self.u_n = None + self.lhs = None + self.solver = None + self.rhs = None + self.B = None + if abc_type is None: + construct_solver_or_matrix_no_pml(self) + elif abc_type == "PML": + V = self.function_space + Z = fire.VectorFunctionSpace(V.ufl_domain(), V.ufl_element()) + self.vector_function_space = Z + self.X = None + self.X_n = None + self.X_nm1 = None + construct_solver_or_matrix_with_pml(self) + + @ensemble_propagator + def wave_propagator(self, dt=None, final_time=None, source_num=0): + """Propagates the wave forward in time. + Currently uses central differences. + + Parameters: + ----------- + dt: Python 'float' (optional) + Time step to be used explicitly. If not mentioned uses the default, + that was estabilished in the wave object. + final_time: Python 'float' (optional) + Time which simulation ends. If not mentioned uses the default, + that was estabilished in the wave object. + + Returns: + -------- + usol: Firedrake 'Function' + Pressure wavefield at the final time. + u_rec: numpy array + Pressure wavefield at the receivers across the timesteps. + """ + if final_time is not None: + self.final_time = final_time + if dt is not None: + self.dt = dt + + usol, usol_recv = time_integrator(self, source_id=source_num) + + return usol, usol_recv + + # def gradient_solve(self, guess=None): + # """Solves the adjoint problem to calculate de gradient. + + # Parameters: + # ----------- + # guess: Firedrake 'Function' (optional) + # Initial guess for the velocity model. If not mentioned uses the + # one currently in the wave object. + + # Returns: + # -------- + # dJ: Firedrake 'Function' + # Gradient of the cost functional. + # """ + # if self.real_shot_record is None: + # warnings.warn("Please load a real shot record first") + # if self.current_time == 0.0 and guess is not None: + # self.c = guess + # warnings.warn( + # "You need to run the forward solver before the adjoint solver,\ + # will do it for you now" + # ) + # self.forward_solve() + # self.misfit = self.real_shot_record - self.forward_solution_receivers + # self.wave_backward_propagator() + + # def wave_backward_propagator(self): + # residual = self.misfit + # guess = self.forward_solution + # V = self.function_space + # receivers = self.receivers + # dxlump = dx(scheme=self.quadrature_rule) + # c = self.c + # final_time = self.final_time + # t = self.current_time + # dt = self.dt + # comm = self.comm + # adjoint_output = self.adjoint_output + # adjoint_output_file = self.adjoint_output_file + # if self.adjoint_output: + # print(f"Saving output in: {adjoint_output_file}", flush=True) + # output = fire.File(adjoint_output_file, comm=comm.comm) + # nt = int((final_time - t) / dt) + 1 # number of timesteps + + # # Define gradient problem + # m_u = fire.Function(V) + # m_v = fire.TestFunction(V) + # mgrad = m_u * m_v * dxlump + + # uuadj = fire.Function(V) # auxiliarly function for the gradient compt. + # uufor = fire.Function(V) # auxiliarly function for the gradient compt. + + # ffG = 2.0 * c * dot(grad(uuadj), grad(uufor)) * m_v * dxlump + + # lhsG = mgrad + # rhsG = ffG + + # gradi = fire.Function(V) + # grad_prob = fire.LinearVariationalProblem(lhsG, rhsG, gradi) + + # grad_solver = fire.LinearVariationalSolver( + # grad_prob, + # solver_parameters=self.solver_parameters, + # ) + + # u_nm1 = fire.Function(V) + # u_n = fire.Function(V) + # u_np1 = fire.Function(V) + + # X = fire.Function(V) + # B = fire.Function(V) + + # rhs_forcing = fire.Function(V) # forcing term + # if adjoint_output: + # adjoint = [ + # fire.Function(V, name="adjoint_pressure") for t in range(nt) + # ] + # for step in range(nt - 1, -1, -1): + # t = step * float(dt) + # rhs_forcing.assign(0.0) + # # Solver - main equation - (I) + # B = fire.assemble(rhsG, tensor=B) + + # f = receivers.apply_receivers_as_source(rhs_forcing, residual, step) + # # add forcing term to solve scalar pressure + # B0 = B.sub(0) + # B0 += f + + # # AX=B --> solve for X = B/Aˆ-1 + # self.solver.solve(X, B) + + # u_np1.assign(X) + + # # only compute for snaps that were saved + # if step % self.gradient_sampling_frequency == 0: + # # compute the gradient increment + # uuadj.assign(u_np1) + # uufor.assign(guess.pop()) + + # grad_solver.solve() + # dJ += gradi + + # u_nm1.assign(u_n) + # u_n.assign(u_np1) + + # if step % self.output_frequency == 0: + # if adjoint_output: + # output.write(u_n, time=t) + # adjoint.append(u_n) + # helpers.display_progress(comm, t) + + # self.gradient = dJ + + # if adjoint_output: + # return dJ, adjoint + # else: + # return dJ diff --git a/spyro/solvers/dg_wave.py b/spyro/solvers/dg_wave.py new file mode 100644 index 00000000..0abe5e89 --- /dev/null +++ b/spyro/solvers/dg_wave.py @@ -0,0 +1,63 @@ +# import firedrake as fire +# from firedrake import dot, grad, jump, avg, dx, ds, dS, Constant +# from spyro import Wave + +# fire.set_log_level(fire.ERROR) + + +# class DG_Wave(Wave): +# def matrix_building(self): +# """Builds solver operators. Doesn't create mass matrices if +# matrix_free option is on, +# which it is by default. +# """ +# V = self.function_space +# # Trial and test functions +# u = fire.TrialFunction(V) +# v = fire.TestFunction(V) + +# # # Previous functions for time integration +# u_n = fire.Function(V) +# u_nm1 = fire.Function(V) +# self.u_nm1 = u_nm1 +# self.u_n = u_n +# c = self.c + +# self.current_time = 0.0 +# dt = self.dt + +# # Normal component, cell size and right-hand side +# h = fire.CellDiameter(self.mesh) +# h_avg = (h("+") + h("-")) / 2 +# n = fire.FacetNormal(self.mesh) + +# # Parameters +# alpha = 4.0 +# gamma = 8.0 + +# # Bilinear form +# a = ( +# dot(grad(v), grad(u)) * dx +# - dot(avg(grad(v)), jump(u, n)) * dS +# - dot(jump(v, n), avg(grad(u))) * dS +# + alpha / h_avg * dot(jump(v, n), jump(u, n)) * dS +# - dot(grad(v), u * n) * ds +# - dot(v * n, grad(u)) * ds +# + (gamma / h) * v * u * ds +# + ((u) / Constant(dt**2)) / c * v * dx +# ) +# # Linear form +# b = ((2.0 * u_n - u_nm1) / Constant(dt**2)) / c * v * dx +# form = a - b + +# lhs = fire.lhs(form) +# rhs = fire.rhs(form) + +# A = fire.assemble(lhs) +# params = {"ksp_type": "gmres"} +# self.solver = fire.LinearSolver(A, solver_parameters=params) + +# # lterar para como o thiago fez +# self.rhs = rhs +# B = fire.Function(V) +# self.B = B diff --git a/spyro/solvers/forward.py b/spyro/solvers/forward.py index 92fa52fe..06d38e85 100644 --- a/spyro/solvers/forward.py +++ b/spyro/solvers/forward.py @@ -1,294 +1,181 @@ -from firedrake import * -from firedrake.assemble import create_assembly_callable - -from .. import utils -from ..domains import quadrature, space -from ..pml import damping -from ..io import ensemble_forward -from . import helpers - -# Note this turns off non-fatal warnings -set_log_level(ERROR) - - -@ensemble_forward -def forward( - model, - mesh, - comm, - c, - excitations, - wavelet, - receivers, - source_num=0, - output=False, -): - """Secord-order in time fully-explicit scheme - with implementation of a Perfectly Matched Layer (PML) using - CG FEM with or without higher order mass lumping (KMV type elements). - - Parameters - ---------- - model: Python `dictionary` - Contains model options and parameters - mesh: Firedrake.mesh object - The 2D/3D triangular mesh - comm: Firedrake.ensemble_communicator - The MPI communicator for parallelism - c: Firedrake.Function - The velocity model interpolated onto the mesh. - excitations: A list Firedrake.Functions - wavelet: array-like - Time series data that's injected at the source location. - receivers: A :class:`spyro.Receivers` object. - Contains the receiver locations and sparse interpolation methods. - source_num: `int`, optional - The source number you wish to simulate - output: `boolean`, optional - Whether or not to write results to pvd files. - - Returns - ------- - usol: list of Firedrake.Functions - The full field solution at `fspool` timesteps - usol_recv: array-like - The solution interpolated to the receivers at all timesteps - - """ - - method = model["opts"]["method"] - degree = model["opts"]["degree"] - dim = model["opts"]["dimension"] - dt = model["timeaxis"]["dt"] - tf = model["timeaxis"]["tf"] - nspool = model["timeaxis"]["nspool"] - fspool = model["timeaxis"]["fspool"] - PML = model["BCs"]["status"] - excitations.current_source = source_num - if PML: - Lx = model["mesh"]["Lx"] - Lz = model["mesh"]["Lz"] - lx = model["BCs"]["lx"] - lz = model["BCs"]["lz"] - x1 = 0.0 - x2 = Lx - a_pml = lx - z1 = 0.0 - z2 = -Lz - c_pml = lz - if dim == 3: - Ly = model["mesh"]["Ly"] - ly = model["BCs"]["ly"] - y1 = 0.0 - y2 = Ly - b_pml = ly - - nt = int(tf / dt) # number of timesteps - - if method == "KMV": - params = {"ksp_type": "preonly", "pc_type": "jacobi"} - elif ( - method == "CG" - and mesh.ufl_cell() != quadrilateral - and mesh.ufl_cell() != hexahedron - ): - params = {"ksp_type": "cg", "pc_type": "jacobi"} - elif method == "CG" and ( - mesh.ufl_cell() == quadrilateral or mesh.ufl_cell() == hexahedron - ): - params = {"ksp_type": "preonly", "pc_type": "jacobi"} - else: - raise ValueError("method is not yet supported") - - element = space.FE_method(mesh, method, degree) - - V = FunctionSpace(mesh, element) - - qr_x, qr_s, _ = quadrature.quadrature_rules(V) - - if dim == 2: - z, x = SpatialCoordinate(mesh) - elif dim == 3: - z, x, y = SpatialCoordinate(mesh) - - if PML: - Z = VectorFunctionSpace(V.ufl_domain(), V.ufl_element()) - if dim == 2: - W = V * Z - u, pp = TrialFunctions(W) - v, qq = TestFunctions(W) - - u_np1, pp_np1 = Function(W).split() - u_n, pp_n = Function(W).split() - u_nm1, pp_nm1 = Function(W).split() - - elif dim == 3: - W = V * V * Z - u, psi, pp = TrialFunctions(W) - v, phi, qq = TestFunctions(W) - - u_np1, psi_np1, pp_np1 = Function(W).split() - u_n, psi_n, pp_n = Function(W).split() - u_nm1, psi_nm1, pp_nm1 = Function(W).split() - - if dim == 2: - sigma_x, sigma_z = damping.functions( - model, V, dim, x, x1, x2, a_pml, z, z1, z2, c_pml - ) - Gamma_1, Gamma_2 = damping.matrices_2D(sigma_z, sigma_x) - pml1 = ( - (sigma_x + sigma_z) - * ((u - u_nm1) / Constant(2.0 * dt)) - * v - * dx(scheme=qr_x) - ) - elif dim == 3: - - sigma_x, sigma_y, sigma_z = damping.functions( - model, - V, - dim, - x, - x1, - x2, - a_pml, - z, - z1, - z2, - c_pml, - y, - y1, - y2, - b_pml, - ) - Gamma_1, Gamma_2, Gamma_3 = damping.matrices_3D(sigma_x, sigma_y, sigma_z) - - # typical CG FEM in 2d/3d - else: - u = TrialFunction(V) - v = TestFunction(V) - - u_nm1 = Function(V) - u_n = Function(V) - u_np1 = Function(V) - - if output: - outfile = helpers.create_output_file("forward.pvd", comm, source_num) - - t = 0.0 - - # ------------------------------------------------------- - m1 = ((u - 2.0 * u_n + u_nm1) / Constant(dt**2)) * v * dx(scheme=qr_x) - a = c * c * dot(grad(u_n), grad(v)) * dx(scheme=qr_x) # explicit - - nf = 0 - if model["BCs"]["outer_bc"] == "non-reflective": - nf = c * ((u_n - u_nm1) / dt) * v * ds(scheme=qr_s) - - FF = m1 + a + nf - - if PML: - X = Function(W) - B = Function(W) - - if dim == 2: - pml2 = sigma_x * sigma_z * u_n * v * dx(scheme=qr_x) - pml3 = inner(pp_n, grad(v)) * dx(scheme=qr_x) - FF += pml1 + pml2 + pml3 - # ------------------------------------------------------- - mm1 = (dot((pp - pp_n), qq) / Constant(dt)) * dx(scheme=qr_x) - mm2 = inner(dot(Gamma_1, pp_n), qq) * dx(scheme=qr_x) - dd = c * c * inner(grad(u_n), dot(Gamma_2, qq)) * dx(scheme=qr_x) - FF += mm1 + mm2 + dd - elif dim == 3: - pml1 = ( - (sigma_x + sigma_y + sigma_z) - * ((u - u_n) / Constant(dt)) - * v - * dx(scheme=qr_x) - ) - pml2 = ( - (sigma_x * sigma_y + sigma_x * sigma_z + sigma_y * sigma_z) - * u_n - * v - * dx(scheme=qr_x) - ) - pml3 = (sigma_x * sigma_y * sigma_z) * psi_n * v * dx(scheme=qr_x) - pml4 = inner(pp_n, grad(v)) * dx(scheme=qr_x) - - FF += pml1 + pml2 + pml3 + pml4 - # ------------------------------------------------------- - mm1 = (dot((pp - pp_n), qq) / Constant(dt)) * dx(scheme=qr_x) - mm2 = inner(dot(Gamma_1, pp_n), qq) * dx(scheme=qr_x) - dd1 = c * c * inner(grad(u_n), dot(Gamma_2, qq)) * dx(scheme=qr_x) - dd2 = -c * c * inner(grad(psi_n), dot(Gamma_3, qq)) * dx(scheme=qr_x) - - FF += mm1 + mm2 + dd1 + dd2 - # ------------------------------------------------------- - mmm1 = (dot((psi - psi_n), phi) / Constant(dt)) * dx(scheme=qr_x) - uuu1 = (-u_n * phi) * dx(scheme=qr_x) - - FF += mmm1 + uuu1 - else: - X = Function(V) - B = Function(V) - - lhs_ = lhs(FF) - rhs_ = rhs(FF) - - A = assemble(lhs_, mat_type="matfree") - solver = LinearSolver(A, solver_parameters=params) - - usol = [Function(V, name="pressure") for t in range(nt) if t % fspool == 0] - usol_recv = [] - save_step = 0 - - assembly_callable = create_assembly_callable(rhs_, tensor=B) - - rhs_forcing = Function(V) - - for step in range(nt): - rhs_forcing.assign(0.0) - assembly_callable() - f = excitations.apply_source(rhs_forcing, wavelet[step]) - B0 = B.sub(0) - B0 += f - solver.solve(X, B) - if PML: - if dim == 2: - u_np1, pp_np1 = X.split() - elif dim == 3: - u_np1, psi_np1, pp_np1 = X.split() - - psi_nm1.assign(psi_n) - psi_n.assign(psi_np1) - - pp_nm1.assign(pp_n) - pp_n.assign(pp_np1) - else: - u_np1.assign(X) - - usol_recv.append(receivers.interpolate(u_np1.dat.data_ro_with_halos[:])) - - if step % fspool == 0: - usol[save_step].assign(u_np1) - save_step += 1 - - if step % nspool == 0: - assert ( - norm(u_n) < 1 - ), "Numerical instability. Try reducing dt or building the mesh differently" - if output: - outfile.write(u_n, time=t, name="Pressure") - if t > 0: - helpers.display_progress(comm, t) - - u_nm1.assign(u_n) - u_n.assign(u_np1) - - t = step * float(dt) - - usol_recv = helpers.fill(usol_recv, receivers.is_local, nt, receivers.num_receivers) - usol_recv = utils.communicate(usol_recv, comm) - - return usol, usol_recv +# import firedrake as fire +# from firedrake.assemble import create_assembly_callable +# from firedrake import Constant, dx, dot, inner, grad, ds +# import FIAT +# import finat + +# from .. import utils +# from ..domains import quadrature, space +# from ..pml import damping +# from ..io import ensemble_forward +# from . import helpers + + +# def gauss_lobatto_legendre_line_rule(degree): +# fiat_make_rule = FIAT.quadrature.GaussLobattoLegendreQuadratureLineRule +# fiat_rule = fiat_make_rule(FIAT.ufc_simplex(1), degree + 1) +# finat_ps = finat.point_set.GaussLobattoLegendrePointSet +# finat_qr = finat.quadrature.QuadratureRule +# return finat_qr(finat_ps(fiat_rule.get_points()), fiat_rule.get_weights()) + + +# # 3D +# def gauss_lobatto_legendre_cube_rule(dimension, degree): +# """Returns GLL integration rule + +# Parameters +# ---------- +# dimension: `int` +# The dimension of the mesh +# degree: `int` +# The degree of the function space + +# Returns +# ------- +# result: `finat.quadrature.QuadratureRule` +# The GLL integration rule +# """ +# make_tensor_rule = finat.quadrature.TensorProductQuadratureRule +# result = gauss_lobatto_legendre_line_rule(degree) +# for _ in range(1, dimension): +# line_rule = gauss_lobatto_legendre_line_rule(degree) +# result = make_tensor_rule([result, line_rule]) +# return result + + +# @ensemble_forward +# def forward( +# model, +# mesh, +# comm, +# c, +# excitations, +# wavelet, +# receivers, +# source_num=0, +# output=False, +# ): +# """Secord-order in time fully-explicit scheme +# with implementation of a Perfectly Matched Layer (PML) using +# CG FEM with or without higher order mass lumping (KMV type elements). + +# Parameters +# ---------- +# model: Python `dictionary` +# Contains model options and parameters +# mesh: Firedrake.mesh object +# The 2D/3D triangular mesh +# comm: Firedrake.ensemble_communicator +# The MPI communicator for parallelism +# c: Firedrake.Function +# The velocity model interpolated onto the mesh. +# excitations: A list Firedrake.Functions +# wavelet: array-like +# Time series data that's injected at the source location. +# receivers: A :class:`spyro.Receivers` object. +# Contains the receiver locations and sparse interpolation methods. +# source_num: `int`, optional +# The source number you wish to simulate +# output: `boolean`, optional +# Whether or not to write results to pvd files. + +# Returns +# ------- +# usol: list of Firedrake.Functions +# The full field solution at `fspool` timesteps +# usol_recv: array-like +# The solution interpolated to the receivers at all timesteps + +# """ + +# method = model["opts"]["method"] +# degree = model["opts"]["degree"] +# dt = model["timeaxis"]["dt"] +# final_time = model["timeaxis"]["tf"] +# nspool = model["timeaxis"]["nspool"] +# fspool = model["timeaxis"]["fspool"] +# excitations.current_source = source_num + +# nt = int(final_time / dt) # number of timesteps + +# element = fire.FiniteElement(method, mesh.ufl_cell(), degree=degree) + +# V = fire.FunctionSpace(mesh, element) + +# # typical CG FEM in 2d/3d +# u = fire.TrialFunction(V) +# v = fire.TestFunction(V) + +# u_nm1 = fire.Function(V) +# u_n = fire.Function(V, name="pressure") +# u_np1 = fire.Function(V) + +# if output: +# outfile = helpers.create_output_file("forward.pvd", comm, source_num) + +# t = 0.0 + +# # ------------------------------------------------------- +# m1 = ((u) / Constant(dt**2)) * v * dx +# a = ( +# c * c * dot(grad(u_n), grad(v)) * dx +# + ((-2.0 * u_n + u_nm1) / Constant(dt**2)) * v * dx +# ) # explicit + +# X = fire.Function(V) +# B = fire.Function(V) + +# lhs = m1 +# rhs = -a + +# A = fire.assemble(lhs) +# solver = fire.LinearSolver(A) + +# usol = [ +# fire.Function(V, name="pressure") for t in range(nt) if t % fspool == 0 +# ] +# usol_recv = [] +# save_step = 0 + +# assembly_callable = create_assembly_callable(rhs, tensor=B) + +# rhs_forcing = fire.Function(V) + +# for step in range(nt): +# rhs_forcing.assign(0.0) +# assembly_callable() +# f = excitations.apply_source(rhs_forcing, wavelet[step]) +# B0 = B.sub(0) +# B0 += f +# solver.solve(X, B) + +# u_np1.assign(X) + +# usol_recv.append( +# receivers.interpolate(u_np1.dat.data_ro_with_halos[:]) +# ) + +# if step % fspool == 0: +# usol[save_step].assign(u_np1) +# save_step += 1 + +# if step % nspool == 0: +# assert ( +# fire.norm(u_n) < 1 +# ), "Numerical instability. Try reducing dt or building the mesh differently" +# if output: +# outfile.write(u_n, time=t, name="Pressure") +# if t > 0: +# helpers.display_progress(comm, t) + +# u_nm1.assign(u_n) +# u_n.assign(u_np1) + +# t = step * float(dt) + +# usol_recv = helpers.fill( +# usol_recv, receivers.is_local, nt, receivers.num_receivers +# ) +# usol_recv = utils.communicate(usol_recv, comm) + +# return usol, usol_recv diff --git a/spyro/solvers/forward_AD.py b/spyro/solvers/forward_AD.py index 522477b3..a8e257f3 100644 --- a/spyro/solvers/forward_AD.py +++ b/spyro/solvers/forward_AD.py @@ -1,177 +1,180 @@ -from firedrake import * - -# from .. import utils -from ..domains import quadrature, space - -# from ..pml import damping -# from ..io import ensemble_forward -from . import helpers - -# Note this turns off non-fatal warnings -set_log_level(ERROR) - - -# @ensemble_forward -def forward( - model, - mesh, - comm, - c, - excitations, - wavelet, - receivers, - source_num=0, - output=False, - **kwargs -): - """Secord-order in time fully-explicit scheme - with implementation of a Perfectly Matched Layer (PML) using - CG FEM with or without higher order mass lumping (KMV type elements). - - Parameters - ---------- - model: Python `dictionary` - Contains model options and parameters - mesh: Firedrake.mesh object - The 2D/3D triangular mesh - comm: Firedrake.ensemble_communicator - The MPI communicator for parallelism - c: Firedrake.Function - The velocity model interpolated onto the mesh. - excitations: A list Firedrake.Functions - wavelet: array-like - Time series data that's injected at the source location. - receivers: A :class:`spyro.Receivers` object. - Contains the receiver locations and sparse interpolation methods. - source_num: `int`, optional - The source number you wish to simulate - output: `boolean`, optional - Whether or not to write results to pvd files. - - Returns - ------- - usol: list of Firedrake.Functions - The full field solution at `fspool` timesteps - usol_recv: array-like - The solution interpolated to the receivers at all timesteps - - """ - - method = model["opts"]["method"] - degree = model["opts"]["degree"] - dim = model["opts"]["dimension"] - dt = model["timeaxis"]["dt"] - tf = model["timeaxis"]["tf"] - nspool = model["timeaxis"]["nspool"] - nt = int(tf / dt) # number of timesteps - excitations.current_source = source_num - params = set_params(method) - element = space.FE_method(mesh, method, degree) - - V = FunctionSpace(mesh, element) - - qr_x, qr_s, _ = quadrature.quadrature_rules(V) - - if dim == 2: - z, x = SpatialCoordinate(mesh) - elif dim == 3: - z, x, y = SpatialCoordinate(mesh) - - u = TrialFunction(V) - v = TestFunction(V) - - u_nm1 = Function(V) - u_n = Function(V) - u_np1 = Function(V) - - if output: - outfile = helpers.create_output_file("forward.pvd", comm, source_num) - - t = 0.0 - m = 1 / (c * c) - m1 = m * ((u - 2.0 * u_n + u_nm1) / Constant(dt**2)) * v * dx(scheme=qr_x) - a = dot(grad(u_n), grad(v)) * dx(scheme=qr_x) # explicit - f = Function(V) - nf = 0 - - if model["BCs"]["outer_bc"] == "non-reflective": - nf = c * ((u_n - u_nm1) / dt) * v * ds(scheme=qr_s) - - h = CellSize(mesh) - FF = m1 + a + nf - (1 / (h / degree * h / degree)) * f * v * dx(scheme=qr_x) - X = Function(V) - - lhs_ = lhs(FF) - rhs_ = rhs(FF) - - problem = LinearVariationalProblem(lhs_, rhs_, X) - solver = LinearVariationalSolver(problem, solver_parameters=params) - - usol_recv = [] - - P = FunctionSpace(receivers, "DG", 0) - interpolator = Interpolator(u_np1, P) - J0 = 0.0 - - for step in range(nt): - - excitations.apply_source(f, wavelet[step]) - - solver.solve() - u_np1.assign(X) - - rec = Function(P) - interpolator.interpolate(output=rec) - - fwi = kwargs.get("fwi") - p_true_rec = kwargs.get("true_rec") - - usol_recv.append(rec.dat.data) - - if fwi: - J0 += calc_objective_func(rec, p_true_rec[step], step, dt, P) - - if step % nspool == 0: - assert ( - norm(u_n) < 1 - ), "Numerical instability. Try reducing dt or building the mesh differently" - if output: - outfile.write(u_n, time=t, name="Pressure") - if t > 0: - helpers.display_progress(comm, t) - - u_nm1.assign(u_n) - u_n.assign(u_np1) - - t = step * float(dt) +# from firedrake import * + +# # from .. import utils +# from ..domains import quadrature, space + +# # from ..pml import damping +# # from ..io import ensemble_forward +# from . import helpers + +# # Note this turns off non-fatal warnings +# set_log_level(ERROR) + + +# # @ensemble_forward +# def forward( +# model, +# mesh, +# comm, +# c, +# excitations, +# wavelet, +# receivers, +# source_num=0, +# output=False, +# **kwargs +# ): +# """Secord-order in time fully-explicit scheme +# with implementation of a Perfectly Matched Layer (PML) using +# CG FEM with or without higher order mass lumping (KMV type elements). + +# Parameters +# ---------- +# model: Python `dictionary` +# Contains model options and parameters +# mesh: Firedrake.mesh object +# The 2D/3D triangular mesh +# comm: Firedrake.ensemble_communicator +# The MPI communicator for parallelism +# c: Firedrake.Function +# The velocity model interpolated onto the mesh. +# excitations: A list Firedrake.Functions +# wavelet: array-like +# Time series data that's injected at the source location. +# receivers: A :class:`spyro.Receivers` object. +# Contains the receiver locations and sparse interpolation methods. +# source_num: `int`, optional +# The source number you wish to simulate +# output: `boolean`, optional +# Whether or not to write results to pvd files. + +# Returns +# ------- +# usol: list of Firedrake.Functions +# The full field solution at `fspool` timesteps +# usol_recv: array-like +# The solution interpolated to the receivers at all timesteps + +# """ + +# method = model["opts"]["method"] +# degree = model["opts"]["degree"] +# dim = model["opts"]["dimension"] +# dt = model["timeaxis"]["dt"] +# tf = model["timeaxis"]["tf"] +# nspool = model["timeaxis"]["nspool"] +# nt = int(tf / dt) # number of timesteps +# excitations.current_source = source_num +# params = set_params(method) +# element = space.FE_method(mesh, method, degree) + +# V = FunctionSpace(mesh, element) + +# qr_x, qr_s, _ = quadrature.quadrature_rules(V) + +# if dim == 2: +# z, x = SpatialCoordinate(mesh) +# elif dim == 3: +# z, x, y = SpatialCoordinate(mesh) + +# u = TrialFunction(V) +# v = TestFunction(V) + +# u_nm1 = Function(V) +# u_n = Function(V) +# u_np1 = Function(V) + +# if output: +# outfile = helpers.create_output_file("forward.pvd", comm, source_num) + +# t = 0.0 +# m = 1 / (c * c) +# m1 = ( +# m * ((u - 2.0 * u_n + u_nm1) / Constant(dt**2)) * v * dx(scheme=qr_x) +# ) +# a = dot(grad(u_n), grad(v)) * dx(scheme=qr_x) # explicit +# f = Function(V) +# nf = 0 + +# if model["BCs"]["outer_bc"] == "non-reflective": +# nf = c * ((u_n - u_nm1) / dt) * v * ds(scheme=qr_s) + +# h = CellSize(mesh) +# FF = ( +# m1 + a + nf - (1 / (h / degree * h / degree)) * f * v * dx(scheme=qr_x) +# ) +# X = Function(V) + +# lhs_ = lhs(FF) +# rhs_ = rhs(FF) + +# problem = LinearVariationalProblem(lhs_, rhs_, X) +# solver = LinearVariationalSolver(problem, solver_parameters=params) + +# usol_recv = [] + +# P = FunctionSpace(receivers, "DG", 0) +# interpolator = Interpolator(u_np1, P) +# J0 = 0.0 + +# for step in range(nt): +# excitations.apply_source(f, wavelet[step]) + +# solver.solve() +# u_np1.assign(X) + +# rec = Function(P) +# interpolator.interpolate(output=rec) + +# fwi = kwargs.get("fwi") +# p_true_rec = kwargs.get("true_rec") + +# usol_recv.append(rec.dat.data) + +# if fwi: +# J0 += calc_objective_func(rec, p_true_rec[step], step, dt, P) + +# if step % nspool == 0: +# assert ( +# norm(u_n) < 1 +# ), "Numerical instability. Try reducing dt or building the mesh differently" +# if output: +# outfile.write(u_n, time=t, name="Pressure") +# if t > 0: +# helpers.display_progress(comm, t) + +# u_nm1.assign(u_n) +# u_n.assign(u_np1) + +# t = step * float(dt) + +# if fwi: +# return usol_recv, J0 +# else: +# return usol_recv + + +# def calc_objective_func(p_rec, p_true_rec, IT, dt, P): +# true_rec = Function(P) +# true_rec.dat.data[:] = p_true_rec +# J = 0.5 * assemble(inner(true_rec - p_rec, true_rec - p_rec) * dx) +# return J + + +# def set_params(method): +# if method == "KMV": +# params = {"ksp_type": "preonly", "pc_type": "jacobi"} +# elif ( +# method == "CG" +# and mesh.ufl_cell() != quadrilateral +# and mesh.ufl_cell() != hexahedron +# ): +# params = {"ksp_type": "cg", "pc_type": "jacobi"} +# elif method == "CG" and ( +# mesh.ufl_cell() == quadrilateral or mesh.ufl_cell() == hexahedron +# ): +# params = {"ksp_type": "preonly", "pc_type": "jacobi"} +# else: +# raise ValueError("method is not yet supported") - if fwi: - return usol_recv, J0 - else: - return usol_recv - - -def calc_objective_func(p_rec, p_true_rec, IT, dt, P): - true_rec = Function(P) - true_rec.dat.data[:] = p_true_rec - J = 0.5 * assemble(inner(true_rec - p_rec, true_rec - p_rec) * dx) - return J - - -def set_params(method): - if method == "KMV": - params = {"ksp_type": "preonly", "pc_type": "jacobi"} - elif ( - method == "CG" - and mesh.ufl_cell() != quadrilateral - and mesh.ufl_cell() != hexahedron - ): - params = {"ksp_type": "cg", "pc_type": "jacobi"} - elif method == "CG" and ( - mesh.ufl_cell() == quadrilateral or mesh.ufl_cell() == hexahedron - ): - params = {"ksp_type": "preonly", "pc_type": "jacobi"} - else: - raise ValueError("method is not yet supported") - - return params +# return params diff --git a/spyro/solvers/gradient.py b/spyro/solvers/gradient.py index e3659b10..3bcde5a2 100644 --- a/spyro/solvers/gradient.py +++ b/spyro/solvers/gradient.py @@ -1,311 +1,210 @@ -from firedrake import * -from firedrake.assemble import create_assembly_callable - -from ..domains import quadrature, space -from ..pml import damping -from ..io import ensemble_gradient -from . import helpers - -# Note this turns off non-fatal warnings -set_log_level(ERROR) - -__all__ = ["gradient"] - - -@ensemble_gradient -def gradient( - model, mesh, comm, c, receivers, guess, residual, output=False, save_adjoint=False -): - """Discrete adjoint with secord-order in time fully-explicit timestepping scheme - with implementation of a Perfectly Matched Layer (PML) using - CG FEM with or without higher order mass lumping (KMV type elements). - - Parameters - ---------- - model: Python `dictionary` - Contains model options and parameters - mesh: Firedrake.mesh object - The 2D/3D triangular mesh - comm: Firedrake.ensemble_communicator - The MPI communicator for parallelism - c: Firedrake.Function - The velocity model interpolated onto the mesh nodes. - receivers: A :class:`spyro.Receivers` object. - Contains the receiver locations and sparse interpolation methods. - guess: A list of Firedrake functions - Contains the forward wavefield at a set of timesteps - residual: array-like [timesteps][receivers] - The difference between the observed and modeled data at - the receivers - output: boolean - optional, write the adjoint to disk (only for debugging) - save_adjoint: A list of Firedrake functions - Contains the adjoint at all timesteps - - Returns - ------- - dJdc_local: A Firedrake.Function containing the gradient of - the functional w.r.t. `c` - adjoint: Optional, a list of Firedrake functions containing the adjoint - - """ - - method = model["opts"]["method"] - degree = model["opts"]["degree"] - dim = model["opts"]["dimension"] - dt = model["timeaxis"]["dt"] - tf = model["timeaxis"]["tf"] - nspool = model["timeaxis"]["nspool"] - fspool = model["timeaxis"]["fspool"] - PML = model["BCs"]["status"] - if PML: - Lx = model["mesh"]["Lx"] - Lz = model["mesh"]["Lz"] - lx = model["BCs"]["lx"] - lz = model["BCs"]["lz"] - x1 = 0.0 - x2 = Lx - a_pml = lx - z1 = 0.0 - z2 = -Lz - c_pml = lz - if dim == 3: - Ly = model["mesh"]["Ly"] - ly = model["BCs"]["ly"] - y1 = 0.0 - y2 = Ly - b_pml = ly - - if method == "KMV": - params = {"ksp_type": "preonly", "pc_type": "jacobi"} - elif method == "CG": - params = {"ksp_type": "cg", "pc_type": "jacobi"} - else: - raise ValueError("method is not yet supported") - - element = space.FE_method(mesh, method, degree) - - V = FunctionSpace(mesh, element) - - qr_x, qr_s, _ = quadrature.quadrature_rules(V) - - nt = int(tf / dt) # number of timesteps - - # receiver_locations = model["acquisition"]["receiver_locations"] - - dJ = Function(V, name="gradient") - - if dim == 2: - z, x = SpatialCoordinate(mesh) - elif dim == 3: - z, x, y = SpatialCoordinate(mesh) - - if PML: - Z = VectorFunctionSpace(V.ufl_domain(), V.ufl_element()) - if dim == 2: - W = V * Z - u, pp = TrialFunctions(W) - v, qq = TestFunctions(W) - - u_np1, pp_np1 = Function(W).split() - u_n, pp_n = Function(W).split() - u_nm1, pp_nm1 = Function(W).split() - - elif dim == 3: - W = V * V * Z - u, psi, pp = TrialFunctions(W) - v, phi, qq = TestFunctions(W) - - u_np1, psi_np1, pp_np1 = Function(W).split() - u_n, psi_n, pp_n = Function(W).split() - u_nm1, psi_nm1, pp_nm1 = Function(W).split() - - if dim == 2: - (sigma_x, sigma_z) = damping.functions( - model, V, dim, x, x1, x2, a_pml, z, z1, z2, c_pml - ) - (Gamma_1, Gamma_2) = damping.matrices_2D(sigma_z, sigma_x) - elif dim == 3: - - sigma_x, sigma_y, sigma_z = damping.functions( - model, - V, - dim, - x, - x1, - x2, - a_pml, - z, - z1, - z2, - c_pml, - y, - y1, - y2, - b_pml, - ) - Gamma_1, Gamma_2, Gamma_3 = damping.matrices_3D(sigma_x, sigma_y, sigma_z) - - # typical CG in N-d - else: - u = TrialFunction(V) - v = TestFunction(V) - - u_nm1 = Function(V) - u_n = Function(V) - u_np1 = Function(V) - - if output: - outfile = helpers.create_output_file("adjoint.pvd", comm, 0) - - t = 0.0 - - # ------------------------------------------------------- - m1 = ((u - 2.0 * u_n + u_nm1) / Constant(dt**2)) * v * dx(scheme=qr_x) - a = c * c * dot(grad(u_n), grad(v)) * dx(scheme=qr_x) # explicit - - nf = 0 - if model["BCs"]["outer_bc"] == "non-reflective": - nf = c * ((u_n - u_nm1) / dt) * v * ds(scheme=qr_s) - - FF = m1 + a + nf - - if PML: - X = Function(W) - B = Function(W) - - if dim == 2: - pml1 = (sigma_x + sigma_z) * ((u - u_n) / dt) * v * dx(scheme=qr_x) - pml2 = sigma_x * sigma_z * u_n * v * dx(scheme=qr_x) - pml3 = c * c * inner(grad(v), dot(Gamma_2, pp_n)) * dx(scheme=qr_x) - - FF += pml1 + pml2 + pml3 - # ------------------------------------------------------- - mm1 = (dot((pp - pp_n), qq) / Constant(dt)) * dx(scheme=qr_x) - mm2 = inner(dot(Gamma_1, pp_n), qq) * dx(scheme=qr_x) - dd = inner(qq, grad(u_n)) * dx(scheme=qr_x) - - FF += mm1 + mm2 + dd - elif dim == 3: - pml1 = (sigma_x + sigma_y + sigma_z) * ((u - u_n) / dt) * v * dx(scheme=qr_x) - uuu1 = (-v * psi_n) * dx(scheme=qr_x) - pml2 = ( - (sigma_x * sigma_y + sigma_x * sigma_z + sigma_y * sigma_z) - * u_n - * v - * dx(scheme=qr_x) - ) - dd1 = c * c * inner(grad(v), dot(Gamma_2, pp_n)) * dx(scheme=qr_x) - - FF += pml1 + pml2 + dd1 + uuu1 - # ------------------------------------------------------- - mm1 = (dot((pp - pp_n), qq) / dt) * dx(scheme=qr_x) - mm2 = inner(dot(Gamma_1, pp_n), qq) * dx(scheme=qr_x) - pml4 = inner(qq, grad(u_n)) * dx(scheme=qr_x) - - FF += mm1 + mm2 + pml4 - # ------------------------------------------------------- - pml3 = (sigma_x * sigma_y * sigma_z) * phi * u_n * dx(scheme=qr_x) - mmm1 = (dot((psi - psi_n), phi) / dt) * dx(scheme=qr_x) - mmm2 = -c * c * inner(grad(phi), dot(Gamma_3, pp_n)) * dx(scheme=qr_x) - - FF += mmm1 + mmm2 + pml3 - else: - X = Function(V) - B = Function(V) - - lhs_ = lhs(FF) - rhs_ = rhs(FF) - - A = assemble(lhs_, mat_type="matfree") - solver = LinearSolver(A, solver_parameters=params) - - # Define gradient problem - m_u = TrialFunction(V) - m_v = TestFunction(V) - mgrad = m_u * m_v * dx(scheme=qr_x) - - uuadj = Function(V) # auxiliarly function for the gradient compt. - uufor = Function(V) # auxiliarly function for the gradient compt. - - ffG = 2.0 * c * dot(grad(uuadj), grad(uufor)) * m_v * dx(scheme=qr_x) - - G = mgrad - ffG - lhsG, rhsG = lhs(G), rhs(G) - - gradi = Function(V) - grad_prob = LinearVariationalProblem(lhsG, rhsG, gradi) - - if method == "KMV": - grad_solver = LinearVariationalSolver( - grad_prob, - solver_parameters={ - "ksp_type": "preonly", - "pc_type": "jacobi", - "mat_type": "matfree", - }, - ) - elif method == "CG": - grad_solver = LinearVariationalSolver( - grad_prob, - solver_parameters={ - "mat_type": "matfree", - }, - ) - - assembly_callable = create_assembly_callable(rhs_, tensor=B) - - rhs_forcing = Function(V) # forcing term - if save_adjoint: - adjoint = [Function(V, name="adjoint_pressure") for t in range(nt)] - for step in range(nt - 1, -1, -1): - t = step * float(dt) - rhs_forcing.assign(0.0) - # Solver - main equation - (I) - # B = assemble(rhs_, tensor=B) - assembly_callable() - - f = receivers.apply_receivers_as_source(rhs_forcing, residual, step) - # add forcing term to solve scalar pressure - B0 = B.sub(0) - B0 += f - - # AX=B --> solve for X = B/Aˆ-1 - solver.solve(X, B) - if PML: - if dim == 2: - u_np1, pp_np1 = X.split() - elif dim == 3: - u_np1, psi_np1, pp_np1 = X.split() - - psi_nm1.assign(psi_n) - psi_n.assign(psi_np1) - - pp_nm1.assign(pp_n) - pp_n.assign(pp_np1) - else: - u_np1.assign(X) - - # only compute for snaps that were saved - if step % fspool == 0: - # compute the gradient increment - uuadj.assign(u_np1) - uufor.assign(guess.pop()) - - grad_solver.solve() - dJ += gradi - - u_nm1.assign(u_n) - u_n.assign(u_np1) - - if step % nspool == 0: - if output: - outfile.write(u_n, time=t) - if save_adjoint: - adjoint.append(u_n) - helpers.display_progress(comm, t) - - if save_adjoint: - return dJ, adjoint - else: - return dJ +# import firedrake as fire +# from firedrake import dx, ds, Constant, grad, inner, dot +# from firedrake.assemble import create_assembly_callable + +# from ..domains import quadrature, space +# from ..pml import damping +# from ..io import ensemble_gradient +# from . import helpers + +# # Note this turns off non-fatal warnings +# # set_log_level(ERROR) + +# __all__ = ["gradient"] + + +# def gauss_lobatto_legendre_line_rule(degree): +# fiat_make_rule = FIAT.quadrature.GaussLobattoLegendreQuadratureLineRule +# fiat_rule = fiat_make_rule(FIAT.ufc_simplex(1), degree + 1) +# finat_ps = finat.point_set.GaussLobattoLegendrePointSet +# finat_qr = finat.quadrature.QuadratureRule +# return finat_qr(finat_ps(fiat_rule.get_points()), fiat_rule.get_weights()) + + +# # 3D +# def gauss_lobatto_legendre_cube_rule(dimension, degree): +# make_tensor_rule = finat.quadrature.TensorProductQuadratureRule +# result = gauss_lobatto_legendre_line_rule(degree) +# for _ in range(1, dimension): +# line_rule = gauss_lobatto_legendre_line_rule(degree) +# result = make_tensor_rule([result, line_rule]) +# return result + + +# @ensemble_gradient +# def gradient( +# model, +# mesh, +# comm, +# c, +# receivers, +# guess, +# residual, +# output=False, +# save_adjoint=False, +# ): +# """Discrete adjoint with secord-order in time fully-explicit timestepping scheme +# with implementation of a Perfectly Matched Layer (PML) using +# CG FEM with or without higher order mass lumping (KMV type elements). + +# Parameters +# ---------- +# model: Python `dictionary` +# Contains model options and parameters +# mesh: Firedrake.mesh object +# The 2D/3D triangular mesh +# comm: Firedrake.ensemble_communicator +# The MPI communicator for parallelism +# c: Firedrake.Function +# The velocity model interpolated onto the mesh nodes. +# receivers: A :class:`spyro.Receivers` object. +# Contains the receiver locations and sparse interpolation methods. +# guess: A list of Firedrake functions +# Contains the forward wavefield at a set of timesteps +# residual: array-like [timesteps][receivers] +# The difference between the observed and modeled data at +# the receivers +# output: boolean +# optional, write the adjoint to disk (only for debugging) +# save_adjoint: A list of Firedrake functions +# Contains the adjoint at all timesteps + +# Returns +# ------- +# dJdc_local: A Firedrake.Function containing the gradient of +# the functional w.r.t. `c` +# adjoint: Optional, a list of Firedrake functions containing the adjoint + +# """ + +# method = model["opts"]["method"] +# degree = model["opts"]["degree"] +# dimension = model["opts"]["dimension"] +# dt = model["timeaxis"]["dt"] +# tf = model["timeaxis"]["tf"] +# nspool = model["timeaxis"]["nspool"] +# fspool = model["timeaxis"]["fspool"] + +# params = {"ksp_type": "cg", "pc_type": "jacobi"} + +# element = fire.FiniteElement( +# method, mesh.ufl_cell(), degree=degree, variant="spectral" +# ) + +# V = fire.FunctionSpace(mesh, element) + +# qr_x = gauss_lobatto_legendre_cube_rule(dimension=dimension, degree=degree) +# qr_s = gauss_lobatto_legendre_cube_rule( +# dimension=(dimension - 1), degree=degree +# ) + +# nt = int(tf / dt) # number of timesteps + +# dJ = fire.Function(V, name="gradient") + +# # typical CG in N-d +# u = fire.TrialFunction(V) +# v = fire.TestFunction(V) + +# u_nm1 = fire.Function(V) +# u_n = fire.Function(V) +# u_np1 = fire.Function(V) + +# if output: +# outfile = helpers.create_output_file("adjoint.pvd", comm, 0) + +# t = 0.0 + +# # ------------------------------------------------------- +# m1 = ((u - 2.0 * u_n + u_nm1) / Constant(dt**2)) * v * dx(scheme=qr_x) +# a = c * c * dot(grad(u_n), grad(v)) * dx(scheme=qr_x) # explicit + +# lhs1 = m1 +# rhs1 = -a + +# X = fire.Function(V) +# B = fire.Function(V) + +# A = fire.assemble(lhs1, mat_type="matfree") +# solver = fire.LinearSolver(A, solver_parameters=params) + +# # Define gradient problem +# m_u = fire.TrialFunction(V) +# m_v = fire.TestFunction(V) +# mgrad = m_u * m_v * dx(rule=qr_x) + +# uuadj = fire.Function(V) # auxiliarly function for the gradient compt. +# uufor = fire.Function(V) # auxiliarly function for the gradient compt. + +# ffG = 2.0 * c * dot(grad(uuadj), grad(uufor)) * m_v * dx(scheme=qr_x) + +# lhsG = mgrad +# rhsG = ffG + +# gradi = fire.Function(V) +# grad_prob = fire.LinearVariationalProblem(lhsG, rhsG, gradi) + +# if method == "KMV": +# grad_solver = fire.LinearVariationalSolver( +# grad_prob, +# solver_parameters={ +# "ksp_type": "preonly", +# "pc_type": "jacobi", +# "mat_type": "matfree", +# }, +# ) +# elif method == "CG": +# grad_solver = fire.LinearVariationalSolver( +# grad_prob, +# solver_parameters={ +# "mat_type": "matfree", +# }, +# ) + +# assembly_callable = create_assembly_callable(rhs1, tensor=B) + +# rhs_forcing = fire.Function(V) # forcing term +# if save_adjoint: +# adjoint = [ +# fire.Function(V, name="adjoint_pressure") for t in range(nt) +# ] +# for step in range(nt - 1, -1, -1): +# t = step * float(dt) +# rhs_forcing.assign(0.0) +# # Solver - main equation - (I) +# # B = assemble(rhs_, tensor=B) +# assembly_callable() + +# f = receivers.apply_receivers_as_source(rhs_forcing, residual, step) +# # add forcing term to solve scalar pressure +# B0 = B.sub(0) +# B0 += f + +# # AX=B --> solve for X = B/Aˆ-1 +# solver.solve(X, B) + +# u_np1.assign(X) + +# # only compute for snaps that were saved +# if step % fspool == 0: +# # compute the gradient increment +# uuadj.assign(u_np1) +# uufor.assign(guess.pop()) + +# grad_solver.solve() +# dJ += gradi + +# u_nm1.assign(u_n) +# u_n.assign(u_np1) + +# if step % nspool == 0: +# if output: +# outfile.write(u_n, time=t) +# if save_adjoint: +# adjoint.append(u_n) +# helpers.display_progress(comm, t) + +# if save_adjoint: +# return dJ, adjoint +# else: +# return dJ diff --git a/spyro/solvers/helpers.py b/spyro/solvers/helpers.py index 3dc71430..a2e0b8c5 100644 --- a/spyro/solvers/helpers.py +++ b/spyro/solvers/helpers.py @@ -17,7 +17,7 @@ def fill(usol_recv, is_local, nt, nr): """Fills usol_recv with -99999 value when it isn't local to any core - + Parameters ---------- usol_recv : list @@ -28,12 +28,12 @@ def fill(usol_recv, is_local, nt, nr): Number of timesteps nr : int Number of receivers - + Returns ------- usol_recv : list List of numpy arrays - + """ usol_recv = np.asarray(usol_recv) for ti in range(nt): @@ -45,7 +45,7 @@ def fill(usol_recv, is_local, nt, nr): def create_output_file(name, comm, source_num): """Saves shots in output file - + Parameters ---------- name : str @@ -75,14 +75,14 @@ def create_output_file(name, comm, source_num): def display(comm, source_num): """Displays current shot and ensemble in terminal - + Parameters ---------- comm : object MPI communicator source_num : int Source number - + """ if comm.comm.rank == 0: print( @@ -97,7 +97,7 @@ def display(comm, source_num): def display_progress(comm, t): """Displays progress time - + Parameters ---------- comm : object @@ -109,15 +109,9 @@ def display_progress(comm, t): print(f"Simulation time is: {t:{10}.{4}} seconds", flush=True) -def parallel_print(string, comm): - """Prints in parallel""" - if comm.ensemble_comm.rank == 0 and comm.comm.rank == 0: - print(string, flush=True) - - def receivers_local(mesh, dimension, receiver_locations): """Locates receivers in cells - + Parameters ---------- mesh : object @@ -126,14 +120,17 @@ def receivers_local(mesh, dimension, receiver_locations): Dimension of the mesh receiver_locations : list List of receiver locations - + Returns ------- list List of receiver locations in cells """ if dimension == 2: - return [mesh.locate_cell([z, x], tolerance=0.01) for z, x in receiver_locations] + return [ + mesh.locate_cell([z, x], tolerance=0.01) + for z, x in receiver_locations + ] elif dimension == 3: return [ mesh.locate_cell([z, x, y], tolerance=0.01) diff --git a/spyro/solvers/mms_acoustic.py b/spyro/solvers/mms_acoustic.py new file mode 100644 index 00000000..ccbfb28c --- /dev/null +++ b/spyro/solvers/mms_acoustic.py @@ -0,0 +1,68 @@ +import firedrake as fire +from .acoustic_wave import AcousticWave + + +class AcousticWaveMMS(AcousticWave): + """Class for solving the acoustic wave equation in 2D or 3D using + the finite element method. This class inherits from the AcousticWave class + and overwrites the matrix_building method to use source propagated along + the whole domain, which generates a known solution for comparison. + """ + + def matrix_building(self): + super().matrix_building() + lhs = self.lhs + bcs = fire.DirichletBC(self.function_space, 0.0, "on_boundary") + A = fire.assemble(lhs, bcs=bcs, mat_type="matfree") + self.mms_source_in_space() + self.solver = fire.LinearSolver( + A, solver_parameters=self.solver_parameters + ) + + def mms_source_in_space(self): + V = self.function_space + self.q_xy = fire.Function(V) + x = self.mesh_z + y = self.mesh_x + if self.dimension == 2: + # xy = fire.project(sin(pi*x)*sin(pi*y), V) + # self.q_xy.assign(xy) + xy = fire.project((-(x**2) - x - y**2 + y), V) + self.q_xy.assign(xy) + elif self.dimension == 3: + z = self.mesh_y + # xyz = fire.project(sin(pi*x)*sin(pi*y)*sin(pi*z), V) + # self.q_xy.assign(xyz) + xyz = fire.project( + ( + -x * y * (x + 1) * (y - 1) + - x * z * (x + 1) * (z - 1) + - y * z * (y - 1) * (z - 1) + ), + V, + ) + self.q_xy.assign(xyz) + + # self.q_xy.interpolate(sin(pi*x)*sin(pi*y)) + + def mms_source_in_time(self, t): + # return fire.Constant(2*pi**2*t**2 + 2.0) + return fire.Constant(2 * t) + + def analytical_solution(self, t): + self.analytical = fire.Function(self.function_space) + x = self.mesh_z + y = self.mesh_x + # analytical = fire.project(sin(pi*x)*sin(pi*y)*t**2, + # self.function_space) + # self.analytical.interpolate(sin(pi*x)*sin(pi*y)*t**2) + if self.dimension == 2: + self.analytical.interpolate(x * (x + 1) * y * (y - 1) * t) + elif self.dimension == 3: + z = self.mesh_y + self.analytical.interpolate( + x * (x + 1) * y * (y - 1) * z * (z - 1) * t + ) + # self.analytical.assign(analytical) + + return self.analytical diff --git a/spyro/solvers/solver_parameters.py b/spyro/solvers/solver_parameters.py new file mode 100644 index 00000000..f299adad --- /dev/null +++ b/spyro/solvers/solver_parameters.py @@ -0,0 +1,17 @@ +def get_default_parameters_for_method(method): + solver_parameters = None + + if method == "mass_lumped_triangle": + solver_parameters = { + "ksp_type": "preonly", + "pc_type": "jacobi", + } + elif method == "spectral_quadrilateral": + solver_parameters = { + "ksp_type": "preonly", + "pc_type": "jacobi", + } + else: + solver_parameters = None + + return solver_parameters diff --git a/spyro/solvers/time_integration.py b/spyro/solvers/time_integration.py new file mode 100644 index 00000000..889fd834 --- /dev/null +++ b/spyro/solvers/time_integration.py @@ -0,0 +1,26 @@ +from .time_integration_central_difference import central_difference +from .time_integration_central_difference import mixed_space_central_difference +from .time_integration_central_difference import central_difference_MMS + + +def time_integrator(Wave_object, source_id=0): + if Wave_object.source_type == "ricker": + return time_integrator_ricker(Wave_object, source_id=source_id) + elif Wave_object.source_type == "MMS": + return time_integrator_mms(Wave_object, source_id=source_id) + + +def time_integrator_ricker(Wave_object, source_id=0): + if Wave_object.time_integrator == "central_difference": + return central_difference(Wave_object, source_id=source_id) + elif Wave_object.time_integrator == "mixed_space_central_difference": + return mixed_space_central_difference(Wave_object, source_id=source_id) + else: + raise ValueError("The time integrator specified is not implemented yet") + + +def time_integrator_mms(Wave_object, source_id=0): + if Wave_object.time_integrator == "central_difference": + return central_difference_MMS(Wave_object, source_id=source_id) + else: + raise ValueError("The time integrator specified is not implemented yet") diff --git a/spyro/solvers/time_integration_central_difference.py b/spyro/solvers/time_integration_central_difference.py new file mode 100644 index 00000000..5d5d5123 --- /dev/null +++ b/spyro/solvers/time_integration_central_difference.py @@ -0,0 +1,290 @@ +import firedrake as fire +from firedrake import Constant, dx, dot, grad +from firedrake.assemble import create_assembly_callable + +from ..io.basicio import parallel_print +from . import helpers +from .. import utils + + +def central_difference(Wave_object, source_id=0): + excitations = Wave_object.sources + excitations.current_source = source_id + receivers = Wave_object.receivers + comm = Wave_object.comm + temp_filename = Wave_object.forward_output_file + + filename, file_extension = temp_filename.split(".") + output_filename = filename + "sn" + str(source_id) + "." + file_extension + if Wave_object.forward_output: + parallel_print(f"Saving output in: {output_filename}", Wave_object.comm) + + output = fire.File(output_filename, comm=comm.comm) + comm.comm.barrier() + + X = fire.Function(Wave_object.function_space) + + final_time = Wave_object.final_time + dt = Wave_object.dt + t = Wave_object.current_time + nt = int((final_time - t) / dt) + 1 # number of timesteps + + u_nm1 = Wave_object.u_nm1 + u_n = Wave_object.u_n + u_np1 = fire.Function(Wave_object.function_space) + + rhs_forcing = fire.Function(Wave_object.function_space) + usol = [ + fire.Function(Wave_object.function_space, name="pressure") + for t in range(nt) + if t % Wave_object.gradient_sampling_frequency == 0 + ] + usol_recv = [] + save_step = 0 + B = Wave_object.B + rhs = Wave_object.rhs + + # assembly_callable = create_assembly_callable(rhs, tensor=B) + + for step in range(nt): + rhs_forcing.assign(0.0) + B = fire.assemble(rhs, tensor=B) + f = excitations.apply_source(rhs_forcing, Wave_object.wavelet[step]) + B0 = B.sub(0) + B0 += f + Wave_object.solver.solve(X, B) + + u_np1.assign(X) + + usol_recv.append( + Wave_object.receivers.interpolate(u_np1.dat.data_ro_with_halos[:]) + ) + + if step % Wave_object.gradient_sampling_frequency == 0: + usol[save_step].assign(u_np1) + save_step += 1 + + if (step - 1) % Wave_object.output_frequency == 0: + assert ( + fire.norm(u_n) < 1 + ), "Numerical instability. Try reducing dt or building the \ + mesh differently" + if Wave_object.forward_output: + output.write(u_n, time=t, name="Pressure") + + helpers.display_progress(Wave_object.comm, t) + + u_nm1.assign(u_n) + u_n.assign(u_np1) + + t = step * float(dt) + + Wave_object.current_time = t + helpers.display_progress(Wave_object.comm, t) + + usol_recv = helpers.fill( + usol_recv, receivers.is_local, nt, receivers.number_of_points + ) + usol_recv = utils.utils.communicate(usol_recv, comm) + Wave_object.receivers_output = usol_recv + + Wave_object.forward_solution = usol + Wave_object.forward_solution_receivers = usol_recv + + return usol, usol_recv + + +def mixed_space_central_difference(Wave_object, source_id=0): + excitations = Wave_object.sources + excitations.current_source = source_id + receivers = Wave_object.receivers + comm = Wave_object.comm + temp_filename = Wave_object.forward_output_file + filename, file_extension = temp_filename.split(".") + output_filename = filename + "sn" + str(source_id) + "." + file_extension + if Wave_object.forward_output: + parallel_print(f"Saving output in: {output_filename}", Wave_object.comm) + + output = fire.File(output_filename, comm=comm.comm) + comm.comm.barrier() + + final_time = Wave_object.final_time + dt = Wave_object.dt + t = Wave_object.current_time + nt = int(final_time / dt) + 1 # number of timesteps + + X = Wave_object.X + X_n = Wave_object.X_n + X_nm1 = Wave_object.X_nm1 + + rhs_forcing = fire.Function(Wave_object.function_space) + usol = [ + fire.Function(Wave_object.function_space, name="pressure") + for t in range(nt) + if t % Wave_object.gradient_sampling_frequency == 0 + ] + usol_recv = [] + save_step = 0 + B = Wave_object.B + rhs_ = Wave_object.rhs + + assembly_callable = create_assembly_callable(rhs_, tensor=B) + + for step in range(nt): + rhs_forcing.assign(0.0) + assembly_callable() + f = excitations.apply_source(rhs_forcing, Wave_object.wavelet[step]) + B0 = B.sub(0) + B0 += f + Wave_object.solver.solve(X, B) + + X_np1 = X + + X_nm1.assign(X_n) + X_n.assign(X_np1) + + usol_recv.append( + Wave_object.receivers.interpolate( + X_np1.dat.data_ro_with_halos[0][:] + ) + ) + + if step % Wave_object.gradient_sampling_frequency == 0: + usol[save_step].assign(X_np1.sub(0)) + save_step += 1 + + if (step - 1) % Wave_object.output_frequency == 0: + assert ( + fire.norm(X_np1.sub(0)) < 1 + ), "Numerical instability. Try reducing dt or building the \ + mesh differently" + if Wave_object.forward_output: + output.write(X_np1.sub(0), time=t, name="Pressure") + + helpers.display_progress(comm, t) + + t = step * float(dt) + + Wave_object.current_time = t + helpers.display_progress(Wave_object.comm, t) + + usol_recv = helpers.fill( + usol_recv, receivers.is_local, nt, receivers.number_of_points + ) + usol_recv = utils.utils.communicate(usol_recv, comm) + Wave_object.receivers_output = usol_recv + + Wave_object.forward_solution = usol + Wave_object.forward_solution_receivers = usol_recv + + return usol, usol_recv + + +def central_difference_MMS(Wave_object, source_id=0): + """Propagates the wave forward in time. + Currently uses central differences. + + Parameters: + ----------- + dt: Python 'float' (optional) + Time step to be used explicitly. If not mentioned uses the default, + that was estabilished in the model_parameters. + final_time: Python 'float' (optional) + Time which simulation ends. If not mentioned uses the default, + that was estabilished in the model_parameters. + """ + receivers = Wave_object.receivers + comm = Wave_object.comm + temp_filename = Wave_object.forward_output_file + filename, file_extension = temp_filename.split(".") + output_filename = filename + "sn_mms_" + "." + file_extension + if Wave_object.forward_output: + print(f"Saving output in: {output_filename}", flush=True) + + output = fire.File(output_filename, comm=comm.comm) + comm.comm.barrier() + + X = fire.Function(Wave_object.function_space) + + final_time = Wave_object.final_time + dt = Wave_object.dt + t = Wave_object.current_time + nt = int((final_time - t) / dt) + 1 # number of timesteps + + u_nm1 = Wave_object.u_nm1 + u_n = Wave_object.u_n + u_nm1.assign(Wave_object.analytical_solution(t - 2 * dt)) + u_n.assign(Wave_object.analytical_solution(t - dt)) + u_np1 = fire.Function(Wave_object.function_space, name="pressure t +dt") + u = fire.TrialFunction(Wave_object.function_space) + v = fire.TestFunction(Wave_object.function_space) + + usol = [ + fire.Function(Wave_object.function_space, name="pressure") + for t in range(nt) + if t % Wave_object.gradient_sampling_frequency == 0 + ] + usol_recv = [] + save_step = 0 + B = Wave_object.B + rhs = Wave_object.rhs + quad_rule = Wave_object.quadrature_rule + + # assembly_callable = create_assembly_callable(rhs, tensor=B) + q_xy = Wave_object.q_xy + + for step in range(nt): + q = q_xy * Wave_object.mms_source_in_time(t) + m1 = ( + 1 + / (Wave_object.c * Wave_object.c) + * ((u - 2.0 * u_n + u_nm1) / Constant(dt**2)) + * v + * dx(scheme=quad_rule) + ) + a = dot(grad(u_n), grad(v)) * dx(scheme=quad_rule) + le = q * v * dx(scheme=quad_rule) + + form = m1 + a - le + rhs = fire.rhs(form) + + B = fire.assemble(rhs, tensor=B) + + Wave_object.solver.solve(X, B) + + u_np1.assign(X) + + usol_recv.append( + Wave_object.receivers.interpolate(u_np1.dat.data_ro_with_halos[:]) + ) + + if step % Wave_object.gradient_sampling_frequency == 0: + usol[save_step].assign(u_np1) + save_step += 1 + + if (step - 1) % Wave_object.output_frequency == 0: + assert ( + fire.norm(u_n) < 1 + ), "Numerical instability. Try reducing dt or building the \ + mesh differently" + if Wave_object.forward_output: + output.write(u_n, time=t, name="Pressure") + if t > 0: + helpers.display_progress(Wave_object.comm, t) + + u_nm1.assign(u_n) + u_n.assign(u_np1) + + t = step * float(dt) + + Wave_object.current_time = t + helpers.display_progress(Wave_object.comm, t) + Wave_object.analytical_solution(t) + + usol_recv = helpers.fill( + usol_recv, receivers.is_local, nt, receivers.number_of_points + ) + usol_recv = utils.utils.communicate(usol_recv, comm) + Wave_object.receivers_output = usol_recv + + return usol, usol_recv diff --git a/spyro/solvers/wave.py b/spyro/solvers/wave.py new file mode 100644 index 00000000..1fe4ae99 --- /dev/null +++ b/spyro/solvers/wave.py @@ -0,0 +1,287 @@ +import os +from abc import abstractmethod +import warnings +import firedrake as fire +from firedrake import sin, cos, pi # noqa: F401 +from SeismicMesh import write_velocity_model + +from ..io import Model_parameters, interpolate +from .. import utils +from ..receivers.Receivers import Receivers +from ..sources.Sources import Sources +from ..domains.space import FE_method +from .solver_parameters import get_default_parameters_for_method + +fire.set_log_level(fire.ERROR) + + +class Wave(Model_parameters): + """ + Base class for wave equation solvers. + + Attributes: + ----------- + comm: MPI communicator + + initial_velocity_model: firedrake function + Initial velocity model + function_space: firedrake function space + Function space for the wave equation + current_time: float + Current time of the simulation + solver_parameters: Python object + Contains solver parameters + real_shot_record: firedrake function + Real shot record + wavelet: list of floats + Values at timesteps of wavelet used in the simulation + mesh: firedrake mesh + Mesh used in the simulation (2D or 3D) + mesh_z: symbolic coordinate z of the mesh object + mesh_x: symbolic coordinate x of the mesh object + mesh_y: symbolic coordinate y of the mesh object + sources: Sources object + Contains information about sources + receivers: Receivers object + Contains information about receivers + + Methods: + -------- + set_mesh: sets or calculates new mesh + set_solver_parameters: sets new or default solver parameters + get_spatial_coordinates: returns spatial coordinates of mesh + set_initial_velocity_model: sets initial velocity model + get_and_set_maximum_dt: calculates and/or sets maximum dt + get_mass_matrix_diagonal: returns diagonal of mass matrix + set_last_solve_as_real_shot_record: sets last solve as real shot record + """ + + def __init__(self, dictionary=None, comm=None): + """Wave object solver. Contains both the forward solver + and gradient calculator methods. + + Parameters: + ----------- + comm: MPI communicator + + model_parameters: Python object + Contains model parameters + """ + super().__init__(dictionary=dictionary, comm=comm) + self.initial_velocity_model = None + + self.function_space = None + self.forward_solution_receivers = None + self.current_time = 0.0 + self.set_solver_parameters() + self.real_shot_record = None + + self.wavelet = self.get_wavelet() + self.mesh = self.get_mesh() + self.c = None + if self.mesh is not None and self.mesh is not False: + self._build_function_space() + self._map_sources_and_receivers() + elif self.mesh_type == "firedrake_mesh": + warnings.warn( + "No mesh file, Firedrake mesh will be automatically generated." + ) + else: + warnings.warn("No mesh found. Please define a mesh.") + + @abstractmethod + def forward_solve(self): + """Solves the forward problem.""" + pass + + @abstractmethod + def matrix_building(self): + """Builds the matrix for the forward problem.""" + pass + + def set_mesh( + self, + dx=None, + user_mesh=None, + mesh_file=None, + length_z=None, + length_x=None, + length_y=None, + periodic=False, + edge_length=None, + ): + super().set_mesh( + dx=dx, + user_mesh=user_mesh, + mesh_file=mesh_file, + length_z=length_z, + length_x=length_x, + length_y=length_y, + periodic=periodic, + edge_length=edge_length, + ) + + self.mesh = self.get_mesh() + self._build_function_space() + self._map_sources_and_receivers() + if self.dimension == 2: + z, x = fire.SpatialCoordinate(self.mesh) + self.mesh_z = z + self.mesh_x = x + elif self.dimension == 3: + z, x, y = fire.SpatialCoordinate(self.mesh) + self.mesh_z = z + self.mesh_x = x + self.mesh_y = y + + def set_solver_parameters(self, parameters=None): + if parameters is not None: + self.solver_parameters = parameters + elif parameters is None: + self.solver_parameters = get_default_parameters_for_method( + self.method + ) + + def get_spatial_coordinates(self): + if self.dimension == 2: + return self.mesh_z, self.mesh_x + elif self.dimension == 3: + return self.mesh_z, self.mesh_x, self.mesh_y + + def set_initial_velocity_model( + self, + constant=None, + conditional=None, + velocity_model_function=None, + expression=None, + new_file=None, + output=False, + ): + """Method to define new user velocity model or file. It is optional. + + Parameters: + ----------- + conditional: (optional) + + velocity_model_function: (optional) + + expression: str (optional) + If you use an expression, you can use the following variables: + x, y, z, pi + + new_file: (optional) + """ + # Resseting old velocity model + self.initial_velocity_model = None + self.initial_velocity_model_file = None + + if self.debug_output: + output = True + + if conditional is not None: + V = fire.FunctionSpace(self.mesh, "DG", 0) + vp = fire.Function(V, name="velocity") + vp.interpolate(conditional) + self.initial_velocity_model = vp + elif expression is not None: + z = self.mesh_z # noqa: F841 + x = self.mesh_x # noqa: F841 + if self.dimension == 3: + y = self.mesh_y # noqa: F841 + expression = eval(expression) + V = self.function_space + vp = fire.Function(V, name="velocity") + vp.interpolate(expression) + self.initial_velocity_model = vp + elif velocity_model_function is not None: + self.initial_velocity_model = velocity_model_function + elif new_file is not None: + self.initial_velocity_model_file = new_file + elif constant is not None: + V = self.function_space + vp = fire.Function(V, name="velocity") + vp.interpolate(fire.Constant(constant)) + self.initial_velocity_model = vp + else: + raise ValueError( + "Please specify either a conditional, expression, firedrake \ + function or new file name (segy or hdf5)." + ) + if output: + fire.File("initial_velocity_model.pvd").write( + self.initial_velocity_model, name="velocity" + ) + + def _map_sources_and_receivers(self): + if self.source_type == "ricker": + self.sources = Sources(self) + else: + self.sources = None + self.receivers = Receivers(self) + + def _get_initial_velocity_model(self): + if self.initial_velocity_model is not None: + return None + + if self.initial_velocity_model_file is None: + raise ValueError("No velocity model or velocity file to load.") + + if self.initial_velocity_model_file.endswith(".segy"): + vp_filename, vp_filetype = os.path.splitext( + self.initial_velocity_model_file + ) + warnings.warn("Converting segy file to hdf5") + write_velocity_model( + self.initial_velocity_model_file, ofname=vp_filename + ) + self.initial_velocity_model_file = vp_filename + ".hdf5" + + if self.initial_velocity_model_file.endswith(".hdf5"): + self.initial_velocity_model = interpolate( + self.model_parameters, + self.initial_velocity_model_file, + self.function_space.sub(0), + ) + + def _build_function_space(self): + self.function_space = FE_method(self.mesh, self.method, self.degree) + + def get_and_set_maximum_dt(self, fraction=0.7): + # if self.method == "mass_lumped_triangle": + # estimate_max_eigenvalue = True + # elif self.method == "spectral_quadrilateral": + # estimate_max_eigenvalue = True + # else: + estimate_max_eigenvalue = False + + if self.c is None: + c = self.initial_velocity_model + else: + c = self.c + + dt = utils.estimate_timestep.estimate_timestep( + self.mesh, + self.function_space, + c, + estimate_max_eigenvalue=estimate_max_eigenvalue, + ) + dt *= fraction + nt = int(self.final_time / dt) + 1 + dt = self.final_time / (nt - 1) + + self.dt = dt + print(dt) + self.wavelet = self.get_wavelet() + return dt + + def get_mass_matrix_diagonal(self): + """Builds a section of the mass matrix for debugging purposes.""" + A = self.solver.A + petsc_matrix = A.petscmat + diagonal = petsc_matrix.getDiagonal() + return diagonal.array + + def set_last_solve_as_real_shot_record(self): + if self.current_time == 0.0: + raise ValueError("No previous solve to set as real shot record.") + self.real_shot_record = self.forward_solution_receivers diff --git a/spyro/sources/Sources.py b/spyro/sources/Sources.py index a31475c4..5955e376 100644 --- a/spyro/sources/Sources.py +++ b/spyro/sources/Sources.py @@ -1,10 +1,10 @@ import math import numpy as np from scipy.signal import butter, filtfilt -import spyro +from spyro.receivers.dirac_delta_projector import Delta_projector -class Sources(spyro.receivers.Receivers.Receivers): +class Sources(Delta_projector): """Methods that inject a wavelet into a mesh ... @@ -41,7 +41,7 @@ class Sources(spyro.receivers.Receivers.Receivers): Applies value at source locations in rhs_forcing operator """ - def __init__(self, model, mesh, V, my_ensemble): + def __init__(self, wave_object): """Initializes class and gets all receiver parameters from input file. @@ -61,43 +61,31 @@ def __init__(self, model, mesh, V, my_ensemble): Sources: :class: 'Source' object """ + super().__init__(wave_object) - self.mesh = mesh - self.space = V - self.my_ensemble = my_ensemble - self.dimension = model["opts"]["dimension"] - self.degree = model["opts"]["degree"] - - self.receiver_locations = model["acquisition"]["source_pos"] - self.num_receivers = len(self.receiver_locations) - - self.cellIDs = None - self.cellVertices = None - self.cell_tabulations = None - self.cellNodeMaps = None - self.nodes_per_cell = None - self.is_local = [0] * self.num_receivers + self.point_locations = wave_object.source_locations + self.number_of_points = wave_object.number_of_sources + self.is_local = [0] * self.number_of_points self.current_source = None - self.quadrilateral = model["opts"]["quadrature"] == "GLL" - super().build_maps() + self.build_maps() def apply_source(self, rhs_forcing, value): """Applies source in a assembled right hand side. - + Parameters ---------- rhs_forcing: Firedrake.Function - The right hand side of the equation + The right hand side of the wave equation value: float - The value to be applied at the source location + The value of the source Returns ------- rhs_forcing: Firedrake.Function - The right hand side of the equation with the source applied + The right hand side of the wave equation with the source applied """ - for source_id in range(self.num_receivers): + for source_id in range(self.number_of_points): if self.is_local[source_id] and source_id == self.current_source: for i in range(len(self.cellNodeMaps[source_id])): rhs_forcing.dat.data_with_halos[ @@ -119,7 +107,9 @@ def timedependentSource(model, t, freq=None, amp=1, delay=1.5): raise ValueError("source not implemented") -def ricker_wavelet(t, freq, amp=1.0, delay=1.5): +def ricker_wavelet( + t, freq, amp=1.0, delay=1.5, delay_type="multiples_of_minimun" +): """Creates a Ricker source function with a delay in term of multiples of the distance between the minimums. @@ -134,24 +124,36 @@ def ricker_wavelet(t, freq, amp=1.0, delay=1.5): Amplitude of the wavelet delay: float Delay in term of multiples of the distance - between the minimums + between the minimums. + delay_type: string + Type of delay. Options are: + - multiples_of_minimun + - time Returns ------- float Value of the wavelet at time t """ - t = t - delay * math.sqrt(6.0) / (math.pi * freq) - return ( - amp - * (1.0 - (1.0 / 2.0) * (2.0 * math.pi * freq) * (2.0 * math.pi * freq) * t * t) - * math.exp( - (-1.0 / 4.0) * (2.0 * math.pi * freq) * (2.0 * math.pi * freq) * t * t - ) - ) - - -def full_ricker_wavelet(dt, tf, freq, amp=1.0, cutoff=None): + if delay_type == "multiples_of_minimun": + time_delay = delay * math.sqrt(6.0) / (math.pi * freq) + elif delay_type == "time": + time_delay = delay + t = t - time_delay + # t = t - delay / freq + tt = (math.pi * freq * t) ** 2 + return amp * (1.0 - (2.0) * tt) * math.exp((-1.0) * tt) + + +def full_ricker_wavelet( + dt, + final_time, + frequency, + amplitude=1.0, + cutoff=None, + delay=1.5, + delay_type="multiples_of_minimun", +): """Compute the Ricker wavelet optionally applying low-pass filtering using cutoff frequency in Hertz. @@ -159,25 +161,34 @@ def full_ricker_wavelet(dt, tf, freq, amp=1.0, cutoff=None): ---------- dt: float Time step - tf: float + final_time: float Final time - freq: float + frequency: float Frequency of the wavelet - amp: float + amplitude: float Amplitude of the wavelet cutoff: float Cutoff frequency in Hertz + delay: float + Delay in term of multiples of the distance + between the minimums. + delay_type: string + Type of delay. Options are: + - multiples_of_minimun + - time Returns ------- - full_wavelet: numpy array - Array containing the wavelet values at each time step + list of float + list of ricker values at each time step """ - nt = int(tf / dt) # number of timesteps + nt = int(final_time / dt) + 1 # number of timesteps time = 0.0 full_wavelet = np.zeros((nt,)) for t in range(nt): - full_wavelet[t] = ricker_wavelet(time, freq, amp) + full_wavelet[t] = ricker_wavelet( + time, frequency, amplitude, delay=delay, delay_type=delay_type + ) time += dt if cutoff is not None: fs = 1.0 / dt @@ -188,65 +199,3 @@ def full_ricker_wavelet(dt, tf, freq, amp=1.0, cutoff=None): b, a = butter(order, normal_cutoff, btype="low", analog=False) full_wavelet = filtfilt(b, a, full_wavelet) return full_wavelet - - -# def MMS_time(t): -# return 2 * t + 2 * math.pi ** 2 * t ** 3 / 3.0 - - -# def MMS_space(x0, z, x): -# """ Mesh variable part of the MMS """ -# return sin(pi * z) * sin(pi * x) * Constant(1.0) - - -# def MMS_space_3d(x0, z, x, y): -# """ Mesh variable part of the MMS """ -# return sin(pi * z) * sin(pi * x) * sin(pi * y) * Constant(1.0) - -# def source_dof_finder(space, model): - -# # getting 1 source position -# source_positions = model["acquisition"]["source_pos"] -# if len(source_positions) != 1: -# raise ValueError("Not yet implemented for more then 1 source.") - -# mesh = space.mesh() -# source_z, source_x = source_positions[0] - -# # Getting mesh coordinates -# z, x = SpatialCoordinate(mesh) -# ux = Function(space).interpolate(x) -# uz = Function(space).interpolate(z) -# datax = ux.dat.data_ro_with_halos[:] -# dataz = uz.dat.data_ro_with_halos[:] -# node_locations = np.zeros((len(datax), 2)) -# node_locations[:, 0] = dataz -# node_locations[:, 1] = datax - -# # generating cell node map -# fdrake_cell_node_map = space.cell_node_map() -# cell_node_map = fdrake_cell_node_map.values_with_halo - -# # finding cell where the source is located -# cell_id = mesh.locate_cell([source_z, source_x], tolerance=0.01) - -# # finding dof where the source is located -# for dof in cell_node_map[cell_id]: -# if np.isclose(dataz[dof], source_z, rtol=1e-8) and np.isclose( -# datax[dof], source_x, rtol=1e-8 -# ): -# model["acquisition"]["source_point_dof"] = dof - -# if model["acquisition"]["source_point_dof"] == False: -# print("Warning not using point source") -# return False - - -# def delta_expr(x0, z, x, sigma_x=500.0): -# sigma_x = Constant(sigma_x) -# return exp(-sigma_x * ((z - x0[0]) ** 2 + (x - x0[1]) ** 2)) - - -# def delta_expr_3d(x0, z, x, y, sigma_x=2000.0): -# sigma_x = Constant(sigma_x) -# return exp(-sigma_x * ((z - x0[0]) ** 2 + (x - x0[1]) ** 2 + (y - x0[2]) ** 2)) diff --git a/spyro/tools/__init__.py b/spyro/tools/__init__.py index 84f49c55..7d2d713c 100644 --- a/spyro/tools/__init__.py +++ b/spyro/tools/__init__.py @@ -1,19 +1,30 @@ -from .grid_point_calculator import wave_solver, generate_mesh, error_calc -from .grid_point_calculator import minimum_grid_point_calculator -from .input_models import create_model_for_grid_point_calculation -from .grid_point_calculator import time_interpolation -from .grid_point_calculator import grid_point_to_mesh_point_converter_for_seismicmesh -from .grid_point_calculator import error_calc_line -from .gradient_test_ad import gradient_test_acoustic as gradient_test_acoustic_ad +from .cells_per_wavelength_calculator import Meshing_parameter_calculator + __all__ = [ - "wave_solver", - "generate_mesh", - "error_calc", - "create_model_for_grid_point_calculation", - "minimum_grid_point_calculator", - "time_interpolation", - "grid_point_to_mesh_point_converter_for_seismicmesh", - "error_calc_line", - "gradient_test_acoustic_ad", + "Meshing_parameter_calculator", ] + +# from .grid_point_calculator import wave_solver, generate_mesh, error_calc +# from .grid_point_calculator import minimum_grid_point_calculator +# from .input_models import create_model_for_grid_point_calculation +# from .grid_point_calculator import time_interpolation +# from .grid_point_calculator import ( +# grid_point_to_mesh_point_converter_for_seismicmesh, +# ) +# from .grid_point_calculator import error_calc_line +# from .gradient_test_ad import ( +# gradient_test_acoustic as gradient_test_acoustic_ad, +# ) + +# __all__ = [ +# "wave_solver", +# "generate_mesh", +# "error_calc", +# "create_model_for_grid_point_calculation", +# "minimum_grid_point_calculator", +# "time_interpolation", +# "grid_point_to_mesh_point_converter_for_seismicmesh", +# "error_calc_line", +# "gradient_test_acoustic_ad", +# ] diff --git a/spyro/tools/cells_per_wavelength_calculator.py b/spyro/tools/cells_per_wavelength_calculator.py new file mode 100644 index 00000000..5fd963b3 --- /dev/null +++ b/spyro/tools/cells_per_wavelength_calculator.py @@ -0,0 +1,205 @@ +import numpy as np +from scipy import interpolate +import time as timinglib +import copy +from .input_models import create_initial_model_for_meshing_parameter +import spyro + + +class Meshing_parameter_calculator: + def __init__(self, parameters_dictionary): + self.parameters_dictionary = parameters_dictionary + self.source_frequency = parameters_dictionary["source_frequency"] + self.minimum_velocity = parameters_dictionary[ + "minimum_velocity_in_the_domain" + ] + self.velocity_profile_type = parameters_dictionary[ + "velocity_profile_type" + ] + self.velocity_model_file_name = parameters_dictionary[ + "velocity_model_file_name" + ] + self.FEM_method_to_evaluate = parameters_dictionary[ + "FEM_method_to_evaluate" + ] + self.dimension = parameters_dictionary["dimension"] + self.receiver_setup = parameters_dictionary["receiver_setup"] + self.accepted_error_threshold = parameters_dictionary[ + "accepted_error_threshold" + ] + self.desired_degree = parameters_dictionary["desired_degree"] + + # Only for use in heterogenoeus models + self.reference_degree = parameters_dictionary["reference_degree"] + self.cpw_reference = parameters_dictionary["C_reference"] + + # Initializing optimization parameters + self.cpw_initial = parameters_dictionary["C_initial"] + self.cpw_accuracy = parameters_dictionary["C_accuracy"] + + # Debugging and testing parameters + if "testing" in parameters_dictionary: + self.reduced_obj_for_testing = parameters_dictionary["testing"] + else: + self.reduced_obj_for_testing = False + + if "save_reference" in parameters_dictionary: + self.save_reference = parameters_dictionary["save_reference"] + else: + self.save_reference = False + + if "load_reference" in parameters_dictionary: + self.load_reference = parameters_dictionary["load_reference"] + else: + self.load_reference = False + + self.initial_guess_object = self.build_initial_guess_model() + self.reference_solution = self.get_reference_solution() + + def build_initial_guess_model(self): + dictionary = create_initial_model_for_meshing_parameter(self) + self.initial_dictionary = dictionary + return spyro.AcousticWave(dictionary) + + def get_reference_solution(self): + if self.load_reference: + if "reference_solution_file" in self.parameters_dictionary: + filename = self.parameters_dictionary["reference_solution_file"] + else: + filename = "reference_solution.npy" + return np.load(filename) + elif self.velocity_profile_type == "heterogeneous": + raise NotImplementedError("Not yet implemented") + # return self.get_referecen_solution_from refined_mesh() + elif self.velocity_profile_type == "homogeneous": + return self.calculate_analytical_solution() + + def calculate_analytical_solution(self): + # Initializing array + Wave_obj = self.initial_guess_object + number_of_receivers = Wave_obj.number_of_receivers + dt = Wave_obj.dt + final_time = Wave_obj.final_time + num_t = int(final_time / dt + 1) + analytical_solution = np.zeros((num_t, number_of_receivers)) + + # Solving analytical solution for each receiver + receiver_locations = Wave_obj.receiver_locations + source_locations = Wave_obj.source_locations + source_location = source_locations[0] + sz, sx = source_location + i = 0 + for receiver in receiver_locations: + rz, rx = receiver + offset = np.sqrt((rz - sz) ** 2 + (rx - sx) ** 2) + r_sol = spyro.utils.nodal_homogeneous_analytical( + Wave_obj, offset, self.minimum_velocity + ) + analytical_solution[:, i] = r_sol + print(i) + i += 1 + analytical_solution = analytical_solution / (self.minimum_velocity**2) + + if self.save_reference: + np.save("reference_solution.npy", analytical_solution) + + return analytical_solution + + def find_minimum(self, starting_cpw=None, TOL=None, accuracy=None): + if starting_cpw is None: + starting_cpw = self.cpw_initial + if TOL is None: + TOL = self.accepted_error_threshold + if accuracy is None: + accuracy = self.cpw_accuracy + + error = 100.0 + cpw = starting_cpw + print("Starting line search", flush=True) + + fast_loop = True + dif = 0.0 + cont = 0 + while error > TOL: + print("Trying cells-per-wavelength = ", cpw, flush=True) + + # Running forward model + Wave_obj = self.build_current_object(cpw) + # Wave_obj.get_and_set_maximum_dt(fraction=0.2) + Wave_obj.forward_solve() + p_receivers = Wave_obj.forward_solution_receivers + spyro.io.save_shots( + Wave_obj, file_name="test_shot_record" + str(cpw) + ) + + error = error_calc( + p_receivers, self.reference_solution, Wave_obj.dt + ) + print("Error is ", error, flush=True) + + if error < TOL and dif > accuracy: + cpw -= dif + error = 100.0 + # Flooring CPW to the neartest decimal point inside accuracy + cpw = np.round( + (cpw + 1e-6) // accuracy * accuracy, + int(-np.log10(accuracy)), + ) + fast_loop = False + else: + dif = calculate_dif(cpw, accuracy, fast_loop=fast_loop) + cpw += dif + + cont += 1 + + return cpw - dif + + def build_current_object(self, cpw): + dictionary = copy.deepcopy(self.initial_dictionary) + dictionary["mesh"]["cells_per_wavelength"] = cpw + Wave_obj = spyro.AcousticWave(dictionary) + lba = self.minimum_velocity / self.source_frequency + + edge_length = lba / cpw + Wave_obj.set_mesh(edge_length=edge_length) + Wave_obj.set_initial_velocity_model(constant=self.minimum_velocity) + return Wave_obj + + +def calculate_dif(cpw, accuracy, fast_loop=False): + if fast_loop: + dif = max(0.1 * cpw, accuracy) + else: + dif = accuracy + + return dif + + +def error_calc(receivers, analytical, dt): + rec_len, num_rec = np.shape(receivers) + + # Interpolate analytical solution into numerical dts + final_time = dt * (rec_len - 1) + time_vector_rec = np.linspace(0.0, final_time, rec_len) + time_vector_ana = np.linspace(0.0, final_time, len(analytical[:, 0])) + ana = np.zeros(np.shape(receivers)) + for i in range(num_rec): + ana[:, i] = np.interp( + time_vector_rec, time_vector_ana, analytical[:, i] + ) + + total_numerator = 0.0 + total_denumenator = 0.0 + for i in range(num_rec): + diff = receivers[:, i] - ana[:, i] + diff_squared = np.power(diff, 2) + numerator = np.trapz(diff_squared, dx=dt) + ref_squared = np.power(ana[:, i], 2) + denominator = np.trapz(ref_squared, dx=dt) + total_numerator += numerator + total_denumenator += denominator + + squared_error = total_numerator / total_denumenator + + error = np.sqrt(squared_error) + return error diff --git a/spyro/tools/demo.py b/spyro/tools/demo.py index b037ad28..f7252c89 100644 --- a/spyro/tools/demo.py +++ b/spyro/tools/demo.py @@ -1,26 +1,35 @@ -# Demo to illustrate how a grid point density calcultor runs the experiments -import spyro +# # Demo to illustrate how a grid point density calcultor runs the experiments +# import spyro -# First we need to define experiment parameters: -grid_point_calculator_parameters = { - # Experiment parameters - "source_frequency": 5.0, # Here we define the frequency of the Ricker wavelet source - "minimum_velocity_in_the_domain": 1.429, # The minimum velocity present in the domain. - # if an homogeneous test case is used this velocity will be defined in the whole domain. - "velocity_profile_type": "homogeneous", # Either or heterogeneous. If heterogeneous is - # chosen be careful to have the desired velocity model below. - "velocity_model_file_name": "vel_z6.25m_x12.5m_exact.segy", - "FEM_method_to_evaluate": "KMV", # FEM to evaluate such as `KMV` or `spectral` (GLL nodes on quads and hexas) - "dimension": 2, # Domain dimension. Either 2 or 3. - "receiver_setup": "near", # Either near or line. Near defines a receiver grid near to the source, - # line defines a line of point receivers with pre-established near and far offsets. - # Line search parameters - "reference_degree": 5, # Degree to use in the reference case (int) - "G_reference": 15.0, # grid point density to use in the reference case (float) - "desired_degree": 4, # degree we are calculating G for. (int) - "G_initial": 6.0, # Initial G for line search (float) - "accepted_error_threshold": 0.05, - "g_accuracy": 1e-1, -} +# # First we need to define experiment parameters: +# grid_point_calculator_parameters = { +# # Experiment parameters +# # Here we define the frequency of the Ricker wavelet source +# "source_frequency": 5.0, +# # The minimum velocity present in the domain. +# "minimum_velocity_in_the_domain": 1.429, +# # if an homogeneous test case is used this velocity will be defined in +# # the whole domain. +# # Either or heterogeneous. If heterogeneous is +# "velocity_profile_type": "homogeneous", +# # chosen be careful to have the desired velocity model below. +# "velocity_model_file_name": "vel_z6.25m_x12.5m_exact.segy", +# # FEM to evaluate such as `KMV` or `spectral` +# # (GLL nodes on quads and hexas) +# "FEM_method_to_evaluate": "KMV", +# "dimension": 2, # Domain dimension. Either 2 or 3. +# # Either near or line. Near defines a receiver grid near to the source, +# "receiver_setup": "near", +# # line defines a line of point receivers with pre-established near and far +# # offsets. +# # Line search parameters +# "reference_degree": 5, # Degree to use in the reference case (int) +# # grid point density to use in the reference case (float) +# "G_reference": 15.0, +# "desired_degree": 4, # degree we are calculating G for. (int) +# "G_initial": 6.0, # Initial G for line search (float) +# "accepted_error_threshold": 0.05, +# "g_accuracy": 1e-1, +# } -G = spyro.tools.minimum_grid_point_calculator(grid_point_calculator_parameters) +# G = spyro.tools.minimum_grid_point_calculator(grid_point_calculator_parameters) diff --git a/spyro/tools/gradient_test_ad.py b/spyro/tools/gradient_test_ad.py index 1ff6b876..ab159962 100644 --- a/spyro/tools/gradient_test_ad.py +++ b/spyro/tools/gradient_test_ad.py @@ -1,144 +1,149 @@ -import numpy as np -from firedrake import * -from pyadjoint import enlisting -import spyro - -forward = spyro.solvers.forward_AD - - -def gradient_test_acoustic(model, mesh, V, comm, vp_exact, vp_guess, mask=None): - """Gradient test for the acoustic FWI problem - - Parameters - ---------- - model : `dictionary` - Contains simulation parameters and options. - mesh : a Firedrake.mesh - 2D/3D simplicial mesh read in by Firedrake.Mesh - V : Firedrake.FunctionSpace object - The space of the finite elements - comm : Firedrake.ensemble_communicator - An ensemble communicator - vp_exact : Firedrake.Function - The exact velocity model - vp_guess : Firedrake.Function - The guess velocity model - mask : Firedrake.Function, optional - A mask for the gradient test - - Returns - ------- - None - """ - import firedrake_adjoint as fire_adj - - with fire_adj.stop_annotating(): - if comm.comm.rank == 0: - print("######## Starting gradient test ########", flush=True) - - sources = spyro.Sources(model, mesh, V, comm) - receivers = spyro.Receivers(model, mesh, V, comm) - - wavelet = spyro.full_ricker_wavelet( - model["timeaxis"]["dt"], - model["timeaxis"]["tf"], - model["acquisition"]["frequency"], - ) - point_cloud = receivers.set_point_cloud(comm) - # simulate the exact model - if comm.comm.rank == 0: - print("######## Running the exact model ########", flush=True) - p_exact_recv = forward( - model, mesh, comm, vp_exact, sources, wavelet, point_cloud - ) - - # simulate the guess model - if comm.comm.rank == 0: - print("######## Running the guess model ########", flush=True) - p_guess_recv, Jm = forward( - model, - mesh, - comm, - vp_guess, - sources, - wavelet, - point_cloud, - fwi=True, - true_rec=p_exact_recv, - ) - if comm.comm.rank == 0: - print("\n Cost functional at fixed point : " + str(Jm) + " \n ", flush=True) - - # compute the gradient of the control (to be verified) - if comm.comm.rank == 0: - print( - "######## Computing the gradient by automatic differentiation ########", - flush=True, - ) - control = fire_adj.Control(vp_guess) - dJ = fire_adj.compute_gradient(Jm, control) - if mask: - dJ *= mask - - # File("gradient.pvd").write(dJ) - - # steps = [1e-3, 1e-4, 1e-5, 1e-6, 1e-7] # step length - # steps = [1e-4, 1e-5, 1e-6, 1e-7] # step length - steps = [1e-5, 1e-6, 1e-7, 1e-8] # step length - with fire_adj.stop_annotating(): - delta_m = Function(V) # model direction (random) - delta_m.assign(dJ) - Jhat = fire_adj.ReducedFunctional(Jm, control) - derivative = enlisting.Enlist(Jhat.derivative()) - hs = enlisting.Enlist(delta_m) - - projnorm = sum(hi._ad_dot(di) for hi, di in zip(hs, derivative)) - - # this deepcopy is important otherwise pertubations accumulate - vp_original = vp_guess.copy(deepcopy=True) - - if comm.comm.rank == 0: - print( - "######## Computing the gradient by finite diferences ########", - flush=True, - ) - errors = [] - for step in steps: # range(3): - # steps.append(step) - # perturb the model and calculate the functional (again) - # J(m + delta_m*h) - vp_guess = vp_original + step * delta_m - p_guess_recv, Jp = forward( - model, - mesh, - comm, - vp_guess, - sources, - wavelet, - point_cloud, - fwi=True, - true_rec=p_exact_recv, - ) - - fd_grad = (Jp - Jm) / step - if comm.comm.rank == 0: - print( - "\n Cost functional for step " - + str(step) - + " : " - + str(Jp) - + ", fd approx.: " - + str(fd_grad) - + ", grad'*dir : " - + str(projnorm) - + " \n ", - flush=True, - ) - - errors.append(100 * ((fd_grad - projnorm) / projnorm)) - - fire_adj.get_working_tape().clear_tape() - - # all errors less than 1 % - errors = np.array(errors) - assert (np.abs(errors) < 5.0).all() +# import numpy as np +# from firedrake import * +# from pyadjoint import enlisting +# import spyro + +# forward = spyro.solvers.forward_AD + + +# def gradient_test_acoustic( +# model, mesh, V, comm, vp_exact, vp_guess, mask=None +# ): +# """Gradient test for the acoustic FWI problem + +# Parameters +# ---------- +# model : `dictionary` +# Contains simulation parameters and options. +# mesh : a Firedrake.mesh +# 2D/3D simplicial mesh read in by Firedrake.Mesh +# V : Firedrake.FunctionSpace object +# The space of the finite elements +# comm : Firedrake.ensemble_communicator +# An ensemble communicator +# vp_exact : Firedrake.Function +# The exact velocity model +# vp_guess : Firedrake.Function +# The guess velocity model +# mask : Firedrake.Function, optional +# A mask for the gradient test + +# Returns +# ------- +# None +# """ +# import firedrake_adjoint as fire_adj + +# with fire_adj.stop_annotating(): +# if comm.comm.rank == 0: +# print("######## Starting gradient test ########", flush=True) + +# sources = spyro.Sources(model, mesh, V, comm) +# receivers = spyro.Receivers(model, mesh, V, comm) + +# wavelet = spyro.full_ricker_wavelet( +# model["timeaxis"]["dt"], +# model["timeaxis"]["tf"], +# model["acquisition"]["frequency"], +# ) +# point_cloud = receivers.set_point_cloud(comm) +# # simulate the exact model +# if comm.comm.rank == 0: +# print("######## Running the exact model ########", flush=True) +# p_exact_recv = forward( +# model, mesh, comm, vp_exact, sources, wavelet, point_cloud +# ) + +# # simulate the guess model +# if comm.comm.rank == 0: +# print("######## Running the guess model ########", flush=True) +# p_guess_recv, Jm = forward( +# model, +# mesh, +# comm, +# vp_guess, +# sources, +# wavelet, +# point_cloud, +# fwi=True, +# true_rec=p_exact_recv, +# ) +# if comm.comm.rank == 0: +# print( +# "\n Cost functional at fixed point : " + str(Jm) + " \n ", +# flush=True, +# ) + +# # compute the gradient of the control (to be verified) +# if comm.comm.rank == 0: +# print( +# "######## Computing the gradient by automatic differentiation ########", +# flush=True, +# ) +# control = fire_adj.Control(vp_guess) +# dJ = fire_adj.compute_gradient(Jm, control) +# if mask: +# dJ *= mask + +# # File("gradient.pvd").write(dJ) + +# # steps = [1e-3, 1e-4, 1e-5, 1e-6, 1e-7] # step length +# # steps = [1e-4, 1e-5, 1e-6, 1e-7] # step length +# steps = [1e-5, 1e-6, 1e-7, 1e-8] # step length +# with fire_adj.stop_annotating(): +# delta_m = Function(V) # model direction (random) +# delta_m.assign(dJ) +# Jhat = fire_adj.ReducedFunctional(Jm, control) +# derivative = enlisting.Enlist(Jhat.derivative()) +# hs = enlisting.Enlist(delta_m) + +# projnorm = sum(hi._ad_dot(di) for hi, di in zip(hs, derivative)) + +# # this deepcopy is important otherwise pertubations accumulate +# vp_original = vp_guess.copy(deepcopy=True) + +# if comm.comm.rank == 0: +# print( +# "######## Computing the gradient by finite diferences ########", +# flush=True, +# ) +# errors = [] +# for step in steps: # range(3): +# # steps.append(step) +# # perturb the model and calculate the functional (again) +# # J(m + delta_m*h) +# vp_guess = vp_original + step * delta_m +# p_guess_recv, Jp = forward( +# model, +# mesh, +# comm, +# vp_guess, +# sources, +# wavelet, +# point_cloud, +# fwi=True, +# true_rec=p_exact_recv, +# ) + +# fd_grad = (Jp - Jm) / step +# if comm.comm.rank == 0: +# print( +# "\n Cost functional for step " +# + str(step) +# + " : " +# + str(Jp) +# + ", fd approx.: " +# + str(fd_grad) +# + ", grad'*dir : " +# + str(projnorm) +# + " \n ", +# flush=True, +# ) + +# errors.append(100 * ((fd_grad - projnorm) / projnorm)) + +# fire_adj.get_working_tape().clear_tape() + +# # all errors less than 1 % +# errors = np.array(errors) +# assert (np.abs(errors) < 5.0).all() diff --git a/spyro/tools/grid_point_calculator.py b/spyro/tools/grid_point_calculator.py index ba14ea9f..5f7305a7 100644 --- a/spyro/tools/grid_point_calculator.py +++ b/spyro/tools/grid_point_calculator.py @@ -1,643 +1,659 @@ -from mpi4py import MPI -import numpy as np -from scipy import interpolate -import meshio -import firedrake as fire -import time -import copy -import spyro - - -def minimum_grid_point_calculator(grid_point_calculator_parameters): - """Function to calculate necessary grid point density. - - Parameters - ---------- - grid_point_calculator_parameters: Python 'dictionary' - Has all parameters related to the experiment. An example is provided in the demo file. - - Returns - ------- - G: `float` - Minimum grid point density necessary for a `experiment_type` mesh with a FEM whith - the degree and method specified within the specified error tolerance - """ - G_reference = grid_point_calculator_parameters["G_reference"] - degree_reference = grid_point_calculator_parameters["reference_degree"] - G_initial = grid_point_calculator_parameters["G_initial"] - desired_degree = grid_point_calculator_parameters["desired_degree"] - TOL = grid_point_calculator_parameters["accepted_error_threshold"] - - model = spyro.tools.create_model_for_grid_point_calculation( - grid_point_calculator_parameters, degree_reference - ) - # print("Model built at time "+str(time.time()-start_time), flush = True) - comm = spyro.utils.mpi_init(model) - # print("Comm built at time "+str(time.time()-start_time), flush = True) - if comm.comm.rank == 0: - print("Entering search", flush=True) - p_exact = wave_solver(model, G=G_reference, comm=comm) - if comm.comm.rank == 0: - print("p_exact calculation finished", flush=True) - - comm.comm.barrier() - - model = spyro.tools.create_model_for_grid_point_calculation( - grid_point_calculator_parameters, desired_degree - ) - G = searching_for_minimum(model, p_exact, TOL, starting_G=G_initial, comm=comm) - - return G - - -def wave_solver(model, G, comm=False): - """Forward solver for the acoustic wave equation - - Parameters - ---------- - model : `dictionary` - Contains simulation parameters and options. - G : `float` - Grid point density - comm : Firedrake.ensemble_communicator, optional - An ensemble communicator - - Returns - ------- - p_recv : - The pressure field at the receivers - - """ - minimum_mesh_velocity = model["testing_parameters"]["minimum_mesh_velocity"] - model["mesh"]["meshfile"] = "meshes/2Dhomogeneous" + str(G) + ".msh" - try: - mesh, V = spyro.io.read_mesh(model, comm) - except: # noqa E722 - model = generate_mesh(model, G, comm) - mesh, V = spyro.io.read_mesh(model, comm) - - if model["testing_parameters"]["experiment_type"] == "homogeneous": - vp_exact = fire.Constant(minimum_mesh_velocity) - elif model["testing_parameters"]["experiment_type"] == "heterogeneous": - vp_exact = spyro.io.interpolate(model, mesh, V, guess=False) - - if model["opts"]["method"] == "KMV": - estimate_max_eigenvalue = True - else: - estimate_max_eigenvalue = False - new_dt = 0.2 * spyro.estimate_timestep( - mesh, V, vp_exact, estimate_max_eigenvalue=estimate_max_eigenvalue - ) - - model["timeaxis"]["dt"] = comm.comm.allreduce(new_dt, op=MPI.MIN) - if comm.comm.rank == 0: - print( - f"Maximum stable timestep is: {model['timeaxis']['dt']} seconds", - flush=True, - ) - if model["timeaxis"]["dt"] > 0.001: - model["timeaxis"]["dt"] = 0.001 - if comm.comm.rank == 0: - print( - f"Timestep used is: {model['timeaxis']['dt']} seconds", - flush=True, - ) - - sources = spyro.Sources(model, mesh, V, comm) - receivers = spyro.Receivers(model, mesh, V, comm) - wavelet = spyro.full_ricker_wavelet( - dt=model["timeaxis"]["dt"], - tf=model["timeaxis"]["tf"], - freq=model["acquisition"]["frequency"], - ) - - for sn in range(model["acquisition"]["num_sources"]): - if spyro.io.is_owner(comm, sn): - t1 = time.time() - p_field, p_recv = spyro.solvers.forward( - model, - mesh, - comm, - vp_exact, - sources, - wavelet, - receivers, - source_num=sn, - output=False, - ) - print(time.time() - t1) - - return p_recv - - -def generate_mesh(model, G, comm): - """Function to generate a mesh - - Parameters - ---------- - model : `dictionary` - Contains simulation parameters and options. - G : `float` - Grid point density - comm : Firedrake.ensemble_communicator - An ensemble communicator - - Returns - ------- - mesh : `firedrake.mesh` - The mesh - """ - if model["opts"]["dimension"] == 2: - mesh = generate_mesh2D(model, G, comm) - elif model["opts"]["dimension"] == 3: - mesh = generate_mesh3D(model, G, comm) - else: - raise ValueError("Wrong dimension in input model.") - return mesh - - -def searching_for_minimum( - model, p_exact, TOL, accuracy=0.1, starting_G=7.0, comm=False -): - """Function to find the minimum grid point density for a given error - - Parameters - ---------- - model : `dictionary` - Contains simulation parameters and options. - p_exact : `firedrake.Function` - The exact pressure field - TOL : `float` - The accepted error threshold - accuracy : `float`, optional - The accuracy of the search - starting_G : `float`, optional - The starting grid point density - comm : Firedrake.ensemble_communicator, optional - An ensemble communicator - - Returns - ------- - G : `float` - The minimum grid point density - - """ - error = 100.0 - G = starting_G - - # fast loop - print("Entering fast loop", flush=True) - while error > TOL: - dif = max(G * 0.1, accuracy) - G = G + dif - print("With G equal to " + str(G)) - print("Entering wave solver", flush=True) - p0 = wave_solver(model, G, comm) - error = error_calc(p_exact, p0, model, comm=comm) - print("Error of " + str(error)) - - G -= dif - G = np.round(G, 1) - accuracy - # slow loop - if dif > accuracy: - print("Entering slow loop", flush=True) - error = 100.0 - while error > TOL: - dif = accuracy - G = G + dif - print("With G equal to " + str(G)) - print("Entering wave solver", flush=True) - p0 = wave_solver(model, G, comm) - error = error_calc(p_exact, p0, model, comm=comm) - print("Error of " + str(error)) - - return G - - -def grid_point_to_mesh_point_converter_for_seismicmesh(model, G): - degree = model["opts"]["degree"] - if model["opts"]["method"] == "KMV": - if degree == 1: - M = G / 0.707813887967734 - if degree == 2: - M = 0.5 * G / 0.8663141029672784 - if degree == 3: - M = 0.2934695559090401 * G / 0.7483761673104953 - if degree == 4: - M = 0.21132486540518713 * G / 0.7010127254535244 - if degree == 5: - M = 0.20231237605867816 * G / 0.9381929803311276 - - if model["opts"]["method"] == "CG": - raise ValueError("Correct M to G conversion to be inputed for CG") - # if degree == 1: - # M = G - # if degree == 2: - # M = 0.5*G - # if degree == 3: - # M = 0.333333333333333*G - # if degree == 4: - # M = 0.25*G - # if degree == 5: - # M = 0.2*G - - if model["opts"]["method"] == "spectral": - raise ValueError("Correct M to G conversion to be inputed for spectral") - # if degree == 1: - # M = G - # if degree == 2: - # M = 0.5*G - # if degree == 3: - # M = 0.27639320225002106*G - # if degree == 4: - # M = 0.32732683535398854*G - # if degree == 5: - # M = 0.23991190372440996*G - - return M - - -def error_calc(p_exact, p, model, comm=False): - """ Calculates the error between the exact and the numerical solution - - Parameters - ---------- - p_exact : `firedrake.Function` - The exact pressure field - p : `firedrake.Function` - The numerical pressure field - model : `dictionary` - Contains simulation parameters and options. - comm : Firedrake.ensemble_communicator, optional - An ensemble communicator - - Returns - ------- - error : `float` - The error between the exact and the numerical solution - - """ - # p0 doesn't necessarily have the same dt as p_exact - # therefore we have to interpolate the missing points - # to have them at the same length - # testing shape - times_p_exact, _ = p_exact.shape - times_p, _ = p.shape - if times_p_exact > times_p: # then we interpolate p_exact - times, receivers = p.shape - dt = model["timeaxis"]["tf"] / times - p_exact = time_interpolation(p_exact, p, model) - elif times_p_exact < times_p: # then we interpolate p - times, receivers = p_exact.shape - dt = model["timeaxis"]["tf"] / times - p = time_interpolation(p, p_exact, model) - else: # then we dont need to interpolate - times, receivers = p.shape - dt = model["timeaxis"]["tf"] / times - # p = time_interpolation(p, p_exact, model) - - max_absolute_diff = 0.0 - max_percentage_diff = 0.0 - - if comm.ensemble_comm.rank == 0: - numerator = 0.0 - denominator = 0.0 - for receiver in range(receivers): - numerator_time_int = 0.0 - denominator_time_int = 0.0 - for t in range(times - 1): - top_integration = (p_exact[t, receiver] - p[t, receiver]) ** 2 * dt - bot_integration = (p_exact[t, receiver]) ** 2 * dt - - # Adding 1e-25 filter to receivers to eliminate noise - numerator_time_int += top_integration - - denominator_time_int += bot_integration - - diff = p_exact[t, receiver] - p[t, receiver] - if abs(diff) > 1e-15 and abs(diff) > max_absolute_diff: - max_absolute_diff = copy.deepcopy(diff) - - if abs(diff) > 1e-15 and abs(p_exact[t, receiver]) > 1e-15: - percentage_diff = abs(diff / p_exact[t, receiver]) * 100 - if percentage_diff > max_percentage_diff: - max_percentage_diff = copy.deepcopy(percentage_diff) - - numerator += numerator_time_int - denominator += denominator_time_int - - if denominator > 1e-15: - error = np.sqrt(numerator / denominator) - - # if numerator < 1e-15: - # print('Warning: error too small to measure correctly.', flush = True) - # error = 0.0 - if denominator < 1e-15: - print("Warning: receivers don't appear to register a shot.", flush=True) - error = 0.0 - - # print("ERROR IS ", flush = True) - # print(error, flush = True) - # print("Maximum absolute error ", flush = True) - # print(max_absolute_diff, flush = True) - # print("Maximum percentage error ", flush = True) - # print(max_percentage_diff, flush = True) - return error - - -def error_calc_line(p_exact, p, model, comm=False): - # p0 doesn't necessarily have the same dt as p_exact - # therefore we have to interpolate the missing points - # to have them at the same length - # testing shape - (times_p_exact,) = p_exact.shape - (times_p,) = p.shape - if times_p_exact > times_p: # then we interpolate p_exact - (times,) = p.shape - dt = model["timeaxis"]["tf"] / times - p_exact = time_interpolation_line(p_exact, p, model) - elif times_p_exact < times_p: # then we interpolate p - (times,) = p_exact.shape - dt = model["timeaxis"]["tf"] / times - p = time_interpolation_line(p, p_exact, model) - else: # then we dont need to interpolate - (times,) = p.shape - dt = model["timeaxis"]["tf"] / times - - if comm.ensemble_comm.rank == 0: - numerator_time_int = 0.0 - denominator_time_int = 0.0 - # Integrating with trapezoidal rule - for t in range(times - 1): - numerator_time_int += (p_exact[t] - p[t]) ** 2 - denominator_time_int += (p_exact[t]) ** 2 - numerator_time_int -= ( - (p_exact[0] - p[0]) ** 2 + (p_exact[times - 1] - p[times - 1]) ** 2 - ) / 2 - numerator_time_int *= dt - denominator_time_int -= (p_exact[0] ** 2 + p_exact[times - 1] ** 2) / 2 - denominator_time_int *= dt - - # if denominator_time_int > 1e-15: - error = np.sqrt(numerator_time_int / denominator_time_int) - - if denominator_time_int < 1e-15: - print("Warning: receivers don't appear to register a shot.", flush=True) - error = 0.0 - - return error - - -def time_interpolation(p_old, p_exact, model): - times, receivers = p_exact.shape - dt = model["timeaxis"]["tf"] / times - - times_old, rec = p_old.shape - dt_old = model["timeaxis"]["tf"] / times_old - time_vector_old = np.zeros((1, times_old)) - for ite in range(times_old): - time_vector_old[0, ite] = dt_old * ite - - time_vector_new = np.zeros((1, times)) - for ite in range(times): - time_vector_new[0, ite] = dt * ite - - p = np.zeros((times, receivers)) - for receiver in range(receivers): - f = interpolate.interp1d(time_vector_old[0, :], p_old[:, receiver]) - p[:, receiver] = f(time_vector_new[0, :]) - - return p - - -def time_interpolation_line(p_old, p_exact, model): - (times,) = p_exact.shape - dt = model["timeaxis"]["tf"] / times - - (times_old,) = p_old.shape - dt_old = model["timeaxis"]["tf"] / times_old - time_vector_old = np.zeros((1, times_old)) - for ite in range(times_old): - time_vector_old[0, ite] = dt_old * ite - - time_vector_new = np.zeros((1, times)) - for ite in range(times): - time_vector_new[0, ite] = dt * ite - - p = np.zeros((times,)) - f = interpolate.interp1d(time_vector_old[0, :], p_old[:]) - p[:] = f(time_vector_new[0, :]) - - return p - - -def generate_mesh2D(model, G, comm): - """Generates 2D mesh using seismicmesh - Parameters - ---------- - model : dict - Dictionary containing the model parameters - G : float - Grid points per wavelength - comm : object - MPI communicator - """ - import SeismicMesh - - if comm.comm.rank == 0: - print("Entering mesh generation", flush=True) - M = grid_point_to_mesh_point_converter_for_seismicmesh(model, G) - - Lz = model["mesh"]["Lz"] - lz = model["BCs"]["lz"] - Lx = model["mesh"]["Lx"] - lx = model["BCs"]["lx"] - - Real_Lz = Lz + lz - Real_Lx = Lx + 2 * lx - - if model["testing_parameters"]["experiment_type"] == "homogeneous": - - minimum_mesh_velocity = model["testing_parameters"]["minimum_mesh_velocity"] - frequency = model["acquisition"]["frequency"] - lbda = minimum_mesh_velocity / frequency - - Real_Lz = Lz + lz - Real_Lx = Lx + 2 * lx - edge_length = lbda / M - - bbox = (-Real_Lz, 0.0, -lx, Real_Lx - lx) - rec = SeismicMesh.Rectangle(bbox) - - if comm.comm.rank == 0: - # Creating rectangular mesh - points, cells = SeismicMesh.generate_mesh( - domain=rec, - edge_length=edge_length, - mesh_improvement=False, - comm=comm.ensemble_comm, - verbose=0, - ) - print("entering spatial rank 0 after mesh generation") - - points, cells = SeismicMesh.geometry.delete_boundary_entities( - points, cells, min_qual=0.6 - ) - - meshio.write_points_cells( - "meshes/2Dhomogeneous" + str(G) + ".msh", - points, - [("triangle", cells)], - file_format="gmsh22", - binary=False, - ) - meshio.write_points_cells( - "meshes/2Dhomogeneous" + str(G) + ".vtk", - points, - [("triangle", cells)], - file_format="vtk", - ) - - if comm.comm.rank == 0: - print("Finishing mesh generation", flush=True) - - elif model["testing_parameters"]["experiment_type"] == "heterogeneous": - # Name of SEG-Y file containg velocity model. - fname = "vel_z6.25m_x12.5m_exact.segy" - - # Bounding box describing domain extents (corner coordinates) - bbox = (-12000.0, 0.0, 0.0, 67000.0) - - rectangle = SeismicMesh.Rectangle(bbox) - - # Desired minimum mesh size in domain - frequency = model["acquisition"]["frequency"] - hmin = 1429.0 / (M * frequency) - - # Construct mesh sizing object from velocity model - ef = SeismicMesh.get_sizing_function_from_segy( - fname, - bbox, - hmin=hmin, - wl=M, - freq=5.0, - grade=0.15, - domain_pad=model["BCs"]["lz"], - pad_style="edge", - ) - - points, cells = SeismicMesh.generate_mesh( - domain=rectangle, edge_length=ef, verbose=0, mesh_improvement=False - ) - - meshio.write_points_cells( - "meshes/2Dheterogeneous" + str(G) + ".msh", - points / 1000, - [("triangle", cells)], - file_format="gmsh22", - binary=False, - ) - meshio.write_points_cells( - "meshes/2Dheterogeneous" + str(G) + ".vtk", - points / 1000, - [("triangle", cells)], - file_format="vtk", - ) - - comm.comm.barrier() - - return model - - -def generate_mesh3D(model, G, comm): - """Generates 3D mesh using seismicmesh - Parameters - ---------- - model : dict - Dictionary containing the model parameters - G : float - Grid points per wavelength - comm : object - MPI communicator - """ - import SeismicMesh - - print("Entering mesh generation", flush=True) - M = grid_point_to_mesh_point_converter_for_seismicmesh(model, G) - method = model["opts"]["method"] - - Lz = model["mesh"]["Lz"] - lz = model["BCs"]["lz"] - Lx = model["mesh"]["Lx"] - lx = model["BCs"]["lx"] - Ly = model["mesh"]["Ly"] - ly = model["BCs"]["ly"] - - Real_Lz = Lz + lz - Real_Lx = Lx + 2 * lx - Real_Ly = Ly + 2 * ly - - minimum_mesh_velocity = model["testing_parameters"]["minimum_mesh_velocity"] - frequency = model["acquisition"]["frequency"] - lbda = minimum_mesh_velocity / frequency - - edge_length = lbda / M - # print(Real_Lz) - - bbox = (-Real_Lz, 0.0, -lx, Real_Lx - lx, -ly, Real_Ly - ly) - cube = SeismicMesh.Cube(bbox) - - if comm.comm.rank == 0: - # Creating rectangular mesh - points, cells = SeismicMesh.generate_mesh( - domain=cube, - edge_length=edge_length, - mesh_improvement=False, - max_iter=75, - comm=comm.ensemble_comm, - verbose=0, - ) - - points, cells = SeismicMesh.sliver_removal( - points=points, - bbox=bbox, - domain=cube, - edge_length=edge_length, - preserve=True, - max_iter=200, - ) - - print("entering spatial rank 0 after mesh generation") - - meshio.write_points_cells( - "meshes/3Dhomogeneous" + str(G) + ".msh", - points, - [("tetra", cells)], - file_format="gmsh22", - binary=False, - ) - meshio.write_points_cells( - "meshes/3Dhomogeneous" + str(G) + ".vtk", - points, - [("tetra", cells)], - file_format="vtk", - ) - - comm.comm.barrier() - if method == "CG" or method == "KMV": - mesh = fire.Mesh( - "meshes/3Dhomogeneous" + str(G) + ".msh", - distribution_parameters={ - "overlap_type": (fire.DistributedMeshOverlapType.NONE, 0) - }, - ) - - print("Finishing mesh generation", flush=True) - return mesh - - -def mesh_generation(model, Gs, comm): - for G in Gs: - _ = generate_mesh(model, G, comm) - - return True +# from mpi4py import MPI +# import numpy as np +# from scipy import interpolate +# import meshio +# import firedrake as fire +# import time +# import copy +# import spyro + + +# def minimum_grid_point_calculator(grid_point_calculator_parameters): +# """Function to calculate necessary grid point density. + +# Parameters +# ---------- +# grid_point_calculator_parameters: Python 'dictionary' +# Has all parameters related to the experiment. An example is provided in the demo file. + +# Returns +# ------- +# G: `float` +# Minimum grid point density necessary for a `experiment_type` mesh with a FEM whith +# the degree and method specified within the specified error tolerance +# """ +# G_reference = grid_point_calculator_parameters["G_reference"] +# degree_reference = grid_point_calculator_parameters["reference_degree"] +# G_initial = grid_point_calculator_parameters["G_initial"] +# desired_degree = grid_point_calculator_parameters["desired_degree"] +# TOL = grid_point_calculator_parameters["accepted_error_threshold"] + +# model = spyro.tools.create_model_for_grid_point_calculation( +# grid_point_calculator_parameters, degree_reference +# ) +# # print("Model built at time "+str(time.time()-start_time), flush = True) +# comm = spyro.utils.mpi_init(model) +# # print("Comm built at time "+str(time.time()-start_time), flush = True) +# if comm.comm.rank == 0: +# print("Entering search", flush=True) +# p_exact = wave_solver(model, G=G_reference, comm=comm) +# if comm.comm.rank == 0: +# print("p_exact calculation finished", flush=True) + +# comm.comm.barrier() + +# model = spyro.tools.create_model_for_grid_point_calculation( +# grid_point_calculator_parameters, desired_degree +# ) +# G = searching_for_minimum( +# model, p_exact, TOL, starting_G=G_initial, comm=comm +# ) + +# return G + + +# def wave_solver(model, G, comm=False): +# """Forward solver for the acoustic wave equation + +# Parameters +# ---------- +# model : `dictionary` +# Contains simulation parameters and options. +# G : `float` +# Grid point density +# comm : Firedrake.ensemble_communicator, optional +# An ensemble communicator + +# Returns +# ------- +# p_recv : +# The pressure field at the receivers + +# """ +# minimum_mesh_velocity = model["testing_parameters"][ +# "minimum_mesh_velocity" +# ] +# model["mesh"]["meshfile"] = "meshes/2Dhomogeneous" + str(G) + ".msh" +# try: +# mesh, V = spyro.basicio.read_mesh(model, comm) +# except: +# model = generate_mesh(model, G, comm) +# mesh, V = spyro.basicio.read_mesh(model, comm) + +# if model["testing_parameters"]["experiment_type"] == "homogeneous": +# vp_exact = fire.Constant(minimum_mesh_velocity) +# elif model["testing_parameters"]["experiment_type"] == "heterogeneous": +# vp_exact = spyro.basicio.interpolate(model, mesh, V, guess=False) + +# if model["opts"]["method"] == "KMV": +# estimate_max_eigenvalue = True +# else: +# estimate_max_eigenvalue = False +# new_dt = 0.2 * spyro.estimate_timestep( +# mesh, V, vp_exact, estimate_max_eigenvalue=estimate_max_eigenvalue +# ) + +# model["timeaxis"]["dt"] = comm.comm.allreduce(new_dt, op=MPI.MIN) +# if comm.comm.rank == 0: +# print( +# f"Maximum stable timestep is: {model['timeaxis']['dt']} seconds", +# flush=True, +# ) +# if model["timeaxis"]["dt"] > 0.001: +# model["timeaxis"]["dt"] = 0.001 +# if comm.comm.rank == 0: +# print( +# f"Timestep used is: {model['timeaxis']['dt']} seconds", +# flush=True, +# ) + +# sources = spyro.Sources(model, mesh, V, comm) +# receivers = spyro.Receivers(model, mesh, V, comm) +# wavelet = spyro.full_ricker_wavelet( +# dt=model["timeaxis"]["dt"], +# tf=model["timeaxis"]["tf"], +# freq=model["acquisition"]["frequency"], +# ) + +# for sn in range(model["acquisition"]["num_sources"]): +# if spyro.basicio.is_owner(comm, sn): +# t1 = time.time() +# p_field, p_recv = spyro.solvers.forward( +# model, +# mesh, +# comm, +# vp_exact, +# sources, +# wavelet, +# receivers, +# source_num=sn, +# output=False, +# ) +# print(time.time() - t1) + +# return p_recv + + +# def generate_mesh(model, G, comm): +# """Function to generate a mesh + +# Parameters +# ---------- +# model : `dictionary` +# Contains simulation parameters and options. +# G : `float` +# Grid point density +# comm : Firedrake.ensemble_communicator +# An ensemble communicator + +# Returns +# ------- +# mesh : `firedrake.mesh` +# The mesh +# """ +# if model["opts"]["dimension"] == 2: +# mesh = generate_mesh2D(model, G, comm) +# elif model["opts"]["dimension"] == 3: +# mesh = generate_mesh3D(model, G, comm) +# else: +# raise ValueError("Wrong dimension in input model.") +# return mesh + + +# def searching_for_minimum( +# model, p_exact, TOL, accuracy=0.1, starting_G=7.0, comm=False +# ): +# """Function to find the minimum grid point density for a given error + +# Parameters +# ---------- +# model : `dictionary` +# Contains simulation parameters and options. +# p_exact : `firedrake.Function` +# The exact pressure field +# TOL : `float` +# The accepted error threshold +# accuracy : `float`, optional +# The accuracy of the search +# starting_G : `float`, optional +# The starting grid point density +# comm : Firedrake.ensemble_communicator, optional +# An ensemble communicator + +# Returns +# ------- +# G : `float` +# The minimum grid point density + +# """ +# error = 100.0 +# G = starting_G + +# # fast loop +# print("Entering fast loop", flush=True) +# while error > TOL: +# dif = max(G * 0.1, accuracy) +# G = G + dif +# print("With G equal to " + str(G)) +# print("Entering wave solver", flush=True) +# p0 = wave_solver(model, G, comm) +# error = error_calc(p_exact, p0, model, comm=comm) +# print("Error of " + str(error)) + +# G -= dif +# G = np.round(G, 1) - accuracy +# # slow loop +# if dif > accuracy: +# print("Entering slow loop", flush=True) +# error = 100.0 +# while error > TOL: +# dif = accuracy +# G = G + dif +# print("With G equal to " + str(G)) +# print("Entering wave solver", flush=True) +# p0 = wave_solver(model, G, comm) +# error = error_calc(p_exact, p0, model, comm=comm) +# print("Error of " + str(error)) + +# return G + + +# def grid_point_to_mesh_point_converter_for_seismicmesh(model, G): +# degree = model["opts"]["degree"] +# if model["opts"]["method"] == "KMV": +# if degree == 1: +# M = G / 0.707813887967734 +# if degree == 2: +# M = 0.5 * G / 0.8663141029672784 +# if degree == 3: +# M = 0.2934695559090401 * G / 0.7483761673104953 +# if degree == 4: +# M = 0.21132486540518713 * G / 0.7010127254535244 +# if degree == 5: +# M = 0.20231237605867816 * G / 0.9381929803311276 + +# if model["opts"]["method"] == "CG": +# raise ValueError("Correct M to G conversion to be inputed for CG") +# # if degree == 1: +# # M = G +# # if degree == 2: +# # M = 0.5*G +# # if degree == 3: +# # M = 0.333333333333333*G +# # if degree == 4: +# # M = 0.25*G +# # if degree == 5: +# # M = 0.2*G + +# if model["opts"]["method"] == "spectral": +# raise ValueError( +# "Correct M to G conversion to be inputed for spectral" +# ) +# # if degree == 1: +# # M = G +# # if degree == 2: +# # M = 0.5*G +# # if degree == 3: +# # M = 0.27639320225002106*G +# # if degree == 4: +# # M = 0.32732683535398854*G +# # if degree == 5: +# # M = 0.23991190372440996*G + +# return M + + +# def error_calc(p_exact, p, model, comm=False): +# """Calculates the error between the exact and the numerical solution + +# Parameters +# ---------- +# p_exact : `firedrake.Function` +# The exact pressure field +# p : `firedrake.Function` +# The numerical pressure field +# model : `dictionary` +# Contains simulation parameters and options. +# comm : Firedrake.ensemble_communicator, optional +# An ensemble communicator + +# Returns +# ------- +# error : `float` +# The error between the exact and the numerical solution + +# """ +# # p0 doesn't necessarily have the same dt as p_exact +# # therefore we have to interpolate the missing points +# # to have them at the same length +# # testing shape +# times_p_exact, _ = p_exact.shape +# times_p, _ = p.shape +# if times_p_exact > times_p: # then we interpolate p_exact +# times, receivers = p.shape +# dt = model["timeaxis"]["tf"] / times +# p_exact = time_interpolation(p_exact, p, model) +# elif times_p_exact < times_p: # then we interpolate p +# times, receivers = p_exact.shape +# dt = model["timeaxis"]["tf"] / times +# p = time_interpolation(p, p_exact, model) +# else: # then we dont need to interpolate +# times, receivers = p.shape +# dt = model["timeaxis"]["tf"] / times +# # p = time_interpolation(p, p_exact, model) + +# max_absolute_diff = 0.0 +# max_percentage_diff = 0.0 + +# if comm.ensemble_comm.rank == 0: +# numerator = 0.0 +# denominator = 0.0 +# for receiver in range(receivers): +# numerator_time_int = 0.0 +# denominator_time_int = 0.0 +# for t in range(times - 1): +# top_integration = ( +# p_exact[t, receiver] - p[t, receiver] +# ) ** 2 * dt +# bot_integration = (p_exact[t, receiver]) ** 2 * dt + +# # Adding 1e-25 filter to receivers to eliminate noise +# numerator_time_int += top_integration + +# denominator_time_int += bot_integration + +# diff = p_exact[t, receiver] - p[t, receiver] +# if abs(diff) > 1e-15 and abs(diff) > max_absolute_diff: +# max_absolute_diff = copy.deepcopy(diff) + +# if abs(diff) > 1e-15 and abs(p_exact[t, receiver]) > 1e-15: +# percentage_diff = abs(diff / p_exact[t, receiver]) * 100 +# if percentage_diff > max_percentage_diff: +# max_percentage_diff = copy.deepcopy(percentage_diff) + +# numerator += numerator_time_int +# denominator += denominator_time_int + +# if denominator > 1e-15: +# error = np.sqrt(numerator / denominator) + +# # if numerator < 1e-15: +# # print('Warning: error too small to measure correctly.', flush = True) +# # error = 0.0 +# if denominator < 1e-15: +# print( +# "Warning: receivers don't appear to register a shot.", flush=True +# ) +# error = 0.0 + +# # print("ERROR IS ", flush = True) +# # print(error, flush = True) +# # print("Maximum absolute error ", flush = True) +# # print(max_absolute_diff, flush = True) +# # print("Maximum percentage error ", flush = True) +# # print(max_percentage_diff, flush = True) +# return error + + +# def error_calc_line(p_exact, p, model, comm=False): +# # p0 doesn't necessarily have the same dt as p_exact +# # therefore we have to interpolate the missing points +# # to have them at the same length +# # testing shape +# (times_p_exact,) = p_exact.shape +# (times_p,) = p.shape +# if times_p_exact > times_p: # then we interpolate p_exact +# (times,) = p.shape +# dt = model["timeaxis"]["tf"] / times +# p_exact = time_interpolation_line(p_exact, p, model) +# elif times_p_exact < times_p: # then we interpolate p +# (times,) = p_exact.shape +# dt = model["timeaxis"]["tf"] / times +# p = time_interpolation_line(p, p_exact, model) +# else: # then we dont need to interpolate +# (times,) = p.shape +# dt = model["timeaxis"]["tf"] / times + +# if comm.ensemble_comm.rank == 0: +# numerator_time_int = 0.0 +# denominator_time_int = 0.0 +# # Integrating with trapezoidal rule +# for t in range(times - 1): +# numerator_time_int += (p_exact[t] - p[t]) ** 2 +# denominator_time_int += (p_exact[t]) ** 2 +# numerator_time_int -= ( +# (p_exact[0] - p[0]) ** 2 + (p_exact[times - 1] - p[times - 1]) ** 2 +# ) / 2 +# numerator_time_int *= dt +# denominator_time_int -= (p_exact[0] ** 2 + p_exact[times - 1] ** 2) / 2 +# denominator_time_int *= dt + +# # if denominator_time_int > 1e-15: +# error = np.sqrt(numerator_time_int / denominator_time_int) + +# if denominator_time_int < 1e-15: +# print( +# "Warning: receivers don't appear to register a shot.", +# flush=True, +# ) +# error = 0.0 + +# return error + + +# def time_interpolation(p_old, p_exact, model): +# times, receivers = p_exact.shape +# dt = model["timeaxis"]["tf"] / times + +# times_old, rec = p_old.shape +# dt_old = model["timeaxis"]["tf"] / times_old +# time_vector_old = np.zeros((1, times_old)) +# for ite in range(times_old): +# time_vector_old[0, ite] = dt_old * ite + +# time_vector_new = np.zeros((1, times)) +# for ite in range(times): +# time_vector_new[0, ite] = dt * ite + +# p = np.zeros((times, receivers)) +# for receiver in range(receivers): +# f = interpolate.interp1d(time_vector_old[0, :], p_old[:, receiver]) +# p[:, receiver] = f(time_vector_new[0, :]) + +# return p + + +# def time_interpolation_line(p_old, p_exact, model): +# (times,) = p_exact.shape +# dt = model["timeaxis"]["tf"] / times + +# (times_old,) = p_old.shape +# dt_old = model["timeaxis"]["tf"] / times_old +# time_vector_old = np.zeros((1, times_old)) +# for ite in range(times_old): +# time_vector_old[0, ite] = dt_old * ite + +# time_vector_new = np.zeros((1, times)) +# for ite in range(times): +# time_vector_new[0, ite] = dt * ite + +# p = np.zeros((times,)) +# f = interpolate.interp1d(time_vector_old[0, :], p_old[:]) +# p[:] = f(time_vector_new[0, :]) + +# return p + + +# def generate_mesh2D(model, G, comm): +# """Generates 2D mesh using seismicmesh +# Parameters +# ---------- +# model : dict +# Dictionary containing the model parameters +# G : float +# Grid points per wavelength +# comm : object +# MPI communicator +# """ +# import SeismicMesh + +# if comm.comm.rank == 0: +# print("Entering mesh generation", flush=True) +# M = grid_point_to_mesh_point_converter_for_seismicmesh(model, G) + +# Lz = model["mesh"]["Lz"] +# lz = model["BCs"]["lz"] +# Lx = model["mesh"]["Lx"] +# lx = model["BCs"]["lx"] + +# Real_Lz = Lz + lz +# Real_Lx = Lx + 2 * lx + +# if model["testing_parameters"]["experiment_type"] == "homogeneous": +# minimum_mesh_velocity = model["testing_parameters"][ +# "minimum_mesh_velocity" +# ] +# frequency = model["acquisition"]["frequency"] +# lbda = minimum_mesh_velocity / frequency + +# Real_Lz = Lz + lz +# Real_Lx = Lx + 2 * lx +# edge_length = lbda / M + +# bbox = (-Real_Lz, 0.0, -lx, Real_Lx - lx) +# rec = SeismicMesh.Rectangle(bbox) + +# if comm.comm.rank == 0: +# # Creating rectangular mesh +# points, cells = SeismicMesh.generate_mesh( +# domain=rec, +# edge_length=edge_length, +# mesh_improvement=False, +# comm=comm.ensemble_comm, +# verbose=0, +# ) +# print("entering spatial rank 0 after mesh generation") + +# points, cells = SeismicMesh.geometry.delete_boundary_entities( +# points, cells, min_qual=0.6 +# ) + +# meshio.write_points_cells( +# "meshes/2Dhomogeneous" + str(G) + ".msh", +# points, +# [("triangle", cells)], +# file_format="gmsh22", +# binary=False, +# ) +# meshio.write_points_cells( +# "meshes/2Dhomogeneous" + str(G) + ".vtk", +# points, +# [("triangle", cells)], +# file_format="vtk", +# ) + +# if comm.comm.rank == 0: +# print("Finishing mesh generation", flush=True) + +# elif model["testing_parameters"]["experiment_type"] == "heterogeneous": +# # Name of SEG-Y file containg velocity model. +# fname = "vel_z6.25m_x12.5m_exact.segy" + +# # Bounding box describing domain extents (corner coordinates) +# bbox = (-12000.0, 0.0, 0.0, 67000.0) + +# rectangle = SeismicMesh.Rectangle(bbox) + +# # Desired minimum mesh size in domain +# frequency = model["acquisition"]["frequency"] +# hmin = 1429.0 / (M * frequency) + +# # Construct mesh sizing object from velocity model +# ef = SeismicMesh.get_sizing_function_from_segy( +# fname, +# bbox, +# hmin=hmin, +# wl=M, +# freq=5.0, +# grade=0.15, +# domain_pad=model["BCs"]["lz"], +# pad_style="edge", +# ) + +# points, cells = SeismicMesh.generate_mesh( +# domain=rectangle, edge_length=ef, verbose=0, mesh_improvement=False +# ) + +# meshio.write_points_cells( +# "meshes/2Dheterogeneous" + str(G) + ".msh", +# points / 1000, +# [("triangle", cells)], +# file_format="gmsh22", +# binary=False, +# ) +# meshio.write_points_cells( +# "meshes/2Dheterogeneous" + str(G) + ".vtk", +# points / 1000, +# [("triangle", cells)], +# file_format="vtk", +# ) + +# comm.comm.barrier() + +# return model + + +# def generate_mesh3D(model, G, comm): +# """Generates 3D mesh using seismicmesh +# Parameters +# ---------- +# model : dict +# Dictionary containing the model parameters +# G : float +# Grid points per wavelength +# comm : object +# MPI communicator +# """ +# import SeismicMesh + +# print("Entering mesh generation", flush=True) +# M = grid_point_to_mesh_point_converter_for_seismicmesh(model, G) +# method = model["opts"]["method"] + +# Lz = model["mesh"]["Lz"] +# lz = model["BCs"]["lz"] +# Lx = model["mesh"]["Lx"] +# lx = model["BCs"]["lx"] +# Ly = model["mesh"]["Ly"] +# ly = model["BCs"]["ly"] + +# Real_Lz = Lz + lz +# Real_Lx = Lx + 2 * lx +# Real_Ly = Ly + 2 * ly + +# minimum_mesh_velocity = model["testing_parameters"][ +# "minimum_mesh_velocity" +# ] +# frequency = model["acquisition"]["frequency"] +# lbda = minimum_mesh_velocity / frequency + +# edge_length = lbda / M +# # print(Real_Lz) + +# bbox = (-Real_Lz, 0.0, -lx, Real_Lx - lx, -ly, Real_Ly - ly) +# cube = SeismicMesh.Cube(bbox) + +# if comm.comm.rank == 0: +# # Creating rectangular mesh +# points, cells = SeismicMesh.generate_mesh( +# domain=cube, +# edge_length=edge_length, +# mesh_improvement=False, +# max_iter=75, +# comm=comm.ensemble_comm, +# verbose=0, +# ) + +# points, cells = SeismicMesh.sliver_removal( +# points=points, +# bbox=bbox, +# domain=cube, +# edge_length=edge_length, +# preserve=True, +# max_iter=200, +# ) + +# print("entering spatial rank 0 after mesh generation") + +# meshio.write_points_cells( +# "meshes/3Dhomogeneous" + str(G) + ".msh", +# points, +# [("tetra", cells)], +# file_format="gmsh22", +# binary=False, +# ) +# meshio.write_points_cells( +# "meshes/3Dhomogeneous" + str(G) + ".vtk", +# points, +# [("tetra", cells)], +# file_format="vtk", +# ) + +# comm.comm.barrier() +# if method == "CG" or method == "KMV": +# mesh = fire.Mesh( +# "meshes/3Dhomogeneous" + str(G) + ".msh", +# distribution_parameters={ +# "overlap_type": (fire.DistributedMeshOverlapType.NONE, 0) +# }, +# ) + +# print("Finishing mesh generation", flush=True) +# return mesh + + +# def mesh_generation(model, Gs, comm): +# for G in Gs: +# _ = generate_mesh(model, G, comm) + +# return True diff --git a/spyro/tools/input_models.py b/spyro/tools/input_models.py index fa435958..46148ce3 100644 --- a/spyro/tools/input_models.py +++ b/spyro/tools/input_models.py @@ -1,504 +1,168 @@ import numpy as np import spyro -import shutil - - -def create_3d_grid(start, end, num): - """Create a 3d grid of `num**3` points between `start1` - and `end1` and `start2` and `end2` - - Parameters - ---------- - start: tuple of floats - starting position coordinate - end: tuple of floats - ending position coordinate - num: integer - number of receivers between `start` and `end` - - Returns - ------- - receiver_locations: a list of tuples - - """ - (start1, start2, start3) = start - (end1, end2, end3) = end - x = np.linspace(start1, end1, num) - y = np.linspace(start2, end2, num) - z = np.linspace(start3, end3, num) - X, Y, Z = np.meshgrid(x, y, z) - points = np.vstack((X.flatten(), Y.flatten(), Z.flatten())).T - return [tuple(point) for point in points] +import warnings + + +def build_on_top_of_base_dictionary(variables): + if variables["method"] == "mass_lumped_triangle": + mesh_type = "SeismicMesh" + elif variables["method"] == "spectral_quadrilateral": + mesh_type = "firedrake_mesh" + model_dictionary = {} + model_dictionary["options"] = { + "method": variables["method"], + "degree": variables["degree"], + "dimension": variables["dimension"], + "automatic_adjoint": False, + } + model_dictionary["parallelism"] = { + "type": "automatic", + } + model_dictionary["mesh"] = { + "Lz": variables["Lz"], + "Lx": variables["Lx"], + "Ly": variables["Ly"], + "cells_per_wavelength": variables["cells_per_wavelength"], + "mesh_type": mesh_type, + } + model_dictionary["absorving_boundary_conditions"] = { + "status": True, + "damping_type": "PML", + "exponent": 2, + "cmax": 4.5, + "R": 1e-6, + "pad_length": variables["pad"], + } + model_dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": variables["source_locations"], + "frequency": variables["frequency"], + "receiver_locations": variables["receiver_locations"], + } + model_dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": variables["final_time"], # Final time for event + "dt": variables["dt"], # timestep size + "amplitude": 1, # the Ricker has an amplitude of 1. + "output_frequency": 1000, # how frequently to output solution to pvds + "gradient_sampling_frequency": 100, # how frequently to save solution to RAM + } + model_dictionary["visualization"] = { + "forward_output": False, + "output_filename": "results/forward_output.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": False, + "gradient_filename": None, + } + + return model_dictionary + + +def create_initial_model_for_meshing_parameter(Meshing_calc_obj): + dimension = Meshing_calc_obj.dimension + if dimension == 2: + return create_initial_model_for_meshing_parameter_2D(Meshing_calc_obj) + elif dimension == 3: + return create_initial_model_for_meshing_parameter_3D(Meshing_calc_obj) + else: + raise ValueError("Dimension is not 2 or 3") -def create_model_2D_homogeneous(grid_point_calculator_parameters, degree): - """Creates models with the correct parameters for for grid point - calculation experiments - on the 2D homogeneous case with a grid of receivers near the source. +def create_initial_model_for_meshing_parameter_2D(Meshing_calc_obj): + velocity_profile_type = Meshing_calc_obj.velocity_profile_type + if velocity_profile_type == "homogeneous": + return create_initial_model_for_meshing_parameter_2D_homogeneous( + Meshing_calc_obj + ) + elif velocity_profile_type == "heterogeneous": + raise NotImplementedError("Not yet implemented") + # return create_initial_model_for_meshing_parameter_2D_heterogeneous(Meshing_calc_obj) + else: + raise ValueError( + "Velocity profile type is not homogeneous or heterogeneous" + ) - Parameters - ---------- - grid_point_calculator_parameters: Python 'dictionary' - Returns - ------- - model: Python `dictionary` - Contains model options and parameters for use in Spyro +def create_initial_model_for_meshing_parameter_3D(Meshing_calc_obj): + velocity_profile_type = Meshing_calc_obj.velocity_profile_type + if velocity_profile_type == "homogeneous": + raise NotImplementedError("Not yet implemented") + # return create_initial_model_for_meshing_parameter_3D_homogeneous(Meshing_calc_obj) + elif velocity_profile_type == "heterogeneous": + raise NotImplementedError("Not yet implemented") + # return create_initial_model_for_meshing_parameter_3D_heterogeneous(Meshing_calc_obj) + else: + raise ValueError( + "Velocity profile type is not homogeneous or heterogeneous" + ) - """ - minimum_mesh_velocity = grid_point_calculator_parameters[ - "minimum_velocity_in_the_domain" - ] - frequency = grid_point_calculator_parameters["source_frequency"] - dimension = grid_point_calculator_parameters["dimension"] - receiver_type = grid_point_calculator_parameters["receiver_setup"] +def create_initial_model_for_meshing_parameter_2D_homogeneous(Meshing_calc_obj): + dimension = 2 + c_value = Meshing_calc_obj.minimum_velocity + frequency = Meshing_calc_obj.source_frequency + cells_per_wavelength = Meshing_calc_obj.cpw_initial - method = grid_point_calculator_parameters["FEM_method_to_evaluate"] + method = Meshing_calc_obj.FEM_method_to_evaluate + degree = Meshing_calc_obj.desired_degree + reduced = Meshing_calc_obj.reduced_obj_for_testing - model = {} + if c_value > 500: + warnings.warn("Velocity in meters per second") - if minimum_mesh_velocity > 500: - print( - "Warning: minimum mesh velocity seems to be in m/s, input should be in km/s", - flush=True, - ) - # domain calculations - pady = 0.0 + # Domain calculations + lbda = c_value / frequency + Lz = 40 * lbda + Lx = 30 * lbda Ly = 0.0 - - lbda = minimum_mesh_velocity / frequency pad = lbda - Lz = 40 * lbda # 100*lbda - Real_Lz = Lz + pad - # print(Real_Lz) - Lx = 30 * lbda # 90*lbda - Real_Lx = Lx + 2 * pad - # source location - source_z = -Real_Lz / 2.0 # 1.0 - # print(source_z) - source_x = Real_Lx / 2.0 - # Source at the center. If this is changes receiver's bin has to also be - # changed. - source_coordinates = [(source_z, source_x)] - padz = pad - padx = pad - - # time calculations - tmin = 1.0 / frequency - final_time = 20 * tmin # Should be 35 - - # receiver calculations + # Source and receiver calculations + source_z = -Lz / 2.0 + source_x = Lx / 2.0 + source_locations = [(source_z, source_x)] receiver_bin_center1 = 10 * lbda # 20*lbda receiver_bin_width = 5 * lbda # 15*lbda - receiver_quantity = 36 # 2500 # 50 squared + if reduced is True: + receiver_quantity = 4 + else: + receiver_quantity = 36 # 2500 # 50 squared bin1_startZ = source_z + receiver_bin_center1 - receiver_bin_width / 2.0 bin1_endZ = source_z + receiver_bin_center1 + receiver_bin_width / 2.0 bin1_startX = source_x - receiver_bin_width / 2.0 bin1_endX = source_x + receiver_bin_width / 2.0 - receiver_coordinates = spyro.create_2d_grid( - bin1_startZ, bin1_endZ, bin1_startX, bin1_endX, int(np.sqrt(receiver_quantity)) + receiver_locations = spyro.create_2d_grid( + bin1_startZ, + bin1_endZ, + bin1_startX, + bin1_endX, + int(np.sqrt(receiver_quantity)), ) - # Choose method and parameters - model["opts"] = { - "method": method, - "quadrature": "KMV", - "variant": None, - "element": "tria", # tria or tetra - "degree": degree, # p order - "dimension": dimension, # dimension - } - - model["BCs"] = { - "status": True, # True or false - "outer_bc": "non-reflective", # neumann, non-reflective (outer boundary condition) - "damping_type": "polynomial", # polynomial. hyperbolic, shifted_hyperbolic - "exponent": 1, - "cmax": 4.7, # maximum acoustic wave velocity in PML - km/s - "R": 0.001, # theoretical reflection coefficient - "lz": padz, # thickness of the pml in the z-direction (km) - always positive - "lx": padx, # thickness of the pml in the x-direction (km) - always positive - "ly": pady, # thickness of the pml in the y-direction (km) - always positive - } - - model["mesh"] = { - "Lz": Lz, # depth in km - always positive - "Lx": Lx, # width in km - always positive - "Ly": Ly, # thickness in km - always positive - "meshfile": "demos/mm_exact.msh", - "initmodel": "velocity_models/bp2004.hdf5", - "truemodel": "velocity_models/bp2004.hdf5", - } - - model["acquisition"] = { - "source_type": "Ricker", - "num_sources": 1, - "source_pos": source_coordinates, - "source_mesh_point": False, - "source_point_dof": False, - "frequency": frequency, - "delay": 1.0, - "num_receivers": receiver_quantity, - "receiver_locations": receiver_coordinates, - } - - model["timeaxis"] = { - "t0": 0.0, # Initial time for event - "tf": final_time, # Final time for event - "dt": 0.001, # timestep size - "nspool": 200, # how frequently to output solution to pvds - "fspool": 100, # how frequently to save solution to RAM - } - model["parallelism"] = { - "type": "spatial", # options: automatic (same number of cores for evey processor), custom, off. - "custom_cores_per_shot": [], # only if the user wants a different number of cores for every shot. - # input is a list of integers with the length of the number of shots. - } - model["testing_parameters"] = { - "minimum_mesh_velocity": minimum_mesh_velocity, - "pml_fraction": padz / Lz, - "receiver_type": receiver_type, - "experiment_type": "homogeneous", - } - - return model - - -def create_model_2D_heterogeneous(grid_point_calculator_parameters, degree): - """Creates models with the correct parameters for for grid point calculation experiments. - - Parameters - ---------- - frequency: `float` - Source frequency to use in calculation - degree: `int` - Polynomial degree of finite element space - method: `string` - The finite element method choosen - minimum_mesh_velocity: `float` - Minimum velocity presented in the medium - experiment_type: `string` - Only options are `homogenous` or `heterogenous` - receiver_type: `string` - Options: `near`, `far` or `near_and_far`. Specifies receiver grid locations for experiment - - Returns - ------- - model: Python `dictionary` - Contains model options and parameters for use in Spyro - - - """ - import SeismicMesh - - minimum_mesh_velocity = grid_point_calculator_parameters[ - "minimum_velocity_in_the_domain" - ] - frequency = grid_point_calculator_parameters["source_frequency"] - dimension = grid_point_calculator_parameters["dimension"] - receiver_type = grid_point_calculator_parameters["receiver_setup"] - - method = grid_point_calculator_parameters["FEM_method_to_evaluate"] - velocity_model = grid_point_calculator_parameters["velocity_model_file_name"] - model = {} - - if minimum_mesh_velocity > 500: - print( - "Warning: minimum mesh velocity seems to be in m/s, input should be in km/s", - flush=True, - ) - # domain calculations - pady = 0.0 - Ly = 0.0 - - # using the BP2004 velocity model - - Lz = 12000.0 / 1000.0 - Lx = 67000.0 / 1000.0 - pad = 1000.0 / 1000.0 - Real_Lx = Lx + 2 * pad - source_z = -1.0 - source_x = Real_Lx / 2.0 - source_coordinates = [(source_z, source_x)] - if velocity_model is not None: - if velocity_model[-4:] == "segy": - SeismicMesh.write_velocity_model( - velocity_model, ofname="velocity_models/gridsweepcalc" - ) - elif velocity_model[-4:] == "hdf5": - shutil.copy(velocity_model, "velocity_models/gridsweepcalc.hdf5") - else: - raise ValueError("Velocity model filetype not recognized.") - else: - print( - "Warning: running without a velocity model is suitable for testing purposes only.", - flush=True, - ) - padz = pad - padx = pad - - if receiver_type == "bins": - - # time calculations - tmin = 1.0 / frequency - final_time = 25 * tmin # should be 35 - - # receiver calculations - - receiver_bin_center1 = 2.5 * 750.0 / 1000 - receiver_bin_width = 500.0 / 1000 - receiver_quantity_in_bin = 100 # 2500 # 50 squared - - bin1_startZ = source_z - receiver_bin_width / 2.0 - bin1_endZ = source_z + receiver_bin_width / 2.0 - bin1_startX = source_x + receiver_bin_center1 - receiver_bin_width / 2.0 - bin1_endX = source_x + receiver_bin_center1 + receiver_bin_width / 2.0 - - receiver_coordinates = spyro.create_2d_grid( - bin1_startZ, - bin1_endZ, - bin1_startX, - bin1_endX, - int(np.sqrt(receiver_quantity_in_bin)), - ) - - receiver_bin_center2 = 6500.0 / 1000 - receiver_bin_width = 500.0 / 1000 - - bin2_startZ = source_z - receiver_bin_width / 2.0 - bin2_endZ = source_z + receiver_bin_width / 2.0 - bin2_startX = source_x + receiver_bin_center2 - receiver_bin_width / 2.0 - bin2_endX = source_x + receiver_bin_center2 + receiver_bin_width / 2.0 - - receiver_coordinates = receiver_coordinates + spyro.create_2d_grid( - bin2_startZ, - bin2_endZ, - bin2_startX, - bin2_endX, - int(np.sqrt(receiver_quantity_in_bin)), - ) - - receiver_quantity = 2 * receiver_quantity_in_bin - - if receiver_type == "line": - - # time calculations - tmin = 1.0 / frequency - final_time = 2 * 10 * tmin + 5.0 # should be 35 - - # receiver calculations - - receiver_bin_center1 = 2000.0 / 1000 - receiver_bin_center2 = 10000.0 / 1000 - receiver_quantity = 500 - - bin1_startZ = source_z - bin1_endZ = source_z - bin1_startX = source_x + receiver_bin_center1 - bin1_endX = source_x + receiver_bin_center2 - - receiver_coordinates = spyro.create_transect( - (bin1_startZ, bin1_startX), (bin1_endZ, bin1_endX), receiver_quantity - ) - - # Choose method and parameters - model["opts"] = { - "method": method, - "variant": None, - "element": "tria", # tria or tetra - "degree": degree, # p order - "dimension": dimension, # dimension - } - - model["BCs"] = { - "status": True, # True or false - "outer_bc": "non-reflective", # neumann, non-reflective (outer boundary condition) - "damping_type": "polynomial", # polynomial. hyperbolic, shifted_hyperbolic - "exponent": 1, - "cmax": 4.7, # maximum acoustic wave velocity in PML - km/s - "R": 0.001, # theoretical reflection coefficient - "lz": padz, # thickness of the pml in the z-direction (km) - always positive - "lx": padx, # thickness of the pml in the x-direction (km) - always positive - "ly": pady, # thickness of the pml in the y-direction (km) - always positive - } - - model["mesh"] = { - "Lz": Lz, # depth in km - always positive - "Lx": Lx, # width in km - always positive - "Ly": Ly, # thickness in km - always positive - "meshfile": "demos/mm_exact.msh", - "initmodel": "velocity_models/gridsweepcalc.hdf5", - "truemodel": "velocity_models/gridsweepcalc.hdf5", - } - - model["acquisition"] = { - "source_type": "Ricker", - "num_sources": 1, - "source_pos": source_coordinates, - "source_mesh_point": False, - "source_point_dof": False, - "frequency": frequency, - "delay": 1.0, - "num_receivers": receiver_quantity, - "receiver_locations": receiver_coordinates, - } - - model["timeaxis"] = { - "t0": 0.0, # Initial time for event - "tf": final_time, # Final time for event - "dt": 0.001, # timestep size - "nspool": 200, # how frequently to output solution to pvds - "fspool": 100, # how frequently to save solution to RAM - } - model["parallelism"] = { - "type": "off", # options: automatic (same number of cores for evey processor), custom, off. - "custom_cores_per_shot": [], # only if the user wants a different number of cores for every shot. - # input is a list of integers with the length of the number of shots. - } - model["testing_parameters"] = { - "minimum_mesh_velocity": minimum_mesh_velocity, - "pml_fraction": padz / Lz, - "receiver_type": receiver_type, - } - - # print(source_coordinates) - # print(receiver_coordinates) - return model - - -def create_model_3D_homogeneous(grid_point_calculator_parameters, degree): - minimum_mesh_velocity = grid_point_calculator_parameters[ - "minimum_velocity_in_the_domain" - ] - frequency = grid_point_calculator_parameters["source_frequency"] - dimension = grid_point_calculator_parameters["dimension"] - - method = grid_point_calculator_parameters["FEM_method_to_evaluate"] - - model = {} - - lbda = minimum_mesh_velocity / frequency - pad = lbda - Lz = 15 * lbda # 100*lbda - Real_Lz = Lz + pad - # print(Real_Lz) - Lx = 30 * lbda # 90*lbda - Ly = Lx - Real_Ly = Ly + 2 * pad - - # source location - source_z = -Real_Lz / 2.0 # 1.0 - # print(source_z) - source_x = lbda * 1.5 - source_y = Real_Ly / 2.0 - source_coordinates = [ - (source_z, source_x, source_y) - ] # Source at the center. If this is changes receiver's bin has to also be changed. - padz = pad - padx = pad - pady = pad - - # time calculations + # Time axis calculations tmin = 1.0 / frequency - final_time = 20 * tmin # should be 35 - - # receiver calculations - - receiver_bin_center1 = 10 * lbda # 20*lbda - receiver_bin_width = 5 * lbda # 15*lbda - receiver_quantity = 36 # 2500 # 50 squared - - bin1_startZ = source_z - receiver_bin_width / 2.0 - bin1_endZ = source_z + receiver_bin_width / 2.0 - bin1_startX = source_x + receiver_bin_center1 - receiver_bin_width / 2.0 - bin1_endX = source_x + receiver_bin_center1 + receiver_bin_width / 2.0 - bin1_startY = source_y - receiver_bin_width / 2.0 - bin1_endY = source_y + receiver_bin_width / 2.0 + final_time = 20 * tmin # Should be 35 - receiver_coordinates = create_3d_grid( - (bin1_startZ, bin1_startX, bin1_startY), (bin1_endZ, bin1_endX, bin1_endY), 6 - ) - # Choose method and parameters - model["opts"] = { + variables = { "method": method, - "variant": None, - "element": "tetra", # tria or tetra - "quadrature": "KMV", - "degree": degree, # p order - "dimension": dimension, # dimension - } - - model["BCs"] = { - "status": True, # True or false - "outer_bc": "non-reflective", # neumann, non-reflective (outer boundary condition) - "damping_type": "polynomial", # polynomial. hyperbolic, shifted_hyperbolic - "exponent": 1, - "cmax": 4.7, # maximum acoustic wave velocity in PML - km/s - "R": 0.001, # theoretical reflection coefficient - "lz": padz, # thickness of the pml in the z-direction (km) - always positive - "lx": padx, # thickness of the pml in the x-direction (km) - always positive - "ly": pady, # thickness of the pml in the y-direction (km) - always positive - } - - model["mesh"] = { - "Lz": Lz, # depth in km - always positive - "Lx": Lx, # width in km - always positive - "Ly": Ly, # thickness in km - always positive - } - - model["acquisition"] = { - "source_type": "Ricker", - "num_sources": 1, - "source_pos": source_coordinates, + "degree": degree, + "dimension": dimension, + "Lz": Lz, + "Lx": Lx, + "Ly": Ly, + "cells_per_wavelength": cells_per_wavelength, + "pad": pad, + "source_locations": source_locations, "frequency": frequency, - "delay": 1.0, - "num_receivers": receiver_quantity, - "receiver_locations": receiver_coordinates, - } - - model["timeaxis"] = { - "t0": 0.0, # Initial time for event - "tf": final_time, # Final time for event - "dt": 0.0002, # timestep size - "nspool": 200, # how frequently to output solution to pvds - "fspool": 100, # how frequently to save solution to RAM - } - model["parallelism"] = { - "type": "spatial", + "receiver_locations": receiver_locations, + "final_time": final_time, + "dt": 0.0005, } - # print(source_coordinates) - # print(receiver_coordinates) - return model - - -def create_model_for_grid_point_calculation(grid_point_calculator_parameters, degree): - """Creates models with the correct parameters for for grid point calculation experiments - on the 2D homogeneous case with a grid of receivers near the source. - - Parameters - ---------- - grid_point_calculator_parameters: Python 'dictionary' - - Returns - ------- - model: Python `dictionary` - Contains model options and parameters for use in Spyro - - - """ - dimension = grid_point_calculator_parameters["dimension"] - experiment_type = grid_point_calculator_parameters["velocity_profile_type"] - if dimension == 2 and experiment_type == "homogeneous": - model = create_model_2D_homogeneous(grid_point_calculator_parameters, degree) - elif dimension == 2 and experiment_type == "heterogeneous": - model = create_model_2D_heterogeneous(grid_point_calculator_parameters, degree) - elif dimension == 3: - model = create_model_3D_homogeneous(grid_point_calculator_parameters, degree) + model_dictionary = build_on_top_of_base_dictionary(variables) - return model + return model_dictionary diff --git a/spyro/utils/__init__.py b/spyro/utils/__init__.py index e7d72643..e7b662f6 100644 --- a/spyro/utils/__init__.py +++ b/spyro/utils/__init__.py @@ -1,3 +1,12 @@ -from . import utils, geometry_creation, estimate_timestep +from . import geometry_creation, estimate_timestep +from .utils import mpi_init, compute_functional +from .analytical_solution_nodal import nodal_homogeneous_analytical -__all__ = ["utils", "geometry_creation", "estimate_timestep"] + +__all__ = [ + "geometry_creation", + "estimate_timestep", + "mpi_init", + "compute_functional", + "nodal_homogeneous_analytical", +] diff --git a/spyro/utils/analytical_solution_nodal.py b/spyro/utils/analytical_solution_nodal.py new file mode 100644 index 00000000..5d3ec09e --- /dev/null +++ b/spyro/utils/analytical_solution_nodal.py @@ -0,0 +1,77 @@ +import numpy as np +from scipy.special import hankel2 +from ..sources import full_ricker_wavelet + + +def nodal_homogeneous_analytical(Wave_object, offset, c_value, n_extra=5000): + """ + This function calculates the analytical solution for an homogeneous + medium with a single source and receiver. + + Parameters + ---------- + Wave_object: spyro.Wave + Wave object + offset: float + Offset between source and receiver. + c_value: float + Velocity of the homogeneous medium. + n_extra: int (optional) + Multiplied factor for the final time. + + Returns + ------- + u_analytical: numpy array + Analytical solution for the wave equation. + """ + + # Generating extended ricker wavelet + dt = Wave_object.dt + final_time = Wave_object.final_time + num_t = int(final_time / dt + 1) + + extended_final_time = n_extra * final_time + + frequency = Wave_object.frequency + delay = Wave_object.delay + amplitude = Wave_object.amplitude + delay_type = Wave_object.delay_type + + ricker_wavelet = full_ricker_wavelet( + dt=dt, + final_time=extended_final_time, + frequency=frequency, + delay=delay - dt, + amplitude=amplitude, + delay_type=delay_type, + ) + + full_u_analytical = analytical_solution( + ricker_wavelet, c_value, extended_final_time, offset + ) + + u_analytical = full_u_analytical[:num_t] + + return u_analytical + + +def analytical_solution(ricker_wavelet, c_value, final_time, offset): + num_t = len(ricker_wavelet) + + # Constantes de Fourier + nf = int(num_t / 2 + 1) + frequency_axis = (1.0 / final_time) * np.arange(nf) + + # FOurier tranform of ricker wavelet + fft_rw = np.fft.fft(ricker_wavelet) + fft_rw = fft_rw[0:nf] + + U_a = np.zeros((nf), dtype=complex) + for a in range(1, nf - 1): + k = 2 * np.pi * frequency_axis[a] / c_value + tmp = k * offset + U_a[a] = -1j * np.pi * hankel2(0.0, tmp) * fft_rw[a] + + U_t = 1.0 / (2.0 * np.pi) * np.real(np.fft.ifft(U_a[:], num_t)) + + return np.real(U_t) diff --git a/spyro/utils/estimate_timestep.py b/spyro/utils/estimate_timestep.py index 3620fa91..a8d7cde2 100644 --- a/spyro/utils/estimate_timestep.py +++ b/spyro/utils/estimate_timestep.py @@ -43,7 +43,8 @@ def estimate_timestep(mesh, V, c, estimate_max_eigenvalue=True): max_eigval = np.amax(np.abs(Lsp.diagonal())) else: print( - "Computing exact eigenvalues is extremely computationally demanding!", + "Computing exact eigenvalues is extremely computationally \ + demanding!", flush=True, ) max_eigval = scipy.sparse.linalg.eigs( @@ -56,7 +57,8 @@ def estimate_timestep(mesh, V, c, estimate_max_eigenvalue=True): else: max_dt = 100000000 # print( - # f"Maximum stable timestep should be about: {np.float(2 / np.sqrt(max_eigval))} seconds", + # f"Maximum stable timestep should be about: {np.float(2 / + # np.sqrt(max_eigval))} seconds", # flush=True, # ) return max_dt diff --git a/spyro/utils/utils.py b/spyro/utils/utils.py index 2fcc9e5b..8f8add49 100644 --- a/spyro/utils/utils.py +++ b/spyro/utils/utils.py @@ -1,5 +1,5 @@ import copy -from firedrake import * +from firedrake import * # noqa: F403 import numpy as np from mpi4py import MPI from scipy.signal import butter, filtfilt @@ -36,24 +36,15 @@ def butter_lowpass_filter(shot, cutoff, fs, order=2): return filtered_shot -def compute_functional(model, residual, velocity=None): +def compute_functional(Wave_object, residual): """Compute the functional to be optimized. Accepts the velocity optionally and uses it if regularization is enabled """ - num_receivers = len(model["acquisition"]["receiver_locations"]) - dt = model["timeaxis"]["dt"] - tf = model["timeaxis"]["tf"] - nt = int(tf / dt) # number of timesteps - if "regularization" in model["opts"]: - regularize = model["opts"]["regularization"] - else: - regularize = False - - if regularize: - gamma = model["opt"]["gamma"] - Ns = model["acquisition"]["num_sources"] - gamma /= Ns + num_receivers = Wave_object.number_of_receivers + dt = Wave_object.dt + tf = Wave_object.final_time + nt = int(tf / dt) + 1 # number of timesteps J = 0.0 for ti in range(nt): @@ -61,9 +52,6 @@ def compute_functional(model, residual, velocity=None): J += residual[ti][rn] ** 2 J *= 0.5 - # if regularize: - # Jreg = assemble(0.5 * gamma * dot(grad(vp), grad(vp)) * dx) - # J += Jreg return J @@ -81,11 +69,11 @@ def evaluate_misfit(model, guess, exact): return ds_exact[:ll] - guess -def myrank(COMM=COMM_SELF): +def myrank(COMM=COMM_SELF): # noqa: F405 return COMM.Get_rank() -def mysize(COMM=COMM_SELF): +def mysize(COMM=COMM_SELF): # noqa: F405 return COMM.Get_size() @@ -93,19 +81,35 @@ def mpi_init(model): """Initialize computing environment""" # rank = myrank() # size = mysize() - available_cores = COMM_WORLD.size - if model["parallelism"]["type"] == "automatic": - num_cores_per_shot = available_cores / len(model["acquisition"]["source_pos"]) - if available_cores % len(model["acquisition"]["source_pos"]) != 0: + available_cores = COMM_WORLD.size # noqa: F405 + if model.parallelism_type == "automatic": + num_cores_per_shot = available_cores / model.number_of_sources + if available_cores % model.number_of_sources != 0: raise ValueError( "Available cores cannot be divided between sources equally." ) - elif model["parallelism"]["type"] == "spatial": + elif model.parallelism_type == "spatial": num_cores_per_shot = available_cores - elif model["parallelism"]["type"] == "custom": + elif model.parallelism_type == "custom": raise ValueError("Custom parallelism not yet implemented") - comm_ens = Ensemble(COMM_WORLD, num_cores_per_shot) + comm_ens = Ensemble(COMM_WORLD, num_cores_per_shot) # noqa: F405 + return comm_ens + + +def mpi_init_simple(number_of_sources): + """Initialize computing environment""" + rank = myrank() # noqa: F841 + size = mysize() # noqa: F841 + available_cores = COMM_WORLD.size # noqa: F405 + + num_cores_per_shot = available_cores / number_of_sources + if available_cores % number_of_sources != 0: + raise ValueError( + "Available cores cannot be divided between sources equally." + ) + + comm_ens = Ensemble(COMM_WORLD, num_cores_per_shot) # noqa: F405 return comm_ens @@ -137,9 +141,11 @@ def communicate(array, my_ensemble): return array_reduced -def analytical_solution_for_pressure_based_on_MMS(model, mesh, time): - degree = model["opts"]["degree"] - V = FunctionSpace(mesh, "CG", degree) - z, x = SpatialCoordinate(mesh) - p = Function(V).interpolate((time**2) * sin(pi * z) * sin(pi * x)) - return p +# def analytical_solution_for_pressure_based_on_MMS(model, mesh, time): +# degree = model["opts"]["degree"] +# V = FunctionSpace(mesh, "CG", degree) # noqa: F405 +# z, x = SpatialCoordinate(mesh) # noqa: F405 +# p = Function(V).interpolate( # noqa: F405 +# (time**2) * sin(pi * z) * sin(pi * x) # noqa: F405 +# ) +# return p diff --git a/test/inputfiles/Model1_2d_CG.py b/test/inputfiles/Model1_2d_CG.py index 7fb9e4fe..ff10efe3 100644 --- a/test/inputfiles/Model1_2d_CG.py +++ b/test/inputfiles/Model1_2d_CG.py @@ -65,6 +65,7 @@ "dt": 0.001, # timestep size "nspool": 20, # how frequently to output solution to pvds "fspool": 10, # how frequently to save solution to RAM + "amplitude": 1, # the Ricker has an amplitude of 1. } # how freq. to output to files and screen inversion = { diff --git a/test/inputfiles/Model1_3d_CG.py b/test/inputfiles/Model1_3d_CG.py index 1678f8f4..cfc44fa1 100644 --- a/test/inputfiles/Model1_3d_CG.py +++ b/test/inputfiles/Model1_3d_CG.py @@ -65,6 +65,7 @@ "dt": 0.001, # timestep size "nspool": 20, # how frequently to output solution to pvds "fspool": 10, # how frequently to save solution to RAM + "amplitude": 1, # the Ricker has an amplitude of 1. } # how freq. to output to files and screen inversion = { diff --git a/test/inputfiles/Model1_gradient_2d_pml.py b/test/inputfiles/Model1_gradient_2d_pml.py index a457aee1..18568b27 100644 --- a/test/inputfiles/Model1_gradient_2d_pml.py +++ b/test/inputfiles/Model1_gradient_2d_pml.py @@ -46,7 +46,9 @@ "source_pos": [(-0.1, 0.5)], "frequency": 5.0, "delay": 1.0, - "receiver_locations": spyro.create_transect((-0.95, 0.1), (-0.95, 0.9), 100), + "receiver_locations": spyro.create_transect( + (-0.95, 0.1), (-0.95, 0.9), 100 + ), } timeaxis = { "t0": 0.0, # Initial time for event diff --git a/test/inputfiles/analytical_solution_dt_0.0001.npy b/test/inputfiles/analytical_solution_dt_0.0001.npy new file mode 100644 index 00000000..a66d3bc6 Binary files /dev/null and b/test/inputfiles/analytical_solution_dt_0.0001.npy differ diff --git a/test/inputfiles/analytical_solution_dt_0.0005.npy b/test/inputfiles/analytical_solution_dt_0.0005.npy new file mode 100644 index 00000000..83ac1597 Binary files /dev/null and b/test/inputfiles/analytical_solution_dt_0.0005.npy differ diff --git a/test/inputfiles/extended_pml_receveirs.pck b/test/inputfiles/extended_pml_receveirs.pck new file mode 100644 index 00000000..06a9c380 Binary files /dev/null and b/test/inputfiles/extended_pml_receveirs.pck differ diff --git a/test/inputfiles/model.py b/test/inputfiles/model.py new file mode 100644 index 00000000..f353642f --- /dev/null +++ b/test/inputfiles/model.py @@ -0,0 +1,57 @@ +dictionary = {} +dictionary["options"] = { + "cell_type": "Q", # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "variant": "lumped", # lumped, equispaced or DG, default is lumped "method":"MLT", # (MLT/spectral_quadrilateral/DG_triangle/DG_quadrilateral) You can either specify a cell_type+variant or a method + "degree": 4, # p order + "dimension": 2, # dimension +} + +# Number of cores for the shot. For simplicity, we keep things serial. +# spyro however supports both spatial parallelism and "shot" parallelism. +dictionary["parallelism"] = { + "type": "automatic", # options: automatic (same number of cores for evey processor) or spatial +} + +# Define the domain size without the PML. Here we'll assume a 1.00 x 1.00 km +# domain and reserve the remaining 250 m for the Perfectly Matched Layer (PML) to absorb +# outgoing waves on three sides (eg., -z, +-x sides) of the domain. +dictionary["mesh"] = { + "Lz": 1.0, # depth in km - always positive + "Lx": 1.0, # width in km - always positive + "Ly": 0.0, # thickness in km - always positive + "mesh_type": "firedrake_mesh", # options: firedrake_mesh or user_mesh + "mesh_file": None, # specify the mesh file +} + +# Create a source injection operator. Here we use a single source with a +# Ricker wavelet that has a peak frequency of 5 Hz injected at the center of the mesh. +# We also specify to record the solution at a microphone near the top of the domain. +# This transect of receivers is created with the helper function `create_transect`. +dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [ + (-1.0, 1.0) + ], # , (-0.605, 1.7), (-0.61, 1.7), (-0.615, 1.7)],#, (-0.1, 1.5), (-0.1, 2.0), (-0.1, 2.5), (-0.1, 3.0)], + "frequency": 5.0, + "delay": 1.5, + "receiver_locations": [(-0.0, 0.5)], +} + +# Simulate for 2.0 seconds. +dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": 1.0, # Final time for event + "dt": 0.0005, # timestep size + "amplitude": 1, # the Ricker has an amplitude of 1. + "output_frequency": 100, # how frequently to output solution to pvds + "gradient_sampling_frequency": 1, # how frequently to save solution to RAM +} + +dictionary["visualization"] = { + "forward_output": False, + "output_filename": "results/forward_output.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": False, + "gradient_filename": None, +} diff --git a/test/inputfiles/reference_solution_cpw.npy b/test/inputfiles/reference_solution_cpw.npy new file mode 100644 index 00000000..50b583aa Binary files /dev/null and b/test/inputfiles/reference_solution_cpw.npy differ diff --git a/test/model.py b/test/model.py index 222474fc..f978b257 100644 --- a/test/model.py +++ b/test/model.py @@ -1,85 +1,55 @@ -# Define mesh file to be used: -meshfile = "blah" - -# Define initial velocity model: -truemodel = "blah" -initmodel = "blah" - - -# Choose method and parameters -opts = { - "method": "DG", - "quadrature": "KMV", - "variant": None, - "type": "SIP", # for DG only - SIP, NIP and IIP - "degree": 1, # p order - "dimension": 3, # dimension - "mesh_size": 0.005, # h - "beta": 0.0, # for Newmark only - "gamma": 0.5, # for Newmark only +dictionary = {} +dictionary["options"] = { + "cell_type": "Q", # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "variant": "lumped", # lumped, equispaced or DG, default is lumped "method":"MLT", # (MLT/spectral_quadrilateral/DG_triangle/DG_quadrilateral) You can either specify a cell_type+variant or a method + "degree": 4, # p order + "dimension": 2, # dimension } -parallelism = { - "type": "automatic", # options: automatic, custom, off +# Number of cores for the shot. For simplicity, we keep things serial. +# spyro however supports both spatial parallelism and "shot" parallelism. +dictionary["parallelism"] = { + "type": "automatic", # options: automatic (same number of cores for evey processor) or spatial } -mesh = { - "Lz": 2.000, # depth in km - always positive - "Lx": 3.00000, # width in km - always positive +# Define the domain size without the PML. Here we'll assume a 1.00 x 1.00 km +# domain and reserve the remaining 250 m for the Perfectly Matched Layer (PML) to absorb +# outgoing waves on three sides (eg., -z, +-x sides) of the domain. +dictionary["mesh"] = { + "Lz": 1.0, # depth in km - always positive + "Lx": 1.0, # width in km - always positive "Ly": 0.0, # thickness in km - always positive - "meshfile": meshfile + ".msh", - "initmodel": initmodel + ".hdf5", - "truemodel": truemodel + ".hdf5", -} - -BCs = { - "status": False, # True or false - "outer_bc": "non-reflective", # neumann, non-reflective (outer boundary condition) - "damping_type": "polynomial", # polynomial. hyperbolic, shifted_hyperbolic - "exponent": 1, - "cmax": 4.7, # maximum acoustic wave velocity in PML - km/s - "R": 0.001, # theoretical reflection coefficient - "lz": 0.250, # thickness of the pml in the z-direction (km) - always positive - "lx": 0.250, # thickness of the pml in the x-direction (km) - always positive - "ly": 0.0, # thickness of the pml in the y-direction (km) - always positive + "mesh_type": "firedrake_mesh", # options: firedrake_mesh or user_mesh + "mesh_file": None, # specify the mesh file } -acquisition = { - "source_type": "MMS", - "num_sources": 1, - "frequency": 2.0, - "delay": 1.0, - "source_pos": [()], - "num_receivers": 256, - "receiver_locations": [()], +# Create a source injection operator. Here we use a single source with a +# Ricker wavelet that has a peak frequency of 5 Hz injected at the center of the mesh. +# We also specify to record the solution at a microphone near the top of the domain. +# This transect of receivers is created with the helper function `create_transect`. +dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-1.0, 1.0)], + "frequency": 5.0, + "delay": 1.5, + "receiver_locations": [(-0.0, 0.5)], } -timeaxis = { - "t0": 0.0, # Initial time for event - "tf": 0.4, # Final time for event - "dt": 0.001, # timestep size - "nspool": 20, # how frequently to output solution to pvds - "fspool": 10, # how frequently to save solution to RAM -} # how freq. to output to files and screen - -inversion = { - "freq_bands": [None] -} # cutoff frequencies (Hz) for Ricker source and to low-pass the observed shot record - -aut_dif = { - "status": False, +# Simulate for 2.0 seconds. +dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": 1.0, # Final time for event + "dt": 0.0005, # timestep size + "amplitude": 1, # the Ricker has an amplitude of 1. + "output_frequency": 100, # how frequently to output solution to pvds + "gradient_sampling_frequency": 1, # how frequently to save solution to RAM } - -# Create your model with all the options -model = { - "self": None, - "inversion": inversion, - "opts": opts, - "BCs": BCs, - "parallelism": parallelism, - "mesh": mesh, - "acquisition": acquisition, - "timeaxis": timeaxis, - "aut_dif": aut_dif, +dictionary["visualization"] = { + "forward_output": False, + "output_filename": "results/forward_output.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": False, + "gradient_filename": None, } diff --git a/test/not_a_test.py b/test/not_a_test.py index f88a0ddd..2100c0ed 100644 --- a/test/not_a_test.py +++ b/test/not_a_test.py @@ -56,7 +56,8 @@ def test_gradient_talyor_remainder_v2(): class L2Inner(object): def __init__(self): self.A = assemble( - TrialFunction(V) * TestFunction(V) * dx(scheme=qr_x), mat_type="matfree" + TrialFunction(V) * TestFunction(V) * dx(scheme=qr_x), + mat_type="matfree", ) self.Ap = as_backend_type(self.A).mat() @@ -86,7 +87,9 @@ def value(self, x, tol): receivers, output=False, ) - self.misfit = spyro.utils.evaluate_misfit(model, p_guess_recv, p_exact_recv) + self.misfit = spyro.utils.evaluate_misfit( + model, p_guess_recv, p_exact_recv + ) J = spyro.utils.compute_functional(model, self.misfit) return J diff --git a/test/test_MMS.py b/test/test_MMS.py index facbecec..92ab0538 100644 --- a/test/test_MMS.py +++ b/test/test_MMS.py @@ -4,117 +4,44 @@ from firedrake import * import spyro -from .model import model +from .model import dictionary as model - -@pytest.fixture(params=["triangle", "tetrahedral", "square"]) -def mesh_type(request): - return request.param +model["acquisition"]["source_type"] = "MMS" -@pytest.fixture -def mesh(mesh_type): +@pytest.fixture(params=["triangle", "square"]) +def mesh_type(request): if mesh_type == "triangle": - model["opts"]["dimension"] = 2 - model["acquisition"]["receiver_locations"] = spyro.create_transect( - (0.0, 1.0), (0.0, 0.9), 256 - ) - model["acquisition"]["source_pos"] = [(-0.05, 1.5)] + model["cell_type"] = "triangles" elif mesh_type == "square": - model["opts"]["quadrature"] == "GLL" - model["opts"]["dimension"] = 2 - model["acquisition"]["receiver_locations"] = spyro.create_transect( - (0.0, 1.0), (0.0, 0.9), 256 - ) - model["acquisition"]["source_pos"] = [(-0.05, 1.5)] - elif mesh_type == "tetrahedral": - model["opts"]["dimension"] = 3 - model["acquisition"]["receiver_locations"] = spyro.create_transect( - (0.0, 0.0, 0.0), (0.0, 0.0, 1.0), 256 - ) - model["acquisition"]["source_pos"] = [(-0.05, 1.5, 1.5)] - - return { - "triangle": lambda n: UnitSquareMesh(2**n, 2**n), - "tetrahedral": lambda n: UnitCubeMesh(2**n, 2**n, 2**n), - "square": lambda n: UnitSquareMesh(2**n, 2**n, quadrilateral=True), - }[mesh_type] - - -@pytest.fixture(params=["CG", "KMV"]) -def method_type(request): + model["cell_type"] = "quadrilaterals" return request.param -@pytest.fixture -def spatial_method(method_type): - model["opts"]["method"] = method_type - return method_type - - -@pytest.fixture(params=["central", "ssprk"]) -def timestep_method_type(request): +@pytest.fixture(params=["lumped", "equispaced"]) +def method_type(request): + if method_type == "lumped": + model["variant"] = "lumped" + elif method_type == "equispaced": + model["variant"] = "equispaced" return request.param -@pytest.fixture -def timestep_method(timestep_method_type): - return timestep_method_type - - -@pytest.fixture -def interpolation_expr(mesh_type): - return { - "square": lambda x, y: (0.10**2) * sin(pi * x) * sin(pi * y), - "triangle": lambda x, y: (0.10**2) * sin(pi * x) * sin(pi * y), - "tetrahedral": lambda x, y, z: (0.10**2) - * sin(pi * x) - * sin(pi * y) - * sin(pi * z), - }[mesh_type] - - -def run_solve(timestep_method, method, model, mesh, expr): +def run_solve(model): testmodel = deepcopy(model) - cell_geometry = mesh.ufl_cell() - if method == "CG" or method == "spectral": - if cell_geometry == quadrilateral or cell_geometry == hexahedron: - variant = "spectral" - testmodel["opts"]["quadrature"] = "GLL" - else: - variant = "equispaced" - elif method == "KMV": - variant = "KMV" - - comm = spyro.utils.mpi_init(testmodel) - - element = FiniteElement(method, mesh.ufl_cell(), degree=1, variant=variant) - V = FunctionSpace(mesh, element) - excitation = spyro.Sources(testmodel, mesh, V, comm) + Wave_obj = spyro.AcousticWaveMMS(dictionary=testmodel) + Wave_obj.set_mesh(dx=0.02) + Wave_obj.set_initial_velocity_model(expression="1 + sin(pi*-z)*sin(pi*x)") + Wave_obj.forward_solve() - wavelet = spyro.full_ricker_wavelet(dt=0.001, tf=1.0, freq=2.0) + u_an = Wave_obj.analytical + u_num = Wave_obj.u_n - receivers = spyro.Receivers(testmodel, mesh, V, comm) + return errornorm(u_num, u_an) - if timestep_method == "central": - p, _ = spyro.solvers.forward( - testmodel, mesh, comm, Constant(1.0), excitation, wavelet, receivers - ) - elif timestep_method == "ssprk": - p, _ = spyro.solvers.SSPRK3( - testmodel, mesh, comm, Constant(1.0), excitation, receivers - ) - expr = expr(*SpatialCoordinate(mesh)) - return errornorm(interpolate(expr, V), p[-1]) +def test_method(mesh_type, method_type): + error = run_solve(model) -def test_method(mesh, timestep_method, spatial_method, interpolation_expr): - if mesh(3).ufl_cell() == quadrilateral and spatial_method == "KMV": - pytest.skip("KMV isn't possible in quadrilaterals.") - if timestep_method == "ssprk": - pytest.skip("KMV is not yet supported in ssprk") - error = run_solve( - timestep_method, spatial_method, model, mesh(3), interpolation_expr - ) - assert math.isclose(error, 0.0, abs_tol=1e-1) + assert math.isclose(error, 0.0, abs_tol=1e-7) diff --git a/test/test_analytical_solution.py b/test/test_analytical_solution.py new file mode 100644 index 00000000..c75816eb --- /dev/null +++ b/test/test_analytical_solution.py @@ -0,0 +1,62 @@ +import numpy as np +import spyro + +# import matplotlib.pyplot as plt + + +def error_calc(p_numerical, p_analytical, nt): + norm = np.linalg.norm(p_numerical, 2) / np.sqrt(nt) + error_time = np.linalg.norm(p_analytical - p_numerical, 2) / np.sqrt(nt) + div_error_time = error_time / norm + return div_error_time + + +def test_analytical_solution(): + frequency = 5.0 + offset = 0.5 + c_value = 1.5 + dictionary = {} + dictionary["absorving_boundary_conditions"] = { + "status": False, + } + dictionary["mesh"] = { + "Lz": 3.0, # depth in km - always positive + "Lx": 3.0, # width in km - always positive + } + dictionary["acquisition"] = { + "delay_type": "time", + "frequency": frequency, + "delay": c_value / frequency, + "source_locations": [(-1.5, 1.5)], + "receiver_locations": [(-1.5 - offset, 1.5)], + } + Wave_obj = spyro.examples.Rectangle_acoustic( + dictionary=dictionary, periodic=True + ) + Wave_obj.set_initial_velocity_model(constant=c_value) + analytical_p = spyro.utils.nodal_homogeneous_analytical( + Wave_obj, offset, c_value + ) + + time_vector = np.linspace(0.0, 1.0, int(1.0 / Wave_obj.dt) + 1) + Wave_obj.forward_solve() + numerical_p = Wave_obj.receivers_output + numerical_p = numerical_p.flatten() + + nt = len(time_vector) + error = error_calc(numerical_p, analytical_p, nt) + print("Error = {:.4e}".format(error)) + + # plt.plot(time_vector, analytical_p, label="Analytical", color="black", linestyle="--") + # plt.plot(time_vector, numerical_p, label="Numerical", color="red") + # plt.legend() + # # plt.plot(time, -(p_analytical - p_numerical)) + # plt.xlabel("Time (s)") + # plt.ylabel("Pressure (Pa)") + # plt.show() + + assert error < 1e-3 + + +if __name__ == "__main__": + test_analytical_solution() diff --git a/test/test_cpw_calc.py b/test/test_cpw_calc.py new file mode 100644 index 00000000..8b14115c --- /dev/null +++ b/test/test_cpw_calc.py @@ -0,0 +1,69 @@ +import numpy as np +import spyro + + +def test_cpw_analytic_calc(): + grid_point_calculator_parameters = { + # Experiment parameters + # Here we define the frequency of the Ricker wavelet source + "source_frequency": 5.0, + # The minimum velocity present in the domain. + "minimum_velocity_in_the_domain": 1.5, + # if an homogeneous test case is used this velocity will be defined in + # the whole domain. + # Either or heterogeneous. If heterogeneous is + "velocity_profile_type": "homogeneous", + # chosen be careful to have the desired velocity model below. + "velocity_model_file_name": None, + # FEM to evaluate such as `KMV` or `spectral` + # (GLL nodes on quads and hexas) + "FEM_method_to_evaluate": "spectral_quadrilateral", + "dimension": 2, # Domain dimension. Either 2 or 3. + # Either near or line. Near defines a receiver grid near to the source, + "receiver_setup": "near", + # line defines a line of point receivers with pre-established near and far + # offsets. + # Line search parameters + "load_reference": True, + "reference_solution_file": "test/inputfiles/reference_solution_cpw.npy", + "save_reference": False, + "reference_degree": None, # Degree to use in the reference case (int) + # grid point density to use in the reference case (float) + "C_reference": None, + "desired_degree": 4, # degree we are calculating G for. (int) + "C_initial": 2.3, # Initial G for line search (float) + "accepted_error_threshold": 0.05, + "C_accuracy": 0.1, + } + + Cpw_calc = spyro.tools.Meshing_parameter_calculator( + grid_point_calculator_parameters + ) + + # Check correct offset + source_location = Cpw_calc.initial_guess_object.source_locations[0] + receiver_location = Cpw_calc.initial_guess_object.receiver_locations[1] + sz, sx = source_location + rz, rx = receiver_location + offset = np.sqrt((sz - rz) ** 2 + (sx - rx) ** 2) + expected_offset_value = 2.6580067720004026 + test1 = np.isclose(offset, expected_offset_value) + print(f"Checked if offset calculation is correct: {test1}") + + # Check if analytical solution has the correct peak location + analytical_solve_one_receiver = Cpw_calc.reference_solution[:, 1] + peak_indice = np.argmax(analytical_solve_one_receiver) + expected_peak_indice = 4052 # 2804 + test2 = expected_peak_indice == peak_indice + print(f"Checked if reference solution seems correct: {test2}") + + # Check if cpw is within error TOL, starting search at min + min = Cpw_calc.find_minimum() + test3 = np.isclose(2.5, min) + + print("END") + assert all([test1, test2, test3]) + + +if __name__ == "__main__": + test_cpw_analytic_calc() diff --git a/test/test_cpw_calc_analytical_gen.py b/test/test_cpw_calc_analytical_gen.py new file mode 100644 index 00000000..4f760e92 --- /dev/null +++ b/test/test_cpw_calc_analytical_gen.py @@ -0,0 +1,67 @@ +import numpy as np +import spyro + + +def test_cpw_analytic_calc_analytical_gen(): + grid_point_calculator_parameters = { + # Experiment parameters + # Here we define the frequency of the Ricker wavelet source + "source_frequency": 5.0, + # The minimum velocity present in the domain. + "minimum_velocity_in_the_domain": 1.5, + # if an homogeneous test case is used this velocity will be defined in + # the whole domain. + # Either or heterogeneous. If heterogeneous is + "velocity_profile_type": "homogeneous", + # chosen be careful to have the desired velocity model below. + "velocity_model_file_name": None, + # FEM to evaluate such as `KMV` or `spectral` + # (GLL nodes on quads and hexas) + "FEM_method_to_evaluate": "spectral_quadrilateral", + "dimension": 2, # Domain dimension. Either 2 or 3. + # Either near or line. Near defines a receiver grid near to the source, + "receiver_setup": "near", + # line defines a line of point receivers with pre-established near and far + # offsets. + # Line search parameters + "load_reference": False, + "testing": True, + "save_reference": False, + "reference_degree": None, # Degree to use in the reference case (int) + # grid point density to use in the reference case (float) + "C_reference": None, + "desired_degree": 4, # degree we are calculating G for. (int) + "C_initial": 2.3, # Initial G for line search (float) + "accepted_error_threshold": 0.05, + "C_accuracy": 0.1, + } + + Cpw_calc = spyro.tools.Meshing_parameter_calculator( + grid_point_calculator_parameters + ) + + # Check correct offset + source_location = Cpw_calc.initial_guess_object.source_locations[0] + receiver_location = Cpw_calc.initial_guess_object.receiver_locations[1] + sz, sx = source_location + rz, rx = receiver_location + offset = np.sqrt((sz - rz) ** 2 + (sx - rx) ** 2) + expected_offset_value = 3.824264635194589 + test1 = np.isclose(offset, expected_offset_value) + print(f"Checked if offset calculation is correct: {test1}") + + # Check if analytical solution has the correct peak location + analytical_solve_one_receiver = Cpw_calc.reference_solution[:, 1] + peak_indice = np.argmax(analytical_solve_one_receiver) + expected_peak_indice = 5607 + test2 = expected_peak_indice == peak_indice + print(f"Checked if reference solution seems correct: {test2}") + + # Check if cpw is within error TOL, starting search at min + + print("END") + assert all([test1, test2]) + + +if __name__ == "__main__": + test_cpw_analytic_calc_analytical_gen() \ No newline at end of file diff --git a/test/test_estimate_timestep.py b/test/test_estimate_timestep.py new file mode 100644 index 00000000..91e8af6b --- /dev/null +++ b/test/test_estimate_timestep.py @@ -0,0 +1,66 @@ +import spyro +from spyro import create_transect +import math + + +def test_estimate_timestep_mlt(): + rectangle_dictionary = {} + rectangle_dictionary["options"] = { + # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "cell_type": "T", + "variant": "lumped", + } + rectangle_dictionary["mesh"] = { + "Lz": 0.75, # depth in km - always positive + "Lx": 1.5, + "h": 0.05, + } + rectangle_dictionary["acquisition"] = { + "source_locations": [(-0.1, 0.75)], + "receiver_locations": create_transect((-0.10, 0.1), (-0.10, 1.4), 50), + "frequency": 8.0, + } + rectangle_dictionary["time_axis"] = { + "final_time": 1.0, # Final time for event + } + Wave_obj = spyro.examples.Rectangle_acoustic( + dictionary=rectangle_dictionary + ) + layer_values = [1.5, 3.0] + z_switches = [-0.5] + Wave_obj.multiple_layer_velocity_model(z_switches, layer_values) + + # Tests value and if stable for 0.7 times estimated timestep + maxdt = Wave_obj.get_and_set_maximum_dt(fraction=0.7) + print(maxdt) + test1 = math.isclose(maxdt, 0.00085928546, rel_tol=1e-3) + + test2 = False + try: + Wave_obj.forward_solve() + test2 = True + except AssertionError: + test2 = False + + # Tests value and if unstable for 1.1 times estimated timestep + Wave_obj.current_time = 0.0 + maxdt = Wave_obj.get_and_set_maximum_dt(fraction=1.1) + test3 = math.isclose(maxdt, 0.001350305724782782, rel_tol=1e-3) + + test4 = False + try: + Wave_obj.forward_solve() + test4 = False + except AssertionError: + test4 = True + + print("Test 1: ", test1) + print("Test 2: ", test2) + print("Test 3: ", test3) + print("Test 4: ", test4) + + assert all([test1, test2, test3, test4]) + + +if __name__ == "__main__": + test_estimate_timestep_mlt() diff --git a/test/test_forward_examples.py b/test/test_forward_examples.py new file mode 100644 index 00000000..f3897d38 --- /dev/null +++ b/test/test_forward_examples.py @@ -0,0 +1,55 @@ +import spyro +import math + + +def test_camembert_forward(): + dictionary = {} + dictionary["absorving_boundary_conditions"] = { + "status": True, + "damping_type": "PML", + "exponent": 2, + "cmax": 4.5, + "R": 1e-6, + "pad_length": 0.25, + } + Wave_obj = spyro.examples.Camembert_acoustic(dictionary=dictionary) + + # Check if velocity model is correct + c_center = 4.6 + c_outside_center = 1.6 + c_wave = Wave_obj.initial_velocity_model + test1 = math.isclose(c_center, c_wave.at(-0.5, 0.5)) + test2 = math.isclose(c_outside_center, c_wave.at(-0.1, 0.5)) + + # Check if forward solve runs + Wave_obj.forward_solve() + test3 = True + + assert all([test1, test2, test3]) + + +def test_rectangle_forward(): + Wave_obj = spyro.examples.Rectangle_acoustic() + + # Check if velocity model is correct + layer_values = [1.5, 2.0, 2.5, 3.0] + z_switches = [-0.25, -0.5, -0.75] + Wave_obj.multiple_layer_velocity_model(z_switches, layer_values) + c_wave = Wave_obj.initial_velocity_model + + c0 = layer_values[0] + test1 = math.isclose(c0, c_wave.at(-0.2, 0.5)) + + c2 = layer_values[2] + test2 = math.isclose(c2, c_wave.at(-0.6, 0.5)) + + # Check if forward solve runs + Wave_obj.forward_solve() + test3 = True + + assert all([test1, test2, test3]) + + +if __name__ == "__main__": + test_camembert_forward() + test_rectangle_forward() diff --git a/test/test_gradient.py b/test/test_gradient.py index f80277fc..c64c67c4 100644 --- a/test/test_gradient.py +++ b/test/test_gradient.py @@ -1,16 +1,100 @@ import numpy as np +import os from firedrake import * import spyro from spyro.domains import quadrature +import pytest from .inputfiles.Model1_gradient_2d import model from .inputfiles.Model1_gradient_2d_pml import model_pml -# outfile_total_gradient = File(os.getcwd() + "/results/Gradient.pvd") - -forward = spyro.solvers.forward -gradient = spyro.solvers.gradient +dictionary = {} +dictionary["options"] = { + "cell_type": "T", # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "variant": "lumped", # lumped, equispaced or DG, default is lumped + "method": "MLT", # (MLT/spectral_quadrilateral/DG_triangle/DG_quadrilateral) You can either specify a cell_type+variant or a method + "degree": 4, # p order + "dimension": 2, # dimension + "automatic_adjoint": False, +} + +# Number of cores for the shot. For simplicity, we keep things serial. +# spyro however supports both spatial parallelism and "shot" parallelism. +dictionary["parallelism"] = { + "type": "automatic", # options: automatic (same number of cores for evey processor) or spatial +} + +# Define the domain size without the PML. Here we'll assume a 0.75 x 1.50 km +# domain and reserve the remaining 250 m for the Perfectly Matched Layer (PML) to absorb +# outgoing waves on three sides (eg., -z, +-x sides) of the domain. +dictionary["mesh"] = { + "Lz": 1.0, # depth in km - always positive # Como ver isso sem ler a malha? + "Lx": 1.0, # width in km - always positive + "Ly": 0.0, # thickness in km - always positive + "mesh_file": None, +} +dictionary[ + "synthetic_data" +] = { # For use only if you are using a synthetic test model or a forward only simulation -adicionar discrição para modelo direto + "real_mesh_file": None, + "real_velocity_file": None, +} +dictionary["inversion"] = { + "perform_fwi": False, # switch to true to make a FWI + "initial_guess_model_file": None, + "shot_record_file": None, + "optimization_parameters": None, +} + +# Specify a 250-m PML on the three sides of the domain to damp outgoing waves. +dictionary["absorving_boundary_conditions"] = { + "status": False, # True or false + "outer_bc": "non-reflective", # None or non-reflective (outer boundary condition) + "damping_type": "polynomial", # polynomial, hyperbolic, shifted_hyperbolic + "exponent": 2, # damping layer has a exponent variation + "cmax": 4.7, # maximum acoustic wave velocity in PML - km/s + "R": 1e-6, # theoretical reflection coefficient + "lz": 0.25, # thickness of the PML in the z-direction (km) - always positive + "lx": 0.25, # thickness of the PML in the x-direction (km) - always positive + "ly": 0.0, # thickness of the PML in the y-direction (km) - always positive +} + +# Create a source injection operator. Here we use a single source with a +# Ricker wavelet that has a peak frequency of 8 Hz injected at the center of the mesh. +# We also specify to record the solution at 101 microphones near the top of the domain. +# This transect of receivers is created with the helper function `create_transect`. +dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-0.1, 0.5)], + "frequency": 5.0, + "delay": 1.0, + "receiver_locations": spyro.create_transect((-0.10, 0.1), (-0.10, 0.9), 20), +} + +# Simulate for 2.0 seconds. +dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": 2.00, # Final time for event + "dt": 0.001, # timestep size + "amplitude": 1, # the Ricker has an amplitude of 1. + "output_frequency": 100, # how frequently to output solution to pvds - Perguntar Daiane ''post_processing_frequnecy' + "gradient_sampling_frequency": 100, # how frequently to save solution to RAM - Perguntar Daiane 'gradient_sampling_frequency' +} +dictionary["visualization"] = { + "forward_output": True, + "output_filename": "results/forward_output.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": False, + "gradient_filename": None, + "adjoint_output": False, + "adjoint_filename": None, +} +outfile_total_gradient = File(os.getcwd() + "/results/Gradient.pvd") + +# forward = spyro.solvers.forward +# gradient = spyro.solvers.gradient functional = spyro.utils.compute_functional @@ -18,9 +102,7 @@ def _make_vp_exact(V, mesh): """Create a circle with higher velocity in the center""" z, x = SpatialCoordinate(mesh) vp_exact = Function(V).interpolate( - 4.0 - + 1.0 * tanh(10.0 * (0.5 - sqrt((z - 1.5) ** 2 + (x + 1.5) ** 2))) - # 5.0 + 0.5 * tanh(10.0 * (0.5 - sqrt((z - 1.5) ** 2 + (x + 1.5) ** 2))) + 4.0 + 1.0 * tanh(10.0 * (0.5 - sqrt((z - 1.5) ** 2 + (x + 1.5) ** 2))) ) File("exact_vel.pvd").write(vp_exact) return vp_exact @@ -43,19 +125,20 @@ def _make_vp_guess(V, mesh): return vp_guess +@pytest.mark.skip(reason="not yet implemented") def test_gradient(): _test_gradient(model) +@pytest.mark.skip(reason="no way of currently testing this") def test_gradient_pml(): _test_gradient(model_pml, pml=True) def _test_gradient(options, pml=False): - comm = spyro.utils.mpi_init(options) - mesh, V = spyro.io.read_mesh(options, comm) + mesh, V = spyro.basicio.read_mesh(options, comm) if pml: vp_exact = _make_vp_exact_pml(V, mesh) @@ -117,7 +200,7 @@ def _test_gradient(options, pml=False): # compute the gradient of the control (to be verified) dJ = gradient(options, mesh, comm, vp_guess, receivers, p_guess, misfit) - dJ.dat.data[:] = dJ.dat.data[:]*mask.dat.data[:] + dJ.dat.data[:] = dJ.dat.data[:] * mask.dat.data[:] File("gradient.pvd").write(dJ) steps = [1e-3, 1e-4, 1e-5] # , 1e-6] # step length diff --git a/test/test_gradient_3d.py b/test/test_gradient_3d.py index d7722f2e..b1bc71c9 100644 --- a/test/test_gradient_3d.py +++ b/test/test_gradient_3d.py @@ -10,8 +10,8 @@ # outfile_total_gradient = File(os.getcwd() + "/results/Gradient.pvd") -forward = spyro.solvers.forward -gradient = spyro.solvers.gradient +# forward = spyro.solvers.forward +# gradient = spyro.solvers.gradient functional = spyro.utils.compute_functional @@ -38,7 +38,6 @@ def test_gradient_3d(): def _test_gradient(options, pml=False): - comm = spyro.utils.mpi_init(options) mesh, V = spyro.io.read_mesh(options, comm) diff --git a/test/test_io.py b/test/test_io.py index b8e473b8..df2dcef3 100644 --- a/test/test_io.py +++ b/test/test_io.py @@ -37,9 +37,9 @@ def test_read_and_write_segy(): model = {} - model["opts"] = { - "method": "CG", # either CG or KMV - "quadrature": "CG", # Equi or KMV + model["options"] = { + "cell_type": "T", # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "variant": "equispaced", # lumped, equispaced or DG, default is lumped "method":"MLT", # (MLT/spectral_quadrilateral/DG_triangle/DG_quadrilateral) You can either specify a cell_type+variant or a method "degree": 3, # p order "dimension": 2, # dimension } @@ -47,15 +47,31 @@ def test_read_and_write_segy(): "Lz": 1.0, # depth in km - always positive "Lx": 1.0, # width in km - always positive "Ly": 0.0, # thickness in km - always positive - "meshfile": None, - "initmodel": None, - "truemodel": hdf5_file, + "user_mesh": mesh, + "mesh_file": None, # specify the mesh file } model["BCs"] = { "status": False, } + model["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": 1.0, # Final time for event + "dt": 0.0005, # timestep size + "amplitude": 1, # the Ricker has an amplitude of 1. + "output_frequency": 100, # how frequently to output solution to pvds + "gradient_sampling_frequency": 1, # how frequently to save solution to RAM + } + model["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-1.0, 1.0)], + "frequency": 5.0, + "delay": 1.5, + "receiver_locations": [(-0.0, 0.5)], + } + + Wave_obj = spyro.AcousticWave(dictionary=model) - vp_read = spyro.io.interpolate(model, mesh, V, guess=False) + vp_read = spyro.io.interpolate(Wave_obj, hdf5_file, Wave_obj.function_space) fire.File("velocity_models/test.pvd").write(vp_read) @@ -66,5 +82,27 @@ def test_read_and_write_segy(): assert all([test1, test2]) +def test_saving_shot_record(): + from .inputfiles.model import dictionary + + dictionary["time_axis"]["final_time"] = 0.5 + Wave_obj = spyro.AcousticWave(dictionary=dictionary) + Wave_obj.set_mesh(dx=0.02) + Wave_obj.set_initial_velocity_model(constant=1.5) + Wave_obj.forward_solve() + spyro.io.save_shots(Wave_obj, file_name="test_shot_record") + + +def test_loading_shot_record(): + from .inputfiles.model import dictionary + + dictionary["time_axis"]["final_time"] = 0.5 + Wave_obj = spyro.AcousticWave(dictionary=dictionary) + Wave_obj.set_mesh(dx=0.02) + spyro.io.load_shots(Wave_obj, file_name="test_shot_record") + + if __name__ == "__main__": test_read_and_write_segy() + test_saving_shot_record() + test_loading_shot_record() diff --git a/test/test_model_parameters.py b/test/test_model_parameters.py new file mode 100644 index 00000000..978ac5fc --- /dev/null +++ b/test/test_model_parameters.py @@ -0,0 +1,405 @@ +import spyro +import pytest +from spyro.io import Model_parameters +from copy import deepcopy + +dictionary = {} +dictionary["options"] = { + "cell_type": "T", # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "variant": "lumped", # lumped, equispaced or DG, default is lumped + # (MLT/spectral_quadrilateral/DG_triangle/DG_quadrilateral) You can either specify a cell_type+variant or a method + "method": "MLT", + "degree": 4, # p order + "dimension": 2, # dimension +} + +# Number of cores for the shot. For simplicity, we keep things serial. +# spyro however supports both spatial parallelism and "shot" parallelism. +dictionary["parallelism"] = { + "type": "automatic", # options: automatic (same number of cores for evey processor) or spatial +} + +# Define the domain size without the PML. Here we'll assume a 1.00 x 1.00 km +# domain and reserve the remaining 250 m for the Perfectly Matched Layer (PML) to absorb +# outgoing waves on three sides (eg., -z, +-x sides) of the domain. +dictionary["mesh"] = { + "Lz": 1.0, # depth in km - always positive + "Lx": 1.0, # width in km - always positive + "Ly": 0.0, # thickness in km - always positive + "mesh_file": None, +} +# For use only if you are using a synthetic test model +dictionary["synthetic_data"] = { + "real_mesh_file": None, + "real_velocity_file": None, +} +dictionary["inversion"] = { + "perform_fwi": False, # switch to true to make a FWI + "initial_guess_model_file": None, + "shot_record_file": None, +} + +# Specify a 250-m PML on the three sides of the domain to damp outgoing waves. +dictionary["absorving_boundary_conditions"] = { + "status": False, # True or false + "outer_bc": "non-reflective", # None or non-reflective (outer boundary condition) + "damping_type": "polynomial", # polynomial, hyperbolic, shifted_hyperbolic + "exponent": 2, # damping layer has a exponent variation + "cmax": 4.7, # maximum acoustic wave velocity in PML - km/s + "R": 1e-6, # theoretical reflection coefficient + "lz": 0.25, # thickness of the PML in the z-direction (km) - always positive + "lx": 0.25, # thickness of the PML in the x-direction (km) - always positive + "ly": 0.0, # thickness of the PML in the y-direction (km) - always positive +} + +# Create a source injection operator. Here we use a single source with a +# Ricker wavelet that has a peak frequency of 5 Hz injected at the center of the mesh. +# We also specify to record the solution at a microphone near the top of the domain. +# This transect of receivers is created with the helper function `create_transect`. +dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-0.1, 0.5)], + "frequency": 5.0, + "delay": 1.0, + "receiver_locations": spyro.create_transect((-0.10, 0.1), (-0.10, 0.9), 20), +} + +# Simulate for 2.0 seconds. +dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": 2.00, # Final time for event + "dt": 0.001, # timestep size + "amplitude": 1, # the Ricker has an amplitude of 1. + "output_frequency": 100, # how frequently to output solution to pvds + "gradient_sampling_frequency": 100, # how frequently to save solution to RAM +} + + +def test_method_reader(): + test_dictionary = deepcopy(dictionary) + test_dictionary["options"] = { + "cell_type": None, # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "variant": None, # lumped, equispaced or DG, default is lumped + # (MLT/spectral_quadrilateral/DG_triangle/DG_quadrilateral) You can either specify a cell_type+variant or a method + "method": None, + "degree": 4, # p order + "dimension": 2, # dimension + } + # Trying out different method entries and seeing if all of them work for MLT + test1 = False + test_dictionary["options"]["method"] = "MLT" + model = Model_parameters(dictionary=test_dictionary) + if model.method == "mass_lumped_triangle": + test1 = True + + test2 = False + test_dictionary["options"]["method"] = "KMV" + model = Model_parameters(dictionary=test_dictionary) + if model.method == "mass_lumped_triangle": + test2 = True + + test3 = False + test_dictionary["options"]["method"] = "mass_lumped_triangle" + model = Model_parameters(dictionary=test_dictionary) + if model.method == "mass_lumped_triangle": + test3 = True + + # Trying out different method entries for spectral quads + test4 = False + test_dictionary["options"]["method"] = "spectral_quadrilateral" + model = Model_parameters(dictionary=test_dictionary) + if model.method == "spectral_quadrilateral": + test4 = True + + test5 = False + test_dictionary["options"]["method"] = "CG" + test_dictionary["options"]["variant"] = "GLL" + model = Model_parameters(dictionary=test_dictionary) + if model.method == "spectral_quadrilateral": + test5 = True + + test6 = False + test_dictionary["options"]["method"] = "SEM" + model = Model_parameters(dictionary=test_dictionary) + if model.method == "spectral_quadrilateral": + test6 = True + + # Trying out some entries for other less used methods + test7 = False + test_dictionary["options"]["method"] = "DG_triangle" + model = Model_parameters(dictionary=test_dictionary) + if model.method == "DG_triangle": + test7 = True + + test8 = False + test_dictionary["options"]["method"] = "DG_quadrilateral" + model = Model_parameters(dictionary=test_dictionary) + if model.method == "DG_quadrilateral": + test8 = True + + assert all([test1, test2, test3, test4, test5, test6, test7, test8]) + + +def test_cell_type_reader(): + ct_dictionary = deepcopy(dictionary) + ct_dictionary["options"] = { + "cell_type": None, # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "variant": None, # lumped, equispaced or DG, default is lumped + "method": None, # (MLT/spectral_quadrilateral/DG_triangle/DG_quadrilateral) You can either specify a cell_type+variant or a method + "degree": 4, # p order + "dimension": 2, # dimension + } + # Testing lumped cases + ct_dictionary["options"]["variant"] = "lumped" + + test1 = False + ct_dictionary["options"]["cell_type"] = "triangle" + model = Model_parameters(dictionary=ct_dictionary) + if model.method == "mass_lumped_triangle": + test1 = True + + test2 = False + ct_dictionary["options"]["cell_type"] = "quadrilateral" + model = Model_parameters(dictionary=ct_dictionary) + if model.method == "spectral_quadrilateral": + test2 = True + + # Testing equispaced cases + ct_dictionary["options"]["variant"] = "equispaced" + + test3 = False + ct_dictionary["options"]["cell_type"] = "triangle" + model = Model_parameters(dictionary=ct_dictionary) + if model.method == "CG_triangle": + test3 = True + + test4 = False + ct_dictionary["options"]["cell_type"] = "quadrilateral" + model = Model_parameters(dictionary=ct_dictionary) + if model.method == "CG_quadrilateral": + test4 = True + + # Testing DG cases + ct_dictionary["options"]["variant"] = "DG" + + test5 = False + ct_dictionary["options"]["cell_type"] = "triangle" + model = Model_parameters(dictionary=ct_dictionary) + if model.method == "DG_triangle": + test5 = True + + test6 = False + ct_dictionary["options"]["cell_type"] = "quadrilateral" + model = Model_parameters(dictionary=ct_dictionary) + if model.method == "DG_quadrilateral": + test6 = True + + assert all([test1, test2, test3, test4, test5, test6]) + + +def test_dictionary_conversion(): + # Define a default dictionary from old model (basing on read me) + old_dictionary = {} + # Choose method and parameters + old_dictionary["opts"] = { + "method": "KMV", # either CG or KMV + "quadrature": "KMV", # Equi or KMV + "degree": 3, # p order + "dimension": 2, # dimension + } + # Number of cores for the shot. For simplicity, we keep things serial. + # spyro however supports both spatial parallelism and "shot" parallelism. + old_dictionary["parallelism"] = { + "type": "automatic", # options: automatic (same number of cores for evey processor) or spatial + } + # Define the domain size without the PML. Here we'll assume a 0.75 x 1.50 km + # domain and reserve the remaining 250 m for the Perfectly Matched Layer (PML) to absorb + # outgoing waves on three sides (eg., -z, +-x sides) of the domain. + old_dictionary["mesh"] = { + "Lz": 0.75, # depth in km - always positive + "Lx": 1.5, # width in km - always positive + "Ly": 0.0, # thickness in km - always positive + "meshfile": "not_used.msh", + "initmodel": "not_used.hdf5", + "truemodel": "not_used.hdf5", + } + # Specify a 250-m PML on the three sides of the domain to damp outgoing waves. + old_dictionary["BCs"] = { + "status": True, # True or false + "outer_bc": "non-reflective", # None or non-reflective (outer boundary condition) + "damping_type": "polynomial", # polynomial, hyperbolic, shifted_hyperbolic + "exponent": 2, # damping layer has a exponent variation + "cmax": 4.7, # maximum acoustic wave velocity in PML - km/s + "R": 1e-6, # theoretical reflection coefficient + "lz": 0.25, # thickness of the PML in the z-direction (km) - always positive + "lx": 0.25, # thickness of the PML in the x-direction (km) - always positive + "ly": 0.0, # thickness of the PML in the y-direction (km) - always positive + } + # Create a source injection operator. Here we use a single source with a + # Ricker wavelet that has a peak frequency of 8 Hz injected at the center of the mesh. + # We also specify to record the solution at 101 microphones near the top of the domain. + # This transect of receivers is created with the helper function `create_transect`. + old_dictionary["acquisition"] = { + "source_type": "ricker", + "source_pos": [(-0.1, 0.75)], + "frequency": 8.0, + "delay": 1.0, + "receiver_locations": spyro.create_transect( + (-0.10, 0.1), (-0.10, 1.4), 100 + ), + } + # Simulate for 2.0 seconds. + old_dictionary["timeaxis"] = { + "t0": 0.0, # Initial time for event + "tf": 2.00, # Final time for event + "dt": 0.0005, # timestep size + "amplitude": 1, # the Ricker has an amplitude of 1. + "nspool": 100, # how frequently to output solution to pvds + "fspool": 100, # how frequently to save solution to RAM + } + + # Setting up the new equivalent dictionary + new_dictionary = {} + new_dictionary["options"] = { + "cell_type": "T", # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "variant": "lumped", # lumped, equispaced or DG, default is lumped + "degree": 3, # p order + "dimension": 2, # dimension + } + # Number of cores for the shot. For simplicity, we keep things serial. + # spyro however supports both spatial parallelism and "shot" parallelism. + new_dictionary["parallelism"] = { + "type": "automatic", # options: automatic (same number of cores for evey processor) or spatial + } + # Define the domain size without the PML. Here we'll assume a 1.00 x 1.00 km + # domain and reserve the remaining 250 m for the Perfectly Matched Layer (PML) to absorb + # outgoing waves on three sides (eg., -z, +-x sides) of the domain. + new_dictionary["mesh"] = { + "Lz": 0.75, # depth in km - always positive + "Lx": 1.50, # width in km - always positive + "Ly": 0.0, # thickness in km - always positive + "mesh_file": None, + } + # For use only if you are using a synthetic test model + new_dictionary["synthetic_data"] = { + "real_mesh_file": None, + "real_velocity_file": None, + } + new_dictionary["inversion"] = { + "perform_fwi": False, # switch to true to make a FWI + "initial_guess_model_file": None, + "shot_record_file": None, + } + + # Specify a 250-m PML on the three sides of the domain to damp outgoing waves. + new_dictionary["absorving_boundary_conditions"] = { + "status": True, # True or false + "damping_type": "PML", # polynomial, hyperbolic, shifted_hyperbolic + "exponent": 2, # damping layer has a exponent variation + "cmax": 4.7, # maximum acoustic wave velocity in PML - km/s + "R": 1e-6, # theoretical reflection coefficient + "pad_length": 0.25, # thickness of the PML - always positive + } + + # Create a source injection operator. Here we use a single source with a + # Ricker wavelet that has a peak frequency of 5 Hz injected at the center of the mesh. + # We also specify to record the solution at a microphone near the top of the domain. + # This transect of receivers is created with the helper function `create_transect`. + new_dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-0.1, 0.75)], + "frequency": 8.0, + "delay": 1.0, + "receiver_locations": spyro.create_transect( + (-0.10, 0.1), (-0.10, 1.4), 100 + ), + } + + # Simulate for 2.0 seconds. + new_dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": 2.00, # Final time for event + "dt": 0.0005, # timestep size + "amplitude": 1, # the Ricker has an amplitude of 1. + "output_frequency": 100, # how frequently to output solution to pvds + "gradient_sampling_frequency": 100, # how frequently to save solution to RAM + } + model_from_old = Model_parameters(dictionary=old_dictionary) + model_from_new = Model_parameters(dictionary=new_dictionary) + + # checking relevant information from models + same = True + if model_from_new.method != model_from_old.method: + same = False + if model_from_new.initial_time != model_from_old.initial_time: + same = False + if model_from_new.degree != model_from_old.degree: + same = False + if model_from_new.dimension != model_from_old.dimension: + same = False + if model_from_new.dt != model_from_old.dt: + same = False + if model_from_new.final_time != model_from_old.final_time: + same = False + if model_from_new.forward_output_file != model_from_old.forward_output_file: + same = False + if model_from_new.running_fwi != model_from_old.running_fwi: + same = False + + assert same + + +def test_degree_exception_2d(): + ex_dictionary = deepcopy(dictionary) + with pytest.raises(Exception): + ex_dictionary["options"]["dimension"] = 2 + ex_dictionary["options"]["degree"] = 6 + model = Model_parameters(dictionary=ex_dictionary) + + +def test_degree_exception_3d(): + ex_dictionary = deepcopy(dictionary) + with pytest.raises(Exception): + ex_dictionary["options"]["dimension"] = 3 + ex_dictionary["options"]["degree"] = 5 + model = Model_parameters(dictionary=ex_dictionary) + + +def test_time_exception(): + ex_dictionary = deepcopy(dictionary) + with pytest.raises(Exception): + ex_dictionary["time_axis"]["final_time"] = -0.5 + model = Model_parameters(dictionary=ex_dictionary) + + +def test_source_exception(): + ex_dictionary = deepcopy(dictionary) + with pytest.raises(Exception): + ex_dictionary["acquistion"]["source_locations"] = [ + (-0.1, 0.5), + (1.0, 0.5), + ] + model = Model_parameters(dictionary=ex_dictionary) + + +def test_receiver_exception(): + ex_dictionary = deepcopy(dictionary) + with pytest.raises(Exception): + ex_dictionary["acquistion"]["receiver_locations"] = [ + (-0.1, 0.5), + (1.0, 0.5), + ] + model = Model_parameters(dictionary=ex_dictionary) + + +if __name__ == "__main__": + test_method_reader() + test_cell_type_reader() + test_dictionary_conversion() + test_degree_exception_2d() + test_degree_exception_3d() + test_time_exception() + test_source_exception() + test_receiver_exception() + + print("END") diff --git a/test/test_newat.py b/test/test_newat.py index c915afb6..d82a41ea 100644 --- a/test/test_newat.py +++ b/test/test_newat.py @@ -4,8 +4,8 @@ from firedrake import * import spyro -from .inputfiles.Model1_2d_CG import model -from .inputfiles.Model1_3d_CG import model as model3D +from .inputfiles.Model1_2d_CG import model as oldmodel +from .inputfiles.Model1_3d_CG import model as oldmodel3D def triangle_area(p1, p2, p3): @@ -19,8 +19,6 @@ def triangle_area(p1, p2, p3): def test_correct_receiver_location_generation2D(): """Tests if receiver locations where generated correctly""" - comm = spyro.utils.mpi_init(model) - mesh, V = spyro.io.read_mesh(model, comm) receivers = spyro.create_transect((-0.1, 0.3), (-0.1, 0.9), 3) answer = np.array([[-0.1, 0.3], [-0.1, 0.6], [-0.1, 0.9]]) @@ -30,22 +28,21 @@ def test_correct_receiver_location_generation2D(): def test_correct_receiver_to_cell_location2D(): """Tests if the receivers where located in the correct cell""" - comm = spyro.utils.mpi_init(model) - model["opts"]["degree"] = 3 - mesh, V = spyro.io.read_mesh(model, comm) - model["acquisition"]["num_receivers"] = 3 + oldmodel["opts"]["degree"] = 3 recvs = spyro.create_transect((-0.1, 0.3), (-0.1, 0.9), 3) - recvs = model["acquisition"]["receiver_locations"] = recvs + oldmodel["acquisition"]["receiver_locations"] = recvs - receivers = spyro.Receivers(model, mesh, V, comm) + model = spyro.Wave(dictionary=oldmodel) + + receivers = spyro.Receivers(model) # test 1 cell_vertex1 = receivers.cellVertices[0][0] cell_vertex2 = receivers.cellVertices[0][1] cell_vertex3 = receivers.cellVertices[0][2] - x = receivers.receiver_locations[0, 0] - y = receivers.receiver_locations[0, 1] + x = receivers.point_locations[0, 0] + y = receivers.point_locations[0, 1] p = (x, y) areaT = triangle_area(cell_vertex1, cell_vertex2, cell_vertex3) @@ -59,8 +56,8 @@ def test_correct_receiver_to_cell_location2D(): cell_vertex1 = receivers.cellVertices[1][0] cell_vertex2 = receivers.cellVertices[1][1] cell_vertex3 = receivers.cellVertices[1][2] - x = receivers.receiver_locations[1, 0] - y = receivers.receiver_locations[1, 1] + x = receivers.point_locations[1, 0] + y = receivers.point_locations[1, 1] p = (x, y) areaT = triangle_area(cell_vertex1, cell_vertex2, cell_vertex3) @@ -74,8 +71,8 @@ def test_correct_receiver_to_cell_location2D(): cell_vertex1 = receivers.cellVertices[2][0] cell_vertex2 = receivers.cellVertices[2][1] cell_vertex3 = receivers.cellVertices[2][2] - x = receivers.receiver_locations[2, 0] - y = receivers.receiver_locations[2, 1] + x = receivers.point_locations[2, 0] + y = receivers.point_locations[2, 1] p = (x, y) areaT = triangle_area(cell_vertex1, cell_vertex2, cell_vertex3) @@ -89,31 +86,31 @@ def test_correct_receiver_to_cell_location2D(): def test_correct_at_value2D(): - comm = spyro.utils.mpi_init(model) - model["opts"]["degree"] = 3 - mesh, V = spyro.io.read_mesh(model, comm) + oldmodel["opts"]["degree"] = 3 pz = -0.1 px = 0.3 recvs = spyro.create_transect((pz, px), (pz, px), 3) # recvs = spyro.create_transect( # (-0.00935421, 3.25160664), (-0.00935421, 3.25160664), 3 # ) - model["acquisition"]["receiver_locations"] = recvs - model["acquisition"]["num_receivers"] = 3 + oldmodel["acquisition"]["receiver_locations"] = recvs + oldmodel["acquisition"]["num_receivers"] = 3 - receivers = spyro.Receivers(model, mesh, V, comm) + model = spyro.Wave(dictionary=oldmodel) + mesh = model.mesh + receivers = spyro.Receivers(model) V = receivers.space z, x = SpatialCoordinate(mesh) u1 = Function(V).interpolate(x + z) test1 = math.isclose( - (pz + px), receivers._Receivers__new_at(u1.dat.data[:], 0), rel_tol=1e-09 + (pz + px), receivers.new_at(u1.dat.data[:], 0), rel_tol=1e-09 ) u1 = Function(V).interpolate(sin(x) * z * 2) test2 = math.isclose( sin(px) * pz * 2, - receivers._Receivers__new_at(u1.dat.data[:], 0), + receivers.new_at(u1.dat.data[:], 0), rel_tol=1e-05, ) @@ -121,33 +118,40 @@ def test_correct_at_value2D(): def test_correct_at_value2D_quad(): - model_quad = deepcopy(model) - comm = spyro.utils.mpi_init(model_quad) - model_quad["opts"]["degree"] = 3 - model_quad["opts"]["degree"] = 3 - mesh, V = spyro.io.read_mesh(model_quad, comm) + oldmodel_quad = deepcopy(oldmodel) + oldmodel_quad["opts"]["degree"] = 3 + oldmodel_quad["opts"]["quadrature"] = "GLL" + oldmodel_quad["mesh"]["initmodel"] = None + oldmodel_quad["mesh"]["truemodel"] = None pz = -0.1 px = 0.3 recvs = spyro.create_transect((pz, px), (pz, px), 3) - # recvs = spyro.create_transect( - # (-0.00935421, 3.25160664), (-0.00935421, 3.25160664), 3 - # ) - model_quad["acquisition"]["receiver_locations"] = recvs - model_quad["acquisition"]["num_receivers"] = 3 - receivers = spyro.Receivers(model_quad, mesh, V, comm) + oldmodel_quad["acquisition"]["receiver_locations"] = recvs + new_dictionary = spyro.io.Dictionary_conversion( + oldmodel_quad + ).new_dictionary + new_dictionary["mesh"]["mesh_file"] = None + new_dictionary["mesh"]["mesh_type"] = "firedrake_mesh" + new_dictionary["options"]["cell_type"] = "quadrilateral" + + model_quad = spyro.Wave(dictionary=new_dictionary) + model_quad.set_mesh(dx=0.02) + mesh = model_quad.mesh + + receivers = spyro.Receivers(model_quad) V = receivers.space z, x = SpatialCoordinate(mesh) u1 = Function(V).interpolate(x + z) test1 = math.isclose( - (pz + px), receivers._Receivers__new_at(u1.dat.data[:], 0), rel_tol=1e-09 + (pz + px), receivers.new_at(u1.dat.data[:], 0), rel_tol=1e-09 ) u1 = Function(V).interpolate(sin(x) * z * 2) test2 = math.isclose( sin(px) * pz * 2, - receivers._Receivers__new_at(u1.dat.data[:], 0), + receivers.new_at(u1.dat.data[:], 0), rel_tol=1e-05, ) @@ -173,40 +177,42 @@ def tetrahedral_volume(p1, p2, p3, p4): def test_correct_receiver_location_generation3D(): """Tests if receiver locations where generated correctly""" - test_model = deepcopy(model3D) - comm = spyro.utils.mpi_init(test_model) - mesh, V = spyro.io.read_mesh(test_model, comm) - test_model["acquisition"]["num_receivers"] = 3 + oldtest_model = deepcopy(oldmodel3D) receivers = spyro.create_transect((-0.05, 0.3, 0.5), (-0.05, 0.9, 0.5), 3) - test_model["acquisition"]["receiver_locations"] = receivers - receivers = spyro.Receivers(test_model, mesh, V, comm) + oldtest_model["acquisition"]["receiver_locations"] = receivers + test_model = spyro.Wave(dictionary=oldtest_model) + + receivers = spyro.Receivers(test_model) + answer = np.array([[-0.05, 0.3, 0.5], [-0.05, 0.6, 0.5], [-0.05, 0.9, 0.5]]) - assert np.allclose(receivers.receiver_locations, answer) + assert np.allclose(receivers.point_locations, answer) def test_correct_receiver_to_cell_location3D(): """Tests if the receivers where located in the correct cell""" - test_model1 = deepcopy(model3D) - comm = spyro.utils.mpi_init(test_model1) - mesh, V = spyro.io.read_mesh(test_model1, comm) + oldtest_model1 = deepcopy(oldmodel3D) rec = spyro.create_transect((-0.05, 0.1, 0.5), (-0.05, 0.9, 0.5), 3) - test_model1["acquisition"]["receiver_locations"] = rec - test_model1["acquisition"]["num_receivers"] = 3 - receivers = spyro.Receivers(test_model1, mesh, V, comm) + oldtest_model1["acquisition"]["receiver_locations"] = rec + + test_model1 = spyro.Wave(dictionary=oldtest_model1) + + receivers = spyro.Receivers(test_model1) # test 1 cell_vertex1 = receivers.cellVertices[0][0] cell_vertex2 = receivers.cellVertices[0][1] cell_vertex3 = receivers.cellVertices[0][2] cell_vertex4 = receivers.cellVertices[0][3] - x = receivers.receiver_locations[0, 0] - y = receivers.receiver_locations[0, 1] - z = receivers.receiver_locations[0, 2] + x = receivers.point_locations[0, 0] + y = receivers.point_locations[0, 1] + z = receivers.point_locations[0, 2] p = (x, y, z) - volumeT = tetrahedral_volume(cell_vertex1, cell_vertex2, cell_vertex3, cell_vertex4) + volumeT = tetrahedral_volume( + cell_vertex1, cell_vertex2, cell_vertex3, cell_vertex4 + ) volume1 = tetrahedral_volume(p, cell_vertex2, cell_vertex3, cell_vertex4) volume2 = tetrahedral_volume(cell_vertex1, p, cell_vertex3, cell_vertex4) volume3 = tetrahedral_volume(cell_vertex1, cell_vertex2, p, cell_vertex4) @@ -221,12 +227,14 @@ def test_correct_receiver_to_cell_location3D(): cell_vertex2 = receivers.cellVertices[1][1] cell_vertex3 = receivers.cellVertices[1][2] cell_vertex4 = receivers.cellVertices[1][3] - x = receivers.receiver_locations[1, 0] - y = receivers.receiver_locations[1, 1] - z = receivers.receiver_locations[1, 2] + x = receivers.point_locations[1, 0] + y = receivers.point_locations[1, 1] + z = receivers.point_locations[1, 2] p = (x, y, z) - volumeT = tetrahedral_volume(cell_vertex1, cell_vertex2, cell_vertex3, cell_vertex4) + volumeT = tetrahedral_volume( + cell_vertex1, cell_vertex2, cell_vertex3, cell_vertex4 + ) volume1 = tetrahedral_volume(p, cell_vertex2, cell_vertex3, cell_vertex4) volume2 = tetrahedral_volume(cell_vertex1, p, cell_vertex3, cell_vertex4) volume3 = tetrahedral_volume(cell_vertex1, cell_vertex2, p, cell_vertex4) @@ -241,12 +249,14 @@ def test_correct_receiver_to_cell_location3D(): cell_vertex2 = receivers.cellVertices[2][1] cell_vertex3 = receivers.cellVertices[2][2] cell_vertex4 = receivers.cellVertices[2][3] - x = receivers.receiver_locations[2, 0] - y = receivers.receiver_locations[2, 1] - z = receivers.receiver_locations[2, 2] + x = receivers.point_locations[2, 0] + y = receivers.point_locations[2, 1] + z = receivers.point_locations[2, 2] p = (x, y, z) - volumeT = tetrahedral_volume(cell_vertex1, cell_vertex2, cell_vertex3, cell_vertex4) + volumeT = tetrahedral_volume( + cell_vertex1, cell_vertex2, cell_vertex3, cell_vertex4 + ) volume1 = tetrahedral_volume(p, cell_vertex2, cell_vertex3, cell_vertex4) volume2 = tetrahedral_volume(cell_vertex1, p, cell_vertex3, cell_vertex4) volume3 = tetrahedral_volume(cell_vertex1, cell_vertex2, p, cell_vertex4) @@ -260,11 +270,10 @@ def test_correct_receiver_to_cell_location3D(): def test_correct_at_value3D(): - test_model2 = deepcopy(model3D) - test_model2["acquisition"]["num_receivers"] = 3 - test_model2["opts"]["degree"] = 3 - comm = spyro.utils.mpi_init(test_model2) - mesh, V = spyro.io.read_mesh(test_model2, comm) + oldtest_model2 = deepcopy(oldmodel3D) + + oldtest_model2["opts"]["degree"] = 3 + x_start = 0.09153949331982138 x_end = 0.09153949331982138 z_start = 0.0 @@ -274,22 +283,27 @@ def test_correct_at_value3D(): x_real, y_real, z_real = x_start, y_start, z_start - recvs = spyro.create_transect((z_start, x_start, y_start), (z_end, x_end, y_end), 3) - test_model2["acquisition"]["receiver_locations"] = recvs - receivers = spyro.Receivers(test_model2, mesh, V, comm) + recvs = spyro.create_transect( + (z_start, x_start, y_start), (z_end, x_end, y_end), 3 + ) + oldtest_model2["acquisition"]["receiver_locations"] = recvs + + test_model2 = spyro.Wave(dictionary=oldtest_model2) + receivers = spyro.Receivers(test_model2) V = receivers.space + mesh = test_model2.mesh z, x, y = SpatialCoordinate(mesh) u1 = Function(V).interpolate(x + z + y) realvalue = x_real + y_real + z_real test1 = math.isclose( - realvalue, receivers._Receivers__new_at(u1.dat.data[:], 0), rel_tol=1e-09 + realvalue, receivers.new_at(u1.dat.data[:], 0), rel_tol=1e-09 ) u1 = Function(V).interpolate(sin(x) * (z + 1) ** 2 * cos(y)) realvalue = sin(x_real) * (z_real + 1) ** 2 * cos(y_real) test2 = math.isclose( - realvalue, receivers._Receivers__new_at(u1.dat.data[:], 0), rel_tol=1e-05 + realvalue, receivers.new_at(u1.dat.data[:], 0), rel_tol=1e-05 ) assert all([test1, test2]) @@ -303,3 +317,5 @@ def test_correct_at_value3D(): test_correct_receiver_location_generation3D() test_correct_receiver_to_cell_location3D() test_correct_at_value3D() + + print("END") diff --git a/test/test_parallel_source.py b/test/test_parallel_source.py index e46289d0..5d404ede 100644 --- a/test/test_parallel_source.py +++ b/test/test_parallel_source.py @@ -10,14 +10,13 @@ from .inputfiles.Model1_parallel_2d import model as options -forward = spyro.solvers.forward -gradient = spyro.solvers.gradient +# forward = spyro.solvers.forward +# gradient = spyro.solvers.gradient functional = spyro.utils.compute_functional @pytest.mark.skip(reason="no way of currently testing this") def test_parallel_source(): - comm = spyro.utils.mpi_init(options) mesh, V = spyro.io.read_mesh(options, comm) diff --git a/test/test_plots.py b/test/test_plots.py new file mode 100644 index 00000000..8dea5b94 --- /dev/null +++ b/test/test_plots.py @@ -0,0 +1,31 @@ +import spyro +from spyro import create_transect + + +def test_plot(): + rectangle_dictionary = {} + rectangle_dictionary["mesh"] = { + "Lz": 0.75, # depth in km - always positive + "Lx": 1.5, + "h": 0.05, + } + rectangle_dictionary["acquisition"] = { + "source_locations": [(-0.1, 0.75)], + "receiver_locations": create_transect((-0.10, 0.1), (-0.10, 1.4), 50), + "frequency": 8.0, + } + rectangle_dictionary["time_axis"] = { + "final_time": 2.0, # Final time for event + } + Wave_obj = spyro.examples.Rectangle_acoustic( + dictionary=rectangle_dictionary + ) + layer_values = [1.5, 3.0] + z_switches = [-0.5] + Wave_obj.multiple_layer_velocity_model(z_switches, layer_values) + Wave_obj.forward_solve() + spyro.plots.plot_shots(Wave_obj) + + +if __name__ == "__main__": + test_plot() diff --git a/test/test_pml_2d.py b/test/test_pml_2d.py new file mode 100644 index 00000000..f8ca17cb --- /dev/null +++ b/test/test_pml_2d.py @@ -0,0 +1,124 @@ +import spyro +import matplotlib.pyplot as plt +import numpy as np +import time as timer +import firedrake as fire +import pickle + + +def error_calc(p_numerical, p_analytical, nt): + norm = np.linalg.norm(p_numerical, 2) / np.sqrt(nt) + error_time = np.linalg.norm(p_analytical - p_numerical, 2) / np.sqrt(nt) + div_error_time = error_time / norm + return div_error_time + + +def run_forward(): + dt = 0.0001 + + t0 = timer.time() + + final_time = 1.4 + + dictionary = {} + dictionary["options"] = { + "cell_type": "T", # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "variant": "lumped", # lumped, equispaced or DG, default is lumped "method":"MLT", # (MLT/spectral_quadrilateral/DG_triangle/DG_quadrilateral) You can either specify a cell_type+variant or a method + "degree": 4, # p order + "dimension": 2, # dimension + } + + # Number of cores for the shot. For simplicity, we keep things serial. + # spyro however supports both spatial parallelism and "shot" parallelism. + dictionary["parallelism"] = { + "type": "automatic", # options: automatic (same number of cores for evey processor) or spatial + } + + # Define the domain size without the PML. Here we'll assume a 1.00 x 1.00 km + # domain and reserve the remaining 250 m for the Perfectly Matched Layer (PML) to absorb + # outgoing waves on three sides (eg., -z, +-x sides) of the domain. + dictionary["mesh"] = { + "Lz": 1.0, # depth in km - always positive + "Lx": 1.0, # width in km - always positive + "Ly": 0.0, # thickness in km - always positive + "mesh_file": None, + "mesh_type": "firedrake_mesh", # options: firedrake_mesh or user_mesh + } + + # Create a source injection operator. Here we use a single source with a + # Ricker wavelet that has a peak frequency of 5 Hz injected at the center of the mesh. + # We also specify to record the solution at a microphone near the top of the domain. + # This transect of receivers is created with the helper function `create_transect`. + dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-0.1, 0.5)], + "frequency": 5.0, + "delay": 0.3, + "receiver_locations": spyro.create_transect( + (-0.15, 0.1), (-0.15, 0.9), 50 + ), + "delay_type": "time", + } + + # Simulate for 2.0 seconds. + dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": final_time, # Final time for event + "dt": dt, # timestep size + "amplitude": 1, # the Ricker has an amplitude of 1. + "output_frequency": 200, # how frequently to output solution to pvds + "gradient_sampling_frequency": 200, # how frequently to save solution to RAM + } + + dictionary["absorving_boundary_conditions"] = { + "status": True, + "damping_type": "PML", + "exponent": 2, + "cmax": 4.5, + "R": 1e-6, + "pad_length": 0.25, + } + + dictionary["visualization"] = { + "forward_output": True, + "forward_output_filename": "results/extended_pml_propagation.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": False, + "gradient_filename": None, + } + + Wave_obj = spyro.solvers.AcousticWave(dictionary=dictionary) + Wave_obj.set_mesh(dx=0.02) + + z = Wave_obj.mesh_z + cond = fire.conditional( + z > -0.333, 1.5, fire.conditional(z > -0.667, 3.0, 4.5) + ) + Wave_obj.set_initial_velocity_model(conditional=cond) + Wave_obj.forward_solve() + + t1 = timer.time() + print("Time elapsed: ", t1 - t0) + nt = int(final_time / dt) + 1 + p_r = Wave_obj.forward_solution_receivers + + return p_r, nt + + +def test_pml(): + """Test that the second order time convergence + of the central difference method is achieved""" + + p_r, nt = run_forward() + with open("test/inputfiles/extended_pml_receveirs.pck", "rb") as f: + array = np.asarray(pickle.load(f), dtype=float) + extended_p_r = array + + error = error_calc(extended_p_r, p_r, nt) + print(f"Error of {error}") + assert np.abs(error) < 0.05 + + +if __name__ == "__main__": + test_pml() diff --git a/test/test_readmesh.py b/test/test_readmesh.py index f34b68c7..522da8fe 100644 --- a/test/test_readmesh.py +++ b/test/test_readmesh.py @@ -6,25 +6,27 @@ def test_readmesh2(): - from .inputfiles.Model1_2d_CG import model + from .inputfiles.Model1_2d_CG import model as oldmodel - comm = spyro.utils.mpi_init(model) + model = spyro.Wave(dictionary=oldmodel) - mesh, V = spyro.io.read_mesh(model, comm) - - vp = spyro.io.interpolate(model, mesh, V) + vp = spyro.io.interpolate( + model, oldmodel["mesh"]["initmodel"], model.function_space + ) assert not np.isnan(np.min(vp.dat.data[:])) def test_readmesh3(): - from .inputfiles.Model1_3d_CG import model - - comm = spyro.utils.mpi_init(model) + from .inputfiles.Model1_3d_CG import model as oldmodel - mesh, V = spyro.io.read_mesh(model, comm) + receivers = spyro.create_transect((-0.05, 0.3, 0.5), (-0.05, 0.9, 0.5), 3) + oldmodel["acquisition"]["receiver_locations"] = receivers + model = spyro.Wave(dictionary=oldmodel) - vp = spyro.io.interpolate(model, mesh, V) + vp = spyro.io.interpolate( + model, oldmodel["mesh"]["initmodel"], model.function_space + ) assert not np.isnan(np.min(vp.dat.data[:])) diff --git a/test/test_seismicmesh_integration.py b/test/test_seismicmesh_integration.py new file mode 100644 index 00000000..6f41bc35 --- /dev/null +++ b/test/test_seismicmesh_integration.py @@ -0,0 +1,66 @@ +import spyro +import firedrake as fire +import numpy as np + + +def mean_edge_length(triangle): + """ + Compute the mean edge length of a triangle + """ + (x0, y0), (x1, y1), (x2, y2) = triangle + l0 = np.sqrt((x1-x0)**2+(y1-y0)**2) + l1 = np.sqrt((x2-x1)**2+(y2-y1)**2) + l2 = np.sqrt((x0-x2)**2+(y0-y2)**2) + return (l0+l1+l2)/3.0 + + +def test_spyro_seimicmesh_2d_homogeneous_generation(): + Lz = 1.0 + Lx = 2.0 + c = 1.5 + freq = 5.0 + lbda = c/freq + pad = 0.3 + cpw = 3 + + Mesh_obj = spyro.meshing.AutomaticMesh( + dimension=2, + abc_pad=pad, + mesh_type="SeismicMesh" + ) + Mesh_obj.set_mesh_size(length_z=Lz, length_x=Lx) + Mesh_obj.set_seismicmesh_parameters(edge_length=lbda/cpw, output_file_name="test.msh") + + mesh = Mesh_obj.create_mesh() + + V = fire.FunctionSpace(mesh, "CG", 1) + z_mesh, x_mesh = fire.SpatialCoordinate(mesh) + uz = fire.Function(V).interpolate(z_mesh) + ux = fire.Function(V).interpolate(x_mesh) + + z = uz.dat.data[:] + x = ux.dat.data[:] + + # Testing if boundaries are correct + test1 = (np.isclose(np.amin(z), -Lz-pad)) + test1 = test1 and (np.isclose(np.amax(x), Lx+pad)) + test1 = test1 and (np.isclose(np.amax(z), 0.0)) + test1 = test1 and (np.isclose(np.amin(x), -pad)) + print(f"Boundary values are correct: {test1}") + + # Checking edge length of an interior cell + node_ids = V.cell_node_list[300] + p0 = (z[node_ids[0]], x[node_ids[0]]) + p1 = (z[node_ids[1]], x[node_ids[1]]) + p2 = (z[node_ids[2]], x[node_ids[2]]) + + le = mean_edge_length((p0, p1, p2)) + le_expected = lbda/cpw + test2 = np.isclose(le, le_expected, rtol=1e-1) + print(f"Edge length is correct: {test2}") + + assert all([test1, test2]) + + +if __name__ == "__main__": + test_spyro_seimicmesh_2d_homogeneous_generation() diff --git a/test/test_sources.py b/test/test_sources.py index dd707b09..236f201f 100644 --- a/test/test_sources.py +++ b/test/test_sources.py @@ -1,10 +1,9 @@ import math from copy import deepcopy - import spyro """Read in an external mesh and interpolate velocity to it""" -from .inputfiles.Model1_2d_CG import model as model +from .inputfiles.Model1_2d_CG import model as oldmodel def test_ricker_varies_in_time(): @@ -13,114 +12,49 @@ def test_ricker_varies_in_time(): firedrake. It tests if the right hand side varies in time and if the applied ricker function behaves correctly """ - # initial ricker tests - modelRicker = deepcopy(model) + + ### initial ricker tests + modelRicker = deepcopy(oldmodel) frequency = 2 amplitude = 3 # tests if ricker starts at zero delay = 1.5 * math.sqrt(6.0) / (math.pi * frequency) t = 0.0 + r0 = spyro.sources.timedependentSource(modelRicker, t, frequency, amplitude) test1 = math.isclose( - spyro.sources.timedependentSource(modelRicker, t, frequency, amplitude), + r0, 0, abs_tol=1e-3, ) - # tests if the minimum value is correct and occurs at correct location + # tests if the minimum value is correct and occurs at correct locations minimum = -amplitude * 2 / math.exp(3.0 / 2.0) t = 0.0 + delay + math.sqrt(6.0) / (2.0 * math.pi * frequency) + rmin1 = spyro.sources.timedependentSource( + modelRicker, t, frequency, amplitude + ) test2 = math.isclose( - spyro.sources.timedependentSource(modelRicker, t, frequency, amplitude), minimum + rmin1, + minimum, ) + t = 0.0 + delay - math.sqrt(6.0) / (2.0 * math.pi * frequency) - test3 = math.isclose( - spyro.sources.timedependentSource(modelRicker, t, frequency, amplitude), minimum + rmin2 = spyro.sources.timedependentSource( + modelRicker, t, frequency, amplitude ) + test3 = math.isclose(rmin2, minimum) # tests if maximum value in correct and occurs at correct location t = 0.0 + delay + rmax = spyro.sources.timedependentSource( + modelRicker, t, frequency, amplitude + ) test4 = math.isclose( - spyro.sources.timedependentSource(modelRicker, t, frequency, amplitude), + rmax, amplitude, ) - # ### model parameters necessary for source creation - # modelRicker["acquisition"]["source_type"] = "Ricker" - # modelRicker["acquisition"]["num_sources"] = 1 - # modelRicker["acquisition"]["source_pos"] = [(0.5, 0.5)] - # modelRicker["opts"]["method"] = "CG" - # modelRicker["opts"]["degree"] = 2 - # modelRicker["opts"]["dimension"] = 2 - # comm = spyro.utils.mpi_init(modelRicker) - # mesh = fire.UnitSquareMesh(10, 10) - # element = fire.FiniteElement("CG", mesh.ufl_cell(), 2, variant="equispaced") - # V = fire.FunctionSpace(mesh, element) - # excitations = spyro.Sources(modelRicker, mesh, V, comm).create() - - # ### Defining variables for our wave problem - # t = 0.0 - # dt = 0.01 - # # dt = fire.Constant(delta_t) - # u = fire.TrialFunction(V) - # v = fire.TestFunction(V) - # u_prevs0 = fire.Function(V) - # u_prevs1 = fire.Function(V) - - # ### Calling ricker source term - # excitation = excitations[0] - # ricker = Constant(0) - # expr = excitation * ricker - # ricker.assign( - # spyro.sources.timedependentSource(modelRicker, t, frequency, amplitude) - # ) - # f = fire.Function(V).assign(expr) - - # ### Creating form of a simple second order time-dependent wave equation with - # ### uniform density and wave velocity of 1 - # F = ( - # (u - 2.0 * u_prevs0 + u_prevs1) / (dt ** 2) * v * dx - # + dot(grad(u_prevs0), grad(v)) * dx - # - f * v * dx - # ) - # a, b = fire.lhs(F), fire.rhs(F) - - # ### Creating solver object - # bcs = fire.DirichletBC(V, 0.0, "on_boundary") - # A = fire.assemble(a, bcs=None) - # B = fire.assemble(b) - - # params = {"ksp_type": "preonly", "pc_type": "jacobi"} - # solver = fire.LinearSolver(A, P=None, solver_parameters=params) - # u_h = fire.Function(V) - - # steps = 50 - # p_y = np.zeros((steps)) - # r_y = np.zeros((steps)) - # t_x = np.zeros((steps)) - - # for step in range(1, steps): - # t = step * float(dt) - # ricker.assign( - # spyro.sources.timedependentSource(modelRicker, t, frequency, amplitude) - # ) - # f.assign(expr) - # B = fire.assemble(b) - - # solver.solve(u_h, B) - - # u_prevs0.assign(u_h) - # u_prevs1.assign(u_prevs0) - - # udat = u_h.dat.data[:] - - # r_y[step - 1] = spyro.sources.timedependentSource( - # modelRicker, t, frequency, amplitude, delay=1.5 - # ) - # p_y[step - 1] = udat[204] # hardcoded mesh and degree dependent location - - # #### Add way to test inside PDE - assert all([test1, test2, test3, test4]) diff --git a/test/test_time_convergence.py b/test/test_time_convergence.py new file mode 100644 index 00000000..2a55ff0b --- /dev/null +++ b/test/test_time_convergence.py @@ -0,0 +1,120 @@ +import spyro +import numpy as np +import math + + +def error_calc(p_numerical, p_analytical, nt): + norm = np.linalg.norm(p_numerical, 2) / np.sqrt(nt) + error_time = np.linalg.norm(p_analytical - p_numerical, 2) / np.sqrt(nt) + div_error_time = error_time / norm + return div_error_time + + +def run_forward(dt): + # dt = float(sys.argv[1]) + + final_time = 1.0 + dx = 0.006546536707079771 + + dictionary = {} + dictionary["options"] = { + "cell_type": "Q", # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "variant": "lumped", # lumped, equispaced or DG, default is lumped "method":"MLT", # (MLT/spectral_quadrilateral/DG_triangle/DG_quadrilateral) You can either specify a cell_type+variant or a method + "degree": 4, # p order + "dimension": 2, # dimension + } + + # Number of cores for the shot. For simplicity, we keep things serial. + # spyro however supports both spatial parallelism and "shot" parallelism. + dictionary["parallelism"] = { + "type": "automatic", # options: automatic (same number of cores for evey processor) or spatial + } + + # Define the domain size without the PML. Here we'll assume a 1.00 x 1.00 km + # domain and reserve the remaining 250 m for the Perfectly Matched Layer (PML) to absorb + # outgoing waves on three sides (eg., -z, +-x sides) of the domain. + dictionary["mesh"] = { + "Lz": 3.0, # depth in km - always positive + "Lx": 3.0, # width in km - always positive + "Ly": 0.0, # thickness in km - always positive + "mesh_file": None, + "mesh_type": "firedrake_mesh", # options: firedrake_mesh or user_mesh + } + + # Create a source injection operator. Here we use a single source with a + # Ricker wavelet that has a peak frequency of 5 Hz injected at the center of the mesh. + # We also specify to record the solution at a microphone near the top of the domain. + # This transect of receivers is created with the helper function `create_transect`. + dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-1.5 - dx, 1.5 + dx)], + "frequency": 5.0, + "delay": 0.3, + "receiver_locations": [(-1.5 - dx, 2.0 + dx)], + "delay_type": "time", + } + + # Simulate for 2.0 seconds. + dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": final_time, # Final time for event + "dt": dt, # timestep size + "amplitude": 1, # the Ricker has an amplitude of 1. + "output_frequency": 100, # how frequently to output solution to pvds + "gradient_sampling_frequency": 100, # how frequently to save solution to RAM + } + + dictionary["visualization"] = { + "forward_output": True, + "output_filename": "results/forward_output.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": False, + "gradient_filename": None, + } + + Wave_obj = spyro.AcousticWave(dictionary=dictionary) + Wave_obj.set_mesh(dx=0.02, periodic=True) + + Wave_obj.set_initial_velocity_model(constant=1.5) + Wave_obj.forward_solve() + + rec_out = Wave_obj.receivers_output + + return rec_out + + +def test_second_order_time_convergence(): + """Test that the second order time convergence + of the central difference method is achieved""" + + dts = [ + 0.0005, + 0.0001, + ] + + analytical_files = [ + "test/inputfiles/analytical_solution_dt_0.0005.npy", + "test/inputfiles/analytical_solution_dt_0.0001.npy", + ] + + numerical_results = [] + errors = [] + + for i in range(len(dts)): + dt = dts[i] + rec_out = run_forward(dt) + rec_anal = np.load(analytical_files[i]) + time = np.linspace(0.0, 1.0, int(1.0 / dts[i]) + 1) + nt = len(time) + numerical_results.append(rec_out.flatten()) + errors.append(error_calc(rec_out.flatten(), rec_anal, nt)) + + theory = [t**2 for t in dts] + theory = [errors[0] * th / theory[0] for th in theory] + + assert math.isclose(np.log(theory[-1]), np.log(errors[-1]), rel_tol=1e-2) + + +if __name__ == "__main__": + test_second_order_time_convergence() diff --git a/test/test_tools.py b/test/test_tools.py index 3c79a9f8..33f29250 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -1,6 +1,7 @@ import numpy as np import math import spyro +import pytest def tetrahedral_volume(p1, p2, p3, p4): @@ -28,6 +29,7 @@ def triangle_area(p1, p2, p3): return abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2 +@pytest.mark.skip(reason="not yet implemented") def test_mesh_generation_for_grid_calc(): grid_point_calculator_parameters = { # Experiment parameters @@ -61,6 +63,7 @@ def test_mesh_generation_for_grid_calc(): model = spyro.tools.generate_mesh(model, G, comm) +@pytest.mark.skip(reason="not yet implemented") def test_input_models_receivers(): test1 = True # testing if 2D receivers are inside the domain on an homogeneous case grid_point_calculator_parameters = { @@ -106,7 +109,9 @@ def test_input_models_receivers(): area2 = triangle_area(p1, p3, r) area3 = triangle_area(p3, p4, r) area4 = triangle_area(p2, p4, r) - test = math.isclose((area1 + area2 + area3 + area4), areaSquare, rel_tol=1e-09) + test = math.isclose( + (area1 + area2 + area3 + area4), areaSquare, rel_tol=1e-09 + ) if test is False: test1 = False @@ -171,6 +176,7 @@ def test_input_models_receivers(): assert all([test1, test2]) +@pytest.mark.skip(reason="not yet implemented") def test_input_models_receivers_heterogeneous(): test1 = True # testing if 2D receivers bins are inside the domain on an heterogeneous case grid_point_calculator_parameters = { @@ -217,7 +223,9 @@ def test_input_models_receivers_heterogeneous(): area2 = triangle_area(p1, p3, r) area3 = triangle_area(p3, p4, r) area4 = triangle_area(p2, p4, r) - test = math.isclose((area1 + area2 + area3 + area4), areaSquare, rel_tol=1e-09) + test = math.isclose( + (area1 + area2 + area3 + area4), areaSquare, rel_tol=1e-09 + ) if test is False: test1 = False @@ -266,13 +274,16 @@ def test_input_models_receivers_heterogeneous(): area2 = triangle_area(p1, p3, r) area3 = triangle_area(p3, p4, r) area4 = triangle_area(p2, p4, r) - test = math.isclose((area1 + area2 + area3 + area4), areaSquare, rel_tol=1e-09) + test = math.isclose( + (area1 + area2 + area3 + area4), areaSquare, rel_tol=1e-09 + ) if test is False: test2 = False assert all([test1, test2]) +@pytest.mark.skip(reason="not yet implemented") def test_grid_calc2d(): grid_point_calculator_parameters = { # Experiment parameters @@ -295,7 +306,9 @@ def test_grid_calc2d(): "g_accuracy": 1e-1, } - G = spyro.tools.minimum_grid_point_calculator(grid_point_calculator_parameters) + G = spyro.tools.minimum_grid_point_calculator( + grid_point_calculator_parameters + ) inside = 6.9 < G and G < 8.0 print(G) assert inside diff --git a/test/test_utils.py b/test/test_utils.py new file mode 100644 index 00000000..f4e38744 --- /dev/null +++ b/test/test_utils.py @@ -0,0 +1,66 @@ +import spyro +import scipy as sp +import numpy as np +import math + + +def test_butter_lowpast_filter(): + Wave_obj = spyro.examples.Rectangle_acoustic() + layer_values = [1.5, 2.0, 2.5, 3.0] + z_switches = [-0.25, -0.5, -0.75] + Wave_obj.multiple_layer_velocity_model(z_switches, layer_values) + Wave_obj.forward_solve() + + spyro.io.save_shots(Wave_obj, file_name="test_butter_prefilter") + shot_record = Wave_obj.forward_solution_receivers + rec10 = shot_record[:, 10] + + fs = 1.0 / Wave_obj.dt + + # Checks if frequency with greater power density is close to 5 + (f, S) = sp.signal.periodogram(rec10, fs) + peak_frequency = f[np.argmax(S)] + test1 = math.isclose(peak_frequency, 5.0, rel_tol=1e-2) + + # Checks if the new frequency is lower than the cutoff + cutoff_frequency = 3.0 + filtered_shot = spyro.utils.utils.butter_lowpass_filter( + shot_record, cutoff_frequency, fs + ) + filtered_rec10 = filtered_shot[:, 10] + + (filt_f, filt_S) = sp.signal.periodogram(filtered_rec10, fs) + filtered_peak_frequency = filt_f[np.argmax(filt_S)] + test2 = filtered_peak_frequency < cutoff_frequency + + print(f"Peak frequency is close to what it is supposed to be: {test1}") + print(f"Filtered peak frequency is lower than cutoff frequency: {test2}") + + assert all([test1, test2]) + + +def test_geometry_creation(): + # Checking 3D grid + points1_3D = spyro.create_3d_grid((0, 0, 0), (1, 1, 1), 5) + test0 = len(points1_3D) == 5**3 + test1 = points1_3D[0] == (0.0, 0.0, 0.0) + test2 = points1_3D[3] == (0.0, 0.0, 0.75) + test3 = points1_3D[6] == (0.25, 0.0, 0.25) + test4 = points1_3D[12] == (0.5, 0.0, 0.5) + test5 = points1_3D[18] == (0.75, 0.0, 0.75) + test6 = points1_3D[124] == (1.0, 1.0, 1.0) + + print("Geometry creation test 0: ", test0) + print("Geometry creation test 1: ", test1) + print("Geometry creation test 2: ", test2) + print("Geometry creation test 3: ", test3) + print("Geometry creation test 4: ", test4) + print("Geometry creation test 5: ", test5) + print("Geometry creation test 6: ", test6) + + assert all([test0, test1, test2, test3, test4, test5, test6]) + + +if __name__ == "__main__": + test_butter_lowpast_filter() + test_geometry_creation() diff --git a/test_3d/test_forward_3d.py b/test_3d/test_forward_3d.py index f2219eff..5dde48ff 100644 --- a/test_3d/test_forward_3d.py +++ b/test_3d/test_forward_3d.py @@ -1,5 +1,6 @@ from firedrake import File import numpy as np +import pytest import spyro @@ -26,6 +27,7 @@ def compare_velocity( return error_percent +@pytest.mark.skip(reason="takes too long") def test_forward_3d(tf=0.6): model = {} diff --git a/test_3d/test_hexahedral_convergence.py b/test_3d/test_hexahedral_convergence.py new file mode 100644 index 00000000..82c9717e --- /dev/null +++ b/test_3d/test_hexahedral_convergence.py @@ -0,0 +1,129 @@ +import numpy as np +import spyro + + +def error_calc(p_numerical, p_analytical, nt): + norm = np.linalg.norm(p_numerical, 2) / np.sqrt(nt) + error_time = np.linalg.norm(p_analytical - p_numerical, 2) / np.sqrt(nt) + div_error_time = error_time / norm + return div_error_time + + +def run_forward_hexahedral(dt, final_time, offset): + # dt = 0.0005 + # final_time = 0.5 + # offset = 0.1 + + dictionary = {} + dictionary["options"] = { + # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q) + "cell_type": "Q", + # lumped, equispaced or DG, default is lumped "method":"MLT", # (MLT/spectral_quadrilateral/DG_triangle/DG_quadrilateral) You can either specify a cell_type+variant or a method + "variant": 'lumped', + # p order + "degree": 4, + # dimension + "dimension": 3, + } + + # Number of cores for the shot. For simplicity, we keep things serial. + # spyro however supports both spatial parallelism and "shot" parallelism. + dictionary["parallelism"] = { + # options: automatic (same number of cores for evey processor) or spatial + "type": "automatic", + } + + # Define the domain size without the PML. Here we'll assume a 1.00 x 1.00 km + # domain and reserve the remaining 250 m for the Perfectly Matched Layer (PML) to absorb + # outgoing waves on three sides (eg., -z, +-x sides) of the domain. + dictionary["mesh"] = { + # depth in km - always positive + "Lz": 0.8, + # width in km - always positive + "Lx": 0.8, + # thickness in km - always positive + "Ly": 0.8, + "mesh_file": None, + # options: firedrake_mesh or user_mesh + "mesh_type": "firedrake_mesh", + } + + # Create a source injection operator. Here we use a single source with a + # Ricker wavelet that has a peak frequency of 5 Hz injected at the center of the mesh. + # We also specify to record the solution at a microphone near the top of the domain. + # This transect of receivers is created with the helper function `create_transect`. + dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-0.4, 0.4, 0.4)], + "frequency": 5.0, + "delay": 1.5, + "receiver_locations": [(-0.4 - offset, 0.4, 0.4)], + } + + # Simulate for 2.0 seconds. + dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": final_time, # Final time for event + "dt": dt, # timestep size + "amplitude": 1, # the Ricker has an amplitude of 1. + "output_frequency": 1000, # how frequently to output solution to pvds + "gradient_sampling_frequency": 1000, # how frequently to save solution to RAM + } + + dictionary["visualization"] = { + "forward_output": False, + "forward_output_filename": "results/forward_3d_output3by1by1.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": False, + "gradient_filename": None, + } + + Wave_obj = spyro.AcousticWave(dictionary=dictionary) + Wave_obj.set_mesh(dx=0.02, periodic=True) + + Wave_obj.set_initial_velocity_model(constant=1.5) + Wave_obj.forward_solve() + # time_vector = np.linspace(0.0, final_time, int(final_time/dt)+1) + + rec_out = Wave_obj.receivers_output + output = rec_out.flatten() + my_ensemble = Wave_obj.comm + if my_ensemble.comm.rank == 0 and my_ensemble.ensemble_comm.rank == 0: + np.save("dofs_3D_quads_rec_out"+str(dt)+".npy", output) + + return output + + +def analytical_solution(dt, final_time, offset): + amplitude = 1/(4*np.pi*offset) + delay = offset/1.5 + 1.5 * np.sqrt(6.0) / (np.pi * 5.0) + p_analytic = spyro.full_ricker_wavelet( + dt, final_time, + 5.0, + delay=delay, + delay_type="time", + amplitude=amplitude, + ) + return p_analytic + + +def test_3d_hexa_one_source_propagation(): + dt = 0.0005 + final_time = 0.5 + offset = 0.1 + + p_numeric = run_forward_hexahedral(dt, final_time, offset) + p_analytic = analytical_solution(dt, final_time, offset) + + error_time = error_calc(p_numeric, p_analytic, len(p_numeric)) + + small_error = error_time < 0.02 + + print(f"Error is smaller than necessary: {small_error}", flush=True) + + assert small_error + + +if __name__ == "__main__": + test_3d_hexa_one_source_propagation()